Spaces:
Sleeping
Sleeping
ok
Browse files- .dockerignore +21 -0
- .gitattributes +4 -0
- .gitignore +9 -0
- DEPLOY.md +86 -0
- DEPLOY_HF.md +76 -0
- Dockerfile +31 -0
- README.md +27 -11
- app/MultiAgent.py +187 -0
- app/VectorDBManagers.py +98 -0
- app/__init__.py +0 -0
- app/__pycache__/MultiAgent.cpython-313.pyc +0 -0
- app/__pycache__/VectorDBManagers.cpython-313.pyc +0 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/function_tool.cpython-313.pyc +0 -0
- app/__pycache__/main.cpython-313.pyc +0 -0
- app/__pycache__/memory_store.cpython-313.pyc +0 -0
- app/__pycache__/sqlite_store.cpython-313.pyc +0 -0
- app/__pycache__/user_state.cpython-313.pyc +0 -0
- app/chatkit_threads.db +0 -0
- app/chatkit_threads.db-shm +0 -0
- app/chatkit_threads.db-wal +3 -0
- app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/data_level0.bin +3 -0
- app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/header.bin +3 -0
- app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/length.bin +3 -0
- app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/link_lists.bin +3 -0
- app/chromafast_db/chroma.sqlite3 +3 -0
- app/function_tool.py +79 -0
- app/main.py +1026 -0
- app/memory_store.py +176 -0
- app/scrapping.py +32 -0
- app/sqlite_store.py +536 -0
- app/user_state.db +0 -0
- app/user_state.py +200 -0
- app/website/output.txt +7 -0
- chatkit_threads.db +0 -0
- chatkit_threads.db-shm +0 -0
- chatkit_threads.db-wal +3 -0
- chromafast_db/chroma.sqlite3 +3 -0
- pyproject.toml +43 -0
- user_state.db +0 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.pyd
|
| 5 |
+
.Python
|
| 6 |
+
env/
|
| 7 |
+
venv/
|
| 8 |
+
.venv/
|
| 9 |
+
.env.local
|
| 10 |
+
.env
|
| 11 |
+
chatkit_threads.db
|
| 12 |
+
chatkit_threads.db-shm
|
| 13 |
+
chatkit_threads.db-wal
|
| 14 |
+
user_state.db
|
| 15 |
+
chromafast_db/
|
| 16 |
+
output/
|
| 17 |
+
website/
|
| 18 |
+
.git/
|
| 19 |
+
.gitignore
|
| 20 |
+
.dockerignore
|
| 21 |
+
Dockerfile
|
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
app/chatkit_threads.db-wal filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
app/chromafast_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
chatkit_threads.db-wal filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
chromafast_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*.egg-info/
|
| 4 |
+
.venv/
|
| 5 |
+
.env
|
| 6 |
+
.ruff_cache/
|
| 7 |
+
.pytest_cache/
|
| 8 |
+
.coverage/
|
| 9 |
+
*.log
|
DEPLOY.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying to Fly.io
|
| 2 |
+
|
| 3 |
+
This backend service uses `Docker` and `uv` for easy deployment. Since it uses SQLite for data persistence, we need to configure a persistent volume on Fly.io.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
1. Install `flyctl` (Fly.io CLI).
|
| 8 |
+
2. Login: `fly auth login`.
|
| 9 |
+
|
| 10 |
+
## Deployment Steps
|
| 11 |
+
|
| 12 |
+
### 1. Initialize the App
|
| 13 |
+
|
| 14 |
+
Run the launch command in this directory (`backend/`):
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
fly launch
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
- **Choose a unique app name** (e.g., `my-chatkit-backend`).
|
| 21 |
+
- **Select a region** (choose one close to you).
|
| 22 |
+
- **Would you like to set up a Postgresql database now?** -> **No** (we use SQLite).
|
| 23 |
+
- **Would you like to set up an Upstash Redis database now?** -> **No**.
|
| 24 |
+
- **Do you want to deploy now?** -> **No** (we need to set secrets first).
|
| 25 |
+
|
| 26 |
+
This creates a `fly.toml` file.
|
| 27 |
+
|
| 28 |
+
### 2. Set Up Data Persistence (Volumes)
|
| 29 |
+
|
| 30 |
+
Since the app uses SQLite (`chatkit_threads.db` and `user_state.db`), you need a persistent volume so data isn't lost when the app restarts.
|
| 31 |
+
|
| 32 |
+
Create a 1GB volume (adjust size if needed):
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
fly volumes create chatkit_data --size 1 --region <your-region>
|
| 36 |
+
```
|
| 37 |
+
*Make sure to use the same region you selected in step 1.*
|
| 38 |
+
|
| 39 |
+
### 3. Update `fly.toml`
|
| 40 |
+
|
| 41 |
+
Open `fly.toml` and add the `[mounts]` section to mount the volume to `/data` in the container. Also add the environment variables to tell the app to use that path.
|
| 42 |
+
|
| 43 |
+
Add this to `fly.toml`:
|
| 44 |
+
|
| 45 |
+
```toml
|
| 46 |
+
[mounts]
|
| 47 |
+
source = "chatkit_data"
|
| 48 |
+
destination = "/data"
|
| 49 |
+
|
| 50 |
+
[env]
|
| 51 |
+
CHATKIT_DB_PATH = "/data/chatkit_threads.db"
|
| 52 |
+
USER_STATE_DB_PATH = "/data/user_state.db"
|
| 53 |
+
PORT = "8000"
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 4. Set Secrets
|
| 57 |
+
|
| 58 |
+
Set your API keys using `fly secrets`. Copy values from your `.env.local`:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
fly secrets set \
|
| 62 |
+
GROQ_API_KEY=your_groq_key \
|
| 63 |
+
OPENAI_API_KEY=your_openai_key \
|
| 64 |
+
OPENROUTER_API_KEY=your_openrouter_key \
|
| 65 |
+
DEEPSEEK_API_KEY=your_deepseek_key \
|
| 66 |
+
GOOGLE_API_KEY=your_google_key \
|
| 67 |
+
CARTESIA_API=your_cartesia_key
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
### 5. Deploy
|
| 71 |
+
|
| 72 |
+
Now you are ready to deploy:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
fly deploy
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
## Verifying Deployment
|
| 79 |
+
|
| 80 |
+
Check the logs to ensure everything started correctly:
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
fly logs
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
Your app should now be running!
|
DEPLOY_HF.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying to Hugging Face Spaces (Docker)
|
| 2 |
+
|
| 3 |
+
You can easily deploy this backend to [Hugging Face Spaces](https://huggingface.co/spaces) using their Docker SDK.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
1. A Hugging Face account.
|
| 8 |
+
2. `pyproject.toml` and `uv.lock` must be in the backend directory.
|
| 9 |
+
|
| 10 |
+
## Deployment Steps
|
| 11 |
+
|
| 12 |
+
### 1. Create a New Space
|
| 13 |
+
|
| 14 |
+
1. Go to [Creates a new Space](https://huggingface.co/new-space).
|
| 15 |
+
2. **Space Name**: e.g., `chatkit-backend`.
|
| 16 |
+
3. **License**: Apache 2.0 or MIT.
|
| 17 |
+
4. **SDK**: Select **Docker**.
|
| 18 |
+
5. **Template**: Choose "Blank".
|
| 19 |
+
6. **Space Hardware**: CPU Basic (Free) is usually enough for this middleware (since generic LLM inference happens externally).
|
| 20 |
+
|
| 21 |
+
### 2. Configure the Space
|
| 22 |
+
|
| 23 |
+
Hugging Face Spaces expects the application to listen on port **7860**.
|
| 24 |
+
|
| 25 |
+
**Option A: Push via Git (Recommended)**
|
| 26 |
+
|
| 27 |
+
You can simply push your `backend` folder contents to the Space's repository. Note that the Space root must be the directory containing the `Dockerfile`.
|
| 28 |
+
|
| 29 |
+
If your repo has `backend/Dockerfile`, you might need to adjust the structure or use the "Docker options" in `README.md` (of the space) to point to the correct folder, OR simply copy the backend contents to the root of the Space repo.
|
| 30 |
+
|
| 31 |
+
**Option B: Adjust Dockerfile for Port 7860**
|
| 32 |
+
|
| 33 |
+
Your `Dockerfile` currently defaults to port 8000. Hugging Face sets the `PORT` environment variable to `7860` automatically in most cases, but to be safe, you can update your Dockerfile or just rely on the env var override.
|
| 34 |
+
|
| 35 |
+
**Env Var Override:**
|
| 36 |
+
The existing Dockerfile uses:
|
| 37 |
+
`CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]`
|
| 38 |
+
|
| 39 |
+
To make it compatible with HF's dynamic port, update the `CMD` in `Dockerfile` to use the shell form or rely on the host variable if we use a starter script. Or simpler, just change the default to 7860.
|
| 40 |
+
|
| 41 |
+
### 3. Persisting Data (SQLite)
|
| 42 |
+
|
| 43 |
+
On Hugging Face Spaces (Free Tier), the filesystem is **ephemeral**. This means **your database (`chatkit_threads.db`) will be deleted** every time the Space restarts (which happens frequently).
|
| 44 |
+
|
| 45 |
+
**To keep your chat history:**
|
| 46 |
+
1. Go to your Space **Settings**.
|
| 47 |
+
2. Scroll to "Persistent Storage".
|
| 48 |
+
3. You can upgrade to a paid tier to get a persistent volume mounted at `/data`.
|
| 49 |
+
|
| 50 |
+
If you enable persistent storage:
|
| 51 |
+
1. Update your Environment Variables (see step 4) to save DBs in `/data`.
|
| 52 |
+
- `CHATKIT_DB_PATH` = `/data/chatkit_threads.db`
|
| 53 |
+
- `USER_STATE_DB_PATH` = `/data/user_state.db`
|
| 54 |
+
|
| 55 |
+
### 4. Set Secrets (Environment Variables)
|
| 56 |
+
|
| 57 |
+
Go to your Space -> **Settings** -> **Variables and secrets**.
|
| 58 |
+
|
| 59 |
+
Add the following **Secrets** (Copy from your `.env.local`):
|
| 60 |
+
|
| 61 |
+
- `GROQ_API_KEY`
|
| 62 |
+
- `OPENAI_API_KEY`
|
| 63 |
+
- `OPENROUTER_API_KEY`
|
| 64 |
+
- `DEEPSEEK_API_KEY`
|
| 65 |
+
- `GOOGLE_API_KEY`
|
| 66 |
+
- `CARTESIA_API`
|
| 67 |
+
|
| 68 |
+
And add the following **Variables** (if using persistent storage):
|
| 69 |
+
- `CHATKIT_DB_PATH` = `chatkit_threads.db` (or `/data/...`)
|
| 70 |
+
- `USER_STATE_DB_PATH` = `user_state.db` (or `/data/...`)
|
| 71 |
+
|
| 72 |
+
### 5. Finalize
|
| 73 |
+
|
| 74 |
+
Once the files are pushed and secrets set, the Space will build locally. Open the **App** tab to see your running backend. The URL will be something like `https://huggingface.co/spaces/<username>/<space-name>`.
|
| 75 |
+
|
| 76 |
+
You can now point your frontend to this URL!
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim-bookworm
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install uv
|
| 7 |
+
RUN pip install --no-cache-dir uv
|
| 8 |
+
|
| 9 |
+
# Copy dependency files
|
| 10 |
+
COPY pyproject.toml uv.lock README.md ./
|
| 11 |
+
|
| 12 |
+
# Install dependencies
|
| 13 |
+
# --frozen: use exact versions from uv.lock
|
| 14 |
+
# --no-dev: do not install development dependencies
|
| 15 |
+
RUN uv sync --frozen --no-dev
|
| 16 |
+
|
| 17 |
+
# Copy application code
|
| 18 |
+
COPY app ./app
|
| 19 |
+
|
| 20 |
+
# Set environment variables
|
| 21 |
+
# Add the virtual environment to PATH
|
| 22 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 23 |
+
ENV HOST=0.0.0.0
|
| 24 |
+
ENV PORT=8000
|
| 25 |
+
|
| 26 |
+
# Expose the port (Hugging Face typically uses 7860, local uses 8000)
|
| 27 |
+
ENV PORT=7860
|
| 28 |
+
EXPOSE $PORT
|
| 29 |
+
|
| 30 |
+
# Command to run the application (using shell form to expand variables)
|
| 31 |
+
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port $PORT"]
|
README.md
CHANGED
|
@@ -1,11 +1,27 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ChatKit Python Backend
|
| 2 |
+
|
| 3 |
+
> For the steps to run both fronend and backend apps in this repo, please refer to the README.md at the top directory insteaad.
|
| 4 |
+
|
| 5 |
+
This FastAPI service wires up a minimal ChatKit server implementation with a single tool capable of recording short facts that users share in the conversation. Facts that are saved through the widget are exposed via the `/facts` REST endpoint so the frontend can render them alongside the chat experience.
|
| 6 |
+
|
| 7 |
+
## Features
|
| 8 |
+
|
| 9 |
+
- **ChatKit endpoint** at `POST /chatkit` that streams responses using the ChatKit protocol when the optional ChatKit Python package is installed.
|
| 10 |
+
- **Fact recording tool** that renders a confirmation widget with _Save_ and _Discard_ actions.
|
| 11 |
+
- **Guardrail-ready system prompt** extracted into `app/constants.py` so it is easy to modify.
|
| 12 |
+
- **Simple fact store** backed by in-memory storage in `app/facts.py`.
|
| 13 |
+
- **REST helpers**
|
| 14 |
+
- `GET /facts` – list saved facts (used by the frontend list view)
|
| 15 |
+
- `POST /facts/{fact_id}/save` – mark a fact as saved
|
| 16 |
+
- `POST /facts/{fact_id}/discard` – discard a pending fact
|
| 17 |
+
- `GET /health` – surface a basic health indicator
|
| 18 |
+
|
| 19 |
+
## Getting started
|
| 20 |
+
|
| 21 |
+
To enable the realtime assistant you need to install both the ChatKit Python package and the OpenAI SDK, then provide an `OPENAI_API_KEY` environment variable.
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
uv sync
|
| 25 |
+
export OPENAI_API_KEY=sk-proj-...
|
| 26 |
+
uv run uvicorn app.main:app --reload
|
| 27 |
+
```
|
app/MultiAgent.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agents import Agent, ModelSettings, Runner, RunConfig,OpenAIResponsesModel ,AsyncOpenAI,function_tool,OpenAIChatCompletionsModel
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from datetime import datetime, date
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
load_dotenv(dotenv_path="./.env.local")
|
| 6 |
+
import asyncio
|
| 7 |
+
from typing import List
|
| 8 |
+
from .VectorDBManagers import VectorDBManager
|
| 9 |
+
from chatkit.agents import AgentContext
|
| 10 |
+
import os
|
| 11 |
+
import nest_asyncio
|
| 12 |
+
nest_asyncio.apply()
|
| 13 |
+
from .function_tool import suggestion_ragtool
|
| 14 |
+
kimi_model = OpenAIResponsesModel(
|
| 15 |
+
model="openai/gpt-oss-20b", # Valid Groq model
|
| 16 |
+
openai_client=AsyncOpenAI(
|
| 17 |
+
base_url="https://api.groq.com/openai/v1",
|
| 18 |
+
api_key=os.getenv("GROQ_API_KEY"),
|
| 19 |
+
)
|
| 20 |
+
)
|
| 21 |
+
google_model = OpenAIChatCompletionsModel(
|
| 22 |
+
model="google/gemini-2.5-flash", # Google Gemini via OpenRouter
|
| 23 |
+
openai_client=AsyncOpenAI(
|
| 24 |
+
base_url="https://openrouter.ai/api/v1",
|
| 25 |
+
api_key=os.getenv("OPENROUTER_API_KEY"),
|
| 26 |
+
)
|
| 27 |
+
)
|
| 28 |
+
deepseek_model = OpenAIChatCompletionsModel(
|
| 29 |
+
model="deepseek/deepseek-chat", # DeepSeek via OpenRouter
|
| 30 |
+
openai_client=AsyncOpenAI(
|
| 31 |
+
base_url="https://openrouter.ai/api/v1",
|
| 32 |
+
api_key=os.getenv("OPENROUTER_API_KEY"),
|
| 33 |
+
)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
sumary_model = OpenAIResponsesModel(
|
| 37 |
+
model="moonshotai/kimi-k2-instruct-0905", # Valid Groq model
|
| 38 |
+
openai_client=AsyncOpenAI(
|
| 39 |
+
base_url="https://api.groq.com/openai/v1",
|
| 40 |
+
api_key=os.getenv("GROQ_API_KEY")
|
| 41 |
+
)
|
| 42 |
+
)
|
| 43 |
+
def build_sugguestion_information_agent()-> Agent[AgentContext]:
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
current_time = datetime.now()
|
| 47 |
+
current_date = date.today()
|
| 48 |
+
current_day = datetime.today().strftime("%A")
|
| 49 |
+
information_agent = Agent[AgentContext](
|
| 50 |
+
name="company_suggestion_information",
|
| 51 |
+
instructions=(
|
| 52 |
+
"You are an information agent and customer service representative for the company. "
|
| 53 |
+
"Your goal is to provide clear, concise answers using ONLY the suggestion_ragtool tool. "
|
| 54 |
+
"For greeting do not call tool reply by your self add company name "
|
| 55 |
+
"Always speak as the company using 'we'. Do NOT guess or assume. "
|
| 56 |
+
"If information is not found, reply politely: "
|
| 57 |
+
"phraphse according to your intellgence 'No information is available regarding to this . You may book an appointment or speak to our sales agent for more details.' praphrase it "
|
| 58 |
+
|
| 59 |
+
"Make only ONE suggestion_ragtool query that fully represents the user’s request. "
|
| 60 |
+
"All company-related answers must come strictly from the suggestion_ragtool tool. "
|
| 61 |
+
"Do not create or assume any details. Always use the correct company name. "
|
| 62 |
+
"The suggestion_ragtool result is your official answer. "
|
| 63 |
+
|
| 64 |
+
"Respond in under not more than 80 words, in a friendly customer-service tone. "
|
| 65 |
+
"Never leave incomplete replies and never ignore earlier conversation context. "
|
| 66 |
+
|
| 67 |
+
"As a customer support agent, always answer using official company information found through the suggestion_ragtool tool. for partcular question liek greeting and user info if you have so do not use tool reply by self "
|
| 68 |
+
f"Current system time : {current_time}, date: {current_date}, day: {current_day}. "
|
| 69 |
+
),
|
| 70 |
+
model=deepseek_model,
|
| 71 |
+
tools=[
|
| 72 |
+
suggestion_ragtool
|
| 73 |
+
],
|
| 74 |
+
model_settings=ModelSettings(
|
| 75 |
+
temperature=1,
|
| 76 |
+
top_p=1,
|
| 77 |
+
max_tokens=2048,
|
| 78 |
+
),
|
| 79 |
+
)
|
| 80 |
+
return information_agent
|
| 81 |
+
|
| 82 |
+
def build_summarizer_agent() -> Agent[AgentContext]:
|
| 83 |
+
"""
|
| 84 |
+
Creates a summarizer agent that condenses chat history
|
| 85 |
+
into a short, factual summary for context preservation.
|
| 86 |
+
"""
|
| 87 |
+
summarizer_agent = Agent[AgentContext](
|
| 88 |
+
name="Summarizer Agent",
|
| 89 |
+
instructions="""
|
| 90 |
+
You are a summarization assistant.
|
| 91 |
+
Your job is to take several user and assistant messages and produce a concise,
|
| 92 |
+
factual summary that captures key intents, facts, and outcomes.
|
| 93 |
+
|
| 94 |
+
Guidelines:
|
| 95 |
+
- Keep the summary under 80 words.
|
| 96 |
+
- Focus on what the user is asking for and the assistant's key responses.
|
| 97 |
+
- Do NOT add new information.
|
| 98 |
+
- Preserve important context like customer concerns, preferences, or goals.
|
| 99 |
+
- Write in plain English.
|
| 100 |
+
""",
|
| 101 |
+
model=sumary_model, # or use default_model if configured in your environment
|
| 102 |
+
model_settings=ModelSettings(
|
| 103 |
+
temperature=0.3,
|
| 104 |
+
top_p=0.9,
|
| 105 |
+
max_tokens=300,
|
| 106 |
+
),
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
return summarizer_agent
|
| 110 |
+
def build_kimi_information_agent()-> Agent[AgentContext]:
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
current_time = datetime.now()
|
| 114 |
+
current_date = date.today()
|
| 115 |
+
current_day = datetime.today().strftime("%A")
|
| 116 |
+
information_agent = Agent[AgentContext](
|
| 117 |
+
name="company_suggestion_information",
|
| 118 |
+
instructions=(
|
| 119 |
+
"You are an information agent and customer service representative for the company. "
|
| 120 |
+
"Your goal is to provide clear, concise answers using ONLY the suggestion_ragtool tool. "
|
| 121 |
+
"For greeting do not call tool reply by your self add company name "
|
| 122 |
+
"Always speak as the company using 'we'. Do NOT guess or assume. "
|
| 123 |
+
"If information is not found, reply politely: "
|
| 124 |
+
"phraphse according to your intellgence 'No information is available regarding to this . You may book an appointment or speak to our sales agent for more details.' praphrase it "
|
| 125 |
+
|
| 126 |
+
"Make only ONE suggestion_ragtool query that fully represents the user’s request. "
|
| 127 |
+
"All company-related answers must come strictly from the suggestion_ragtool tool. "
|
| 128 |
+
"Do not create or assume any details. Always use the correct company name. "
|
| 129 |
+
"The suggestion_ragtool result is your official answer. "
|
| 130 |
+
|
| 131 |
+
"Respond in under not more than 80 words, in a friendly customer-service tone. "
|
| 132 |
+
"Never leave incomplete replies and never ignore earlier conversation context. "
|
| 133 |
+
|
| 134 |
+
"As a customer support agent, always answer using official company information found through the suggestion_ragtool tool. for partcular question liek greeting and user info if you have so do not use tool reply by self "
|
| 135 |
+
f"Current system time : {current_time}, date: {current_date}, day: {current_day}. "
|
| 136 |
+
),
|
| 137 |
+
model=kimi_model,
|
| 138 |
+
tools=[
|
| 139 |
+
suggestion_ragtool
|
| 140 |
+
],
|
| 141 |
+
model_settings=ModelSettings(
|
| 142 |
+
temperature=1,
|
| 143 |
+
top_p=1,
|
| 144 |
+
max_tokens=2048,
|
| 145 |
+
),
|
| 146 |
+
)
|
| 147 |
+
return information_agent
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def build_google_information_agent()-> Agent[AgentContext]:
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
current_time = datetime.now()
|
| 154 |
+
current_date = date.today()
|
| 155 |
+
current_day = datetime.today().strftime("%A")
|
| 156 |
+
information_agent = Agent[AgentContext](
|
| 157 |
+
name="company_suggestion_information",
|
| 158 |
+
instructions=(
|
| 159 |
+
"You are an information agent and customer service representative for the company. "
|
| 160 |
+
"Your goal is to provide clear, concise answers using ONLY the suggestion_ragtool tool. "
|
| 161 |
+
"For greeting do not call tool reply by your self add company name "
|
| 162 |
+
"Always speak as the company using 'we'. Do NOT guess or assume. "
|
| 163 |
+
"If information is not found, reply politely: "
|
| 164 |
+
"phraphse according to your intellgence 'No information is available regarding to this . You may book an appointment or speak to our sales agent for more details.' praphrase it "
|
| 165 |
+
|
| 166 |
+
"Make only ONE suggestion_ragtool query that fully represents the user’s request. "
|
| 167 |
+
"All company-related answers must come strictly from the suggestion_ragtool tool. "
|
| 168 |
+
"Do not create or assume any details. Always use the correct company name. "
|
| 169 |
+
"The suggestion_ragtool result is your official answer. "
|
| 170 |
+
|
| 171 |
+
"Respond in under not more than 80 words, in a friendly customer-service tone. "
|
| 172 |
+
"Never leave incomplete replies and never ignore earlier conversation context. "
|
| 173 |
+
|
| 174 |
+
"As a customer support agent, always answer using official company information found through the suggestion_ragtool tool. for partcular question liek greeting and user info if you have so do not use tool reply by self "
|
| 175 |
+
f"Current system time : {current_time}, date: {current_date}, day: {current_day}. "
|
| 176 |
+
),
|
| 177 |
+
model=google_model,
|
| 178 |
+
tools=[
|
| 179 |
+
suggestion_ragtool
|
| 180 |
+
],
|
| 181 |
+
model_settings=ModelSettings(
|
| 182 |
+
temperature=1,
|
| 183 |
+
top_p=1,
|
| 184 |
+
max_tokens=2048,
|
| 185 |
+
),
|
| 186 |
+
)
|
| 187 |
+
return information_agent
|
app/VectorDBManagers.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, ServiceContext, Settings
|
| 3 |
+
from llama_index.vector_stores.chroma import ChromaVectorStore
|
| 4 |
+
from llama_index.core.storage.storage_context import StorageContext
|
| 5 |
+
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
| 6 |
+
from llama_index.llms.groq import Groq
|
| 7 |
+
import chromadb
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
load_dotenv(dotenv_path="./.env.local")
|
| 11 |
+
|
| 12 |
+
# --- Configure global settings for Groq and embeddings ---
|
| 13 |
+
Settings.llm = Groq(
|
| 14 |
+
model="meta-llama/llama-4-scout-17b-16e-instruct",
|
| 15 |
+
api_key=os.getenv("GROQ_API_KEY"),
|
| 16 |
+
system_prompt="provide information according to context Do NOT guess or make assumptions please do not tell other that overlapping context. Respond briefly in one paragraph.",
|
| 17 |
+
)
|
| 18 |
+
Settings.embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class VectorDBManager:
|
| 22 |
+
def __init__(self, db_path: str = "./chromafast_db", collection_name: str = "DB_collection"):
|
| 23 |
+
self.db_path = db_path
|
| 24 |
+
self.collection_name = collection_name
|
| 25 |
+
|
| 26 |
+
# Persistent Chroma client (never ephemeral)
|
| 27 |
+
self.db_client = chromadb.PersistentClient(path=db_path)
|
| 28 |
+
self.collection = self.db_client.get_or_create_collection(collection_name)
|
| 29 |
+
|
| 30 |
+
# Build vector + storage contexts
|
| 31 |
+
self.vector_store = ChromaVectorStore(chroma_collection=self.collection)
|
| 32 |
+
self.storage_context = StorageContext.from_defaults(vector_store=self.vector_store)
|
| 33 |
+
self.index = None
|
| 34 |
+
|
| 35 |
+
def is_collection_empty(self) -> bool:
|
| 36 |
+
"""Check if the Chroma collection has any stored embeddings."""
|
| 37 |
+
try:
|
| 38 |
+
return len(self.collection.get()["ids"]) == 0
|
| 39 |
+
except Exception:
|
| 40 |
+
return True
|
| 41 |
+
|
| 42 |
+
def build_index_from_documents(self, data_path: str):
|
| 43 |
+
"""Build and save a new index from document directory."""
|
| 44 |
+
print(f"📂 Loading documents from: {data_path}")
|
| 45 |
+
documents = SimpleDirectoryReader(data_path).load_data()
|
| 46 |
+
print(f"📄 Loaded {len(documents)} documents.")
|
| 47 |
+
|
| 48 |
+
self.index = VectorStoreIndex.from_documents(
|
| 49 |
+
documents,
|
| 50 |
+
storage_context=self.storage_context,
|
| 51 |
+
)
|
| 52 |
+
print(f"✅ Index built and stored in Chroma at {self.db_path}")
|
| 53 |
+
|
| 54 |
+
def load_existing_index(self):
|
| 55 |
+
"""Load index from existing Chroma vector store."""
|
| 56 |
+
print(f"📦 Loading existing Chroma DB from {self.db_path}")
|
| 57 |
+
self.index = VectorStoreIndex.from_vector_store(self.vector_store)
|
| 58 |
+
print("✅ Loaded existing index successfully")
|
| 59 |
+
|
| 60 |
+
def get_query_engine(self):
|
| 61 |
+
if not self.index:
|
| 62 |
+
raise ValueError("❌ Index not initialized. Build or load it first.")
|
| 63 |
+
return self.index.as_query_engine(use_async=True)
|
| 64 |
+
|
| 65 |
+
def query(self, text: str):
|
| 66 |
+
"""Run a query against the existing or newly built index."""
|
| 67 |
+
query_engine = self.get_query_engine()
|
| 68 |
+
response = query_engine.query(text)
|
| 69 |
+
return response
|
| 70 |
+
async def aquery(self, text: str):
|
| 71 |
+
"""Run a query against the existing or newly built index."""
|
| 72 |
+
import time
|
| 73 |
+
start_t = time.time()
|
| 74 |
+
query_engine = self.get_query_engine()
|
| 75 |
+
response = await query_engine.aquery(text)
|
| 76 |
+
print(f"🔍 Async query completed in {time.time() - start_t:.2f}s")
|
| 77 |
+
return response
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
DATA_DIR = "../companyData1"
|
| 82 |
+
DB_PATH = "../chromafast_db"
|
| 83 |
+
|
| 84 |
+
manager = VectorDBManager(db_path=DB_PATH, collection_name="DB_collection")
|
| 85 |
+
|
| 86 |
+
# Detect if DB exists and has embeddings
|
| 87 |
+
if not os.path.exists(DB_PATH) or manager.is_collection_empty():
|
| 88 |
+
print("🆕 No existing embeddings found. Building new Chroma DB...")
|
| 89 |
+
manager.build_index_from_documents(DATA_DIR)
|
| 90 |
+
else:
|
| 91 |
+
print("📂 Existing Chroma DB found. Loading it...")
|
| 92 |
+
manager.load_existing_index()
|
| 93 |
+
|
| 94 |
+
# Test query
|
| 95 |
+
question = "What are some of the main contributions of this new bitswits?"
|
| 96 |
+
response = manager.query(question)
|
| 97 |
+
print("\n🔍 Query Result:\n")
|
| 98 |
+
print(response)
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/MultiAgent.cpython-313.pyc
ADDED
|
Binary file (5.8 kB). View file
|
|
|
app/__pycache__/VectorDBManagers.cpython-313.pyc
ADDED
|
Binary file (6.12 kB). View file
|
|
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (172 Bytes). View file
|
|
|
app/__pycache__/function_tool.cpython-313.pyc
ADDED
|
Binary file (3.7 kB). View file
|
|
|
app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (36.3 kB). View file
|
|
|
app/__pycache__/memory_store.cpython-313.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
app/__pycache__/sqlite_store.cpython-313.pyc
ADDED
|
Binary file (23.8 kB). View file
|
|
|
app/__pycache__/user_state.cpython-313.pyc
ADDED
|
Binary file (12.1 kB). View file
|
|
|
app/chatkit_threads.db
ADDED
|
Binary file (4.1 kB). View file
|
|
|
app/chatkit_threads.db-shm
ADDED
|
Binary file (32.8 kB). View file
|
|
|
app/chatkit_threads.db-wal
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a572712d1a8c1c300bbaf0b5f58720eac241013c1afa5c8435d7bbedf3b14b0d
|
| 3 |
+
size 449112
|
app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/data_level0.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bfc20179bd13ac7810d64fe2b63cd66a87248cb9e41f3ad4ac48e39619041a77
|
| 3 |
+
size 167600
|
app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/header.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a0e81c3b22454233bc12d0762f06dcca48261a75231cf87c79b75e69a6c00150
|
| 3 |
+
size 100
|
app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/length.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:41f6bb80280dc803baa57e0ba719186b7c0ee549e8f47c571d5e779d79b89017
|
| 3 |
+
size 400
|
app/chromafast_db/6d7f2a08-25fb-473d-a268-7ec2315530e1/link_lists.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
| 3 |
+
size 0
|
app/chromafast_db/chroma.sqlite3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:069858c77f19d2ecccc3b4b4acabbe14f56b2bdfa0d6aea20b5c92029795879c
|
| 3 |
+
size 520192
|
app/function_tool.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, date, timedelta
|
| 2 |
+
from typing import Optional as _Optional
|
| 3 |
+
import json
|
| 4 |
+
import httpx
|
| 5 |
+
from urllib.parse import urljoin
|
| 6 |
+
from llama_index.llms.groq import Groq
|
| 7 |
+
import asyncio
|
| 8 |
+
import random
|
| 9 |
+
import os
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
load_dotenv(dotenv_path="./.env.local")
|
| 12 |
+
from agents import function_tool , RunContextWrapper
|
| 13 |
+
from .VectorDBManagers import VectorDBManager
|
| 14 |
+
|
| 15 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 16 |
+
CHROMA_DB_PATH = os.path.join(BASE_DIR, "chromafast_db")
|
| 17 |
+
WEBSITE_PATH = os.path.join(BASE_DIR, "website")
|
| 18 |
+
|
| 19 |
+
manager = VectorDBManager(db_path=CHROMA_DB_PATH, collection_name="DB_collection")
|
| 20 |
+
if not os.path.exists(CHROMA_DB_PATH) or manager.is_collection_empty():
|
| 21 |
+
print("🆕 No existing embeddings found. Building new Chroma DB...")
|
| 22 |
+
manager.build_index_from_documents(WEBSITE_PATH)
|
| 23 |
+
else:
|
| 24 |
+
print("📂 Existing Chroma DB found. Loading it...")
|
| 25 |
+
manager.load_existing_index()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@function_tool(
|
| 29 |
+
name_override="suggestion_ragtool",
|
| 30 |
+
description_override="""
|
| 31 |
+
Name: suggestion_ragtool
|
| 32 |
+
Query the company's knowledge base for information.
|
| 33 |
+
|
| 34 |
+
Description:
|
| 35 |
+
all Question except any meeting , call , invitation like schedule
|
| 36 |
+
Searches the company's internal knowledge base to provide informative, paragraph-style answers
|
| 37 |
+
related to services, policies, technologies, or any general information embedded in the vector store.
|
| 38 |
+
|
| 39 |
+
Parameters:
|
| 40 |
+
context (RunContextWrapper): The openai session context used for communicating with the user.
|
| 41 |
+
query (str): The question or query asked by the user about the company.
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
str:
|
| 45 |
+
""",
|
| 46 |
+
)
|
| 47 |
+
async def suggestion_ragtool(ctx: RunContextWrapper, query: str) :
|
| 48 |
+
"""
|
| 49 |
+
🔍 Tool Name: suggestion_ragtool
|
| 50 |
+
|
| 51 |
+
Description:
|
| 52 |
+
all Question except any meeting , call , invitation like schedule
|
| 53 |
+
Searches the company's internal knowledge base to provide informative, paragraph-style answers
|
| 54 |
+
related to services, policies, technologies, or any general information embedded in the vector store.
|
| 55 |
+
|
| 56 |
+
Parameters:
|
| 57 |
+
context (RunContext): The Openai session context used for communicating with the user.
|
| 58 |
+
query (str): The question or query asked by the user about the company.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
str:
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
print(f"Answering from knowledgebase: {query}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
res = await manager.aquery(query)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
print("Query result:", res)
|
| 74 |
+
result=str(res)
|
| 75 |
+
return result
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"Error: {e}")
|
| 78 |
+
return f"❌ Failed: Unable to answer the question. {str(e)}"
|
| 79 |
+
|
app/main.py
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, AsyncIterator,List
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from agents import RunConfig, Runner, SQLiteSession
|
| 6 |
+
from agents.model_settings import ModelSettings
|
| 7 |
+
from chatkit.agents import AgentContext, stream_agent_response
|
| 8 |
+
from chatkit.server import ChatKitServer, StreamingResult
|
| 9 |
+
|
| 10 |
+
from chatkit.types import (
|
| 11 |
+
Attachment,
|
| 12 |
+
ClientToolCallItem,
|
| 13 |
+
ThreadMetadata,
|
| 14 |
+
ThreadStreamEvent,
|
| 15 |
+
UserMessageItem,
|
| 16 |
+
AssistantMessageItem
|
| 17 |
+
)
|
| 18 |
+
from .MultiAgent import build_sugguestion_information_agent, build_kimi_information_agent, build_summarizer_agent,build_google_information_agent
|
| 19 |
+
from chatkit.types import AssistantMessageItem as AssistantMsg
|
| 20 |
+
from fastapi import Depends, FastAPI, Query, Request, HTTPException, BackgroundTasks, Header, File, UploadFile
|
| 21 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
+
from fastapi.responses import Response, StreamingResponse
|
| 23 |
+
from openai.types.responses import ResponseInputContentParam
|
| 24 |
+
from starlette.responses import JSONResponse
|
| 25 |
+
import json
|
| 26 |
+
import asyncio
|
| 27 |
+
from collections import defaultdict
|
| 28 |
+
import random
|
| 29 |
+
import string
|
| 30 |
+
import traceback
|
| 31 |
+
from datetime import datetime, timezone, timedelta
|
| 32 |
+
from .sqlite_store import SQLiteStore
|
| 33 |
+
from .memory_store import MemoryStore
|
| 34 |
+
from .user_state import UserStateManager
|
| 35 |
+
from dotenv import load_dotenv
|
| 36 |
+
load_dotenv(dotenv_path="./.env.local")
|
| 37 |
+
DEFAULT_THREAD_ID = "demo_default_thread"
|
| 38 |
+
def _user_message_text(item: UserMessageItem) -> str:
|
| 39 |
+
parts: list[str] = []
|
| 40 |
+
for part in item.content:
|
| 41 |
+
text = getattr(part, "text", None)
|
| 42 |
+
if text:
|
| 43 |
+
parts.append(text)
|
| 44 |
+
return " ".join(parts).strip()
|
| 45 |
+
|
| 46 |
+
def _is_tool_completion_item(item: Any) -> bool:
|
| 47 |
+
return isinstance(item, ClientToolCallItem)
|
| 48 |
+
|
| 49 |
+
class deepseek_CustomerSupportServer(ChatKitServer[dict[str, Any]]):
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
agent_state: UserStateManager,
|
| 53 |
+
) -> None:
|
| 54 |
+
store = SQLiteStore(db_path=os.getenv("CHATKIT_DB_PATH", "chatkit_threads.db"))
|
| 55 |
+
super().__init__(store)
|
| 56 |
+
self.store = store
|
| 57 |
+
self.agent_state = agent_state
|
| 58 |
+
|
| 59 |
+
self.information_agent = build_sugguestion_information_agent()
|
| 60 |
+
self.summarizer_agent = build_summarizer_agent()
|
| 61 |
+
def _resolve_thread_id(self, thread: ThreadMetadata | None) -> str:
|
| 62 |
+
return thread.id if thread and thread.id else DEFAULT_THREAD_ID
|
| 63 |
+
|
| 64 |
+
async def prepare_conversation_context(self, thread_key: str, message_text: str, status:str) -> str:
|
| 65 |
+
# Await handle_history which is now fast because summarization is backgrounded
|
| 66 |
+
summary_text, recent_context = await self.handle_history(thread_key)
|
| 67 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 68 |
+
|
| 69 |
+
customer_context = (
|
| 70 |
+
"Customer context:\n"
|
| 71 |
+
f"- Name: {user_data.customer_name or ''}\n"
|
| 72 |
+
f"- Email: {user_data.customer_email or ''}\n"
|
| 73 |
+
f"- Phone: {user_data.customer_phone or ''}\n"
|
| 74 |
+
|
| 75 |
+
f"- Timezone: {user_data.Timezone or ''}\n"
|
| 76 |
+
)
|
| 77 |
+
if status.lower() == "offline":
|
| 78 |
+
combined_prompt = (
|
| 79 |
+
f"{customer_context}\n"
|
| 80 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 81 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 82 |
+
f"If the user asks to talk to a human sales agent, respond: "
|
| 83 |
+
f"-This Company you are representing for : Sunmarke School\n"
|
| 84 |
+
f"Current request: {message_text}\n"
|
| 85 |
+
)
|
| 86 |
+
return combined_prompt
|
| 87 |
+
|
| 88 |
+
combined_prompt = (
|
| 89 |
+
f"{customer_context}\n"
|
| 90 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 91 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 92 |
+
f"-This Company you are representing for : Sunmarke School\n"
|
| 93 |
+
f"Current request: {message_text}\n"
|
| 94 |
+
)
|
| 95 |
+
return combined_prompt
|
| 96 |
+
|
| 97 |
+
# Await handle_history which is now fast because summarization is backgrounded
|
| 98 |
+
summary_text, recent_context = await self.handle_history(thread_key)
|
| 99 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 100 |
+
|
| 101 |
+
customer_context = (
|
| 102 |
+
"Customer context:\n"
|
| 103 |
+
f"- Name: {user_data.customer_name or ''}\n"
|
| 104 |
+
f"- Email: {user_data.customer_email or ''}\n"
|
| 105 |
+
f"- Phone: {user_data.customer_phone or ''}\n"
|
| 106 |
+
f"- Company: {user_data.company_name or ''}\n"
|
| 107 |
+
f"- Timezone: {user_data.Timezone or ''}\n"
|
| 108 |
+
)
|
| 109 |
+
if status.lower() == "offline":
|
| 110 |
+
combined_prompt = (
|
| 111 |
+
f"{customer_context}\n"
|
| 112 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 113 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 114 |
+
f"If the user asks to talk to a human sales agent, respond: "
|
| 115 |
+
f'\"Our human sales agent is currently offline, May i help in book an appointment for you.\" '
|
| 116 |
+
f"Current request: {message_text}\n"
|
| 117 |
+
)
|
| 118 |
+
return combined_prompt
|
| 119 |
+
|
| 120 |
+
combined_prompt = (
|
| 121 |
+
f"{customer_context}\n"
|
| 122 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 123 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 124 |
+
f"Current request: {message_text}\n"
|
| 125 |
+
)
|
| 126 |
+
return combined_prompt
|
| 127 |
+
|
| 128 |
+
async def _async_summarize(self, thread_key: str, user_messages: list, previous_summary: str):
|
| 129 |
+
"""Background task to perform summarization without blocking the main flow."""
|
| 130 |
+
try:
|
| 131 |
+
to_summarize = user_messages[:-5]
|
| 132 |
+
combined_text = "\n".join(
|
| 133 |
+
f"{'User' if isinstance(i, UserMessageItem) else 'Assistant'}: {_user_message_text(i)}"
|
| 134 |
+
for i in to_summarize
|
| 135 |
+
)
|
| 136 |
+
summarizer_prompt = (
|
| 137 |
+
f"Previous summary:\n{previous_summary}\n\n"
|
| 138 |
+
f"Add the following messages into the summary:\n{combined_text}\n"
|
| 139 |
+
f"Return a concise updated summary of the entire conversation."
|
| 140 |
+
)
|
| 141 |
+
session = SQLiteSession(thread_key)
|
| 142 |
+
result = await Runner.run(
|
| 143 |
+
self.summarizer_agent,
|
| 144 |
+
summarizer_prompt,
|
| 145 |
+
session=session,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
self.agent_state.set_summary(thread_key, result.final_output)
|
| 149 |
+
print(f"🧠 [BACKGROUND] Summary updated for thread: {thread_key}")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"⚠️ [BACKGROUND] Summarization failed for thread {thread_key}: {e}")
|
| 152 |
+
|
| 153 |
+
async def handle_history(self, thread_key: str) -> tuple[str, str]:
|
| 154 |
+
"""Handles message history, returns current state, and triggers summarization in background if needed."""
|
| 155 |
+
# 1. Fetch history from store (Fast)
|
| 156 |
+
history = self.store._items(thread_key)
|
| 157 |
+
user_messages = [i for i in history if isinstance(i, (UserMessageItem, AssistantMessageItem))]
|
| 158 |
+
|
| 159 |
+
# Keep context within limits
|
| 160 |
+
if len(user_messages) > 15:
|
| 161 |
+
user_messages = user_messages[-15:]
|
| 162 |
+
|
| 163 |
+
# 2. Get existing summary from persistence (Fast)
|
| 164 |
+
summary_text = self.agent_state.get_summary(thread_key)
|
| 165 |
+
|
| 166 |
+
# 3. Trigger summarization in background if criteria met (Non-blocking)
|
| 167 |
+
if len(user_messages) >= 5 and len(user_messages) % 5 == 0:
|
| 168 |
+
print(f"🧠 [HISTORY] Triggering background summarization for {thread_key}")
|
| 169 |
+
asyncio.create_task(self._async_summarize(thread_key, user_messages, summary_text))
|
| 170 |
+
|
| 171 |
+
# 4. Compute recent context for the prompt (Fast)
|
| 172 |
+
last_five = user_messages[-10:]
|
| 173 |
+
recent_context = "\n".join(
|
| 174 |
+
f"{'User' if isinstance(i, UserMessageItem) else 'Assistant'}: {_user_message_text(i)}"
|
| 175 |
+
for i in last_five
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
return summary_text, recent_context
|
| 179 |
+
|
| 180 |
+
async def respond(
|
| 181 |
+
self,
|
| 182 |
+
thread: ThreadMetadata,
|
| 183 |
+
item: UserMessageItem | None,
|
| 184 |
+
context: dict[str, Any],
|
| 185 |
+
) -> AsyncIterator[ThreadStreamEvent]:
|
| 186 |
+
if item is None:
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
message_text = _user_message_text(item)
|
| 190 |
+
if not message_text:
|
| 191 |
+
return
|
| 192 |
+
|
| 193 |
+
thread_key = self._resolve_thread_id(thread)
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 197 |
+
request_context_enriched = {
|
| 198 |
+
**(context or {}),
|
| 199 |
+
|
| 200 |
+
}
|
| 201 |
+
except Exception:
|
| 202 |
+
request_context_enriched = context or {}
|
| 203 |
+
|
| 204 |
+
session = SQLiteSession(thread_key)
|
| 205 |
+
agent_context = AgentContext(thread=thread, store=self.store, request_context=request_context_enriched)
|
| 206 |
+
|
| 207 |
+
combined_prompt = await self.prepare_conversation_context(thread_key, message_text, 'offline')
|
| 208 |
+
|
| 209 |
+
result_stream = Runner.run_streamed(self.information_agent, combined_prompt, context=agent_context, session=session)
|
| 210 |
+
async for event in stream_agent_response(agent_context, result_stream):
|
| 211 |
+
yield event
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class kimi_CustomerSupportServer(ChatKitServer[dict[str, Any]]):
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
agent_state: UserStateManager,
|
| 218 |
+
) -> None:
|
| 219 |
+
store = SQLiteStore(db_path="chatkit_threads.db")
|
| 220 |
+
super().__init__(store)
|
| 221 |
+
self.store = store
|
| 222 |
+
self.agent_state = agent_state
|
| 223 |
+
self.summarizer_agent = build_summarizer_agent()
|
| 224 |
+
self.information_agent = build_kimi_information_agent()
|
| 225 |
+
|
| 226 |
+
def _resolve_thread_id(self, thread: ThreadMetadata | None) -> str:
|
| 227 |
+
return thread.id if thread and thread.id else DEFAULT_THREAD_ID
|
| 228 |
+
|
| 229 |
+
async def prepare_conversation_context(self, thread_key: str, message_text: str, status:str) -> str:
|
| 230 |
+
# Await handle_history which is now fast because summarization is backgrounded
|
| 231 |
+
summary_text, recent_context = await self.handle_history(thread_key)
|
| 232 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 233 |
+
|
| 234 |
+
customer_context = (
|
| 235 |
+
"Customer context:\n"
|
| 236 |
+
f"- Name: {user_data.customer_name or ''}\n"
|
| 237 |
+
f"- Email: {user_data.customer_email or ''}\n"
|
| 238 |
+
f"- Phone: {user_data.customer_phone or ''}\n"
|
| 239 |
+
|
| 240 |
+
f"- Timezone: {user_data.Timezone or ''}\n"
|
| 241 |
+
)
|
| 242 |
+
if status.lower() == "offline":
|
| 243 |
+
combined_prompt = (
|
| 244 |
+
f"{customer_context}\n"
|
| 245 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 246 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 247 |
+
f"If the user asks to talk to a human sales agent, respond: "
|
| 248 |
+
f"-This Company you are representing for : Sunmarke School\n"
|
| 249 |
+
f"Current request: {message_text}\n"
|
| 250 |
+
)
|
| 251 |
+
return combined_prompt
|
| 252 |
+
|
| 253 |
+
combined_prompt = (
|
| 254 |
+
f"{customer_context}\n"
|
| 255 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 256 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 257 |
+
f"-This Company you are representing for : Sunmarke School\n"
|
| 258 |
+
f"Current request: {message_text}\n"
|
| 259 |
+
)
|
| 260 |
+
return combined_prompt
|
| 261 |
+
|
| 262 |
+
# Await handle_history which is now fast because summarization is backgrounded
|
| 263 |
+
summary_text, recent_context = await self.handle_history(thread_key)
|
| 264 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 265 |
+
|
| 266 |
+
customer_context = (
|
| 267 |
+
"Customer context:\n"
|
| 268 |
+
f"- Name: {user_data.customer_name or ''}\n"
|
| 269 |
+
f"- Email: {user_data.customer_email or ''}\n"
|
| 270 |
+
f"- Phone: {user_data.customer_phone or ''}\n"
|
| 271 |
+
f"- Company: {user_data.company_name or ''}\n"
|
| 272 |
+
f"- Timezone: {user_data.Timezone or ''}\n"
|
| 273 |
+
)
|
| 274 |
+
if status.lower() == "offline":
|
| 275 |
+
combined_prompt = (
|
| 276 |
+
f"{customer_context}\n"
|
| 277 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 278 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 279 |
+
f"If the user asks to talk to a human sales agent, respond: "
|
| 280 |
+
f'\"Our human sales agent is currently offline, May i help in book an appointment for you.\" '
|
| 281 |
+
f"Current request: {message_text}\n"
|
| 282 |
+
)
|
| 283 |
+
return combined_prompt
|
| 284 |
+
|
| 285 |
+
combined_prompt = (
|
| 286 |
+
f"{customer_context}\n"
|
| 287 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 288 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 289 |
+
f"Current request: {message_text}\n"
|
| 290 |
+
)
|
| 291 |
+
return combined_prompt
|
| 292 |
+
|
| 293 |
+
async def _async_summarize(self, thread_key: str, user_messages: list, previous_summary: str):
|
| 294 |
+
"""Background task to perform summarization without blocking the main flow."""
|
| 295 |
+
try:
|
| 296 |
+
to_summarize = user_messages[:-5]
|
| 297 |
+
combined_text = "\n".join(
|
| 298 |
+
f"{'User' if isinstance(i, UserMessageItem) else 'Assistant'}: {_user_message_text(i)}"
|
| 299 |
+
for i in to_summarize
|
| 300 |
+
)
|
| 301 |
+
summarizer_prompt = (
|
| 302 |
+
f"Previous summary:\n{previous_summary}\n\n"
|
| 303 |
+
f"Add the following messages into the summary:\n{combined_text}\n"
|
| 304 |
+
f"Return a concise updated summary of the entire conversation."
|
| 305 |
+
)
|
| 306 |
+
session = SQLiteSession(thread_key)
|
| 307 |
+
result = await Runner.run(
|
| 308 |
+
self.summarizer_agent,
|
| 309 |
+
summarizer_prompt,
|
| 310 |
+
session=session,
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
self.agent_state.set_summary(thread_key, result.final_output)
|
| 314 |
+
print(f"🧠 [BACKGROUND] Summary updated for thread: {thread_key}")
|
| 315 |
+
except Exception as e:
|
| 316 |
+
print(f"⚠️ [BACKGROUND] Summarization failed for thread {thread_key}: {e}")
|
| 317 |
+
|
| 318 |
+
async def handle_history(self, thread_key: str) -> tuple[str, str]:
|
| 319 |
+
"""Handles message history, returns current state, and triggers summarization in background if needed."""
|
| 320 |
+
# 1. Fetch history from store (Fast)
|
| 321 |
+
history = self.store._items(thread_key)
|
| 322 |
+
user_messages = [i for i in history if isinstance(i, (UserMessageItem, AssistantMessageItem))]
|
| 323 |
+
|
| 324 |
+
# Keep context within limits
|
| 325 |
+
if len(user_messages) > 15:
|
| 326 |
+
user_messages = user_messages[-15:]
|
| 327 |
+
|
| 328 |
+
# 2. Get existing summary from persistence (Fast)
|
| 329 |
+
summary_text = self.agent_state.get_summary(thread_key)
|
| 330 |
+
|
| 331 |
+
# 3. Trigger summarization in background if criteria met (Non-blocking)
|
| 332 |
+
if len(user_messages) >= 5 and len(user_messages) % 5 == 0:
|
| 333 |
+
print(f"🧠 [HISTORY] Triggering background summarization for {thread_key}")
|
| 334 |
+
asyncio.create_task(self._async_summarize(thread_key, user_messages, summary_text))
|
| 335 |
+
|
| 336 |
+
# 4. Compute recent context for the prompt (Fast)
|
| 337 |
+
last_five = user_messages[-10:]
|
| 338 |
+
recent_context = "\n".join(
|
| 339 |
+
f"{'User' if isinstance(i, UserMessageItem) else 'Assistant'}: {_user_message_text(i)}"
|
| 340 |
+
for i in last_five
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
return summary_text, recent_context
|
| 344 |
+
|
| 345 |
+
async def respond(
|
| 346 |
+
self,
|
| 347 |
+
thread: ThreadMetadata,
|
| 348 |
+
item: UserMessageItem | None,
|
| 349 |
+
context: dict[str, Any],
|
| 350 |
+
) -> AsyncIterator[ThreadStreamEvent]:
|
| 351 |
+
if item is None:
|
| 352 |
+
return
|
| 353 |
+
|
| 354 |
+
message_text = _user_message_text(item)
|
| 355 |
+
if not message_text:
|
| 356 |
+
return
|
| 357 |
+
|
| 358 |
+
thread_key = self._resolve_thread_id(thread)
|
| 359 |
+
|
| 360 |
+
try:
|
| 361 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 362 |
+
request_context_enriched = {
|
| 363 |
+
**(context or {}),
|
| 364 |
+
|
| 365 |
+
}
|
| 366 |
+
except Exception:
|
| 367 |
+
request_context_enriched = context or {}
|
| 368 |
+
|
| 369 |
+
session = SQLiteSession(thread_key)
|
| 370 |
+
agent_context = AgentContext(thread=thread, store=self.store, request_context=request_context_enriched)
|
| 371 |
+
|
| 372 |
+
combined_prompt = await self.prepare_conversation_context(thread_key, message_text, 'offline')
|
| 373 |
+
|
| 374 |
+
result_stream = Runner.run_streamed(self.information_agent, combined_prompt, context=agent_context, session=session)
|
| 375 |
+
async for event in stream_agent_response(agent_context, result_stream):
|
| 376 |
+
yield event
|
| 377 |
+
|
| 378 |
+
class google_CustomerSupportServer(ChatKitServer[dict[str, Any]]):
|
| 379 |
+
def __init__(
|
| 380 |
+
self,
|
| 381 |
+
agent_state: UserStateManager,
|
| 382 |
+
) -> None:
|
| 383 |
+
store = SQLiteStore(db_path="chatkit_threads.db")
|
| 384 |
+
super().__init__(store)
|
| 385 |
+
self.store = store
|
| 386 |
+
self.agent_state = agent_state
|
| 387 |
+
|
| 388 |
+
self.information_agent = build_google_information_agent()
|
| 389 |
+
self.summarizer_agent = build_summarizer_agent()
|
| 390 |
+
def _resolve_thread_id(self, thread: ThreadMetadata | None) -> str:
|
| 391 |
+
return thread.id if thread and thread.id else DEFAULT_THREAD_ID
|
| 392 |
+
|
| 393 |
+
async def prepare_conversation_context(self, thread_key: str, message_text: str, status:str) -> str:
|
| 394 |
+
# Await handle_history which is now fast because summarization is backgrounded
|
| 395 |
+
summary_text, recent_context = await self.handle_history(thread_key)
|
| 396 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 397 |
+
|
| 398 |
+
customer_context = (
|
| 399 |
+
"Customer context:\n"
|
| 400 |
+
f"- Name: {user_data.customer_name or ''}\n"
|
| 401 |
+
f"- Email: {user_data.customer_email or ''}\n"
|
| 402 |
+
f"- Phone: {user_data.customer_phone or ''}\n"
|
| 403 |
+
|
| 404 |
+
f"- Timezone: {user_data.Timezone or ''}\n"
|
| 405 |
+
)
|
| 406 |
+
if status.lower() == "offline":
|
| 407 |
+
combined_prompt = (
|
| 408 |
+
f"{customer_context}\n"
|
| 409 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 410 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 411 |
+
f"If the user asks to talk to a human sales agent, respond: "
|
| 412 |
+
f"-This Company you are representing for : Sunmarke School\n"
|
| 413 |
+
f"Current request: {message_text}\n"
|
| 414 |
+
)
|
| 415 |
+
return combined_prompt
|
| 416 |
+
|
| 417 |
+
combined_prompt = (
|
| 418 |
+
f"{customer_context}\n"
|
| 419 |
+
f"Previous summary:\n{summary_text}\n\n"
|
| 420 |
+
f"Recent conversation (last 5 messages):\n{recent_context}\n\n"
|
| 421 |
+
f"-This Company you are representing for : Sunmarke School\n"
|
| 422 |
+
f"Current request: {message_text}\n"
|
| 423 |
+
)
|
| 424 |
+
return combined_prompt
|
| 425 |
+
|
| 426 |
+
async def _async_summarize(self, thread_key: str, user_messages: list, previous_summary: str):
|
| 427 |
+
"""Background task to perform summarization without blocking the main flow."""
|
| 428 |
+
try:
|
| 429 |
+
to_summarize = user_messages[:-5]
|
| 430 |
+
combined_text = "\n".join(
|
| 431 |
+
f"{'User' if isinstance(i, UserMessageItem) else 'Assistant'}: {_user_message_text(i)}"
|
| 432 |
+
for i in to_summarize
|
| 433 |
+
)
|
| 434 |
+
summarizer_prompt = (
|
| 435 |
+
f"Previous summary:\n{previous_summary}\n\n"
|
| 436 |
+
f"Add the following messages into the summary:\n{combined_text}\n"
|
| 437 |
+
f"Return a concise updated summary of the entire conversation."
|
| 438 |
+
)
|
| 439 |
+
session = SQLiteSession(thread_key)
|
| 440 |
+
result = await Runner.run(
|
| 441 |
+
self.summarizer_agent,
|
| 442 |
+
summarizer_prompt,
|
| 443 |
+
session=session,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
self.agent_state.set_summary(thread_key, result.final_output)
|
| 447 |
+
print(f"🧠 [BACKGROUND] Summary updated for thread: {thread_key}")
|
| 448 |
+
except Exception as e:
|
| 449 |
+
print(f"⚠️ [BACKGROUND] Summarization failed for thread {thread_key}: {e}")
|
| 450 |
+
|
| 451 |
+
async def handle_history(self, thread_key: str) -> tuple[str, str]:
|
| 452 |
+
"""Handles message history, returns current state, and triggers summarization in background if needed."""
|
| 453 |
+
# 1. Fetch history from store (Fast)
|
| 454 |
+
history = self.store._items(thread_key)
|
| 455 |
+
user_messages = [i for i in history if isinstance(i, (UserMessageItem, AssistantMessageItem))]
|
| 456 |
+
|
| 457 |
+
# Keep context within limits
|
| 458 |
+
if len(user_messages) > 15:
|
| 459 |
+
user_messages = user_messages[-15:]
|
| 460 |
+
|
| 461 |
+
# 2. Get existing summary from persistence (Fast)
|
| 462 |
+
summary_text = self.agent_state.get_summary(thread_key)
|
| 463 |
+
|
| 464 |
+
# 3. Trigger summarization in background if criteria met (Non-blocking)
|
| 465 |
+
if len(user_messages) >= 5 and len(user_messages) % 5 == 0:
|
| 466 |
+
print(f"🧠 [HISTORY] Triggering background summarization for {thread_key}")
|
| 467 |
+
asyncio.create_task(self._async_summarize(thread_key, user_messages, summary_text))
|
| 468 |
+
|
| 469 |
+
# 4. Compute recent context for the prompt (Fast)
|
| 470 |
+
last_five = user_messages[-10:]
|
| 471 |
+
recent_context = "\n".join(
|
| 472 |
+
f"{'User' if isinstance(i, UserMessageItem) else 'Assistant'}: {_user_message_text(i)}"
|
| 473 |
+
for i in last_five
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
return summary_text, recent_context
|
| 477 |
+
|
| 478 |
+
async def respond(
|
| 479 |
+
self,
|
| 480 |
+
thread: ThreadMetadata,
|
| 481 |
+
item: UserMessageItem | None,
|
| 482 |
+
context: dict[str, Any],
|
| 483 |
+
) -> AsyncIterator[ThreadStreamEvent]:
|
| 484 |
+
if item is None:
|
| 485 |
+
return
|
| 486 |
+
|
| 487 |
+
message_text = _user_message_text(item)
|
| 488 |
+
if not message_text:
|
| 489 |
+
return
|
| 490 |
+
|
| 491 |
+
thread_key = self._resolve_thread_id(thread)
|
| 492 |
+
|
| 493 |
+
try:
|
| 494 |
+
user_data = self.agent_state.get_user(thread_key)
|
| 495 |
+
request_context_enriched = {
|
| 496 |
+
**(context or {}),
|
| 497 |
+
|
| 498 |
+
}
|
| 499 |
+
except Exception:
|
| 500 |
+
request_context_enriched = context or {}
|
| 501 |
+
|
| 502 |
+
session = SQLiteSession(thread_key)
|
| 503 |
+
agent_context = AgentContext(thread=thread, store=self.store, request_context=request_context_enriched)
|
| 504 |
+
|
| 505 |
+
combined_prompt = await self.prepare_conversation_context(thread_key, message_text, 'offline')
|
| 506 |
+
|
| 507 |
+
result_stream = Runner.run_streamed(self.information_agent, combined_prompt, context=agent_context, session=session)
|
| 508 |
+
async for event in stream_agent_response(agent_context, result_stream):
|
| 509 |
+
yield event
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
state_manager = UserStateManager(db_path=os.getenv("USER_STATE_DB_PATH", "user_state.db"))
|
| 513 |
+
support_server = deepseek_CustomerSupportServer(agent_state=state_manager)
|
| 514 |
+
kimi_support_server = kimi_CustomerSupportServer(agent_state=state_manager)
|
| 515 |
+
google_support_server = google_CustomerSupportServer(agent_state=state_manager)
|
| 516 |
+
|
| 517 |
+
app = FastAPI(title="ChatKit Customer Support API")
|
| 518 |
+
|
| 519 |
+
app.add_middleware(
|
| 520 |
+
CORSMiddleware,
|
| 521 |
+
allow_origins=["*"],
|
| 522 |
+
allow_methods=["*"],
|
| 523 |
+
allow_headers=["*"],
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def get_server() -> deepseek_CustomerSupportServer:
|
| 528 |
+
return support_server
|
| 529 |
+
|
| 530 |
+
def get_kimi_server() -> kimi_CustomerSupportServer:
|
| 531 |
+
return kimi_support_server
|
| 532 |
+
|
| 533 |
+
def get_google_server() -> google_CustomerSupportServer:
|
| 534 |
+
return google_support_server
|
| 535 |
+
|
| 536 |
+
@app.post("/deepseek/support/chatkit")
|
| 537 |
+
async def chatkit_endpoint(
|
| 538 |
+
request: Request, server: deepseek_CustomerSupportServer = Depends(get_server)
|
| 539 |
+
) -> Response:
|
| 540 |
+
payload = await request.body()
|
| 541 |
+
result = await server.process(payload, {"request": request})
|
| 542 |
+
if isinstance(result, StreamingResult):
|
| 543 |
+
return StreamingResponse(result, media_type="text/event-stream")
|
| 544 |
+
if hasattr(result, "json"):
|
| 545 |
+
return Response(content=result.json, media_type="application/json")
|
| 546 |
+
return JSONResponse(result)
|
| 547 |
+
|
| 548 |
+
@app.post("/kimi/support/chatkit")
|
| 549 |
+
async def chatkit_endpoint(
|
| 550 |
+
request: Request, server: kimi_CustomerSupportServer = Depends(get_kimi_server)
|
| 551 |
+
) -> Response:
|
| 552 |
+
payload = await request.body()
|
| 553 |
+
result = await server.process(payload, {"request": request})
|
| 554 |
+
if isinstance(result, StreamingResult):
|
| 555 |
+
return StreamingResponse(result, media_type="text/event-stream")
|
| 556 |
+
if hasattr(result, "json"):
|
| 557 |
+
return Response(content=result.json, media_type="application/json")
|
| 558 |
+
return JSONResponse(result)
|
| 559 |
+
@app.post("/google/support/chatkit")
|
| 560 |
+
async def chatkit_endpoint(
|
| 561 |
+
request: Request, server: google_CustomerSupportServer = Depends(get_google_server)
|
| 562 |
+
) -> Response:
|
| 563 |
+
payload = await request.body()
|
| 564 |
+
result = await server.process(payload, {"request": request})
|
| 565 |
+
if isinstance(result, StreamingResult):
|
| 566 |
+
return StreamingResponse(result, media_type="text/event-stream")
|
| 567 |
+
if hasattr(result, "json"):
|
| 568 |
+
return Response(content=result.json, media_type="application/json")
|
| 569 |
+
return JSONResponse(result)
|
| 570 |
+
|
| 571 |
+
@app.post("/api/chat/debug")
|
| 572 |
+
async def chat_debug(request: Request):
|
| 573 |
+
body = await request.body()
|
| 574 |
+
print("RAW BODY RECEIVED:", body)
|
| 575 |
+
return {"received": body.decode()}
|
| 576 |
+
def _thread_param(thread_id: str | None) -> str:
|
| 577 |
+
return thread_id or DEFAULT_THREAD_ID
|
| 578 |
+
|
| 579 |
+
@app.get("/deepseek/support/customer")
|
| 580 |
+
async def deepseek_customer_snapshot(
|
| 581 |
+
thread_id: str | None = Query(None, description="ChatKit thread identifier"),
|
| 582 |
+
server: deepseek_CustomerSupportServer = Depends(get_server),
|
| 583 |
+
) -> dict[str, Any]:
|
| 584 |
+
key = _thread_param(thread_id)
|
| 585 |
+
data = server.agent_state.to_dict(key)
|
| 586 |
+
print("data", data)
|
| 587 |
+
return {"customer": data}
|
| 588 |
+
|
| 589 |
+
@app.get("/kimi/support/customer")
|
| 590 |
+
async def kimi_customer_snapshot(
|
| 591 |
+
thread_id: str | None = Query(None, description="ChatKit thread identifier"),
|
| 592 |
+
server: kimi_CustomerSupportServer = Depends(get_kimi_server),
|
| 593 |
+
) -> dict[str, Any]:
|
| 594 |
+
key = _thread_param(thread_id)
|
| 595 |
+
data = server.agent_state.to_dict(key)
|
| 596 |
+
print("data", data)
|
| 597 |
+
return {"customer": data}
|
| 598 |
+
|
| 599 |
+
@app.get("/google/support/customer")
|
| 600 |
+
async def google_customer_snapshot(
|
| 601 |
+
thread_id: str | None = Query(None, description="ChatKit thread identifier"),
|
| 602 |
+
server: google_CustomerSupportServer = Depends(get_google_server),
|
| 603 |
+
) -> dict[str, Any]:
|
| 604 |
+
key = _thread_param(thread_id)
|
| 605 |
+
data = server.agent_state.to_dict(key)
|
| 606 |
+
print("data", data)
|
| 607 |
+
return {"customer": data}
|
| 608 |
+
|
| 609 |
+
@app.get("/support/customer")
|
| 610 |
+
async def customer_snapshot(
|
| 611 |
+
thread_id: str | None = Query(None, description="ChatKit thread identifier"),
|
| 612 |
+
server: google_CustomerSupportServer = Depends(get_google_server),
|
| 613 |
+
) -> dict[str, Any]:
|
| 614 |
+
key = _thread_param(thread_id)
|
| 615 |
+
data = server.agent_state.to_dict(key)
|
| 616 |
+
print("data", data)
|
| 617 |
+
return {"customer": data}
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
@app.post("/support/transcribe")
|
| 623 |
+
async def transcribe_audio(file: UploadFile = File(...)):
|
| 624 |
+
"""Transcribe audio using Groq Whisper model"""
|
| 625 |
+
import tempfile
|
| 626 |
+
import os
|
| 627 |
+
from groq import Groq
|
| 628 |
+
|
| 629 |
+
try:
|
| 630 |
+
# Initialize Groq client with API key from environment
|
| 631 |
+
groq_api_key = os.getenv("GROQ_API_KEY")
|
| 632 |
+
if not groq_api_key:
|
| 633 |
+
raise HTTPException(status_code=500, detail="GROQ_API_KEY not found in environment")
|
| 634 |
+
|
| 635 |
+
client = Groq(api_key=groq_api_key)
|
| 636 |
+
|
| 637 |
+
# Read audio file
|
| 638 |
+
audio_data = await file.read()
|
| 639 |
+
|
| 640 |
+
# Create temporary file with original extension or default to .webm
|
| 641 |
+
file_extension = os.path.splitext(file.filename)[1] if file.filename else ".webm"
|
| 642 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
|
| 643 |
+
temp_file.write(audio_data)
|
| 644 |
+
temp_file_path = temp_file.name
|
| 645 |
+
|
| 646 |
+
# Transcribe with Groq Whisper
|
| 647 |
+
with open(temp_file_path, "rb") as audio_file:
|
| 648 |
+
transcription = client.audio.transcriptions.create(
|
| 649 |
+
file=audio_file,
|
| 650 |
+
model="whisper-large-v3-turbo",
|
| 651 |
+
response_format="verbose_json",
|
| 652 |
+
timestamp_granularities=["word", "segment"],
|
| 653 |
+
language="en",
|
| 654 |
+
temperature=0.0
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
# Cleanup temporary file
|
| 658 |
+
os.unlink(temp_file_path)
|
| 659 |
+
|
| 660 |
+
# Return transcription text and full details
|
| 661 |
+
return {
|
| 662 |
+
"text": transcription.text,
|
| 663 |
+
"details": json.loads(json.dumps(transcription, default=str))
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
except Exception as e:
|
| 667 |
+
# Cleanup temp file if it exists
|
| 668 |
+
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
|
| 669 |
+
os.unlink(temp_file_path)
|
| 670 |
+
|
| 671 |
+
print(f"❌ Transcription error: {str(e)}")
|
| 672 |
+
traceback.print_exc()
|
| 673 |
+
raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
|
| 674 |
+
@app.post("/deepseek/support/transcribe")
|
| 675 |
+
async def deepseek_transcribe_audio(file: UploadFile = File(...)):
|
| 676 |
+
"""Transcribe audio using Groq Whisper model"""
|
| 677 |
+
import tempfile
|
| 678 |
+
import os
|
| 679 |
+
from groq import Groq
|
| 680 |
+
|
| 681 |
+
try:
|
| 682 |
+
# Initialize Groq client with API key from environment
|
| 683 |
+
groq_api_key = os.getenv("GROQ_API_KEY")
|
| 684 |
+
if not groq_api_key:
|
| 685 |
+
raise HTTPException(status_code=500, detail="GROQ_API_KEY not found in environment")
|
| 686 |
+
|
| 687 |
+
client = Groq(api_key=groq_api_key)
|
| 688 |
+
|
| 689 |
+
# Read audio file
|
| 690 |
+
audio_data = await file.read()
|
| 691 |
+
|
| 692 |
+
# Create temporary file with original extension or default to .webm
|
| 693 |
+
file_extension = os.path.splitext(file.filename)[1] if file.filename else ".webm"
|
| 694 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
|
| 695 |
+
temp_file.write(audio_data)
|
| 696 |
+
temp_file_path = temp_file.name
|
| 697 |
+
|
| 698 |
+
# Transcribe with Groq Whisper
|
| 699 |
+
with open(temp_file_path, "rb") as audio_file:
|
| 700 |
+
transcription = client.audio.transcriptions.create(
|
| 701 |
+
file=audio_file,
|
| 702 |
+
model="whisper-large-v3-turbo",
|
| 703 |
+
response_format="verbose_json",
|
| 704 |
+
timestamp_granularities=["word", "segment"],
|
| 705 |
+
language="en",
|
| 706 |
+
temperature=0.0
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
# Cleanup temporary file
|
| 710 |
+
os.unlink(temp_file_path)
|
| 711 |
+
|
| 712 |
+
# Return transcription text and full details
|
| 713 |
+
return {
|
| 714 |
+
"text": transcription.text,
|
| 715 |
+
"details": json.loads(json.dumps(transcription, default=str))
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
except Exception as e:
|
| 719 |
+
# Cleanup temp file if it exists
|
| 720 |
+
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
|
| 721 |
+
os.unlink(temp_file_path)
|
| 722 |
+
|
| 723 |
+
print(f"❌ Transcription error: {str(e)}")
|
| 724 |
+
traceback.print_exc()
|
| 725 |
+
raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
|
| 726 |
+
|
| 727 |
+
@app.post("/google/support/transcribe")
|
| 728 |
+
async def google_transcribe_audio(file: UploadFile = File(...)):
|
| 729 |
+
"""Transcribe audio using Groq Whisper model"""
|
| 730 |
+
import tempfile
|
| 731 |
+
import os
|
| 732 |
+
from groq import Groq
|
| 733 |
+
|
| 734 |
+
try:
|
| 735 |
+
# Initialize Groq client with API key from environment
|
| 736 |
+
groq_api_key = os.getenv("GROQ_API_KEY")
|
| 737 |
+
if not groq_api_key:
|
| 738 |
+
raise HTTPException(status_code=500, detail="GROQ_API_KEY not found in environment")
|
| 739 |
+
|
| 740 |
+
client = Groq(api_key=groq_api_key)
|
| 741 |
+
|
| 742 |
+
# Read audio file
|
| 743 |
+
audio_data = await file.read()
|
| 744 |
+
|
| 745 |
+
# Create temporary file with original extension or default to .webm
|
| 746 |
+
file_extension = os.path.splitext(file.filename)[1] if file.filename else ".webm"
|
| 747 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
|
| 748 |
+
temp_file.write(audio_data)
|
| 749 |
+
temp_file_path = temp_file.name
|
| 750 |
+
|
| 751 |
+
# Transcribe with Groq Whisper
|
| 752 |
+
with open(temp_file_path, "rb") as audio_file:
|
| 753 |
+
transcription = client.audio.transcriptions.create(
|
| 754 |
+
file=audio_file,
|
| 755 |
+
model="whisper-large-v3-turbo",
|
| 756 |
+
response_format="verbose_json",
|
| 757 |
+
timestamp_granularities=["word", "segment"],
|
| 758 |
+
language="en",
|
| 759 |
+
temperature=0.0
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
# Cleanup temporary file
|
| 763 |
+
os.unlink(temp_file_path)
|
| 764 |
+
|
| 765 |
+
# Return transcription text and full details
|
| 766 |
+
return {
|
| 767 |
+
"text": transcription.text,
|
| 768 |
+
"details": json.loads(json.dumps(transcription, default=str))
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
except Exception as e:
|
| 772 |
+
# Cleanup temp file if it exists
|
| 773 |
+
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
|
| 774 |
+
os.unlink(temp_file_path)
|
| 775 |
+
|
| 776 |
+
print(f"❌ Transcription error: {str(e)}")
|
| 777 |
+
traceback.print_exc()
|
| 778 |
+
raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
|
| 779 |
+
|
| 780 |
+
@app.post("/kimi/support/transcribe")
|
| 781 |
+
async def kimi_transcribe_audio(file: UploadFile = File(...)):
|
| 782 |
+
"""Transcribe audio using Groq Whisper model"""
|
| 783 |
+
import tempfile
|
| 784 |
+
import os
|
| 785 |
+
from groq import Groq
|
| 786 |
+
|
| 787 |
+
try:
|
| 788 |
+
# Initialize Groq client with API key from environment
|
| 789 |
+
groq_api_key = os.getenv("GROQ_API_KEY")
|
| 790 |
+
if not groq_api_key:
|
| 791 |
+
raise HTTPException(status_code=500, detail="GROQ_API_KEY not found in environment")
|
| 792 |
+
|
| 793 |
+
client = Groq(api_key=groq_api_key)
|
| 794 |
+
|
| 795 |
+
# Read audio file
|
| 796 |
+
audio_data = await file.read()
|
| 797 |
+
|
| 798 |
+
# Create temporary file with original extension or default to .webm
|
| 799 |
+
file_extension = os.path.splitext(file.filename)[1] if file.filename else ".webm"
|
| 800 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
|
| 801 |
+
temp_file.write(audio_data)
|
| 802 |
+
temp_file_path = temp_file.name
|
| 803 |
+
|
| 804 |
+
# Transcribe with Groq Whisper
|
| 805 |
+
with open(temp_file_path, "rb") as audio_file:
|
| 806 |
+
transcription = client.audio.transcriptions.create(
|
| 807 |
+
file=audio_file,
|
| 808 |
+
model="whisper-large-v3-turbo",
|
| 809 |
+
response_format="verbose_json",
|
| 810 |
+
timestamp_granularities=["word", "segment"],
|
| 811 |
+
language="en",
|
| 812 |
+
temperature=0.0
|
| 813 |
+
)
|
| 814 |
+
|
| 815 |
+
# Cleanup temporary file
|
| 816 |
+
os.unlink(temp_file_path)
|
| 817 |
+
|
| 818 |
+
# Return transcription text and full details
|
| 819 |
+
return {
|
| 820 |
+
"text": transcription.text,
|
| 821 |
+
"details": json.loads(json.dumps(transcription, default=str))
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
except Exception as e:
|
| 825 |
+
# Cleanup temp file if it exists
|
| 826 |
+
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
|
| 827 |
+
os.unlink(temp_file_path)
|
| 828 |
+
|
| 829 |
+
print(f"❌ Transcription error: {str(e)}")
|
| 830 |
+
traceback.print_exc()
|
| 831 |
+
raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
@app.get("/deepseek/support/threads/{thread_id}/messages")
|
| 835 |
+
async def deepseek_get_thread_messages(
|
| 836 |
+
thread_id: str,
|
| 837 |
+
server: deepseek_CustomerSupportServer = Depends(get_server)
|
| 838 |
+
):
|
| 839 |
+
"""Get last 10 messages for a specific thread."""
|
| 840 |
+
try:
|
| 841 |
+
# Get all items from the thread
|
| 842 |
+
items = server.store._items(thread_id)
|
| 843 |
+
|
| 844 |
+
# Filter to only UserMessageItem and AssistantMessageItem
|
| 845 |
+
messages = []
|
| 846 |
+
for item in items:
|
| 847 |
+
if isinstance(item, (UserMessageItem, AssistantMessageItem)):
|
| 848 |
+
message_dict = item.model_dump()
|
| 849 |
+
|
| 850 |
+
# Ensure created_at is a string
|
| 851 |
+
if hasattr(message_dict.get('created_at'), 'isoformat'):
|
| 852 |
+
message_dict['created_at'] = message_dict['created_at'].isoformat()
|
| 853 |
+
elif message_dict.get('created_at'):
|
| 854 |
+
message_dict['created_at'] = str(message_dict['created_at'])
|
| 855 |
+
|
| 856 |
+
messages.append(message_dict)
|
| 857 |
+
|
| 858 |
+
# Get last 10 messages
|
| 859 |
+
last_10_messages = messages[-10:] if len(messages) > 10 else messages
|
| 860 |
+
|
| 861 |
+
return {
|
| 862 |
+
"thread_id": thread_id,
|
| 863 |
+
"total_message_count": len(messages),
|
| 864 |
+
"returned_message_count": len(last_10_messages),
|
| 865 |
+
"messages": last_10_messages
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
except Exception as e:
|
| 869 |
+
print(f"❌ Error fetching messages for thread {thread_id}: {e}")
|
| 870 |
+
traceback.print_exc()
|
| 871 |
+
raise HTTPException(status_code=500, detail=f"Failed to fetch messages: {str(e)}")
|
| 872 |
+
|
| 873 |
+
@app.get("/support/threads/{thread_id}/messages")
|
| 874 |
+
async def get_thread_messages(
|
| 875 |
+
thread_id: str,
|
| 876 |
+
server: deepseek_CustomerSupportServer = Depends(get_server)
|
| 877 |
+
):
|
| 878 |
+
"""Get last 10 messages for a specific thread."""
|
| 879 |
+
try:
|
| 880 |
+
# Get all items from the thread
|
| 881 |
+
items = server.store._items(thread_id)
|
| 882 |
+
|
| 883 |
+
# Filter to only UserMessageItem and AssistantMessageItem
|
| 884 |
+
messages = []
|
| 885 |
+
for item in items:
|
| 886 |
+
if isinstance(item, (UserMessageItem, AssistantMessageItem)):
|
| 887 |
+
message_dict = item.model_dump()
|
| 888 |
+
|
| 889 |
+
# Ensure created_at is a string
|
| 890 |
+
if hasattr(message_dict.get('created_at'), 'isoformat'):
|
| 891 |
+
message_dict['created_at'] = message_dict['created_at'].isoformat()
|
| 892 |
+
elif message_dict.get('created_at'):
|
| 893 |
+
message_dict['created_at'] = str(message_dict['created_at'])
|
| 894 |
+
|
| 895 |
+
messages.append(message_dict)
|
| 896 |
+
|
| 897 |
+
# Get last 10 messages
|
| 898 |
+
last_10_messages = messages[-10:] if len(messages) > 10 else messages
|
| 899 |
+
|
| 900 |
+
return {
|
| 901 |
+
"thread_id": thread_id,
|
| 902 |
+
"total_message_count": len(messages),
|
| 903 |
+
"returned_message_count": len(last_10_messages),
|
| 904 |
+
"messages": last_10_messages
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
except Exception as e:
|
| 908 |
+
print(f"❌ Error fetching messages for thread {thread_id}: {e}")
|
| 909 |
+
traceback.print_exc()
|
| 910 |
+
raise HTTPException(status_code=500, detail=f"Failed to fetch messages: {str(e)}")
|
| 911 |
+
|
| 912 |
+
@app.get("/kimi/support/threads/{thread_id}/messages")
|
| 913 |
+
async def kimi_get_thread_messages(
|
| 914 |
+
thread_id: str,
|
| 915 |
+
server:kimi_CustomerSupportServer = Depends(get_kimi_server)
|
| 916 |
+
):
|
| 917 |
+
"""Get last 10 messages for a specific thread."""
|
| 918 |
+
try:
|
| 919 |
+
# Get all items from the thread
|
| 920 |
+
items = server.store._items(thread_id)
|
| 921 |
+
|
| 922 |
+
# Filter to only UserMessageItem and AssistantMessageItem
|
| 923 |
+
messages = []
|
| 924 |
+
for item in items:
|
| 925 |
+
if isinstance(item, (UserMessageItem, AssistantMessageItem)):
|
| 926 |
+
message_dict = item.model_dump()
|
| 927 |
+
|
| 928 |
+
# Ensure created_at is a string
|
| 929 |
+
if hasattr(message_dict.get('created_at'), 'isoformat'):
|
| 930 |
+
message_dict['created_at'] = message_dict['created_at'].isoformat()
|
| 931 |
+
elif message_dict.get('created_at'):
|
| 932 |
+
message_dict['created_at'] = str(message_dict['created_at'])
|
| 933 |
+
|
| 934 |
+
messages.append(message_dict)
|
| 935 |
+
|
| 936 |
+
# Get last 10 messages
|
| 937 |
+
last_10_messages = messages[-10:] if len(messages) > 10 else messages
|
| 938 |
+
|
| 939 |
+
return {
|
| 940 |
+
"thread_id": thread_id,
|
| 941 |
+
"total_message_count": len(messages),
|
| 942 |
+
"returned_message_count": len(last_10_messages),
|
| 943 |
+
"messages": last_10_messages
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
except Exception as e:
|
| 947 |
+
print(f"❌ Error fetching messages for thread {thread_id}: {e}")
|
| 948 |
+
traceback.print_exc()
|
| 949 |
+
raise HTTPException(status_code=500, detail=f"Failed to fetch messages: {str(e)}")
|
| 950 |
+
|
| 951 |
+
@app.get("/google/support/threads/{thread_id}/messages")
|
| 952 |
+
async def google_get_thread_messages(
|
| 953 |
+
thread_id: str,
|
| 954 |
+
server:google_CustomerSupportServer = Depends(get_google_server)
|
| 955 |
+
):
|
| 956 |
+
"""Get last 10 messages for a specific thread."""
|
| 957 |
+
try:
|
| 958 |
+
# Get all items from the thread
|
| 959 |
+
items = server.store._items(thread_id)
|
| 960 |
+
|
| 961 |
+
# Filter to only UserMessageItem and AssistantMessageItem
|
| 962 |
+
messages = []
|
| 963 |
+
for item in items:
|
| 964 |
+
if isinstance(item, (UserMessageItem, AssistantMessageItem)):
|
| 965 |
+
message_dict = item.model_dump()
|
| 966 |
+
|
| 967 |
+
# Ensure created_at is a string
|
| 968 |
+
if hasattr(message_dict.get('created_at'), 'isoformat'):
|
| 969 |
+
message_dict['created_at'] = message_dict['created_at'].isoformat()
|
| 970 |
+
elif message_dict.get('created_at'):
|
| 971 |
+
message_dict['created_at'] = str(message_dict['created_at'])
|
| 972 |
+
|
| 973 |
+
messages.append(message_dict)
|
| 974 |
+
|
| 975 |
+
# Get last 10 messages
|
| 976 |
+
last_10_messages = messages[-10:] if len(messages) > 10 else messages
|
| 977 |
+
|
| 978 |
+
return {
|
| 979 |
+
"thread_id": thread_id,
|
| 980 |
+
"total_message_count": len(messages),
|
| 981 |
+
"returned_message_count": len(last_10_messages),
|
| 982 |
+
"messages": last_10_messages
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
except Exception as e:
|
| 986 |
+
print(f"❌ Error fetching messages for thread {thread_id}: {e}")
|
| 987 |
+
traceback.print_exc()
|
| 988 |
+
raise HTTPException(status_code=500, detail=f"Failed to fetch messages: {str(e)}")
|
| 989 |
+
|
| 990 |
+
@app.post("/support/customer")
|
| 991 |
+
async def customer_update(
|
| 992 |
+
request: Request,
|
| 993 |
+
thread_id: str | None = Query(None, description="ChatKit thread identifier"),
|
| 994 |
+
server: deepseek_CustomerSupportServer = Depends(get_server),
|
| 995 |
+
) -> dict[str, str]:
|
| 996 |
+
try:
|
| 997 |
+
payload = await request.json()
|
| 998 |
+
except Exception:
|
| 999 |
+
payload = {}
|
| 1000 |
+
key = _thread_param(thread_id)
|
| 1001 |
+
try:
|
| 1002 |
+
print(f"payload: {payload}")
|
| 1003 |
+
name = (payload.get("name") or "").strip()
|
| 1004 |
+
email = (payload.get("email") or "").strip()
|
| 1005 |
+
phone = (payload.get("phone") or "").strip()
|
| 1006 |
+
company_name = (payload.get("company_name") or payload.get("company") or "").strip()
|
| 1007 |
+
timezone = (payload.get("timeZone") or payload.get("timezone") or "").strip()
|
| 1008 |
+
server.agent_state.set_customer_info(
|
| 1009 |
+
key,
|
| 1010 |
+
name=name or None,
|
| 1011 |
+
email=email or None,
|
| 1012 |
+
phone=phone or None,
|
| 1013 |
+
company_name=company_name or None,
|
| 1014 |
+
)
|
| 1015 |
+
if timezone:
|
| 1016 |
+
server.agent_state.set_timezone(key, timezone)
|
| 1017 |
+
|
| 1018 |
+
# 🔥 Preload vector index for company to avoid 19s delay on first message
|
| 1019 |
+
|
| 1020 |
+
return {"status": "ok"}
|
| 1021 |
+
except Exception:
|
| 1022 |
+
return {"status": "error"}
|
| 1023 |
+
|
| 1024 |
+
@app.get("/support/health")
|
| 1025 |
+
async def health_check() -> dict[str, str]:
|
| 1026 |
+
return {"status": "healthy"}
|
app/memory_store.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
|
| 7 |
+
from chatkit.store import NotFoundError, Store
|
| 8 |
+
from chatkit.types import Attachment, Page, Thread, ThreadItem, ThreadMetadata
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class _ThreadState:
|
| 13 |
+
thread: ThreadMetadata
|
| 14 |
+
items: List[ThreadItem]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class MemoryStore(Store[dict[str, Any]]):
|
| 18 |
+
"""Simple in-memory store compatible with the ChatKit server interface."""
|
| 19 |
+
|
| 20 |
+
def __init__(self) -> None:
|
| 21 |
+
self._threads: Dict[str, _ThreadState] = {}
|
| 22 |
+
# Attachments intentionally unsupported; use a real store that enforces auth.
|
| 23 |
+
|
| 24 |
+
@staticmethod
|
| 25 |
+
def _coerce_thread_metadata(thread: ThreadMetadata | Thread) -> ThreadMetadata:
|
| 26 |
+
"""Return thread metadata without any embedded items (openai-chatkit>=1.0)."""
|
| 27 |
+
has_items = isinstance(thread, Thread) or "items" in getattr(
|
| 28 |
+
thread, "model_fields_set", set()
|
| 29 |
+
)
|
| 30 |
+
if not has_items:
|
| 31 |
+
return thread.model_copy(deep=True)
|
| 32 |
+
|
| 33 |
+
data = thread.model_dump()
|
| 34 |
+
data.pop("items", None)
|
| 35 |
+
return ThreadMetadata(**data).model_copy(deep=True)
|
| 36 |
+
|
| 37 |
+
# -- Thread metadata -------------------------------------------------
|
| 38 |
+
async def load_thread(self, thread_id: str, context: dict[str, Any]) -> ThreadMetadata:
|
| 39 |
+
state = self._threads.get(thread_id)
|
| 40 |
+
if not state:
|
| 41 |
+
raise NotFoundError(f"Thread {thread_id} not found")
|
| 42 |
+
return self._coerce_thread_metadata(state.thread)
|
| 43 |
+
|
| 44 |
+
async def save_thread(self, thread: ThreadMetadata, context: dict[str, Any]) -> None:
|
| 45 |
+
metadata = self._coerce_thread_metadata(thread)
|
| 46 |
+
state = self._threads.get(thread.id)
|
| 47 |
+
if state:
|
| 48 |
+
state.thread = metadata
|
| 49 |
+
else:
|
| 50 |
+
self._threads[thread.id] = _ThreadState(
|
| 51 |
+
thread=metadata,
|
| 52 |
+
items=[],
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
async def load_threads(
|
| 56 |
+
self,
|
| 57 |
+
limit: int,
|
| 58 |
+
after: str | None,
|
| 59 |
+
order: str,
|
| 60 |
+
context: dict[str, Any],
|
| 61 |
+
) -> Page[ThreadMetadata]:
|
| 62 |
+
threads = sorted(
|
| 63 |
+
(self._coerce_thread_metadata(state.thread) for state in self._threads.values()),
|
| 64 |
+
key=lambda t: t.created_at or datetime.min,
|
| 65 |
+
reverse=(order == "desc"),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if after:
|
| 69 |
+
index_map = {thread.id: idx for idx, thread in enumerate(threads)}
|
| 70 |
+
start = index_map.get(after, -1) + 1
|
| 71 |
+
else:
|
| 72 |
+
start = 0
|
| 73 |
+
|
| 74 |
+
slice_threads = threads[start : start + limit + 1]
|
| 75 |
+
has_more = len(slice_threads) > limit
|
| 76 |
+
slice_threads = slice_threads[:limit]
|
| 77 |
+
next_after = slice_threads[-1].id if has_more and slice_threads else None
|
| 78 |
+
return Page(
|
| 79 |
+
data=slice_threads,
|
| 80 |
+
has_more=has_more,
|
| 81 |
+
after=next_after,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
async def delete_thread(self, thread_id: str, context: dict[str, Any]) -> None:
|
| 85 |
+
self._threads.pop(thread_id, None)
|
| 86 |
+
|
| 87 |
+
# -- Thread items ----------------------------------------------------
|
| 88 |
+
def _items(self, thread_id: str) -> List[ThreadItem]:
|
| 89 |
+
state = self._threads.get(thread_id)
|
| 90 |
+
if state is None:
|
| 91 |
+
state = _ThreadState(
|
| 92 |
+
thread=ThreadMetadata(id=thread_id, created_at=datetime.utcnow()),
|
| 93 |
+
items=[],
|
| 94 |
+
)
|
| 95 |
+
self._threads[thread_id] = state
|
| 96 |
+
return state.items
|
| 97 |
+
|
| 98 |
+
async def load_thread_items(
|
| 99 |
+
self,
|
| 100 |
+
thread_id: str,
|
| 101 |
+
after: str | None,
|
| 102 |
+
limit: int,
|
| 103 |
+
order: str,
|
| 104 |
+
context: dict[str, Any],
|
| 105 |
+
) -> Page[ThreadItem]:
|
| 106 |
+
items = [item.model_copy(deep=True) for item in self._items(thread_id)]
|
| 107 |
+
items.sort(
|
| 108 |
+
key=lambda item: getattr(item, "created_at", datetime.utcnow()),
|
| 109 |
+
reverse=(order == "desc"),
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if after:
|
| 113 |
+
index_map = {item.id: idx for idx, item in enumerate(items)}
|
| 114 |
+
start = index_map.get(after, -1) + 1
|
| 115 |
+
else:
|
| 116 |
+
start = 0
|
| 117 |
+
|
| 118 |
+
slice_items = items[start : start + limit + 1]
|
| 119 |
+
has_more = len(slice_items) > limit
|
| 120 |
+
slice_items = slice_items[:limit]
|
| 121 |
+
next_after = slice_items[-1].id if has_more and slice_items else None
|
| 122 |
+
return Page(data=slice_items, has_more=has_more, after=next_after)
|
| 123 |
+
|
| 124 |
+
async def add_thread_item(
|
| 125 |
+
self, thread_id: str, item: ThreadItem, context: dict[str, Any]
|
| 126 |
+
) -> None:
|
| 127 |
+
print("Adding item", item)
|
| 128 |
+
self._items(thread_id).append(item.model_copy(deep=True))
|
| 129 |
+
|
| 130 |
+
async def save_item(self, thread_id: str, item: ThreadItem, context: dict[str, Any]) -> None:
|
| 131 |
+
items = self._items(thread_id)
|
| 132 |
+
for idx, existing in enumerate(items):
|
| 133 |
+
if existing.id == item.id:
|
| 134 |
+
items[idx] = item.model_copy(deep=True)
|
| 135 |
+
return
|
| 136 |
+
items.append(item.model_copy(deep=True))
|
| 137 |
+
|
| 138 |
+
async def load_item(self, thread_id: str, item_id: str, context: dict[str, Any]) -> ThreadItem:
|
| 139 |
+
for item in self._items(thread_id):
|
| 140 |
+
if item.id == item_id:
|
| 141 |
+
return item.model_copy(deep=True)
|
| 142 |
+
raise NotFoundError(f"Item {item_id} not found")
|
| 143 |
+
|
| 144 |
+
async def delete_thread_item(
|
| 145 |
+
self, thread_id: str, item_id: str, context: dict[str, Any]
|
| 146 |
+
) -> None:
|
| 147 |
+
items = self._items(thread_id)
|
| 148 |
+
self._threads[thread_id].items = [item for item in items if item.id != item_id]
|
| 149 |
+
|
| 150 |
+
# -- Files -----------------------------------------------------------
|
| 151 |
+
# These methods are not currently used but required to be compatible with the Store interface.
|
| 152 |
+
|
| 153 |
+
async def save_attachment(
|
| 154 |
+
self,
|
| 155 |
+
attachment: Attachment,
|
| 156 |
+
context: dict[str, Any],
|
| 157 |
+
) -> None:
|
| 158 |
+
raise NotImplementedError(
|
| 159 |
+
"MemoryStore does not persist attachments. Provide a Store implementation "
|
| 160 |
+
"that enforces authentication and authorization before enabling uploads."
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
async def load_attachment(
|
| 164 |
+
self,
|
| 165 |
+
attachment_id: str,
|
| 166 |
+
context: dict[str, Any],
|
| 167 |
+
) -> Attachment:
|
| 168 |
+
raise NotImplementedError(
|
| 169 |
+
"MemoryStore does not load attachments. Provide a Store implementation "
|
| 170 |
+
"that enforces authentication and authorization before enabling uploads."
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
async def delete_attachment(self, attachment_id: str, context: dict[str, Any]) -> None:
|
| 174 |
+
raise NotImplementedError(
|
| 175 |
+
"MemoryStore does not delete attachments because they are never stored."
|
| 176 |
+
)
|
app/scrapping.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai_tools import ScrapeWebsiteTool
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# List of pages you want to scrape
|
| 5 |
+
pages = [
|
| 6 |
+
"https://www.sunmarke.com/",
|
| 7 |
+
]
|
| 8 |
+
|
| 9 |
+
all_text = []
|
| 10 |
+
|
| 11 |
+
for url in pages:
|
| 12 |
+
tool = ScrapeWebsiteTool(website_url=url)
|
| 13 |
+
content = tool.run()
|
| 14 |
+
all_text.append(content)
|
| 15 |
+
|
| 16 |
+
# Remove duplicates
|
| 17 |
+
all_text = list(set(all_text))
|
| 18 |
+
|
| 19 |
+
# Combine into one string
|
| 20 |
+
text = "\n\n".join(all_text)
|
| 21 |
+
|
| 22 |
+
# Save to file
|
| 23 |
+
output_folder = "website"
|
| 24 |
+
os.makedirs(output_folder, exist_ok=True)
|
| 25 |
+
file_path = os.path.join(output_folder, "output.txt")
|
| 26 |
+
|
| 27 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 28 |
+
f.write("=== Output Start ===\n\n")
|
| 29 |
+
f.write(text.strip())
|
| 30 |
+
f.write("\n\n=== Output End ===\n")
|
| 31 |
+
|
| 32 |
+
print(f"Text saved to {file_path}")
|
app/sqlite_store.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
import threading
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Any, Dict, List
|
| 8 |
+
|
| 9 |
+
from chatkit.store import NotFoundError, Store
|
| 10 |
+
from chatkit.types import Attachment, Page, Thread, ThreadItem, ThreadMetadata
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SQLiteStore(Store[dict[str, Any]]):
|
| 14 |
+
"""Persistent SQLite-backed store compatible with the ChatKit server interface."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, db_path: str = "chatkit_threads.db") -> None:
|
| 17 |
+
self.db_path = db_path
|
| 18 |
+
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 19 |
+
self.conn.row_factory = sqlite3.Row
|
| 20 |
+
|
| 21 |
+
# 🚀 Performance optimizations
|
| 22 |
+
self.conn.execute("PRAGMA journal_mode=WAL") # Better concurrency
|
| 23 |
+
self.conn.execute("PRAGMA synchronous=NORMAL") # Faster writes
|
| 24 |
+
self.conn.execute("PRAGMA cache_size=10000") # Larger cache (10MB)
|
| 25 |
+
self.conn.execute("PRAGMA temp_store=MEMORY") # Temp tables in RAM
|
| 26 |
+
self.conn.execute("PRAGMA mmap_size=268435456") # 256MB memory-mapped I/O
|
| 27 |
+
self.conn.execute("PRAGMA page_size=4096") # Optimal page size
|
| 28 |
+
|
| 29 |
+
self._lock = threading.RLock() # Thread-safe locking
|
| 30 |
+
self._init_db()
|
| 31 |
+
|
| 32 |
+
def _init_db(self) -> None:
|
| 33 |
+
"""Initialize database tables."""
|
| 34 |
+
with self._lock:
|
| 35 |
+
cursor = self.conn.cursor()
|
| 36 |
+
|
| 37 |
+
# Threads table
|
| 38 |
+
cursor.execute("""
|
| 39 |
+
CREATE TABLE IF NOT EXISTS threads (
|
| 40 |
+
thread_id TEXT PRIMARY KEY,
|
| 41 |
+
metadata TEXT NOT NULL,
|
| 42 |
+
created_at TEXT NOT NULL
|
| 43 |
+
)
|
| 44 |
+
""")
|
| 45 |
+
|
| 46 |
+
# Thread items table with sequence number for guaranteed ordering
|
| 47 |
+
cursor.execute("""
|
| 48 |
+
CREATE TABLE IF NOT EXISTS thread_items (
|
| 49 |
+
item_id TEXT PRIMARY KEY,
|
| 50 |
+
thread_id TEXT NOT NULL,
|
| 51 |
+
item_data TEXT NOT NULL,
|
| 52 |
+
created_at TEXT NOT NULL,
|
| 53 |
+
sequence_num INTEGER NOT NULL,
|
| 54 |
+
FOREIGN KEY (thread_id) REFERENCES threads(thread_id) ON DELETE CASCADE
|
| 55 |
+
)
|
| 56 |
+
""")
|
| 57 |
+
|
| 58 |
+
# Create optimized indexes for fast retrieval
|
| 59 |
+
cursor.execute("""
|
| 60 |
+
CREATE INDEX IF NOT EXISTS idx_thread_items_thread_id
|
| 61 |
+
ON thread_items(thread_id)
|
| 62 |
+
""")
|
| 63 |
+
|
| 64 |
+
# 🚀 Composite index for ORDER BY queries - CRITICAL for performance!
|
| 65 |
+
cursor.execute("""
|
| 66 |
+
CREATE INDEX IF NOT EXISTS idx_thread_items_thread_sequence
|
| 67 |
+
ON thread_items(thread_id, sequence_num, created_at)
|
| 68 |
+
""")
|
| 69 |
+
|
| 70 |
+
# Index for created_at lookups
|
| 71 |
+
cursor.execute("""
|
| 72 |
+
CREATE INDEX IF NOT EXISTS idx_thread_items_created_at
|
| 73 |
+
ON thread_items(created_at)
|
| 74 |
+
""")
|
| 75 |
+
|
| 76 |
+
# Index for item_id lookups (primary key already indexed, but explicit is better)
|
| 77 |
+
cursor.execute("""
|
| 78 |
+
CREATE INDEX IF NOT EXISTS idx_thread_items_item_id
|
| 79 |
+
ON thread_items(item_id)
|
| 80 |
+
""")
|
| 81 |
+
|
| 82 |
+
# Analyze tables for query optimizer
|
| 83 |
+
cursor.execute("ANALYZE")
|
| 84 |
+
|
| 85 |
+
self.conn.commit()
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def _coerce_thread_metadata(thread: ThreadMetadata | Thread) -> ThreadMetadata:
|
| 89 |
+
"""Return thread metadata without any embedded items (openai-chatkit>=1.0)."""
|
| 90 |
+
has_items = isinstance(thread, Thread) or "items" in getattr(
|
| 91 |
+
thread, "model_fields_set", set()
|
| 92 |
+
)
|
| 93 |
+
if not has_items:
|
| 94 |
+
return thread.model_copy(deep=True)
|
| 95 |
+
|
| 96 |
+
data = thread.model_dump()
|
| 97 |
+
data.pop("items", None)
|
| 98 |
+
return ThreadMetadata(**data).model_copy(deep=True)
|
| 99 |
+
|
| 100 |
+
# -- Thread metadata -------------------------------------------------
|
| 101 |
+
async def load_thread(self, thread_id: str, context: dict[str, Any]) -> ThreadMetadata:
|
| 102 |
+
with self._lock:
|
| 103 |
+
cursor = self.conn.cursor()
|
| 104 |
+
cursor.execute(
|
| 105 |
+
"SELECT metadata FROM threads WHERE thread_id = ?",
|
| 106 |
+
(thread_id,)
|
| 107 |
+
)
|
| 108 |
+
row = cursor.fetchone()
|
| 109 |
+
if not row:
|
| 110 |
+
raise NotFoundError(f"Thread {thread_id} not found")
|
| 111 |
+
|
| 112 |
+
metadata_dict = json.loads(row["metadata"])
|
| 113 |
+
return ThreadMetadata(**metadata_dict)
|
| 114 |
+
|
| 115 |
+
async def save_thread(self, thread: ThreadMetadata, context: dict[str, Any]) -> None:
|
| 116 |
+
with self._lock:
|
| 117 |
+
metadata = self._coerce_thread_metadata(thread)
|
| 118 |
+
metadata_json = metadata.model_dump_json()
|
| 119 |
+
created_at = (metadata.created_at or datetime.utcnow()).isoformat()
|
| 120 |
+
|
| 121 |
+
cursor = self.conn.cursor()
|
| 122 |
+
cursor.execute(
|
| 123 |
+
"""
|
| 124 |
+
INSERT INTO threads (thread_id, metadata, created_at)
|
| 125 |
+
VALUES (?, ?, ?)
|
| 126 |
+
ON CONFLICT(thread_id) DO UPDATE SET
|
| 127 |
+
metadata = excluded.metadata
|
| 128 |
+
""",
|
| 129 |
+
(thread.id, metadata_json, created_at)
|
| 130 |
+
)
|
| 131 |
+
self.conn.commit()
|
| 132 |
+
|
| 133 |
+
async def load_threads(
|
| 134 |
+
self,
|
| 135 |
+
limit: int,
|
| 136 |
+
after: str | None,
|
| 137 |
+
order: str,
|
| 138 |
+
context: dict[str, Any],
|
| 139 |
+
) -> Page[ThreadMetadata]:
|
| 140 |
+
with self._lock:
|
| 141 |
+
cursor = self.conn.cursor()
|
| 142 |
+
|
| 143 |
+
order_clause = "DESC" if order == "desc" else "ASC"
|
| 144 |
+
|
| 145 |
+
if after:
|
| 146 |
+
# Get the created_at of the 'after' thread
|
| 147 |
+
cursor.execute(
|
| 148 |
+
"SELECT created_at FROM threads WHERE thread_id = ?",
|
| 149 |
+
(after,)
|
| 150 |
+
)
|
| 151 |
+
after_row = cursor.fetchone()
|
| 152 |
+
if after_row:
|
| 153 |
+
after_time = after_row["created_at"]
|
| 154 |
+
comparison = "<" if order == "desc" else ">"
|
| 155 |
+
cursor.execute(
|
| 156 |
+
f"""
|
| 157 |
+
SELECT metadata FROM threads
|
| 158 |
+
WHERE created_at {comparison} ?
|
| 159 |
+
ORDER BY created_at {order_clause}
|
| 160 |
+
LIMIT ?
|
| 161 |
+
""",
|
| 162 |
+
(after_time, limit + 1)
|
| 163 |
+
)
|
| 164 |
+
else:
|
| 165 |
+
cursor.execute(
|
| 166 |
+
f"""
|
| 167 |
+
SELECT metadata FROM threads
|
| 168 |
+
ORDER BY created_at {order_clause}
|
| 169 |
+
LIMIT ?
|
| 170 |
+
""",
|
| 171 |
+
(limit + 1,)
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
cursor.execute(
|
| 175 |
+
f"""
|
| 176 |
+
SELECT metadata FROM threads
|
| 177 |
+
ORDER BY created_at {order_clause}
|
| 178 |
+
LIMIT ?
|
| 179 |
+
""",
|
| 180 |
+
(limit + 1,)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
rows = cursor.fetchall()
|
| 184 |
+
threads = [ThreadMetadata(**json.loads(row["metadata"])) for row in rows]
|
| 185 |
+
|
| 186 |
+
has_more = len(threads) > limit
|
| 187 |
+
threads = threads[:limit]
|
| 188 |
+
next_after = threads[-1].id if has_more and threads else None
|
| 189 |
+
|
| 190 |
+
return Page(
|
| 191 |
+
data=threads,
|
| 192 |
+
has_more=has_more,
|
| 193 |
+
after=next_after,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
async def delete_thread(self, thread_id: str, context: dict[str, Any]) -> None:
|
| 197 |
+
with self._lock:
|
| 198 |
+
cursor = self.conn.cursor()
|
| 199 |
+
cursor.execute("DELETE FROM threads WHERE thread_id = ?", (thread_id,))
|
| 200 |
+
cursor.execute("DELETE FROM thread_items WHERE thread_id = ?", (thread_id,))
|
| 201 |
+
self.conn.commit()
|
| 202 |
+
|
| 203 |
+
# -- Thread items ----------------------------------------------------
|
| 204 |
+
def _get_next_sequence_num(self, thread_id: str) -> int:
|
| 205 |
+
"""Get the next sequence number for a thread."""
|
| 206 |
+
cursor = self.conn.cursor()
|
| 207 |
+
cursor.execute(
|
| 208 |
+
"SELECT MAX(sequence_num) as max_seq FROM thread_items WHERE thread_id = ?",
|
| 209 |
+
(thread_id,)
|
| 210 |
+
)
|
| 211 |
+
row = cursor.fetchone()
|
| 212 |
+
max_seq = row["max_seq"] if row and row["max_seq"] is not None else 0
|
| 213 |
+
return max_seq + 1
|
| 214 |
+
|
| 215 |
+
async def load_thread_items(
|
| 216 |
+
self,
|
| 217 |
+
thread_id: str,
|
| 218 |
+
after: str | None,
|
| 219 |
+
limit: int,
|
| 220 |
+
order: str,
|
| 221 |
+
context: dict[str, Any],
|
| 222 |
+
) -> Page[ThreadItem]:
|
| 223 |
+
with self._lock:
|
| 224 |
+
cursor = self.conn.cursor()
|
| 225 |
+
|
| 226 |
+
# Use sequence_num for reliable ordering, with created_at as secondary
|
| 227 |
+
order_clause = "DESC" if order == "desc" else "ASC"
|
| 228 |
+
|
| 229 |
+
if after:
|
| 230 |
+
# Get the sequence_num of the 'after' item
|
| 231 |
+
cursor.execute(
|
| 232 |
+
"SELECT sequence_num FROM thread_items WHERE item_id = ?",
|
| 233 |
+
(after,)
|
| 234 |
+
)
|
| 235 |
+
after_row = cursor.fetchone()
|
| 236 |
+
if after_row:
|
| 237 |
+
after_seq = after_row["sequence_num"]
|
| 238 |
+
comparison = "<" if order == "desc" else ">"
|
| 239 |
+
cursor.execute(
|
| 240 |
+
f"""
|
| 241 |
+
SELECT item_data FROM thread_items
|
| 242 |
+
WHERE thread_id = ? AND sequence_num {comparison} ?
|
| 243 |
+
ORDER BY sequence_num {order_clause}, created_at {order_clause}
|
| 244 |
+
LIMIT ?
|
| 245 |
+
""",
|
| 246 |
+
(thread_id, after_seq, limit + 1)
|
| 247 |
+
)
|
| 248 |
+
else:
|
| 249 |
+
cursor.execute(
|
| 250 |
+
f"""
|
| 251 |
+
SELECT item_data FROM thread_items
|
| 252 |
+
WHERE thread_id = ?
|
| 253 |
+
ORDER BY sequence_num {order_clause}, created_at {order_clause}
|
| 254 |
+
LIMIT ?
|
| 255 |
+
""",
|
| 256 |
+
(thread_id, limit + 1)
|
| 257 |
+
)
|
| 258 |
+
else:
|
| 259 |
+
cursor.execute(
|
| 260 |
+
f"""
|
| 261 |
+
SELECT item_data FROM thread_items
|
| 262 |
+
WHERE thread_id = ?
|
| 263 |
+
ORDER BY sequence_num {order_clause}, created_at {order_clause}
|
| 264 |
+
LIMIT ?
|
| 265 |
+
""",
|
| 266 |
+
(thread_id, limit + 1)
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
rows = cursor.fetchall()
|
| 270 |
+
items = []
|
| 271 |
+
for row in rows:
|
| 272 |
+
try:
|
| 273 |
+
item_dict = json.loads(row["item_data"])
|
| 274 |
+
item_type = item_dict.get("type")
|
| 275 |
+
|
| 276 |
+
# Import all available message types
|
| 277 |
+
from chatkit.types import (
|
| 278 |
+
UserMessageItem,
|
| 279 |
+
AssistantMessageItem,
|
| 280 |
+
ClientToolCallItem,
|
| 281 |
+
WorkflowItem,
|
| 282 |
+
WidgetItem,
|
| 283 |
+
TaskItem,
|
| 284 |
+
HiddenContextItem,
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Reconstruct based on type
|
| 288 |
+
if item_type == "user_message":
|
| 289 |
+
items.append(UserMessageItem(**item_dict))
|
| 290 |
+
elif item_type == "assistant_message":
|
| 291 |
+
items.append(AssistantMessageItem(**item_dict))
|
| 292 |
+
elif item_type == "client_tool_call":
|
| 293 |
+
items.append(ClientToolCallItem(**item_dict))
|
| 294 |
+
elif item_type == "workflow":
|
| 295 |
+
items.append(WorkflowItem(**item_dict))
|
| 296 |
+
elif item_type == "widget":
|
| 297 |
+
items.append(WidgetItem(**item_dict))
|
| 298 |
+
elif item_type == "task":
|
| 299 |
+
items.append(TaskItem(**item_dict))
|
| 300 |
+
elif item_type == "hidden_context_item":
|
| 301 |
+
items.append(HiddenContextItem(**item_dict))
|
| 302 |
+
else:
|
| 303 |
+
# Unknown type - log but continue
|
| 304 |
+
print(f"⚠️ Skipping unknown item type: {item_type}")
|
| 305 |
+
continue
|
| 306 |
+
|
| 307 |
+
except (ImportError, TypeError, KeyError, ValueError, Exception) as e:
|
| 308 |
+
# If reconstruction fails, skip this item
|
| 309 |
+
print(f"⚠️ Failed to reconstruct item: {e}")
|
| 310 |
+
continue
|
| 311 |
+
|
| 312 |
+
has_more = len(items) > limit
|
| 313 |
+
items = items[:limit]
|
| 314 |
+
next_after = items[-1].id if has_more and items else None
|
| 315 |
+
|
| 316 |
+
return Page(data=items, has_more=has_more, after=next_after)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
async def add_thread_item(
|
| 320 |
+
self, thread_id: str, item: ThreadItem, context: dict[str, Any]
|
| 321 |
+
) -> None:
|
| 322 |
+
with self._lock:
|
| 323 |
+
# FIX: ChatKit/ChatCompletionsModel produces "__fake_id__" which causes collisions.
|
| 324 |
+
# We enforce a unique ID only for items with actual content to ensure their persistence.
|
| 325 |
+
if item.id == "__fake_id__":
|
| 326 |
+
has_content = False
|
| 327 |
+
# Check if message has meaningful text content
|
| 328 |
+
if hasattr(item, 'content'):
|
| 329 |
+
for part in item.content:
|
| 330 |
+
if getattr(part, 'type', '') == 'output_text' and getattr(part, 'text', '').strip():
|
| 331 |
+
has_content = True
|
| 332 |
+
break
|
| 333 |
+
|
| 334 |
+
if has_content:
|
| 335 |
+
import uuid
|
| 336 |
+
new_id = f"gen_{uuid.uuid4().hex[:12]}"
|
| 337 |
+
item = item.model_copy(update={"id": new_id})
|
| 338 |
+
print(f"🔄 Resolved fake_id (with content) to unique ID: {new_id}")
|
| 339 |
+
else:
|
| 340 |
+
print(f"⚠️ Keeping __fake_id__ for empty/tool placeholder")
|
| 341 |
+
|
| 342 |
+
item_json = item.model_dump_json()
|
| 343 |
+
created_at_val = getattr(item, "created_at", None)
|
| 344 |
+
if created_at_val:
|
| 345 |
+
if isinstance(created_at_val, str):
|
| 346 |
+
created_at = created_at_val
|
| 347 |
+
else:
|
| 348 |
+
created_at = created_at_val.isoformat()
|
| 349 |
+
else:
|
| 350 |
+
created_at = datetime.utcnow().isoformat()
|
| 351 |
+
|
| 352 |
+
cursor = self.conn.cursor()
|
| 353 |
+
|
| 354 |
+
# Check if item already exists
|
| 355 |
+
cursor.execute(
|
| 356 |
+
"SELECT item_id FROM thread_items WHERE item_id = ?",
|
| 357 |
+
(item.id,)
|
| 358 |
+
)
|
| 359 |
+
existing = cursor.fetchone()
|
| 360 |
+
|
| 361 |
+
if existing:
|
| 362 |
+
# Update existing item - keep original sequence_num
|
| 363 |
+
cursor.execute(
|
| 364 |
+
"""
|
| 365 |
+
UPDATE thread_items
|
| 366 |
+
SET item_data = ?, created_at = ?
|
| 367 |
+
WHERE item_id = ?
|
| 368 |
+
""",
|
| 369 |
+
(item_json, created_at, item.id)
|
| 370 |
+
)
|
| 371 |
+
print(f"✅ Updated existing item {item.id}")
|
| 372 |
+
else:
|
| 373 |
+
# Insert new item with next sequence number
|
| 374 |
+
sequence_num = self._get_next_sequence_num(thread_id)
|
| 375 |
+
cursor.execute(
|
| 376 |
+
"""
|
| 377 |
+
INSERT INTO thread_items (item_id, thread_id, item_data, created_at, sequence_num)
|
| 378 |
+
VALUES (?, ?, ?, ?, ?)
|
| 379 |
+
""",
|
| 380 |
+
(item.id, thread_id, item_json, created_at, sequence_num)
|
| 381 |
+
)
|
| 382 |
+
print(f"✅ Inserted new item {item.id} with sequence {sequence_num}")
|
| 383 |
+
|
| 384 |
+
self.conn.commit()
|
| 385 |
+
|
| 386 |
+
# Verify insertion
|
| 387 |
+
cursor.execute(
|
| 388 |
+
"SELECT COUNT(*) as count FROM thread_items WHERE thread_id = ?",
|
| 389 |
+
(thread_id,)
|
| 390 |
+
)
|
| 391 |
+
count = cursor.fetchone()["count"]
|
| 392 |
+
print(f"🔍 Total items in thread {thread_id}: {count}")
|
| 393 |
+
|
| 394 |
+
async def save_item(self, thread_id: str, item: ThreadItem, context: dict[str, Any]) -> None:
|
| 395 |
+
# Use add_thread_item which handles both insert and update
|
| 396 |
+
await self.add_thread_item(thread_id, item, context)
|
| 397 |
+
|
| 398 |
+
async def load_item(self, thread_id: str, item_id: str, context: dict[str, Any]) -> ThreadItem:
|
| 399 |
+
with self._lock:
|
| 400 |
+
cursor = self.conn.cursor()
|
| 401 |
+
cursor.execute(
|
| 402 |
+
"SELECT item_data FROM thread_items WHERE thread_id = ? AND item_id = ?",
|
| 403 |
+
(thread_id, item_id)
|
| 404 |
+
)
|
| 405 |
+
row = cursor.fetchone()
|
| 406 |
+
if not row:
|
| 407 |
+
raise NotFoundError(f"Item {item_id} not found")
|
| 408 |
+
|
| 409 |
+
item_dict = json.loads(row["item_data"])
|
| 410 |
+
item_type = item_dict.get("type")
|
| 411 |
+
|
| 412 |
+
try:
|
| 413 |
+
from chatkit.types import (
|
| 414 |
+
UserMessageItem,
|
| 415 |
+
AssistantMessageItem,
|
| 416 |
+
ClientToolCallItem,
|
| 417 |
+
WorkflowItem,
|
| 418 |
+
WidgetItem,
|
| 419 |
+
TaskItem,
|
| 420 |
+
HiddenContextItem,
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Reconstruct based on type
|
| 424 |
+
if item_type == "user_message":
|
| 425 |
+
return UserMessageItem(**item_dict)
|
| 426 |
+
elif item_type == "assistant_message":
|
| 427 |
+
return AssistantMessageItem(**item_dict)
|
| 428 |
+
elif item_type == "client_tool_call":
|
| 429 |
+
return ClientToolCallItem(**item_dict)
|
| 430 |
+
elif item_type == "workflow":
|
| 431 |
+
return WorkflowItem(**item_dict)
|
| 432 |
+
elif item_type == "widget":
|
| 433 |
+
return WidgetItem(**item_dict)
|
| 434 |
+
elif item_type == "task":
|
| 435 |
+
return TaskItem(**item_dict)
|
| 436 |
+
elif item_type == "hidden_context_item":
|
| 437 |
+
return HiddenContextItem(**item_dict)
|
| 438 |
+
else:
|
| 439 |
+
raise NotFoundError(f"Item {item_id} has unknown type: {item_type}")
|
| 440 |
+
except (ImportError, TypeError, KeyError, ValueError) as e:
|
| 441 |
+
raise NotFoundError(f"Failed to load item {item_id}: {e}")
|
| 442 |
+
|
| 443 |
+
async def delete_thread_item(
|
| 444 |
+
self, thread_id: str, item_id: str, context: dict[str, Any]
|
| 445 |
+
) -> None:
|
| 446 |
+
with self._lock:
|
| 447 |
+
cursor = self.conn.cursor()
|
| 448 |
+
cursor.execute(
|
| 449 |
+
"DELETE FROM thread_items WHERE thread_id = ? AND item_id = ?",
|
| 450 |
+
(thread_id, item_id)
|
| 451 |
+
)
|
| 452 |
+
self.conn.commit()
|
| 453 |
+
|
| 454 |
+
# -- Files -----------------------------------------------------------
|
| 455 |
+
async def save_attachment(
|
| 456 |
+
self,
|
| 457 |
+
attachment: Attachment,
|
| 458 |
+
context: dict[str, Any],
|
| 459 |
+
) -> None:
|
| 460 |
+
raise NotImplementedError(
|
| 461 |
+
"SQLiteStore does not persist attachments. Provide a Store implementation "
|
| 462 |
+
"that enforces authentication and authorization before enabling uploads."
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
async def load_attachment(
|
| 466 |
+
self,
|
| 467 |
+
attachment_id: str,
|
| 468 |
+
context: dict[str, Any],
|
| 469 |
+
) -> Attachment:
|
| 470 |
+
raise NotImplementedError(
|
| 471 |
+
"SQLiteStore does not load attachments. Provide a Store implementation "
|
| 472 |
+
"that enforces authentication and authorization before enabling uploads."
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
async def delete_attachment(self, attachment_id: str, context: dict[str, Any]) -> None:
|
| 476 |
+
raise NotImplementedError(
|
| 477 |
+
"SQLiteStore does not delete attachments because they are never stored."
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
# Helper method to get items (used by zendesk integration)
|
| 481 |
+
def _items(self, thread_id: str) -> List[ThreadItem]:
|
| 482 |
+
"""Synchronous helper to get all items for a thread (for compatibility with existing code)."""
|
| 483 |
+
with self._lock:
|
| 484 |
+
cursor = self.conn.cursor()
|
| 485 |
+
cursor.execute(
|
| 486 |
+
"""
|
| 487 |
+
SELECT item_data FROM thread_items
|
| 488 |
+
WHERE thread_id = ?
|
| 489 |
+
ORDER BY sequence_num ASC, created_at ASC
|
| 490 |
+
""",
|
| 491 |
+
(thread_id,)
|
| 492 |
+
)
|
| 493 |
+
rows = cursor.fetchall()
|
| 494 |
+
items = []
|
| 495 |
+
for row in rows:
|
| 496 |
+
try:
|
| 497 |
+
item_dict = json.loads(row["item_data"])
|
| 498 |
+
item_type = item_dict.get("type")
|
| 499 |
+
|
| 500 |
+
from chatkit.types import (
|
| 501 |
+
UserMessageItem,
|
| 502 |
+
AssistantMessageItem,
|
| 503 |
+
ClientToolCallItem,
|
| 504 |
+
WorkflowItem,
|
| 505 |
+
WidgetItem,
|
| 506 |
+
TaskItem,
|
| 507 |
+
HiddenContextItem,
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
# Reconstruct based on type
|
| 511 |
+
if item_type == "user_message":
|
| 512 |
+
items.append(UserMessageItem(**item_dict))
|
| 513 |
+
elif item_type == "assistant_message":
|
| 514 |
+
items.append(AssistantMessageItem(**item_dict))
|
| 515 |
+
elif item_type == "client_tool_call":
|
| 516 |
+
items.append(ClientToolCallItem(**item_dict))
|
| 517 |
+
elif item_type == "workflow":
|
| 518 |
+
items.append(WorkflowItem(**item_dict))
|
| 519 |
+
elif item_type == "widget":
|
| 520 |
+
items.append(WidgetItem(**item_dict))
|
| 521 |
+
elif item_type == "task":
|
| 522 |
+
items.append(TaskItem(**item_dict))
|
| 523 |
+
elif item_type == "hidden_context_item":
|
| 524 |
+
items.append(HiddenContextItem(**item_dict))
|
| 525 |
+
else:
|
| 526 |
+
print(f"⚠️ Skipping unknown item type in _items: {item_type}")
|
| 527 |
+
continue
|
| 528 |
+
except (ImportError, TypeError, KeyError, ValueError, Exception) as e:
|
| 529 |
+
print(f"⚠️ Failed to reconstruct item in _items: {e}")
|
| 530 |
+
continue
|
| 531 |
+
return items
|
| 532 |
+
|
| 533 |
+
def close(self):
|
| 534 |
+
"""Close the database connection."""
|
| 535 |
+
with self._lock:
|
| 536 |
+
self.conn.close()
|
app/user_state.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
app/user_state.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
from dataclasses import asdict, dataclass, field
|
| 6 |
+
from typing import Any, Dict, Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass(slots=True)
|
| 10 |
+
class UserData:
|
| 11 |
+
customer_id: Optional[str] = None
|
| 12 |
+
customer_name: Optional[str] = None
|
| 13 |
+
customer_email: Optional[str] = None
|
| 14 |
+
customer_phone: Optional[str] = None
|
| 15 |
+
purpose_call: Optional[str] = None
|
| 16 |
+
is_paused: bool = False
|
| 17 |
+
Timezone: Optional[str] = None
|
| 18 |
+
agent_memories: Dict[str, Any] = field(default_factory=dict)
|
| 19 |
+
access_token: Optional[str] = None
|
| 20 |
+
company_name: Optional[str] = None
|
| 21 |
+
Booked_appointment: Optional[str] = None
|
| 22 |
+
agents: Dict[str, Any] = field(default_factory=dict)
|
| 23 |
+
prev_agent: Any | None = None
|
| 24 |
+
summary: Optional[str] = None
|
| 25 |
+
|
| 26 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 27 |
+
return asdict(self)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class UserStateManager:
|
| 31 |
+
"""Manages per-thread user/customer state for the multi-agent flow with SQLite persistence."""
|
| 32 |
+
|
| 33 |
+
def __init__(self, db_path: str = "user_state.db") -> None:
|
| 34 |
+
self._states: Dict[str, UserData] = {}
|
| 35 |
+
self.db_path = db_path
|
| 36 |
+
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 37 |
+
self._init_db()
|
| 38 |
+
self._load_all_states()
|
| 39 |
+
|
| 40 |
+
def _init_db(self) -> None:
|
| 41 |
+
"""Initialize the user state database table."""
|
| 42 |
+
cursor = self.conn.cursor()
|
| 43 |
+
cursor.execute("""
|
| 44 |
+
CREATE TABLE IF NOT EXISTS user_states (
|
| 45 |
+
thread_id TEXT PRIMARY KEY,
|
| 46 |
+
state_data TEXT NOT NULL,
|
| 47 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 48 |
+
)
|
| 49 |
+
""")
|
| 50 |
+
self.conn.commit()
|
| 51 |
+
|
| 52 |
+
def _load_all_states(self) -> None:
|
| 53 |
+
"""Load all user states from database into memory on startup."""
|
| 54 |
+
cursor = self.conn.cursor()
|
| 55 |
+
cursor.execute("SELECT thread_id, state_data FROM user_states")
|
| 56 |
+
for row in cursor.fetchall():
|
| 57 |
+
thread_id, state_json = row
|
| 58 |
+
try:
|
| 59 |
+
state_dict = json.loads(state_json)
|
| 60 |
+
self._states[thread_id] = UserData(**state_dict)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"⚠️ Failed to load state for thread {thread_id}: {e}")
|
| 63 |
+
|
| 64 |
+
def _save_state(self, thread_id: str) -> None:
|
| 65 |
+
"""Persist a thread's state to the database."""
|
| 66 |
+
state = self._states.get(thread_id)
|
| 67 |
+
if state is None:
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
state_json = json.dumps(state.to_dict())
|
| 72 |
+
cursor = self.conn.cursor()
|
| 73 |
+
cursor.execute("""
|
| 74 |
+
INSERT INTO user_states (thread_id, state_data, updated_at)
|
| 75 |
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
| 76 |
+
ON CONFLICT(thread_id) DO UPDATE SET
|
| 77 |
+
state_data = excluded.state_data,
|
| 78 |
+
updated_at = CURRENT_TIMESTAMP
|
| 79 |
+
""", (thread_id, state_json))
|
| 80 |
+
self.conn.commit()
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"⚠️ Failed to save state for thread {thread_id}: {e}")
|
| 83 |
+
|
| 84 |
+
def _create_default_state(self) -> UserData:
|
| 85 |
+
return UserData()
|
| 86 |
+
|
| 87 |
+
def get_user(self, thread_id: str) -> UserData:
|
| 88 |
+
if thread_id not in self._states:
|
| 89 |
+
self._states[thread_id] = self._create_default_state()
|
| 90 |
+
self._save_state(thread_id)
|
| 91 |
+
return self._states[thread_id]
|
| 92 |
+
|
| 93 |
+
# -- Mutators --------------------------------------------------------
|
| 94 |
+
def set_customer_info(
|
| 95 |
+
self,
|
| 96 |
+
thread_id: str,
|
| 97 |
+
*,
|
| 98 |
+
customer_id: Optional[str] = None,
|
| 99 |
+
name: Optional[str] = None,
|
| 100 |
+
email: Optional[str] = None,
|
| 101 |
+
phone: Optional[str] = None,
|
| 102 |
+
company_name: Optional[str] = None,
|
| 103 |
+
) -> None:
|
| 104 |
+
data = self.get_user(thread_id)
|
| 105 |
+
if customer_id is not None:
|
| 106 |
+
data.customer_id = customer_id
|
| 107 |
+
if name is not None:
|
| 108 |
+
data.customer_name = name
|
| 109 |
+
if email is not None:
|
| 110 |
+
data.customer_email = email
|
| 111 |
+
if phone is not None:
|
| 112 |
+
data.customer_phone = phone
|
| 113 |
+
if company_name is not None:
|
| 114 |
+
data.company_name = company_name
|
| 115 |
+
self._save_state(thread_id)
|
| 116 |
+
|
| 117 |
+
def set_purpose(self, thread_id: str, purpose: Optional[str]) -> None:
|
| 118 |
+
self.get_user(thread_id).purpose_call = purpose
|
| 119 |
+
self._save_state(thread_id)
|
| 120 |
+
|
| 121 |
+
def set_timezone(self, thread_id: str, timezone: Optional[str]) -> None:
|
| 122 |
+
self.get_user(thread_id).Timezone = timezone
|
| 123 |
+
self._save_state(thread_id)
|
| 124 |
+
|
| 125 |
+
def set_paused(self, thread_id: str, paused: bool) -> None:
|
| 126 |
+
self.get_user(thread_id).is_paused = bool(paused)
|
| 127 |
+
self._save_state(thread_id)
|
| 128 |
+
|
| 129 |
+
def set_access_token(self, thread_id: str, token: Optional[str]) -> None:
|
| 130 |
+
self.get_user(thread_id).access_token = token
|
| 131 |
+
self._save_state(thread_id)
|
| 132 |
+
|
| 133 |
+
def set_booked_appointment(self, thread_id: str, value: Optional[str]) -> None:
|
| 134 |
+
self.get_user(thread_id).Booked_appointment = value
|
| 135 |
+
self._save_state(thread_id)
|
| 136 |
+
|
| 137 |
+
def get_cached_access_token(self, thread_id: str) -> Optional[str]:
|
| 138 |
+
"""Get the access token if it exists and is less than 12 hours old."""
|
| 139 |
+
cursor = self.conn.cursor()
|
| 140 |
+
cursor.execute("""
|
| 141 |
+
SELECT state_data, updated_at FROM user_states WHERE thread_id = ?
|
| 142 |
+
""", (thread_id,))
|
| 143 |
+
row = cursor.fetchone()
|
| 144 |
+
if not row:
|
| 145 |
+
return None
|
| 146 |
+
|
| 147 |
+
state_json, updated_at_str = row
|
| 148 |
+
from datetime import datetime, timezone, timedelta
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
# updated_at is stored in UTC by the DB
|
| 152 |
+
updated_at = datetime.fromisoformat(updated_at_str.replace(' ', 'T') + "+00:00")
|
| 153 |
+
if datetime.now(timezone.utc) - updated_at > timedelta(hours=12):
|
| 154 |
+
print(f"🕒 Access token for {thread_id} expired (> 12h)")
|
| 155 |
+
return None
|
| 156 |
+
|
| 157 |
+
state_dict = json.loads(state_json)
|
| 158 |
+
return state_dict.get("access_token")
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"⚠️ Error checking token expiration: {e}")
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
def update_access_token(self, thread_id: str, token: str) -> None:
|
| 164 |
+
"""Update the access token and reset the updated_at timestamp."""
|
| 165 |
+
data = self.get_user(thread_id)
|
| 166 |
+
data.access_token = token
|
| 167 |
+
# _save_state updates updated_at to CURRENT_TIMESTAMP
|
| 168 |
+
self._save_state(thread_id)
|
| 169 |
+
|
| 170 |
+
def set_summary(self, thread_id: str, summary: str) -> None:
|
| 171 |
+
self.get_user(thread_id).summary = summary
|
| 172 |
+
self._save_state(thread_id)
|
| 173 |
+
|
| 174 |
+
def get_summary(self, thread_id: str) -> str:
|
| 175 |
+
return self.get_user(thread_id).summary or ""
|
| 176 |
+
|
| 177 |
+
# Agent registry helpers
|
| 178 |
+
def register_agent(self, thread_id: str, key: str, agent: Any) -> None:
|
| 179 |
+
self.get_user(thread_id).agents[key] = agent
|
| 180 |
+
self._save_state(thread_id)
|
| 181 |
+
|
| 182 |
+
def get_agent(self, thread_id: str, key: str) -> Any:
|
| 183 |
+
return self.get_user(thread_id).agents.get(key)
|
| 184 |
+
|
| 185 |
+
def set_prev_agent(self, thread_id: str, agent: Any | None) -> None:
|
| 186 |
+
self.get_user(thread_id).prev_agent = agent
|
| 187 |
+
self._save_state(thread_id)
|
| 188 |
+
|
| 189 |
+
# Memories helpers
|
| 190 |
+
def remember(self, thread_id: str, key: str, value: Any) -> None:
|
| 191 |
+
self.get_user(thread_id).agent_memories[key] = value
|
| 192 |
+
self._save_state(thread_id)
|
| 193 |
+
|
| 194 |
+
def recall(self, thread_id: str, key: str, default: Any = None) -> Any:
|
| 195 |
+
return self.get_user(thread_id).agent_memories.get(key, default)
|
| 196 |
+
|
| 197 |
+
# -- Serialization ---------------------------------------------------
|
| 198 |
+
def to_dict(self, thread_id: str) -> Dict[str, Any]:
|
| 199 |
+
return self.get_user(thread_id).to_dict()
|
| 200 |
+
|
app/website/output.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=== Output Start ===
|
| 2 |
+
|
| 3 |
+
The following text is scraped website content:
|
| 4 |
+
|
| 5 |
+
British Curriculum Schools - Sunmarke School, Best School in Dubai Apply Visit Enquire VLE Login About Principal’s Message Mission, Vision, Values A Positive Education School Leadership Academic Results Inspection Reports Our Achievements Sunmarke Alumni Wellbeing The Achievement Centre – Inclusion Department Our Campus Learning Nursery Our Approach Enriched Learning EYFS Our Approach Early Years Curriculum Enriched Learning Primary Our Approach Primary Curriculum Sports & PE Creative & Performing Arts Enriched Learning Coding Student Council and Ambassadors Secondary Our Approach Secondary Curriculum Sports & PE Creative & Performing Arts Enriched Learning Coding Sunmarke Mini MBA Student Council and Ambassadors Sixth Form Our Approach A-Levels BTECs IB Diploma Programme (IBDP) Student Enrichment Coding CIVITAS Careers Counsellor Student Leadership Signature Programmes STEAM & Design Thinking Environment & Sustainability University & Career Readiness Programme Fortes Pro Sports Academy Positive Education Global Language Academy Financial Literacy Programme Enterprise & Mini-MBA Programme Communications, Media & the Performing Arts Programme Admissions Apply Online Schedule a Visit Tuition Fees Scholarships Re-enrol Online School Forms FAQs For Parents School Uniform School Timings Academic Calendar School Events Calendar Policies Transport Services Dining & Catering Sports Hub Parent Engagement Governance Committee Activities Sunmarke Free Extracurricular Activities (ECAs) Sunmarke Paid Extracurricular Activities Third-Party Paid Extracurricular Activities News & Events Weekly Buzz Blog Press Podcasts Contact Us Work With Us Apply Visit Enquire VLE Login About Principal’s Message Mission, Vision, Values A Positive Education School Leadership Academic Results Inspection Reports Our Achievements Sunmarke Alumni Wellbeing The Achievement Centre – Inclusion Department Our Campus Learning Nursery Our Approach Enriched Learning EYFS Our Approach Early Years Curriculum Enriched Learning Primary Our Approach Primary Curriculum Sports & PE Creative & Performing Arts Enriched Learning Coding Student Council and Ambassadors Secondary Our Approach Secondary Curriculum Sports & PE Creative & Performing Arts Enriched Learning Coding Sunmarke Mini MBA Student Council and Ambassadors Sixth Form Our Approach A-Levels BTECs IB Diploma Programme (IBDP) Student Enrichment Coding CIVITAS Careers Counsellor Student Leadership Signature Programmes STEAM & Design Thinking Environment & Sustainability University & Career Readiness Programme Fortes Pro Sports Academy Positive Education Global Language Academy Financial Literacy Programme Enterprise & Mini-MBA Programme Communications, Media & the Performing Arts Programme Admissions Apply Online Schedule a Visit Tuition Fees Scholarships Re-enrol Online School Forms FAQs For Parents School Uniform School Timings Academic Calendar School Events Calendar Policies Transport Services Dining & Catering Sports Hub Parent Engagement Governance Committee Activities Sunmarke Free Extracurricular Activities (ECAs) Sunmarke Paid Extracurricular Activities Third-Party Paid Extracurricular Activities News & Events Weekly Buzz Blog Press Podcasts Contact Us Work With Us Carlota & Ruben win the ‘World’s Best Debater’ competition at the ‘World Scholars Cup’ Global Finals held at Yale University competing against 1000’s of students from schools across 40 countries International School Welcome to Sunmarke School A Passionately Character-Building & Innovative School in the Middle East Sunmarke provides an ambitious and inspiring values-based education nurturing the hearts and minds of young boys and girls so that they can flourish and achieve their dreams. Simply put, we transform young lives. Powered by our unique, leading-edge Signature Programmes, we are an incredibly innovative school reshaping and inspiring tomorrow’s generation. We are amongst the top 5 schools in Dubai, as seen by our outstanding IB and A-Level results, top university placements, and notable student achievements in the arts, sports, and extracurriculars. We are a recognised global leader in Positive Education – a paradigm shift in education worldwide – where it’s increasingly acknowledged that truly outstanding teaching and learning is only attained by developing “academic and personal excellence” as well as “well-being and character”. We are one of the few schools in the UAE to offer both the IB and A-Level curriculum pathways, as well as the BTECs. Backed by Fortes Education’s rich history of over 40 years in academia, Sunmarke is rated “Outstanding” by the British School Overseas Inspection Authority and “Very Good” by KHDA. Our Purpose We aim to inspire young boys and girls to thrive and flourish with purpose. A Rigorous & Ambitious Education We deliver an education that emphasises academic excellence and participation in diverse sports, performing arts and extracurricular activities. Outstanding Senior Leaders & Teachers Our educators are ambassadors of Positive Education and inspirational teachers. 40 Years of History Sunmarke School is backed by Fortes Education’s four decades of excellence in education Nursery Ages 45 days to 3 years EYFS Ages 3 to 5 years Primary Ages 5 to 11 years Secondary Ages 11 to 16 years Sixth Form Ages 16 to 18 years Sunmarke - Pathway to Top Universities Top University Destinations Sunmarke’s Unique & Leading-edge Signature Programmes STEAM & Design Thinking Students become creative problem-solvers through hands-on projects in coding, robotics, AI, 3D printing, and sustainable design, developing critical thinking, collaboration, and innovative solutions with real-world impact. Learn more Environment & Sustainability Students develop eco-leadership through outdoor learning, gardens, and real-world experiences, gaining sustainability skills, respect for nature, and the ability to drive positive environmental change in communities. Learn more University & Career Readiness The University & Career Readiness Programme empowers students with personalised guidance, internships, global engagement, and career counselling, equipping them with skills, confidence, and clarity for future success. Learn more Pro Sports Academy Sunmarke’s Pro Sports Academy develops athletes through elite coaching, 100+ squads, and global competitions, integrating fitness, nutrition, and resilience for lifelong excellence, discipline, and success beyond sport. Learn more Positive Education Internationally recognised for its holistic approach, the school blends wellbeing, character development, and Positive Psychology, nurturing resilient, empathetic, confident learners who thrive academically, socially, and emotionally. Learn more Global Language Programme The Global Languages Programme immerses students in Hindi and Mandarin, fostering communication, cultural awareness, empathy, and adaptability through storytelling, projects, traditions, and real-life experiences for global readiness. Learn more Financial Literacy The programme equips students with practical financial skills, ethical investment habits, and entrepreneurial thinking through interactive learning, fostering confidence, critical thinking, and responsible decision-making in real-world financial contexts. Learn more Enterprise & Mini-MBA Programme Sunmarke’s Enterprise & Mini-MBA empowers Years 10–13 with business, leadership, and innovation skills through workshops, mentorship, and industry exposure, preparing resilient future leaders and entrepreneurs. Learn more Communications, Media & the Performing Arts A dynamic programme combining drama, media, film, and communication, building confidence, creativity, and critical thinking through storytelling, performance, collaboration, and industry workshops from EYFS to Year 13. Learn more Excellent Academic Results 2024-25 0 IBDP AVERAGE POINTS SCORE more than 6 points above the world average 0 % IBDP RESULTS students exceeding the world average 0 % IBDP RESULTS students exceeding 35+ points 0 % IBDP RESULTS more than one in four students achieved 40+ points 0 % A-LEVEL & BTEC RESULTS 100% student pass rate at A-Level & BTEC 0 % A-LEVEL A*- C BTEC D*-M 0 % A-LEVEL A*- B BTEC D*-D 0 % A-LEVEL A*- A BTEC D* 0 % GCSE RESULTS 100% student pass rate at GCSE 0 % GCSE 9-4 (A*- C) including English & Maths 0 % GCSE 9-6 (A*- B) 0 % GCSE 9-7 (A*- A) Sunmarke - Launchpad to Future Success Our Summer Workplace Internships for Students Parent Testimonials “ My daughter has discovered an incredible passion for media over the last 2 years under the coaching of the teachers here at Sunmarke. Mrs Rebekah Parent, Sunmarke School “ Seeing our daughter excel right from understanding pre-production, production to post-production, we absolutely love Sunmarake School. Mrs Nandita Parent, Sunmarke School “ Today’s experience has been really valuable for me, appreciating just how well the science, technology, arts, and mathematical elements come together. Parent Sunmarke School “ I’d like to say thank you to Sunmarke that you have such a perfect opportunity to have this experience with our kids. Parent Sunmarke School “ I’m super happy for my daughter that has the opportunity to make experiments, and prototypes, try something, and create something. It’s amazing for every child. Parent Sunmarke School “ It’s great to see how they want to develop kids more with the creative mind in the STEAM Programme. We loved it. We are looking forward to more! Parent Sunmarke School “ We are very happy to come today. I think it’s a very nice experience for both the kids and the parents…it’s amazing. Parent Sunmarke School “ Our children are growing up as future citizens of the world, and they are super happy and enthusiastic to go to school every day. Emily Parent, Sunmarke School “ I am genuinely impressed with everything that the kids are exposed to here at Sunmarke School. Also, immensely thankful for everything that the school is providing them. Sherif Parent, Sunmarke School “ What I like about Sunmarke is its environment and sustainability programme, as it raises children’s awareness about the world. Christina & Mike Parent, Sunmarke School “ Sunmarke is the best school choice for us because my daughter is very happy to go to school every day. As a parent, I am highly satisfied with the quality of education provided here at Sunmarke School, as I can witness my daughter’s consistent progress and improvement each day. Tetiana Parent, Sunmarke School “ I am really impressed with the STEAM programme they have here at Sunmarke. Moreover, my kids have the opportunity to explore and learn about environmental and sustainability programme as well. I am very thankful for all the opportunities Sunmarke is providing for my kids. Leandra Parent, Sunmarke School “ Sunmarke School is truly amazing. I couldn’t have asked for more. It’s been the perfect school for my son. Saireh Parent, Sunmarke School “ As a family, we have travelled a lot and our children have attended the top schools in the world. I can confidently say Sunmarke is amongst the top schools. It’s competitive both in academics and sports and offers a wide range of extracurriculars. The school keeps surprising us with its innovative programs and yet never forgets the heart and soul of the child. Pillaar Parent, Sunmarke School “ We have been at Sunmarke from the beginning and have seen our children grow into character-strong, confident young adults. They are well-balanced, have a strong sense of values, and approach each day with a sense of gratitude. Honestly, as parents, we could not have asked for more. Parent Sunmarke School “ This is a fantastic school. We have been here for 5 years and our kids are really, really happy. The school is a home away from home. The teachers are really what make a difference – passionate about teaching, the children, and dynamic practitioners. Parent Sunmarke School “ I was blown away and super impressed with Sunmarke’s STEAM programme where multiple classes such as science, technology and computing, math, art and media, and design technology are collapsed into one massive lab which the school calls SPACE-X (in nod to Elon Musk himself). STEAM is interwoven across the whole curriculum. Sunmarke takes the learning journey to the next level. My son is always very excited about STEAM. Sunmarke’s teaching style has lifted his critical thinking skills, enabling him to find solutions to real-life problems. Rasha Khalil Parent, Sunmarke School “ Sunmarke is not just about the regular staple diet of education schools give. It goes way beyond anything we have experienced. My child, Alfa, has benefited tremendously with the school’s various signature programmes, such as the Forest School and the STEAM programme. During our visit to the school’s STEAM showcase, Alfa, my child mentioned, showed what he had created with great interest. As parents we were fascinated by his learning journey, how he took the concept of catapult studied in Stone Age man to a full blown model with all the ways the catapult was used in hunting for food. Sunmarke encapsulates real and practical learning and engagement at its best. Mrs. Breman Parent, Sunmarke School “ My son’s learning from Sunmarke’s unbelievably creative Signature Programmes has been the hallmark of his growth at school. He has grown to have a real love for Nature and truly cares about conservation, all learned through Sunmarke’s Forest School programme. At Sunmarke’s STEAM programme and Space-X, he tinkers, ideates and tries, fails, and tries and finally successfully makes products and solutions, learning perseverance and the patience that even if you fail, always keep trying till you succeed. Marianne Boushra Parent, Sunmarke School “ Children truly learn when they have fun and that is exactly what Sunmarke achieves daily in and out of the classroom. My child has shown his immense excitement about STEAM and constantly updates me about the progress of his projects. I love being able to see my child express his work and his learning journey. He is engaged, involved, and passionate, and he looks forward to going to school every day. Nura Al Gaddah Parent, Sunmarke School “ Sunmarke is an amazing school for students of all ages, with its state of the art campus, well behaved students, qualified faculty and efficient admin team. That’s when we decided to enrol our child in the IB curriculum. It’s been a great experience for us as parents. Anita Isaac Parent, Sunmarke School “ Focuses on the individual. Mutual respect. Building a genuine relationship with child and parent. Parent Sunmarke School “ Happy to have my daughter in this amazing school. Ahlam Bounaim Parent, Sunmarke School “ Great school to learn and play. The teacher and assistant are all benevolent with children. Virginie Parent, Sunmarke School “ The most important thing about Sunmarke is the fact that it is a school that listens positively and take prompt action and my child’s progress is its number one priority. Parent Sunmarke School “ My elder son is in primary at Sunmarke School. We are really happy with the curriculum here and all the Signature programmes offered especially Performing Arts where my son got the opportunity to showcase his talent. Okesia Panina Parent, Sunmarke School “ Beside the impressive facilities and very strong academic track record, the school is remarkable in ensuring it’s ‘positive education’ philosophy is lived and breathed every day. Far from being just a tag line applied by a marketeer, the pastoral care and support for the children is second to none. Parent Sunmarke School “ My children at Sunmarke are extremely happy with the environment and the encouragement that’s being provided by the teachers. Its indeed commendable for a friendly atmosphere where all the kids are provided platform to showcase their potential. Sheeba Suresh Parent, Sunmarke School “ No bullying policy, positive education, good teachers and organization. Parent Sunmarke School “ Sunmarke’s education is beyond compare – strong academics, a caring atmosphere, attention to every child, and a wide range of extracurricular and sports activities. Our children and us as a family have been extremely happy with Sunmarke. Pillaar Parent, Sunmarke School “ At Sunmarke, my daughter has grown into a young adult with a strong sense of values and character. She excels in her academics, is thoughtful and kind, approaches each day with a positive attitude, and is a joy to be around. As a family, we are deeply grateful to Sunmarke and its absolutely wonderful and caring teachers who are wonderful role models and truly embody the character strengths and wellbeing the school and its ethos of Positive Education espouse. Shereen Raggel Parent, Sunmarke School “ I have been truly impressed by my child’s learning at Sunmarke. However, this level of education is evident amongst his entire class. My child and his cohort’s depth of knowledge, innovative approach to problem-solving and thinking, and respectful nature have all impressed me. Each zone in the school is very well resourced, equipped both technologically and academically. Keep up the good work Sunmarke! Zaid Hassan Parent, Sunmarke School “ Sunmarke School is an across the board “Outstanding School” and provides an outstanding quality of education for all its pupils. BSO Inspection Report “ Sunmarke School is outstanding in almost all respects. Student’s learning skills are very strong. A significant key strength of the school is the outstanding wellbeing and personal development of all students. KHDA Report “ Sunmarke School is genuinely a ground-breaking school and offers the broadest curriculum options of ANY other school in the Emirates. SchoolsCompared.com “ We would encourage interested parents to visit this school. We are impressed by the commitment of the school to an open policy towards students of all abilities and the development of the curriculum to provide a far wider range of Sixth Form options than has been seen to date in Dubai. WhichSchoolAdvisor.com “ Sunmarke meets the criteria of a ‘School of Character’ many times over. It is an ‘Outstanding School of Character’ and makes a huge difference to the lives of the children in this school. Character and the idea of ‘flourishing’ is embedded within everything this school does. I am hugely impressed! Prof. James Arthur, Director of the Jubilee Centre for Character & Virtues at The University of Birmingham, UK Insights and Latest News Visit Our Blog 28/11/2025 Career Readiness: Why It Matters for Students Today In today’s fast-evolving world, preparing students for the future goes... Read More 14/11/2025 How Schools Can Lead the Way in Sustainable Practices In a world facing growing environmental challenges, schools have a... Read More 07/11/2025 The Rise of AI in Media: Opportunities for Young Creators In the past decade, artificial intelligence (AI) has transformed industries... Read More 21/10/2025 From Passion to Profession: Student-Led Startups & Enterprise Projects at Sunmarke At Sunmarke School, we believe that education should go beyond... Read More 14/10/2025 Well-being in the Digital World: Balancing Screen Time, Sleep & Study for Teen Learners In today’s fast-paced digital age, our students live in a... Read More 07/10/2025 Beyond Grammar: How the Fortes English Language Programme Shapes Confident Communicators At Sunmarke School, we believe that mastering a language is... Read More 28/09/2025 Beyond the Textbook: Sunmarke’s Innovative Approach to Secondary Education In today’s fast-changing world, education can no longer be confined... Read More 21/09/2025 Learning Through Doing: The Power of Projects in Sunmarke’s STEAM Education At Sunmarke School, education goes far beyond textbooks and exams.... Read More 14/09/2025 Little Learners, Big Numbers: Bringing Maths to Life in the Early Years Mathematics is often thought of as a subject of precision,... Read More Follow Us Our Stories Sunmarke School Oliver - Secondary Musical Teaser ✨ Get Ready! Lionel Bart’s Oliver! Is Coming to the Sunmarke Stage ✨ Step into the streets of Victorian London and experience a powerful story of hope, resilience, and the courage to dream. Born with nothing. Fighting for everything. One chance ... Posted: 4 days ago Sunmarke School Discover Sunmarke School | Our Principal on Learning, Values & Community In this Principal's Message, Mr Nicholas Rickford shares insights into Sunmarke School’s academic pathways and the thinking behind our carefully designed curriculum. He highlights our signature programmes, strong positive education ethos, and commitment to nurturing confident, well-rounded learners. From A-Levels ... Posted: 2 weeks ago Sunmarke School Think & Thrive Career Development Programme Think & Thrive is Sunmarke’s signature speaker series designed to inspire, inform, and empower our Secondary students as they explore future pathways. Bringing together innovators, artists, sports personalities, entrepreneurs, and business leaders from across the region, the programme creates meaningful ... Posted: 2 weeks ago Sunmarke School Think & Thrive with Mr. Aaron Stroble, Director at Oceaneering (UAE: Surf Abu Dhabi) Our Year 10 student, Daria, led an engaging ‘Think & Thrive’ session, an integral part of our signature programme, with Mr Aaron Stroble, Director at Oceaneering, where they explored the future of sustainability and renewable energy. 🤩👏✨ It was a wonderful ... Posted: Last month Sunmarke School Think & Thrive with Mr. Aaron Stroble, Director at Oceaneering (UAE: Surf Abu Dhabi) Posted: Last month Sunmarke School Parent Testimonial: Growing and Thriving at Sunmarke Posted: 2 months ago Sunmarke School Sunmarke Parent Survey 2025-2026 At Sunmarke School, we truly value our parents’ feedback. 🤩💯 Today, our Principal, Mr Nicholas Rickford, launched the Your Voice Counts survey — an opportunity for our parent community to share what they love most about Sunmarke and the areas they’d ... Posted: 2 months ago Sunmarke School GCSE Results 2024–2025: Outstanding Achiever – Parent Testimonial Posted: 4 months ago Sunmarke School GCSE Results 2024–2025: Outstanding Achiever – Student Testimonial Posted: 4 months ago Sunmarke School GCSE Results 2024–2025: Outstanding Achiever – Student Testimonial Posted: 4 months ago Sunmarke School BTEC Results 2024-2025: Outstanding Achiever Posted: 4 months ago Sunmarke School Sunmarke Synergy Summer Internship Programme At Sunmarke, we take pride in equipping our students with real-world skills that help them thrive in professional setups beyond school. Our recent graduate, Kaveh, got a glimpse of how our Marketing team functions as he completed a four-week stint with ... Posted: 5 months ago Sunmarke School Sunmarke Synergy Summer Internship Programme Our recent graduate, Nathan, who will soon be flying to the University of Toronto for further studies, completed an exciting four-week stint with our Marketing team as part of the Sunmarke Synergy Summer Internship Programme. Nathan worked across multiple marketing ... Posted: 5 months ago Sunmarke School The Wiz – A Magical Secondary Production at Sunmarke We enjoyed a delightful retelling of the timeless classic The Wiz at Sunmarke. Each character brought their unique energy to the stage with their vibrant performances, soulful tunes and colourful costumes. The bold Dorothy, the heartwarming Lion, the Tin ... Posted: 5 months ago Sunmarke School Sunmarke Synergy Summer Internship Programme Synergising with Sunmarke! 🤩 Hear what our Year 10 student Paloma has to say about her exciting four-week stint with our Admissions team as part of the Sunmarke Synergy Summer Internship Programme. She dived into a dynamic environment, leading school tours, ... Posted: 5 months ago Virtual Tour Tour Sunmarke and our amazing facilities Disclaimer: Sunmarke School is not responsible for any losses or damages incurred as a result of interacting with or making transactions on scam or fraudulent websites. We do not endorse, verify, or guarantee the authenticity of any third-party websites or services linked to or mentioned on our platform. Sunmarke School Telephone: +971 4 423 8900 Email: Click here to contact us Behind Limitless Building Al Khail Road District 5, Jumeirah Village Triangle PO Box 24857, Dubai, UAE Useful Links Parent Portal (VLE) Tuition Fees Vacancies DSIB Inspection Report IBDP, GCSE, A-Level & BTEC Results Sunmarke Sixth Form School Dubai EYFS Curriculum Dubai Sunmarke Primary School Dubai Sunmarke Secondary School Dubai Fortes Family Sunmarke School Regent International School Jumeirah International Nurseries Fortes Education Copyright All Right Reserved 2025. Sunmarke School | Terms & Conditions
|
| 6 |
+
|
| 7 |
+
=== Output End ===
|
chatkit_threads.db
ADDED
|
Binary file (61.4 kB). View file
|
|
|
chatkit_threads.db-shm
ADDED
|
Binary file (32.8 kB). View file
|
|
|
chatkit_threads.db-wal
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8b2c676248e91d338f81d69ab6b9b09d759e310cc0de47a2bacf06e32e892274
|
| 3 |
+
size 2657432
|
chromafast_db/chroma.sqlite3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f4b89606a53e90d5da168959f098b209854f25d8bb9182de97cbc067d03b3b0e
|
| 3 |
+
size 167936
|
pyproject.toml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "chatkit-backend"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "FastAPI backend service for ChatKit boilerplate"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"fastapi>=0.114.1,<0.116",
|
| 9 |
+
"httpx>=0.28,<0.29",
|
| 10 |
+
"uvicorn[standard]>=0.36,<0.37",
|
| 11 |
+
"openai>=1.40",
|
| 12 |
+
"openai-chatkit>=1.0.2,<2",
|
| 13 |
+
"llama-index-llms-groq",
|
| 14 |
+
"llama-index",
|
| 15 |
+
"llama-index-embeddings-huggingface",
|
| 16 |
+
"llama-index-vector-stores-chroma",
|
| 17 |
+
"nest_asyncio",
|
| 18 |
+
"InstructorEmbedding",
|
| 19 |
+
"chromadb",
|
| 20 |
+
"lc-sdk-python>=0.4.1",
|
| 21 |
+
"loguru>=0.7.3",
|
| 22 |
+
"groq>=0.37.1",
|
| 23 |
+
"crewai-tools>=0.76.0",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
[project.optional-dependencies]
|
| 27 |
+
dev = [
|
| 28 |
+
"ruff>=0.6.4,<0.7",
|
| 29 |
+
"mypy>=1.8,<2",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
[build-system]
|
| 33 |
+
requires = ["setuptools>=68.0", "wheel"]
|
| 34 |
+
build-backend = "setuptools.build_meta"
|
| 35 |
+
|
| 36 |
+
[tool.setuptools]
|
| 37 |
+
packages = ["app"]
|
| 38 |
+
|
| 39 |
+
[tool.ruff]
|
| 40 |
+
line-length = 100
|
| 41 |
+
|
| 42 |
+
[tool.ruff.lint]
|
| 43 |
+
extend-select = ["I"]
|
user_state.db
ADDED
|
Binary file (28.7 kB). View file
|
|
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|