Skip to content

Commit

Permalink
improve
Browse files Browse the repository at this point in the history
  • Loading branch information
vemonet committed Jan 10, 2024
1 parent 3dc6bb4 commit b5ddc0e
Show file tree
Hide file tree
Showing 13 changed files with 80 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ coverage/
# Virtual envs
.venv/
venv/
.env

# Notebooks
.ipynb_checkpoints
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Easily configure and deploy a **fully self-hosted chatbot web service** based on
- 🌐 Free and Open Source chatbot web service with UI and API
- 🏡 Fully self-hosted, not tied to any service, and offline capable. Forget about API keys! Models and embeddings can be pre-downloaded, and the training and inference processes can run off-line if necessary.
- 🔌 Web API described using OpenAPI specs: GET/POST operations, websocket for streaming response
- 🪶 Chat web UI (Gradio-based, or custom HTML) working well on desktop and mobile, with streaming response, and markdown rendering
- 🪶 Chat web UI working well on desktop and mobile, with streaming response, and markdown rendering. Alternative gradio-based UI also available.
- 🚀 Easy to setup, no need to program, just configure the service with a [YAML](https://yaml.org/) file, and start it with 1 command
- 📦 Available as a `pip` package 🐍, or `docker` image 🐳
- 🐌 No need for GPU, this will work even on your laptop CPU! That said, just running on CPUs can be quite slow (up to 1min to answer a documents-base question on recent laptops).
Expand All @@ -29,22 +29,23 @@ Easily configure and deploy a **fully self-hosted chatbot web service** based on

For more details on how to use Libre Chat check the documentation at **[vemonet.github.io/libre-chat](http://vemonet.github.io/libre-chat)**


![UI screenshot](https://raw.github.com/vemonet/libre-chat/main/docs/docs/assets/screenshot.png)

![UI screenshot](https://raw.github.com/vemonet/libre-chat/main/docs/docs/assets/screenshot-light.png)

## 🏗️ Work in progress

⚠️ This project is a work in progress, use it with caution
> [!WARNING]
> This project is a work in progress, use it with caution.
Those checkpoints are features we plan to work on in the future, feel free to let us know in the issues if you have any comment or request.

- [x] Stream response to the websocket to show words as they are generated
- [ ] Add button to let the user stop the chatbot generation
- [ ] Add an admin dashboard web UI to enable users to upload/inspect/delete documents for QA, see/edit the config of the chatbot. Migrate to solidjs or svelte with config retrieved from API?
- [ ] Add authentication mechanisms? (OAuth/OpenID Connect) https://github.com/vemonet/libre-chat/issues/5
- [ ] Add an admin dashboard web UI to enable users to upload/inspect/delete documents for QA, see/edit the config of the chatbot.
- [ ] Kubernetes deployment (Helm chart?)
- [ ] Add authentication mechanisms? (OAuth/OpenID Connect)


![UI screenshot](https://raw.github.com/vemonet/libre-chat/main/docs/docs/assets/screenshot.png)

![UI screenshot](https://raw.github.com/vemonet/libre-chat/main/docs/docs/assets/screenshot-light.png)

## 🐳 Deploy with docker

Expand Down
Binary file modified docs/docs/assets/screenshot-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/docs/assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions frontend/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,6 @@ export default function Chat() {
<div class={`border-b border-slate-500 ${msg.type === "user" ? "bg-accent" : "bg-secondary"}`}>
<div class="px-2 py-8 mx-auto max-w-5xl">
<div class="container flex items-center">
{msg.type === "user" ? (
<i class="fas fa-user-astronaut text-xl mr-4"></i>
) : (
<i class="fas fa-robot text-xl mr-4"></i>
)}
<div>
<article class="prose max-w-full" innerHTML={marked.parse(msg.message).toString()}>
</article>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
8 changes: 8 additions & 0 deletions src/libre_chat/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ class SettingsLlm(BaseConf):

class SettingsAuth(BaseConf):
admin_pass: Optional[str] = None
client_id: Optional[str] = None
client_secret: Optional[str] = None
redirect_uri: str = "http://localhost:8000/auth/callback"
scope: str = "https://www.googleapis.com/auth/userinfo.email"
token_url: str = "https://oauth2.googleapis.com/token"
authorization_url: str = "https://accounts.google.com/o/oauth2/auth"
admin_users: List[str] = []
regular_users: List[str] = []


class ChatConf(BaseConf):
Expand Down
40 changes: 40 additions & 0 deletions src/libre_chat/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,46 @@ async def add_process_time_header(request: Request, call_next: Any) -> Response:
response.headers["X-Process-Time"] = str(time.time() - start_time)
return response

# TODO: Add OAuth
# Move get_current_user to conf.py
# async def get_current_user(token: str = Depends(oauth2_scheme)):
# if not self.conf.auth.client_id:
# return {"sub": "anonymous"} # Bypass auth and use a default user
# # Else, proceed with the usual token verification process
# async with httpx.AsyncClient() as client:
# response = await client.get("https://www.googleapis.com/oauth2/v2/userinfo", headers={"Authorization": f"Bearer {token}"})
# user_info = response.json()
# return user_info

# if self.conf.auth.client_id:
# from fastapi import Depends, status
# from fastapi.security import OAuth2AuthorizationCodeBearer
# import httpx
# from starlette.responses import RedirectResponse

# oauth2_scheme = OAuth2AuthorizationCodeBearer(
# authorizationUrl=f"{self.conf.auth.authorization_url}?response_type=code&client_id={self.conf.auth.client_id}&redirect_uri={self.conf.auth.redirect_uri}&scope={self.conf.auth.scope}",
# tokenUrl=self.conf.auth.token_url,
# )
# @self.get("/login")
# def login():
# return RedirectResponse(url=oauth2_scheme.authorizationUrl)

# @self.get("/auth/callback")
# async def auth_callback(code: str = Depends(oauth2_scheme)):
# token_payload = {
# "client_id": self.conf.auth.client_id,
# "client_secret": self.conf.auth.client_secret,
# "code": code,
# "grant_type": "authorization_code",
# "redirect_uri": self.conf.auth.redirect_uri,
# }
# async with httpx.AsyncClient() as client:
# response = await client.post(self.conf.auth.token_url, data=token_payload)
# response.raise_for_status()
# token = response.json()
# return token

# Mount web wroker asset:
# self.mount(
# "/static",
Expand Down
1 change: 1 addition & 0 deletions src/libre_chat/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def upload_documents(
)
def list_documents(
admin_pass: Optional[str] = None,
# Depends(get_current_user)
) -> JSONResponse:
"""List all documents in the documents folder."""
if self.conf.auth.admin_pass and admin_pass != self.conf.auth.admin_pass:
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/libre_chat/webapp/admin/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!DOCTYPE html><html lang="en" data-theme="dark"> <head><meta charset="UTF-8"><meta name="description" content="Open source chatbot"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v4.1.1"><title>Libre Chat</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><link rel="stylesheet" href="/_astro/admin.lY5BAdgp.css" /></head> <body class="flex flex-col h-screen"> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();;(()=>{var b=Object.defineProperty;var f=(c,o,i)=>o in c?b(c,o,{enumerable:!0,configurable:!0,writable:!0,value:i}):c[o]=i;var l=(c,o,i)=>(f(c,typeof o!="symbol"?o+"":o,i),i);var p;{let c={0:t=>m(t),1:t=>i(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(i(t)),5:t=>new Set(i(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t)},o=t=>{let[e,r]=t;return e in c?c[e](r):void 0},i=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([e,r])=>[e,o(r)]));customElements.get("astro-island")||customElements.define("astro-island",(p=class extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var d;if(!this.hydrator||!this.isConnected)return;let e=(d=this.parentElement)==null?void 0:d.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let r=this.querySelectorAll("astro-slot"),a={},h=this.querySelectorAll("template[data-astro-template]");for(let n of h){let s=n.closest(this.tagName);s!=null&&s.isSameNode(this)&&(a[n.getAttribute("data-astro-template")||"default"]=n.innerHTML,n.remove())}for(let n of r){let s=n.closest(this.tagName);s!=null&&s.isSameNode(this)&&(a[n.getAttribute("name")||"default"]=n.innerHTML)}let u;try{u=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(n){let s=this.getAttribute("component-url")||"<unknown>",y=this.getAttribute("component-export");throw y&&(s+=` (export ${y})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),n),n}await this.hydrator(this)(this.Component,u,a,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),r.disconnect(),this.childrenConnectedCallback()},r=new MutationObserver(()=>{var a;((a=this.lastChild)==null?void 0:a.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});r.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}start(){let e=JSON.parse(this.getAttribute("opts")),r=this.getAttribute("client");if(Astro[r]===void 0){window.addEventListener(`astro:${r}`,()=>this.start(),{once:!0});return}Astro[r](async()=>{let a=this.getAttribute("renderer-url"),[h,{default:u}]=await Promise.all([import(this.getAttribute("component-url")),a?import(a):()=>()=>{}]),d=this.getAttribute("component-export")||"default";if(!d.includes("."))this.Component=h[d];else{this.Component=h;for(let n of d.split("."))this.Component=this.Component[n]}return this.hydrator=u,this.hydrate},e,this)}attributeChangedCallback(){this.hydrate()}},l(p,"observedAttributes",["props"]),p))}})();</script><astro-island uid="Z296hu9" component-url="/_astro/Nav.2xMXVGgD.js" component-export="default" renderer-url="/_astro/client.XqyIkt4q.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;Nav&quot;,&quot;value&quot;:&quot;solid&quot;}"></astro-island> <astro-island uid="ZbLOwO" component-url="/_astro/admin.uwsUQjhr.js" component-export="default" renderer-url="/_astro/client.XqyIkt4q.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;AdminUi&quot;,&quot;value&quot;:&quot;solid&quot;}"></astro-island> </body></html>
<!DOCTYPE html><html lang="en" data-theme="dark"> <head><meta charset="UTF-8"><meta name="description" content="Open source chatbot"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v4.1.1"><title>Libre Chat</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><link rel="stylesheet" href="/_astro/admin.Y2MsZ9Hq.css" /></head> <body class="flex flex-col h-screen"> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();;(()=>{var b=Object.defineProperty;var f=(c,o,i)=>o in c?b(c,o,{enumerable:!0,configurable:!0,writable:!0,value:i}):c[o]=i;var l=(c,o,i)=>(f(c,typeof o!="symbol"?o+"":o,i),i);var p;{let c={0:t=>m(t),1:t=>i(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(i(t)),5:t=>new Set(i(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t)},o=t=>{let[e,r]=t;return e in c?c[e](r):void 0},i=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([e,r])=>[e,o(r)]));customElements.get("astro-island")||customElements.define("astro-island",(p=class extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var d;if(!this.hydrator||!this.isConnected)return;let e=(d=this.parentElement)==null?void 0:d.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let r=this.querySelectorAll("astro-slot"),a={},h=this.querySelectorAll("template[data-astro-template]");for(let n of h){let s=n.closest(this.tagName);s!=null&&s.isSameNode(this)&&(a[n.getAttribute("data-astro-template")||"default"]=n.innerHTML,n.remove())}for(let n of r){let s=n.closest(this.tagName);s!=null&&s.isSameNode(this)&&(a[n.getAttribute("name")||"default"]=n.innerHTML)}let u;try{u=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(n){let s=this.getAttribute("component-url")||"<unknown>",y=this.getAttribute("component-export");throw y&&(s+=` (export ${y})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),n),n}await this.hydrator(this)(this.Component,u,a,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),r.disconnect(),this.childrenConnectedCallback()},r=new MutationObserver(()=>{var a;((a=this.lastChild)==null?void 0:a.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});r.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}start(){let e=JSON.parse(this.getAttribute("opts")),r=this.getAttribute("client");if(Astro[r]===void 0){window.addEventListener(`astro:${r}`,()=>this.start(),{once:!0});return}Astro[r](async()=>{let a=this.getAttribute("renderer-url"),[h,{default:u}]=await Promise.all([import(this.getAttribute("component-url")),a?import(a):()=>()=>{}]),d=this.getAttribute("component-export")||"default";if(!d.includes("."))this.Component=h[d];else{this.Component=h;for(let n of d.split("."))this.Component=this.Component[n]}return this.hydrator=u,this.hydrate},e,this)}attributeChangedCallback(){this.hydrate()}},l(p,"observedAttributes",["props"]),p))}})();</script><astro-island uid="Z296hu9" component-url="/_astro/Nav.2xMXVGgD.js" component-export="default" renderer-url="/_astro/client.XqyIkt4q.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;Nav&quot;,&quot;value&quot;:&quot;solid&quot;}"></astro-island> <astro-island uid="ZbLOwO" component-url="/_astro/admin.uwsUQjhr.js" component-export="default" renderer-url="/_astro/client.XqyIkt4q.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;AdminUi&quot;,&quot;value&quot;:&quot;solid&quot;}"></astro-island> </body></html>
Loading

0 comments on commit b5ddc0e

Please sign in to comment.