database support
This commit is contained in:
parent
f7b23a3cec
commit
c49636c766
10 changed files with 780 additions and 860 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,3 +10,6 @@ wheels/
|
||||||
.venv
|
.venv
|
||||||
.python-version
|
.python-version
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Databases
|
||||||
|
*.sqlite3
|
||||||
82
README.md
82
README.md
|
|
@ -1,82 +0,0 @@
|
||||||
# ChatSBT - Multi-Model Chat Application
|
|
||||||
|
|
||||||
A modern chat application supporting multiple AI models through OpenRouter API.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Chat with multiple AI models (Qwen, Deepseek, Kimi)
|
|
||||||
- Real-time streaming responses
|
|
||||||
- Conversation history
|
|
||||||
- Simple REST API backend
|
|
||||||
- Modern Svelte frontend
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- Svelte
|
|
||||||
- DaisyUI (Tailwind component library)
|
|
||||||
- Vite
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Starlette (async Python web framework)
|
|
||||||
- LangChain (LLM orchestration)
|
|
||||||
- LangGraph (for potential future agent workflows)
|
|
||||||
- OpenRouter API (multi-model provider)
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
| Method | Path | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| POST | /chats | Create new chat session |
|
|
||||||
| GET | /chats/{chat_id} | Get chat history |
|
|
||||||
| POST | /chats/{chat_id}/messages | Post new message |
|
|
||||||
| GET | /chats/{chat_id}/stream | Stream response from AI |
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Python 3.11+
|
|
||||||
- Deno
|
|
||||||
- UV (Python package manager)
|
|
||||||
- OpenRouter API key (set in `.env` file)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Clone the repository
|
|
||||||
2. Set up environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "OPENROUTER_API_KEY=your_key_here" > .env
|
|
||||||
echo "OPENROUTER_BASE_URL=https://openrouter.ai/api/v1" >> .env
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
4. Install frontend dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd chatsbt
|
|
||||||
deno install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
1. Start backend server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the frontend (another terminal):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd chatsbt
|
|
||||||
deno run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at `http://localhost:5173`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Available models:
|
|
||||||
- `qwen/qwen3-235b-a22b-2507`
|
|
||||||
- `deepseek/deepseek-r1-0528`
|
|
||||||
- `moonshotai/kimi-k2`
|
|
||||||
|
|
@ -15,4 +15,5 @@ def get_llm(provider: str):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_messages(chats, chat_id):
|
def get_messages(chats, chat_id):
|
||||||
return [HumanMessage(**m) if m["role"] == "human" else AIMessage(**m) for m in chats[chat_id]]
|
print(chats)
|
||||||
|
return [HumanMessage(**m) if m["role"] == "human" else AIMessage(**m) for m in chats]
|
||||||
|
|
|
||||||
1
config/__init__.py
Normal file
1
config/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Masonite-orm module
|
||||||
11
config/database.py
Normal file
11
config/database.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from masoniteorm.connections import ConnectionResolver
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": "sqlite",
|
||||||
|
"sqlite": {
|
||||||
|
"driver": "sqlite",
|
||||||
|
"database": "database.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB = ConnectionResolver().set_connection_details(DATABASES)
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from sse_starlette.sse import EventSourceResponse
|
from sse_starlette.sse import EventSourceResponse
|
||||||
from chatgraph import get_messages, get_llm
|
from chatgraph import get_messages, get_llm
|
||||||
|
from models.Chat import Chat
|
||||||
|
|
||||||
|
|
||||||
CHATS: Dict[str, List[dict]] = {} # chat_id -> messages
|
|
||||||
PENDING: Dict[str, Tuple[str, str]] = {} # message_id -> (chat_id, provider)
|
PENDING: Dict[str, Tuple[str, str]] = {} # message_id -> (chat_id, provider)
|
||||||
|
|
||||||
MODELS = {
|
MODELS = {
|
||||||
|
|
@ -32,16 +33,22 @@ async def create_chat(request: Request):
|
||||||
provider = body.get("model","")
|
provider = body.get("model","")
|
||||||
if provider not in MODELS:
|
if provider not in MODELS:
|
||||||
return JSONResponse({"error": "Unknown model"}, status_code=400)
|
return JSONResponse({"error": "Unknown model"}, status_code=400)
|
||||||
chat_id = str(uuid.uuid4())[:8]
|
chat = Chat()
|
||||||
CHATS[chat_id] = []
|
chat_id = str(uuid.uuid4())
|
||||||
|
chat.id = chat_id
|
||||||
|
chat.title = "New Chat"
|
||||||
|
chat.messages = json.dumps([])
|
||||||
|
chat.save()
|
||||||
return JSONResponse({"id": chat_id, "model": provider})
|
return JSONResponse({"id": chat_id, "model": provider})
|
||||||
|
|
||||||
async def history(request : Request):
|
async def history(request : Request):
|
||||||
"""GET /chats/{chat_id} -> previous messages"""
|
"""GET /chats/{chat_id} -> previous messages"""
|
||||||
chat_id = request.path_params["chat_id"]
|
chat_id = request.path_params["chat_id"]
|
||||||
if chat_id not in CHATS:
|
chat = Chat.find(chat_id)
|
||||||
|
if not chat:
|
||||||
return JSONResponse({"error": "Not found"}, status_code=404)
|
return JSONResponse({"error": "Not found"}, status_code=404)
|
||||||
return JSONResponse({"messages": CHATS[chat_id]})
|
messages = json.loads(chat.messages) if chat.messages else []
|
||||||
|
return JSONResponse({"messages": messages})
|
||||||
|
|
||||||
async def post_message(request: Request):
|
async def post_message(request: Request):
|
||||||
"""POST /chats/{chat_id}/messages
|
"""POST /chats/{chat_id}/messages
|
||||||
|
|
@ -49,7 +56,8 @@ async def post_message(request: Request):
|
||||||
Returns: {"message_id": "<chat_id>"}
|
Returns: {"message_id": "<chat_id>"}
|
||||||
"""
|
"""
|
||||||
chat_id = request.path_params["chat_id"]
|
chat_id = request.path_params["chat_id"]
|
||||||
if chat_id not in CHATS:
|
chat = Chat.find(chat_id)
|
||||||
|
if not chat:
|
||||||
return JSONResponse({"error": "Chat not found"}, status_code=404)
|
return JSONResponse({"error": "Chat not found"}, status_code=404)
|
||||||
|
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
|
|
@ -58,9 +66,14 @@ async def post_message(request: Request):
|
||||||
if provider not in MODELS:
|
if provider not in MODELS:
|
||||||
return JSONResponse({"error": "Unknown model"}, status_code=400)
|
return JSONResponse({"error": "Unknown model"}, status_code=400)
|
||||||
|
|
||||||
|
# Load existing messages and add the new user message
|
||||||
|
messages = json.loads(chat.messages) if chat.messages else []
|
||||||
|
messages.append({"role": "human", "content": user_text})
|
||||||
|
chat.messages = json.dumps(messages)
|
||||||
|
chat.save()
|
||||||
|
|
||||||
message_id = str(uuid.uuid4())
|
message_id = str(uuid.uuid4())
|
||||||
PENDING[message_id] = (chat_id, provider)
|
PENDING[message_id] = (chat_id, provider)
|
||||||
CHATS[chat_id].append({"role": "human", "content": user_text})
|
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"status": "queued",
|
"status": "queued",
|
||||||
|
|
@ -72,13 +85,18 @@ async def chat_stream(request):
|
||||||
chat_id = request.path_params["chat_id"]
|
chat_id = request.path_params["chat_id"]
|
||||||
message_id = request.query_params.get("message_id")
|
message_id = request.query_params.get("message_id")
|
||||||
|
|
||||||
if chat_id not in CHATS or message_id not in PENDING:
|
if message_id not in PENDING:
|
||||||
return JSONResponse({"error": "Not found"}, status_code=404)
|
return JSONResponse({"error": "Not found"}, status_code=404)
|
||||||
|
|
||||||
chat_id_from_map, provider = PENDING.pop(message_id)
|
chat_id_from_map, provider = PENDING.pop(message_id)
|
||||||
assert chat_id == chat_id_from_map
|
assert chat_id == chat_id_from_map
|
||||||
|
|
||||||
msgs = get_messages(CHATS, chat_id)
|
chat = Chat.find(chat_id)
|
||||||
|
if not chat:
|
||||||
|
return JSONResponse({"error": "Chat not found"}, status_code=404)
|
||||||
|
|
||||||
|
messages = json.loads(chat.messages) if chat.messages else []
|
||||||
|
msgs = get_messages( messages , chat_id)
|
||||||
llm = get_llm(provider)
|
llm = get_llm(provider)
|
||||||
|
|
||||||
async def event_generator():
|
async def event_generator():
|
||||||
|
|
@ -88,7 +106,10 @@ async def chat_stream(request):
|
||||||
buffer += token
|
buffer += token
|
||||||
yield {"data": token}
|
yield {"data": token}
|
||||||
# Finished: store assistant reply
|
# Finished: store assistant reply
|
||||||
CHATS[chat_id].append({"role": "assistant", "content": buffer})
|
messages.append({"role": "assistant", "content": buffer})
|
||||||
|
chat.messages = json.dumps(messages)
|
||||||
|
chat.save()
|
||||||
yield {"event": "done", "data": ""}
|
yield {"event": "done", "data": ""}
|
||||||
|
|
||||||
return EventSourceResponse(event_generator())
|
return EventSourceResponse(event_generator())
|
||||||
|
|
||||||
|
|
|
||||||
12
databases/migrations/2025_08_04_00001_create_chats_table.py
Normal file
12
databases/migrations/2025_08_04_00001_create_chats_table.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from masoniteorm.migrations import Migration
|
||||||
|
|
||||||
|
|
||||||
|
class CreateChatsTable(Migration):
|
||||||
|
def up(self):
|
||||||
|
with self.schema.create("chats") as table:
|
||||||
|
table.uuid("id").primary()
|
||||||
|
table.string("title")
|
||||||
|
table.json("messages")
|
||||||
|
|
||||||
|
def down(self):
|
||||||
|
self.schema.drop("chats")
|
||||||
10
models/Chat.py
Normal file
10
models/Chat.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from masoniteorm.models import Model
|
||||||
|
from masoniteorm.scopes import UUIDPrimaryKeyMixin
|
||||||
|
|
||||||
|
class Chat(Model, UUIDPrimaryKeyMixin):
|
||||||
|
__table__ = "chats"
|
||||||
|
__timestamps__ = False
|
||||||
|
__primary_key__ = "id"
|
||||||
|
__incrementing__ = False
|
||||||
|
|
||||||
|
__fillable__ = ["id", "title", "messages"]
|
||||||
|
|
@ -8,13 +8,11 @@ dependencies = [
|
||||||
"jinja2>=3.1.6",
|
"jinja2>=3.1.6",
|
||||||
"langchain>=0.3.26",
|
"langchain>=0.3.26",
|
||||||
"langchain-core>=0.3.68",
|
"langchain-core>=0.3.68",
|
||||||
|
"langchain-openai>=0.3.28",
|
||||||
"starlette>=0.47.1",
|
"starlette>=0.47.1",
|
||||||
"uvicorn>=0.35.0",
|
"uvicorn>=0.35.0",
|
||||||
"python-dotenv>=1.1.1",
|
"python-dotenv>=1.1.1",
|
||||||
"websockets>=15.0.1",
|
|
||||||
"sse-starlette>=2.4.1",
|
"sse-starlette>=2.4.1",
|
||||||
"langchain-openai>=0.3.28",
|
|
||||||
"langgraph>=0.5.4",
|
|
||||||
"langgraph-checkpoint-sqlite>=2.0.11",
|
|
||||||
"aiosqlite>=0.21.0",
|
"aiosqlite>=0.21.0",
|
||||||
|
"masonite-orm>=3.0.0",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue