Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- .dockerignore +9 -0
- .gitattributes +132 -35
- .gitignore +14 -0
- .python-version +1 -0
- .sonarlint/connectedMode.json +5 -0
- Dockerfile +47 -0
- README.md +6 -7
- assets/image/Einstein.jpg +3 -0
- assets/image/Napoleon.jpg +0 -0
- pyproject.toml +28 -0
- src/chattr/__init__.py +76 -0
- src/chattr/__main__.py +24 -0
- src/chattr/graph.py +96 -0
- src/chattr/gui.py +55 -0
- src/chattr/tools.py +25 -0
- uv.lock +0 -0
.dockerignore
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/__pycache__/
|
2 |
+
.deepsource.toml
|
3 |
+
.env
|
4 |
+
.github/
|
5 |
+
.idea/
|
6 |
+
.mypy_cache/
|
7 |
+
.ruff_cache/
|
8 |
+
.venv/
|
9 |
+
.vscode/
|
.gitattributes
CHANGED
@@ -1,35 +1,132 @@
|
|
1 |
-
|
2 |
-
*.
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
*.
|
9 |
-
*.
|
10 |
-
*.
|
11 |
-
*.
|
12 |
-
*.
|
13 |
-
*.
|
14 |
-
*.
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
*.
|
19 |
-
*.
|
20 |
-
*.
|
21 |
-
*.
|
22 |
-
*.
|
23 |
-
*.
|
24 |
-
*.
|
25 |
-
|
26 |
-
|
27 |
-
*.
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Apply override to all files in the directory
|
2 |
+
*.md linguist-detectable
|
3 |
+
|
4 |
+
# Basic .gitattributes for a python repo.
|
5 |
+
|
6 |
+
# Source files
|
7 |
+
# ============
|
8 |
+
*.pxd text diff=python
|
9 |
+
*.py text diff=python
|
10 |
+
*.py3 text diff=python
|
11 |
+
*.pyw text diff=python
|
12 |
+
*.pyx text diff=python
|
13 |
+
*.pyz text diff=python
|
14 |
+
*.pyi text diff=python
|
15 |
+
|
16 |
+
# Binary files
|
17 |
+
# ============
|
18 |
+
*.db binary
|
19 |
+
*.p binary
|
20 |
+
*.pkl binary
|
21 |
+
*.pickle binary
|
22 |
+
*.pyc binary export-ignore
|
23 |
+
*.pyo binary export-ignore
|
24 |
+
*.pyd binary
|
25 |
+
|
26 |
+
# Jupyter notebook
|
27 |
+
*.ipynb text eol=lf
|
28 |
+
|
29 |
+
# Note: .db, .p, and .pkl files are associated
|
30 |
+
# with the python modules ``pickle``, ``dbm.*``,
|
31 |
+
# ``shelve``, ``marshal``, ``anydbm``, & ``bsddb``
|
32 |
+
# (among others).
|
33 |
+
|
34 |
+
# Common settings that generally should always be used with your language specific settings
|
35 |
+
|
36 |
+
# Auto detect text files and perform LF normalization
|
37 |
+
* text=auto
|
38 |
+
|
39 |
+
#
|
40 |
+
# The above will handle all files NOT found below
|
41 |
+
#
|
42 |
+
|
43 |
+
# Documents
|
44 |
+
*.bibtex text diff=bibtex
|
45 |
+
*.doc diff=astextplain
|
46 |
+
*.DOC diff=astextplain
|
47 |
+
*.docx diff=astextplain
|
48 |
+
*.DOCX diff=astextplain
|
49 |
+
*.dot diff=astextplain
|
50 |
+
*.DOT diff=astextplain
|
51 |
+
*.pdf diff=astextplain
|
52 |
+
*.PDF diff=astextplain
|
53 |
+
*.rtf diff=astextplain
|
54 |
+
*.RTF diff=astextplain
|
55 |
+
*.md text diff=markdown
|
56 |
+
*.mdx text diff=markdown
|
57 |
+
*.tex text diff=tex
|
58 |
+
*.adoc text
|
59 |
+
*.textile text
|
60 |
+
*.mustache text
|
61 |
+
*.csv text eol=crlf
|
62 |
+
*.tab text
|
63 |
+
*.tsv text
|
64 |
+
*.txt text
|
65 |
+
*.sql text
|
66 |
+
*.epub diff=astextplain
|
67 |
+
|
68 |
+
# Graphics
|
69 |
+
*.png binary
|
70 |
+
*.jpg binary
|
71 |
+
*.jpeg binary
|
72 |
+
*.gif binary
|
73 |
+
*.tif binary
|
74 |
+
*.tiff binary
|
75 |
+
*.ico binary
|
76 |
+
# SVG treated as text by default.
|
77 |
+
*.svg text
|
78 |
+
# If you want to treat it as binary,
|
79 |
+
# use the following line instead.
|
80 |
+
# *.svg binary
|
81 |
+
*.eps binary
|
82 |
+
|
83 |
+
# Scripts
|
84 |
+
*.bash text eol=lf
|
85 |
+
*.fish text eol=lf
|
86 |
+
*.ksh text eol=lf
|
87 |
+
*.sh text eol=lf
|
88 |
+
*.zsh text eol=lf
|
89 |
+
# These are explicitly windows files and should use crlf
|
90 |
+
*.bat text eol=crlf
|
91 |
+
*.cmd text eol=crlf
|
92 |
+
*.ps1 text eol=crlf
|
93 |
+
|
94 |
+
# Serialisation
|
95 |
+
*.json text eol=lf
|
96 |
+
*.toml text eol=lf
|
97 |
+
*.xml text eol=lf
|
98 |
+
*.yaml text eol=lf
|
99 |
+
*.yml text eol=lf
|
100 |
+
|
101 |
+
# Archives
|
102 |
+
*.7z binary
|
103 |
+
*.bz binary
|
104 |
+
*.bz2 binary
|
105 |
+
*.bzip2 binary
|
106 |
+
*.gz binary
|
107 |
+
*.lz binary
|
108 |
+
*.lzma binary
|
109 |
+
*.rar binary
|
110 |
+
*.tar binary
|
111 |
+
*.taz binary
|
112 |
+
*.tbz binary
|
113 |
+
*.tbz2 binary
|
114 |
+
*.tgz binary
|
115 |
+
*.tlz binary
|
116 |
+
*.txz binary
|
117 |
+
*.xz binary
|
118 |
+
*.Z binary
|
119 |
+
*.zip binary
|
120 |
+
*.zst binary
|
121 |
+
|
122 |
+
# Text files where line endings should be preserved
|
123 |
+
*.patch -text
|
124 |
+
|
125 |
+
#
|
126 |
+
# Exclude files from exporting
|
127 |
+
#
|
128 |
+
|
129 |
+
.gitattributes export-ignore
|
130 |
+
.gitignore export-ignore
|
131 |
+
.gitkeep export-ignore
|
132 |
+
assets/image/Einstein.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/__pycache__/
|
2 |
+
.env
|
3 |
+
.mypy_cache/
|
4 |
+
.ruff_cache/
|
5 |
+
.venv/
|
6 |
+
logs/
|
7 |
+
results/
|
8 |
+
.github/
|
9 |
+
.trunk/
|
10 |
+
.idea/
|
11 |
+
renovate.json
|
12 |
+
.deepsource.toml
|
13 |
+
.pre-commit-config.yaml
|
14 |
+
compose.yaml
|
.python-version
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
3.13
|
.sonarlint/connectedMode.json
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"sonarCloudOrganization": "alphaspheredotai",
|
3 |
+
"projectKey": "AlphaSphereDotAI_chatacter_backend_app",
|
4 |
+
"region": "EU"
|
5 |
+
}
|
Dockerfile
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.13@sha256:5f69d22a88dd4cc4ee1576def19aef48c8faa1b566054c44291183831cbad13b AS builder
|
2 |
+
|
3 |
+
SHELL ["/bin/bash", "-c"]
|
4 |
+
|
5 |
+
ENV UV_LINK_MODE=copy \
|
6 |
+
UV_COMPILE_BYTECODE=1 \
|
7 |
+
UV_PYTHON_DOWNLOADS=0
|
8 |
+
|
9 |
+
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:68a26194ea8da0dbb014e8ae1d8ab08a469ee3ba0f4e2ac07b8bb66c0f8185c1 \
|
10 |
+
/uv /uvx /bin/
|
11 |
+
|
12 |
+
WORKDIR /app
|
13 |
+
|
14 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
15 |
+
--mount=type=bind,source=uv.lock,target=uv.lock \
|
16 |
+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
17 |
+
--mount=type=bind,source=README.md,target=README.md \
|
18 |
+
uv sync --no-install-project --no-dev --locked --no-editable
|
19 |
+
|
20 |
+
COPY . /app
|
21 |
+
|
22 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
23 |
+
uv sync --no-dev --locked --no-editable
|
24 |
+
|
25 |
+
FROM python:3.13-slim@sha256:f2fdaec50160418e0c2867ba3e254755edd067171725886d5d303fd7057bbf81 AS production
|
26 |
+
|
27 |
+
SHELL ["/bin/bash", "-c"]
|
28 |
+
|
29 |
+
ENV GRADIO_SERVER_PORT=7860 \
|
30 |
+
GRADIO_SERVER_NAME=0.0.0.0
|
31 |
+
|
32 |
+
RUN groupadd app && \
|
33 |
+
useradd -m -g app -s /bin/bash app && \
|
34 |
+
apt-get update > /dev/null && \
|
35 |
+
apt-get install -y --no-install-recommends curl > /dev/null && \
|
36 |
+
apt-get clean > /dev/null && \
|
37 |
+
rm -rf /var/lib/apt/lists/*
|
38 |
+
|
39 |
+
WORKDIR /home/app
|
40 |
+
|
41 |
+
COPY --from=builder --chown=app:app --chmod=555 /app/.venv /app/.venv
|
42 |
+
|
43 |
+
USER app
|
44 |
+
|
45 |
+
EXPOSE ${GRADIO_SERVER_PORT}
|
46 |
+
|
47 |
+
CMD ["/app/.venv/bin/chattr"]
|
README.md
CHANGED
@@ -1,12 +1,11 @@
|
|
1 |
---
|
2 |
title: Chattr
|
3 |
-
emoji:
|
4 |
colorFrom: gray
|
5 |
-
colorTo:
|
6 |
-
sdk:
|
7 |
-
|
8 |
-
|
9 |
-
pinned: false
|
10 |
---
|
11 |
|
12 |
-
|
|
|
1 |
---
|
2 |
title: Chattr
|
3 |
+
emoji: 💬
|
4 |
colorFrom: gray
|
5 |
+
colorTo: blue
|
6 |
+
sdk: docker
|
7 |
+
app_port: 7860
|
8 |
+
short_description: Chat with Characters
|
|
|
9 |
---
|
10 |
|
11 |
+
# **Chattr**: App part of the Chatacter Backend
|
assets/image/Einstein.jpg
ADDED
![]() |
Git LFS Details
|
assets/image/Napoleon.jpg
ADDED
![]() |
pyproject.toml
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "chattr"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "App part of the Chatacter Backend"
|
5 |
+
readme = "README.md"
|
6 |
+
authors = [
|
7 |
+
{ name = "Mohamed Hisham Abdelzaher", email = "[email protected]" },
|
8 |
+
]
|
9 |
+
requires-python = ">=3.12"
|
10 |
+
dependencies = [
|
11 |
+
"gradio>=5.36.2",
|
12 |
+
"langchain>=0.3.26",
|
13 |
+
"langchain-mcp-adapters>=0.1.9",
|
14 |
+
"langchain-openai>=0.3.27",
|
15 |
+
"langgraph>=0.5.2",
|
16 |
+
"loguru>=0.7.3",
|
17 |
+
"python-dotenv>=1.1.1",
|
18 |
+
]
|
19 |
+
|
20 |
+
[project.scripts]
|
21 |
+
chattr = "chattr.__main__:main"
|
22 |
+
|
23 |
+
[build-system]
|
24 |
+
requires = ["uv_build"]
|
25 |
+
build-backend = "uv_build"
|
26 |
+
|
27 |
+
[dependency-groups]
|
28 |
+
dev = ["ruff>=0.12.2", "ty>=0.0.1a12"]
|
src/chattr/__init__.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from os import getenv
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
from loguru import logger
|
7 |
+
from requests import get
|
8 |
+
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
SERVER_URL: str = getenv(key="SERVER_URL", default="127.0.0.1")
|
12 |
+
SERVER_PORT: int = getenv(key="SERVER_PORT", default=7860)
|
13 |
+
CURRENT_DATE: str = datetime.now().strftime(format="%Y-%m-%d_%H-%M-%S")
|
14 |
+
MCP_VOICE_GENERATOR: str = getenv(
|
15 |
+
key="MCP_VOICE_GENERATOR", default="http://localhost:8001/"
|
16 |
+
)
|
17 |
+
MCP_VIDEO_GENERATOR: str = getenv(
|
18 |
+
key="MCP_VIDEO_GENERATOR", default="http://localhost:8002/"
|
19 |
+
)
|
20 |
+
VECTOR_DATABASE_NAME: str = getenv(key="VECTOR_DATABASE_NAME", default="chattr")
|
21 |
+
DOCKER_MODEL_RUNNER_URL: str = getenv(
|
22 |
+
key="DOCKER_MODEL_RUNNER_URL", default="http://127.0.0.1:12434/engines/v1"
|
23 |
+
)
|
24 |
+
DOCKER_MODEL_RUNNER_MODEL_NAME: str = getenv(
|
25 |
+
key="DOCKER_MODEL_RUNNER_MODEL_NAME",
|
26 |
+
default="ai/qwen3:0.6B-Q4_0",
|
27 |
+
)
|
28 |
+
GROQ_URL: str = getenv(key="MODEL_URL", default="https://api.groq.com/openai/v1")
|
29 |
+
GROQ_MODEL_NAME: str = getenv(key="GROQ_MODEL_NAME", default="llama3-70b-8192")
|
30 |
+
|
31 |
+
BASE_DIR: Path = Path.cwd()
|
32 |
+
ASSETS_DIR: Path = BASE_DIR / "assets"
|
33 |
+
LOG_DIR: Path = BASE_DIR / "logs"
|
34 |
+
IMAGE_DIR: Path = ASSETS_DIR / "image"
|
35 |
+
AUDIO_DIR: Path = ASSETS_DIR / "audio"
|
36 |
+
VIDEO_DIR: Path = ASSETS_DIR / "video"
|
37 |
+
|
38 |
+
LOG_FILE_PATH: Path = LOG_DIR / f"{CURRENT_DATE}.log"
|
39 |
+
AUDIO_FILE_PATH: Path = AUDIO_DIR / f"{CURRENT_DATE}.wav"
|
40 |
+
VIDEO_FILE_PATH: Path = VIDEO_DIR / f"{CURRENT_DATE}.mp4"
|
41 |
+
|
42 |
+
ASSETS_DIR.mkdir(exist_ok=True)
|
43 |
+
IMAGE_DIR.mkdir(exist_ok=True)
|
44 |
+
AUDIO_DIR.mkdir(exist_ok=True)
|
45 |
+
VIDEO_DIR.mkdir(exist_ok=True)
|
46 |
+
LOG_DIR.mkdir(exist_ok=True)
|
47 |
+
|
48 |
+
MODEL_URL: str = (
|
49 |
+
DOCKER_MODEL_RUNNER_URL
|
50 |
+
if get(DOCKER_MODEL_RUNNER_URL, timeout=5).status_code == 200
|
51 |
+
else GROQ_URL
|
52 |
+
)
|
53 |
+
MODEL_NAME: str = (
|
54 |
+
DOCKER_MODEL_RUNNER_MODEL_NAME
|
55 |
+
if MODEL_URL == DOCKER_MODEL_RUNNER_URL
|
56 |
+
else GROQ_MODEL_NAME
|
57 |
+
)
|
58 |
+
MODEL_API_KEY: str = (
|
59 |
+
"not-needed" if MODEL_URL == DOCKER_MODEL_RUNNER_URL else getenv("GROQ_API_KEY")
|
60 |
+
)
|
61 |
+
MODEL_TEMPERATURE: float = float(getenv(key="MODEL_TEMPERATURE", default=0.0))
|
62 |
+
|
63 |
+
logger.add(
|
64 |
+
sink=LOG_FILE_PATH,
|
65 |
+
format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}",
|
66 |
+
colorize=True,
|
67 |
+
)
|
68 |
+
logger.info(f"Current date: {CURRENT_DATE}")
|
69 |
+
logger.info(f"Base directory: {BASE_DIR}")
|
70 |
+
logger.info(f"Assets directory: {ASSETS_DIR}")
|
71 |
+
logger.info(f"Log directory: {LOG_DIR}")
|
72 |
+
logger.info(f"Audio file path: {AUDIO_FILE_PATH}")
|
73 |
+
logger.info(f"Log file path: {LOG_FILE_PATH}")
|
74 |
+
logger.info(f"Model URL is going to be used is {MODEL_URL}")
|
75 |
+
logger.info(f"Model name is going to be used is {MODEL_NAME}")
|
76 |
+
logger.info(f"Model temperature is going to be used is {MODEL_TEMPERATURE}")
|
src/chattr/__main__.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from gradio import Blocks
|
2 |
+
|
3 |
+
from chattr import SERVER_PORT, SERVER_URL
|
4 |
+
from chattr.gui import app_block
|
5 |
+
|
6 |
+
|
7 |
+
def main() -> None:
|
8 |
+
"""
|
9 |
+
Initializes and launches the Gradio-based Chattr application server with API access, monitoring, and PWA support enabled.
|
10 |
+
"""
|
11 |
+
app: Blocks = app_block()
|
12 |
+
app.queue(api_open=True).launch(
|
13 |
+
server_name=SERVER_URL,
|
14 |
+
server_port=SERVER_PORT,
|
15 |
+
debug=True,
|
16 |
+
show_api=True,
|
17 |
+
enable_monitoring=True,
|
18 |
+
show_error=True,
|
19 |
+
pwa=True,
|
20 |
+
)
|
21 |
+
|
22 |
+
|
23 |
+
if __name__ == "__main__":
|
24 |
+
main()
|
src/chattr/graph.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
2 |
+
from langchain_core.tools import BaseTool
|
3 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
4 |
+
from langchain_openai import ChatOpenAI
|
5 |
+
from langgraph.graph import START, StateGraph
|
6 |
+
from langgraph.graph.message import MessagesState
|
7 |
+
from langgraph.graph.state import CompiledStateGraph
|
8 |
+
from langgraph.prebuilt import ToolNode, tools_condition
|
9 |
+
|
10 |
+
from chattr import (
|
11 |
+
ASSETS_DIR,
|
12 |
+
MODEL_API_KEY,
|
13 |
+
MODEL_NAME,
|
14 |
+
MODEL_TEMPERATURE,
|
15 |
+
MODEL_URL,
|
16 |
+
)
|
17 |
+
|
18 |
+
SYSTEM_MESSAGE: SystemMessage = SystemMessage(
|
19 |
+
content="You are a helpful assistant that can answer questions about the time."
|
20 |
+
)
|
21 |
+
|
22 |
+
|
23 |
+
async def create_graph() -> CompiledStateGraph:
|
24 |
+
"""
|
25 |
+
Asynchronously creates and compiles a conversational state graph for a time-answering assistant with integrated external tools.
|
26 |
+
|
27 |
+
Returns:
|
28 |
+
CompiledStateGraph: The compiled state graph ready for execution, with nodes for agent responses and tool invocation.
|
29 |
+
"""
|
30 |
+
_mcp_client = MultiServerMCPClient(
|
31 |
+
{
|
32 |
+
"time": {
|
33 |
+
"command": "docker",
|
34 |
+
"args": ["run", "-i", "--rm", "mcp/time"],
|
35 |
+
"transport": "stdio",
|
36 |
+
}
|
37 |
+
}
|
38 |
+
)
|
39 |
+
_tools: list[BaseTool] = await _mcp_client.get_tools()
|
40 |
+
try:
|
41 |
+
_model: ChatOpenAI = ChatOpenAI(
|
42 |
+
base_url=MODEL_URL,
|
43 |
+
model=MODEL_NAME,
|
44 |
+
api_key=MODEL_API_KEY,
|
45 |
+
temperature=MODEL_TEMPERATURE,
|
46 |
+
)
|
47 |
+
_model = _model.bind_tools(_tools, parallel_tool_calls=False)
|
48 |
+
except Exception as e:
|
49 |
+
raise RuntimeError(f"Failed to initialize ChatOpenAI model: {e}") from e
|
50 |
+
|
51 |
+
def call_model(state: MessagesState) -> MessagesState:
|
52 |
+
"""
|
53 |
+
Generate a new message state by invoking the chat model with the system message prepended to the current messages.
|
54 |
+
|
55 |
+
Parameters:
|
56 |
+
state (MessagesState): The current state containing a list of messages.
|
57 |
+
|
58 |
+
Returns:
|
59 |
+
MessagesState: A new state with the model's response appended to the messages.
|
60 |
+
"""
|
61 |
+
return {"messages": [_model.invoke([SYSTEM_MESSAGE] + state["messages"])]}
|
62 |
+
|
63 |
+
_builder: StateGraph = StateGraph(MessagesState)
|
64 |
+
_builder.add_node("agent", call_model)
|
65 |
+
_builder.add_node("tools", ToolNode(_tools))
|
66 |
+
_builder.add_edge(START, "agent")
|
67 |
+
_builder.add_conditional_edges("agent", tools_condition)
|
68 |
+
_builder.add_edge("tools", "agent")
|
69 |
+
graph: CompiledStateGraph = _builder.compile()
|
70 |
+
return graph
|
71 |
+
|
72 |
+
|
73 |
+
def draw_graph(graph: CompiledStateGraph) -> None:
|
74 |
+
"""
|
75 |
+
Render the compiled state graph as a Mermaid PNG image and save it to the assets directory.
|
76 |
+
"""
|
77 |
+
graph.get_graph().draw_mermaid_png(output_file_path=ASSETS_DIR / "graph.png")
|
78 |
+
|
79 |
+
|
80 |
+
if __name__ == "__main__":
|
81 |
+
import asyncio
|
82 |
+
|
83 |
+
async def test_graph():
|
84 |
+
"""
|
85 |
+
Asynchronously creates and tests the conversational state graph by sending a time-related query and printing the resulting messages.
|
86 |
+
"""
|
87 |
+
g: CompiledStateGraph = await create_graph()
|
88 |
+
|
89 |
+
messages = await g.ainvoke(
|
90 |
+
{"messages": [HumanMessage(content="What is the time?")]}
|
91 |
+
)
|
92 |
+
|
93 |
+
for m in messages["messages"]:
|
94 |
+
m.pretty_print()
|
95 |
+
|
96 |
+
asyncio.run(test_graph())
|
src/chattr/gui.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio
|
2 |
+
from gradio import Blocks, Button, Chatbot, ChatMessage, Row, Textbox
|
3 |
+
|
4 |
+
|
5 |
+
def generate_response(history: list[ChatMessage], thread_id: str) -> list[ChatMessage]:
|
6 |
+
"""
|
7 |
+
Appends an assistant message about a quarterly sales plot to the chat history for the specified thread ID.
|
8 |
+
|
9 |
+
If the thread ID is 0, raises a Gradio error prompting for a valid thread ID.
|
10 |
+
|
11 |
+
Returns:
|
12 |
+
The updated chat history including the new assistant message.
|
13 |
+
"""
|
14 |
+
if thread_id == 0:
|
15 |
+
raise gradio.Error("Please enter a thread ID.")
|
16 |
+
history.append(
|
17 |
+
ChatMessage(
|
18 |
+
role="assistant",
|
19 |
+
content=f"Here is the plot of quarterly sales for {thread_id}.",
|
20 |
+
metadata={
|
21 |
+
"title": "🛠️ Used tool Weather API",
|
22 |
+
},
|
23 |
+
)
|
24 |
+
)
|
25 |
+
return history
|
26 |
+
|
27 |
+
|
28 |
+
def app_block() -> Blocks:
|
29 |
+
"""
|
30 |
+
Constructs and returns the main Gradio chat application interface with a thread ID input, chatbot display, and control buttons.
|
31 |
+
|
32 |
+
Returns:
|
33 |
+
Blocks: The complete Gradio Blocks interface for the chat application.
|
34 |
+
"""
|
35 |
+
|
36 |
+
history = [
|
37 |
+
ChatMessage(role="assistant", content="How can I help you?"),
|
38 |
+
ChatMessage(role="user", content="Can you make me a plot of quarterly sales?"),
|
39 |
+
ChatMessage(
|
40 |
+
role="assistant",
|
41 |
+
content="I am happy to provide you that report and plot.",
|
42 |
+
),
|
43 |
+
]
|
44 |
+
with Blocks() as app:
|
45 |
+
with Row():
|
46 |
+
thread_id: Textbox = Textbox(label="Thread ID", info="Enter Thread ID")
|
47 |
+
|
48 |
+
chatbot: Chatbot = Chatbot(history, type="messages")
|
49 |
+
|
50 |
+
with Row():
|
51 |
+
generate_btn: Button = Button(value="Generate", variant="primary")
|
52 |
+
stop_btn: Button = Button(value="Stop", variant="stop")
|
53 |
+
_event = generate_btn.click(generate_response, [chatbot, thread_id], chatbot)
|
54 |
+
stop_btn.click(cancels=[_event])
|
55 |
+
return app
|
src/chattr/tools.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Literal
|
2 |
+
|
3 |
+
from langchain_core.tools import tool
|
4 |
+
|
5 |
+
|
6 |
+
@tool
|
7 |
+
def get_weather(city: Literal["nyc", "sf"]) -> str:
|
8 |
+
"""
|
9 |
+
Returns a weather description for the specified city.
|
10 |
+
|
11 |
+
Parameters:
|
12 |
+
city (Literal["nyc", "sf"]): The city for which to retrieve weather information.
|
13 |
+
|
14 |
+
Returns:
|
15 |
+
str: A message describing the weather in the specified city.
|
16 |
+
|
17 |
+
Raises:
|
18 |
+
AssertionError: If the city is not "nyc" or "sf".
|
19 |
+
"""
|
20 |
+
if city == "nyc":
|
21 |
+
return "It might be cloudy in nyc"
|
22 |
+
elif city == "sf":
|
23 |
+
return "It's always sunny in sf"
|
24 |
+
else:
|
25 |
+
raise AssertionError("Unknown city")
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|