Upload 62 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .codespellignore +0 -0
- .dockerignore +11 -0
- .env.example +11 -0
- .gitattributes +37 -35
- .github/workflows/integration-tests.yml +47 -0
- .github/workflows/unit-tests.yml +60 -0
- .gitignore +164 -0
- Dockerfile +8 -0
- LICENSE +21 -0
- Makefile +67 -0
- README.md +10 -10
- agent.py +148 -0
- app.py +78 -0
- langgraph.json +8 -0
- pyproject.toml +68 -0
- requirements.txt +7 -0
- src/react_agent/__init__.py +9 -0
- src/react_agent/context.py +46 -0
- src/react_agent/graph.py +116 -0
- src/react_agent/prompts.py +5 -0
- src/react_agent/state.py +60 -0
- src/react_agent/tools.py +29 -0
- src/react_agent/utils.py +27 -0
- static/studio_ui.png +3 -0
- testeclone/docktent/.codespellignore +0 -0
- testeclone/docktent/.dockerignore +11 -0
- testeclone/docktent/.env.example +11 -0
- testeclone/docktent/.gitattributes +36 -0
- testeclone/docktent/.github/workflows/integration-tests.yml +47 -0
- testeclone/docktent/.github/workflows/unit-tests.yml +60 -0
- testeclone/docktent/.gitignore +164 -0
- testeclone/docktent/Dockerfile +8 -0
- testeclone/docktent/LICENSE +21 -0
- testeclone/docktent/Makefile +67 -0
- testeclone/docktent/README.md +10 -0
- testeclone/docktent/agent.py +135 -0
- testeclone/docktent/app.py +80 -0
- testeclone/docktent/langgraph.json +8 -0
- testeclone/docktent/pyproject.toml +68 -0
- testeclone/docktent/requirements.txt +5 -0
- testeclone/docktent/src/react_agent/__init__.py +9 -0
- testeclone/docktent/src/react_agent/context.py +46 -0
- testeclone/docktent/src/react_agent/graph.py +116 -0
- testeclone/docktent/src/react_agent/prompts.py +5 -0
- testeclone/docktent/src/react_agent/state.py +60 -0
- testeclone/docktent/src/react_agent/tools.py +29 -0
- testeclone/docktent/src/react_agent/utils.py +27 -0
- testeclone/docktent/static/studio_ui.png +3 -0
- testeclone/docktent/tests/cassettes/103fe67e-a040-4e4e-aadb-b20a7057f904.yaml +0 -0
- testeclone/docktent/tests/conftest.py +7 -0
.codespellignore
ADDED
|
File without changes
|
.dockerignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
.git
|
| 5 |
+
.gitignore
|
| 6 |
+
static
|
| 7 |
+
tests
|
| 8 |
+
uv.lock
|
| 9 |
+
.codespellignore
|
| 10 |
+
Makefile
|
| 11 |
+
README.md
|
.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TAVILY_API_KEY=...
|
| 2 |
+
|
| 3 |
+
# To separate your traces from other application
|
| 4 |
+
LANGSMITH_PROJECT=react-agent
|
| 5 |
+
|
| 6 |
+
# The following depend on your selected configuration
|
| 7 |
+
|
| 8 |
+
## LLM choice:
|
| 9 |
+
ANTHROPIC_API_KEY=....
|
| 10 |
+
FIREWORKS_API_KEY=...
|
| 11 |
+
OPENAI_API_KEY=...
|
.gitattributes
CHANGED
|
@@ -1,35 +1,37 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz 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
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz 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 |
+
static/studio_ui.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
testeclone/docktent/static/studio_ui.png filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/integration-tests.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This workflow will run integration tests for the current project once per day
|
| 2 |
+
|
| 3 |
+
name: Integration Tests
|
| 4 |
+
|
| 5 |
+
on:
|
| 6 |
+
schedule:
|
| 7 |
+
- cron: "37 14 * * *" # Run at 7:37 AM Pacific Time (14:37 UTC) every day
|
| 8 |
+
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
| 9 |
+
|
| 10 |
+
permissions:
|
| 11 |
+
contents: read
|
| 12 |
+
|
| 13 |
+
# If another scheduled run starts while this workflow is still running,
|
| 14 |
+
# cancel the earlier run in favor of the next run.
|
| 15 |
+
concurrency:
|
| 16 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 17 |
+
cancel-in-progress: true
|
| 18 |
+
|
| 19 |
+
jobs:
|
| 20 |
+
integration-tests:
|
| 21 |
+
name: Integration Tests
|
| 22 |
+
strategy:
|
| 23 |
+
matrix:
|
| 24 |
+
os: [ubuntu-latest]
|
| 25 |
+
python-version: ["3.11", "3.12"]
|
| 26 |
+
runs-on: ${{ matrix.os }}
|
| 27 |
+
steps:
|
| 28 |
+
- uses: actions/checkout@v4
|
| 29 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 30 |
+
uses: actions/setup-python@v4
|
| 31 |
+
with:
|
| 32 |
+
python-version: ${{ matrix.python-version }}
|
| 33 |
+
- name: Install dependencies
|
| 34 |
+
run: |
|
| 35 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 36 |
+
uv venv
|
| 37 |
+
uv pip install -r pyproject.toml
|
| 38 |
+
uv pip install -U pytest-asyncio vcrpy
|
| 39 |
+
- name: Run integration tests
|
| 40 |
+
env:
|
| 41 |
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
| 42 |
+
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
|
| 43 |
+
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
| 44 |
+
LANGSMITH_TRACING: true
|
| 45 |
+
LANGSMITH_TEST_CACHE: tests/cassettes
|
| 46 |
+
run: |
|
| 47 |
+
uv run pytest tests/integration_tests
|
.github/workflows/unit-tests.yml
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This workflow will run unit tests for the current project
|
| 2 |
+
|
| 3 |
+
name: CI
|
| 4 |
+
|
| 5 |
+
on:
|
| 6 |
+
push:
|
| 7 |
+
branches: ["main"]
|
| 8 |
+
pull_request:
|
| 9 |
+
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
| 10 |
+
|
| 11 |
+
permissions:
|
| 12 |
+
contents: read
|
| 13 |
+
|
| 14 |
+
# If another push to the same PR or branch happens while this workflow is still running,
|
| 15 |
+
# cancel the earlier run in favor of the next run.
|
| 16 |
+
concurrency:
|
| 17 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 18 |
+
cancel-in-progress: true
|
| 19 |
+
|
| 20 |
+
jobs:
|
| 21 |
+
unit-tests:
|
| 22 |
+
name: Unit Tests
|
| 23 |
+
strategy:
|
| 24 |
+
matrix:
|
| 25 |
+
os: [ubuntu-latest]
|
| 26 |
+
python-version: ["3.11", "3.12"]
|
| 27 |
+
runs-on: ${{ matrix.os }}
|
| 28 |
+
steps:
|
| 29 |
+
- uses: actions/checkout@v4
|
| 30 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 31 |
+
uses: actions/setup-python@v4
|
| 32 |
+
with:
|
| 33 |
+
python-version: ${{ matrix.python-version }}
|
| 34 |
+
- name: Install dependencies
|
| 35 |
+
run: |
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 37 |
+
uv venv
|
| 38 |
+
uv pip install -r pyproject.toml
|
| 39 |
+
- name: Lint with ruff
|
| 40 |
+
run: |
|
| 41 |
+
uv pip install ruff
|
| 42 |
+
uv run ruff check .
|
| 43 |
+
- name: Lint with mypy
|
| 44 |
+
run: |
|
| 45 |
+
uv pip install mypy
|
| 46 |
+
uv run mypy --strict src/
|
| 47 |
+
- name: Check README spelling
|
| 48 |
+
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2
|
| 49 |
+
with:
|
| 50 |
+
ignore_words_file: .codespellignore
|
| 51 |
+
path: README.md
|
| 52 |
+
- name: Check code spelling
|
| 53 |
+
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2
|
| 54 |
+
with:
|
| 55 |
+
ignore_words_file: .codespellignore
|
| 56 |
+
path: src/
|
| 57 |
+
- name: Run tests with pytest
|
| 58 |
+
run: |
|
| 59 |
+
uv pip install pytest
|
| 60 |
+
uv run pytest tests/unit_tests
|
.gitignore
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# poetry
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 102 |
+
#poetry.lock
|
| 103 |
+
|
| 104 |
+
# pdm
|
| 105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 106 |
+
#pdm.lock
|
| 107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 108 |
+
# in version control.
|
| 109 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 110 |
+
.pdm.toml
|
| 111 |
+
.pdm-python
|
| 112 |
+
.pdm-build/
|
| 113 |
+
|
| 114 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 115 |
+
__pypackages__/
|
| 116 |
+
|
| 117 |
+
# Celery stuff
|
| 118 |
+
celerybeat-schedule
|
| 119 |
+
celerybeat.pid
|
| 120 |
+
|
| 121 |
+
# SageMath parsed files
|
| 122 |
+
*.sage.py
|
| 123 |
+
|
| 124 |
+
# Environments
|
| 125 |
+
.env
|
| 126 |
+
.venv
|
| 127 |
+
env/
|
| 128 |
+
venv/
|
| 129 |
+
ENV/
|
| 130 |
+
env.bak/
|
| 131 |
+
venv.bak/
|
| 132 |
+
|
| 133 |
+
# Spyder project settings
|
| 134 |
+
.spyderproject
|
| 135 |
+
.spyproject
|
| 136 |
+
|
| 137 |
+
# Rope project settings
|
| 138 |
+
.ropeproject
|
| 139 |
+
|
| 140 |
+
# mkdocs documentation
|
| 141 |
+
/site
|
| 142 |
+
|
| 143 |
+
# mypy
|
| 144 |
+
.mypy_cache/
|
| 145 |
+
.dmypy.json
|
| 146 |
+
dmypy.json
|
| 147 |
+
|
| 148 |
+
# Pyre type checker
|
| 149 |
+
.pyre/
|
| 150 |
+
|
| 151 |
+
# pytype static type analyzer
|
| 152 |
+
.pytype/
|
| 153 |
+
|
| 154 |
+
# Cython debug symbols
|
| 155 |
+
cython_debug/
|
| 156 |
+
|
| 157 |
+
# PyCharm
|
| 158 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 159 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 160 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 161 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 162 |
+
#.idea/
|
| 163 |
+
|
| 164 |
+
.langgraph_api
|
Dockerfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
WORKDIR /code
|
| 3 |
+
RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*
|
| 4 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 5 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 6 |
+
COPY . .
|
| 7 |
+
EXPOSE 7860
|
| 8 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 LangChain
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
Makefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: all format lint test tests test_watch integration_tests docker_tests help extended_tests
|
| 2 |
+
|
| 3 |
+
# Default target executed when no arguments are given to make.
|
| 4 |
+
all: help
|
| 5 |
+
|
| 6 |
+
# Define a variable for the test file path.
|
| 7 |
+
TEST_FILE ?= tests/unit_tests/
|
| 8 |
+
|
| 9 |
+
test:
|
| 10 |
+
python -m pytest $(TEST_FILE)
|
| 11 |
+
|
| 12 |
+
integration_tests:
|
| 13 |
+
python -m pytest tests/integration_tests
|
| 14 |
+
|
| 15 |
+
test_watch:
|
| 16 |
+
python -m ptw --snapshot-update --now . -- -vv tests/unit_tests
|
| 17 |
+
|
| 18 |
+
test_profile:
|
| 19 |
+
python -m pytest -vv tests/unit_tests/ --profile-svg
|
| 20 |
+
|
| 21 |
+
extended_tests:
|
| 22 |
+
python -m pytest --only-extended $(TEST_FILE)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
######################
|
| 26 |
+
# LINTING AND FORMATTING
|
| 27 |
+
######################
|
| 28 |
+
|
| 29 |
+
# Define a variable for Python and notebook files.
|
| 30 |
+
PYTHON_FILES=src/
|
| 31 |
+
MYPY_CACHE=.mypy_cache
|
| 32 |
+
lint format: PYTHON_FILES=.
|
| 33 |
+
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --diff-filter=d main | grep -E '\.py$$|\.ipynb$$')
|
| 34 |
+
lint_package: PYTHON_FILES=src
|
| 35 |
+
lint_tests: PYTHON_FILES=tests
|
| 36 |
+
lint_tests: MYPY_CACHE=.mypy_cache_test
|
| 37 |
+
|
| 38 |
+
lint lint_diff lint_package lint_tests:
|
| 39 |
+
python -m ruff check .
|
| 40 |
+
[ "$(PYTHON_FILES)" = "" ] || python -m ruff format $(PYTHON_FILES) --diff
|
| 41 |
+
[ "$(PYTHON_FILES)" = "" ] || python -m ruff check --select I $(PYTHON_FILES)
|
| 42 |
+
[ "$(PYTHON_FILES)" = "" ] || python -m mypy --strict $(PYTHON_FILES)
|
| 43 |
+
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) && python -m mypy --strict $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
| 44 |
+
|
| 45 |
+
format format_diff:
|
| 46 |
+
ruff format $(PYTHON_FILES)
|
| 47 |
+
ruff check --select I --fix $(PYTHON_FILES)
|
| 48 |
+
|
| 49 |
+
spell_check:
|
| 50 |
+
codespell --toml pyproject.toml
|
| 51 |
+
|
| 52 |
+
spell_fix:
|
| 53 |
+
codespell --toml pyproject.toml -w
|
| 54 |
+
|
| 55 |
+
######################
|
| 56 |
+
# HELP
|
| 57 |
+
######################
|
| 58 |
+
|
| 59 |
+
help:
|
| 60 |
+
@echo '----'
|
| 61 |
+
@echo 'format - run code formatters'
|
| 62 |
+
@echo 'lint - run linters'
|
| 63 |
+
@echo 'test - run unit tests'
|
| 64 |
+
@echo 'tests - run unit tests'
|
| 65 |
+
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
| 66 |
+
@echo 'test_watch - run unit tests in watch mode'
|
| 67 |
+
|
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Stateless-Agent-Service
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
# Stateless Agent Engine
|
agent.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from typing import Any, Dict, List, Optional
|
| 3 |
+
|
| 4 |
+
from langchain_openai import ChatOpenAI
|
| 5 |
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
| 6 |
+
from langchain_core.tools import tool
|
| 7 |
+
from langgraph.prebuilt import create_react_agent
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _make_file_tools(workspace: Path):
|
| 11 |
+
workspace = workspace.resolve()
|
| 12 |
+
|
| 13 |
+
@tool
|
| 14 |
+
def read_file(path: str) -> str:
|
| 15 |
+
"""Read the contents of a file from the workspace."""
|
| 16 |
+
full = (workspace / path).resolve()
|
| 17 |
+
if not str(full).startswith(str(workspace)):
|
| 18 |
+
return "Error: path outside workspace."
|
| 19 |
+
if not full.exists():
|
| 20 |
+
return f"Error: file not found: {path}"
|
| 21 |
+
return full.read_text(encoding="utf-8", errors="replace")
|
| 22 |
+
|
| 23 |
+
@tool
|
| 24 |
+
def write_file(path: str, content: str) -> str:
|
| 25 |
+
"""Write content to a file in the workspace (creates parent dirs)."""
|
| 26 |
+
full = (workspace / path).resolve()
|
| 27 |
+
if not str(full).startswith(str(workspace)):
|
| 28 |
+
return "Error: path outside workspace."
|
| 29 |
+
full.parent.mkdir(parents=True, exist_ok=True)
|
| 30 |
+
full.write_text(content, encoding="utf-8")
|
| 31 |
+
return f"Written: {path}"
|
| 32 |
+
|
| 33 |
+
@tool
|
| 34 |
+
def list_files(path: str = ".") -> str:
|
| 35 |
+
"""List files and directories under a path in the workspace."""
|
| 36 |
+
full = (workspace / path).resolve()
|
| 37 |
+
if not str(full).startswith(str(workspace)):
|
| 38 |
+
return "Error: path outside workspace."
|
| 39 |
+
if not full.exists():
|
| 40 |
+
return f"Error: path not found: {path}"
|
| 41 |
+
items = []
|
| 42 |
+
for p in sorted(full.iterdir()):
|
| 43 |
+
tag = "[DIR]" if p.is_dir() else "[FILE]"
|
| 44 |
+
items.append(f"{tag} {p.relative_to(workspace)}")
|
| 45 |
+
return "\n".join(items) if items else "(empty)"
|
| 46 |
+
|
| 47 |
+
@tool
|
| 48 |
+
def delete_file(path: str) -> str:
|
| 49 |
+
"""Delete a file or empty directory in the workspace."""
|
| 50 |
+
full = (workspace / path).resolve()
|
| 51 |
+
if not str(full).startswith(str(workspace)):
|
| 52 |
+
return "Error: path outside workspace."
|
| 53 |
+
if not full.exists():
|
| 54 |
+
return f"Error: not found: {path}"
|
| 55 |
+
if full.is_file():
|
| 56 |
+
full.unlink()
|
| 57 |
+
return f"Deleted file: {path}"
|
| 58 |
+
if full.is_dir():
|
| 59 |
+
full.rmdir()
|
| 60 |
+
return f"Deleted empty directory: {path}"
|
| 61 |
+
return f"Error: {path} is neither a file nor empty directory."
|
| 62 |
+
|
| 63 |
+
return [read_file, write_file, list_files, delete_file]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _build_system_message(workspace: Path) -> SystemMessage:
|
| 67 |
+
return SystemMessage(
|
| 68 |
+
content=(
|
| 69 |
+
f"Your workspace is at {workspace}.\n"
|
| 70 |
+
"You have file tools to read, write, list, and delete files here.\n"
|
| 71 |
+
"You have built-in web search capabilities through your model.\n\n"
|
| 72 |
+
"=== LEAN WORKSPACE RULES ===\n"
|
| 73 |
+
"1. KEEP ONLY HIGH-VALUE FILES: Retain only final artifacts the user would want\n"
|
| 74 |
+
" (reports, generated scripts, datasets, or files the user explicitly asked for).\n"
|
| 75 |
+
"2. AUTO-DELETE JUNK: Before finishing your response, use 'delete_file' to remove\n"
|
| 76 |
+
" all intermediate scratch files, .tmp files, raw HTML dumps, logs, or any file\n"
|
| 77 |
+
" that is NOT a final deliverable.\n"
|
| 78 |
+
"3. OVERWRITE — NEVER DUPLICATE: If you need to update a file, overwrite it in place\n"
|
| 79 |
+
" using 'write_file' with the same path. Never create versioned copies (e.g.,\n"
|
| 80 |
+
" report_v1.txt, report_v2.txt) unless the user explicitly asks for a new version.\n\n"
|
| 81 |
+
"When the user asks about their files or folder contents, use the file tools."
|
| 82 |
+
)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
async def run_agent(
|
| 87 |
+
api_key: str,
|
| 88 |
+
model: str,
|
| 89 |
+
provider: str,
|
| 90 |
+
workspace: Path,
|
| 91 |
+
user_message: str,
|
| 92 |
+
chat_history: List[Dict[str, str]],
|
| 93 |
+
) -> Dict[str, Any]:
|
| 94 |
+
tools = _make_file_tools(workspace)
|
| 95 |
+
|
| 96 |
+
if provider == "gemini":
|
| 97 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 98 |
+
|
| 99 |
+
llm = ChatGoogleGenerativeAI(
|
| 100 |
+
model=model or "gemini-2.0-flash",
|
| 101 |
+
google_api_key=api_key,
|
| 102 |
+
temperature=0.7,
|
| 103 |
+
)
|
| 104 |
+
else:
|
| 105 |
+
llm = ChatOpenAI(
|
| 106 |
+
model=model or "perplexity/llama-3-sonar-large-32k-online",
|
| 107 |
+
openai_api_key=api_key,
|
| 108 |
+
base_url="https://openrouter.ai/api/v1",
|
| 109 |
+
default_headers={
|
| 110 |
+
"HTTP-Referer": "https://huggingface.co/spaces/react-agent",
|
| 111 |
+
"X-Title": "React Agent",
|
| 112 |
+
},
|
| 113 |
+
temperature=0.7,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
agent = create_react_agent(model=llm, tools=tools)
|
| 117 |
+
|
| 118 |
+
messages = [_build_system_message(workspace)]
|
| 119 |
+
|
| 120 |
+
for msg in chat_history:
|
| 121 |
+
role = msg.get("role", "")
|
| 122 |
+
content = msg.get("content", "")
|
| 123 |
+
if role == "user":
|
| 124 |
+
messages.append(HumanMessage(content=content))
|
| 125 |
+
elif role == "assistant":
|
| 126 |
+
messages.append(AIMessage(content=content))
|
| 127 |
+
|
| 128 |
+
messages.append(HumanMessage(content=user_message))
|
| 129 |
+
|
| 130 |
+
result = await agent.ainvoke({"messages": messages})
|
| 131 |
+
|
| 132 |
+
agent_response = ""
|
| 133 |
+
for msg in reversed(result.get("messages", [])):
|
| 134 |
+
if isinstance(msg, AIMessage) and msg.content:
|
| 135 |
+
agent_response = msg.content
|
| 136 |
+
break
|
| 137 |
+
|
| 138 |
+
updated_chat_history = []
|
| 139 |
+
for msg in result.get("messages", []):
|
| 140 |
+
if isinstance(msg, HumanMessage):
|
| 141 |
+
updated_chat_history.append({"role": "user", "content": msg.content})
|
| 142 |
+
elif isinstance(msg, AIMessage):
|
| 143 |
+
updated_chat_history.append({"role": "assistant", "content": msg.content})
|
| 144 |
+
|
| 145 |
+
return {
|
| 146 |
+
"agent_response": agent_response,
|
| 147 |
+
"updated_chat_history": updated_chat_history,
|
| 148 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import shutil
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
from contextlib import asynccontextmanager
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI, HTTPException
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from agent import run_agent
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ChatRequest(BaseModel):
|
| 14 |
+
user_id: str
|
| 15 |
+
user_message: str
|
| 16 |
+
api_key: str
|
| 17 |
+
provider: str = "openrouter"
|
| 18 |
+
model: str = "perplexity/llama-3-sonar-large-32k-online"
|
| 19 |
+
current_chat_history: Optional[List[Dict[str, str]]] = None
|
| 20 |
+
user_files: Optional[Dict[str, str]] = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ChatResponse(BaseModel):
|
| 24 |
+
agent_response: str
|
| 25 |
+
updated_chat_history: List[Dict[str, str]]
|
| 26 |
+
updated_files: Dict[str, str]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@asynccontextmanager
|
| 30 |
+
async def lifespan(app: FastAPI):
|
| 31 |
+
yield
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
app = FastAPI(title="React Agent API", lifespan=lifespan)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@app.post("/chat", response_model=ChatResponse)
|
| 38 |
+
async def chat(request: ChatRequest):
|
| 39 |
+
if not request.api_key:
|
| 40 |
+
raise HTTPException(status_code=400, detail="api_key is required")
|
| 41 |
+
|
| 42 |
+
workspace = Path(f"/tmp/{request.user_id}")
|
| 43 |
+
workspace.mkdir(parents=True, exist_ok=True)
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
if request.user_files:
|
| 47 |
+
for filename, content_b64 in request.user_files.items():
|
| 48 |
+
filepath = workspace / filename
|
| 49 |
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
filepath.write_bytes(base64.b64decode(content_b64))
|
| 51 |
+
|
| 52 |
+
result = await run_agent(
|
| 53 |
+
api_key=request.api_key,
|
| 54 |
+
model=request.model,
|
| 55 |
+
provider=request.provider,
|
| 56 |
+
workspace=workspace,
|
| 57 |
+
user_message=request.user_message,
|
| 58 |
+
chat_history=request.current_chat_history or [],
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
updated_files = {}
|
| 62 |
+
if workspace.exists():
|
| 63 |
+
for filepath in sorted(workspace.rglob("*")):
|
| 64 |
+
if filepath.is_file():
|
| 65 |
+
rel_path = str(filepath.relative_to(workspace))
|
| 66 |
+
updated_files[rel_path] = base64.b64encode(
|
| 67 |
+
filepath.read_bytes()
|
| 68 |
+
).decode()
|
| 69 |
+
|
| 70 |
+
return ChatResponse(
|
| 71 |
+
agent_response=result["agent_response"],
|
| 72 |
+
updated_chat_history=result["updated_chat_history"],
|
| 73 |
+
updated_files=updated_files,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
finally:
|
| 77 |
+
if workspace.exists():
|
| 78 |
+
shutil.rmtree(workspace, ignore_errors=True)
|
langgraph.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://langgra.ph/schema.json",
|
| 3 |
+
"dependencies": ["."],
|
| 4 |
+
"graphs": {
|
| 5 |
+
"agent": "./src/react_agent/graph.py:graph"
|
| 6 |
+
},
|
| 7 |
+
"env": ".env"
|
| 8 |
+
}
|
pyproject.toml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "react-agent"
|
| 3 |
+
version = "0.0.1"
|
| 4 |
+
description = "Starter template for making a custom Reasoning and Action agent (using tool calling) in LangGraph."
|
| 5 |
+
authors = [
|
| 6 |
+
{ name = "William Fu-Hinthorn", email = "13333726+hinthornw@users.noreply.github.com" },
|
| 7 |
+
]
|
| 8 |
+
readme = "README.md"
|
| 9 |
+
license = { text = "MIT" }
|
| 10 |
+
requires-python = ">=3.11,<4.0"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"langgraph>=1.0.0",
|
| 13 |
+
"langchain-openai>=0.2.0",
|
| 14 |
+
"langchain-core>=0.3.0",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
[project.optional-dependencies]
|
| 19 |
+
dev = ["mypy>=1.11.1", "ruff>=0.6.1", "fastapi>=0.115.0", "uvicorn[standard]>=0.32.0"]
|
| 20 |
+
|
| 21 |
+
[build-system]
|
| 22 |
+
requires = ["setuptools>=73.0.0", "wheel"]
|
| 23 |
+
build-backend = "setuptools.build_meta"
|
| 24 |
+
|
| 25 |
+
[tool.setuptools]
|
| 26 |
+
packages = ["langgraph.templates.react_agent", "react_agent"]
|
| 27 |
+
[tool.setuptools.package-dir]
|
| 28 |
+
"langgraph.templates.react_agent" = "src/react_agent"
|
| 29 |
+
"react_agent" = "src/react_agent"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
[tool.setuptools.package-data]
|
| 33 |
+
"*" = ["py.typed"]
|
| 34 |
+
|
| 35 |
+
[tool.ruff]
|
| 36 |
+
lint.select = [
|
| 37 |
+
"E", # pycodestyle
|
| 38 |
+
"F", # pyflakes
|
| 39 |
+
"I", # isort
|
| 40 |
+
"D", # pydocstyle
|
| 41 |
+
"D401", # First line should be in imperative mood
|
| 42 |
+
"T201",
|
| 43 |
+
"UP",
|
| 44 |
+
]
|
| 45 |
+
lint.ignore = [
|
| 46 |
+
"UP006",
|
| 47 |
+
"UP007",
|
| 48 |
+
# We actually do want to import from typing_extensions
|
| 49 |
+
"UP035",
|
| 50 |
+
# Relax the convention by _not_ requiring documentation for every function parameter.
|
| 51 |
+
"D417",
|
| 52 |
+
"E501",
|
| 53 |
+
]
|
| 54 |
+
[tool.ruff.lint.per-file-ignores]
|
| 55 |
+
"tests/*" = ["D", "UP"]
|
| 56 |
+
[tool.ruff.lint.pydocstyle]
|
| 57 |
+
convention = "google"
|
| 58 |
+
|
| 59 |
+
[tool.uv]
|
| 60 |
+
# CVE-2026-32597: pyjwt < 2.12.0 accepts unknown `crit` header extensions.
|
| 61 |
+
# Transitive via langgraph-api -> pyjwt. Remove once langgraph-api requires pyjwt>=2.12.0.
|
| 62 |
+
constraint-dependencies = ["pyjwt>=2.12.0"]
|
| 63 |
+
|
| 64 |
+
[dependency-groups]
|
| 65 |
+
dev = [
|
| 66 |
+
"anyio>=4.7.0",
|
| 67 |
+
"pytest>=9.0.3",
|
| 68 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
| 3 |
+
langchain==0.3.19
|
| 4 |
+
langchain-openai==0.3.7
|
| 5 |
+
langchain-google-genai==2.1.4
|
| 6 |
+
langgraph==0.3.5
|
| 7 |
+
pydantic==2.10.5
|
src/react_agent/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""React Agent.
|
| 2 |
+
|
| 3 |
+
This module defines a custom reasoning and action agent graph.
|
| 4 |
+
It invokes tools in a simple loop.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from react_agent.graph import graph
|
| 8 |
+
|
| 9 |
+
__all__ = ["graph"]
|
src/react_agent/context.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Define the configurable parameters for the agent."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from dataclasses import dataclass, field, fields
|
| 7 |
+
from typing import Annotated
|
| 8 |
+
|
| 9 |
+
from . import prompts
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(kw_only=True)
|
| 13 |
+
class Context:
|
| 14 |
+
"""The context for the agent."""
|
| 15 |
+
|
| 16 |
+
system_prompt: str = field(
|
| 17 |
+
default=prompts.SYSTEM_PROMPT,
|
| 18 |
+
metadata={
|
| 19 |
+
"description": "The system prompt to use for the agent's interactions. "
|
| 20 |
+
"This prompt sets the context and behavior for the agent."
|
| 21 |
+
},
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
|
| 25 |
+
default="anthropic/claude-sonnet-4-5-20250929",
|
| 26 |
+
metadata={
|
| 27 |
+
"description": "The name of the language model to use for the agent's main interactions. "
|
| 28 |
+
"Should be in the form: provider/model-name."
|
| 29 |
+
},
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
max_search_results: int = field(
|
| 33 |
+
default=10,
|
| 34 |
+
metadata={
|
| 35 |
+
"description": "The maximum number of search results to return for each search query."
|
| 36 |
+
},
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def __post_init__(self) -> None:
|
| 40 |
+
"""Fetch env vars for attributes that were not passed as args."""
|
| 41 |
+
for f in fields(self):
|
| 42 |
+
if not f.init:
|
| 43 |
+
continue
|
| 44 |
+
|
| 45 |
+
if getattr(self, f.name) == f.default:
|
| 46 |
+
setattr(self, f.name, os.environ.get(f.name.upper(), f.default))
|
src/react_agent/graph.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Define a custom Reasoning and Action agent.
|
| 2 |
+
|
| 3 |
+
Works with a chat model with tool calling support.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import UTC, datetime
|
| 7 |
+
from typing import Dict, List, Literal, cast
|
| 8 |
+
|
| 9 |
+
from langchain_core.messages import AIMessage
|
| 10 |
+
from langgraph.graph import StateGraph
|
| 11 |
+
from langgraph.prebuilt import ToolNode
|
| 12 |
+
from langgraph.runtime import Runtime
|
| 13 |
+
|
| 14 |
+
from react_agent.context import Context
|
| 15 |
+
from react_agent.state import InputState, State
|
| 16 |
+
from react_agent.tools import TOOLS
|
| 17 |
+
from react_agent.utils import load_chat_model
|
| 18 |
+
|
| 19 |
+
# Define the function that calls the model
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
async def call_model(
|
| 23 |
+
state: State, runtime: Runtime[Context]
|
| 24 |
+
) -> Dict[str, List[AIMessage]]:
|
| 25 |
+
"""Call the LLM powering our "agent".
|
| 26 |
+
|
| 27 |
+
This function prepares the prompt, initializes the model, and processes the response.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
state (State): The current state of the conversation.
|
| 31 |
+
config (RunnableConfig): Configuration for the model run.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
dict: A dictionary containing the model's response message.
|
| 35 |
+
"""
|
| 36 |
+
# Initialize the model with tool binding. Change the model or add more tools here.
|
| 37 |
+
model = load_chat_model(runtime.context.model).bind_tools(TOOLS)
|
| 38 |
+
|
| 39 |
+
# Format the system prompt. Customize this to change the agent's behavior.
|
| 40 |
+
system_message = runtime.context.system_prompt.format(
|
| 41 |
+
system_time=datetime.now(tz=UTC).isoformat()
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Get the model's response
|
| 45 |
+
response = cast( # type: ignore[redundant-cast]
|
| 46 |
+
AIMessage,
|
| 47 |
+
await model.ainvoke(
|
| 48 |
+
[{"role": "system", "content": system_message}, *state.messages]
|
| 49 |
+
),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Handle the case when it's the last step and the model still wants to use a tool
|
| 53 |
+
if state.is_last_step and response.tool_calls:
|
| 54 |
+
return {
|
| 55 |
+
"messages": [
|
| 56 |
+
AIMessage(
|
| 57 |
+
id=response.id,
|
| 58 |
+
content="Sorry, I could not find an answer to your question in the specified number of steps.",
|
| 59 |
+
)
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# Return the model's response as a list to be added to existing messages
|
| 64 |
+
return {"messages": [response]}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Define a new graph
|
| 68 |
+
|
| 69 |
+
builder = StateGraph(State, input_schema=InputState, context_schema=Context)
|
| 70 |
+
|
| 71 |
+
# Define the two nodes we will cycle between
|
| 72 |
+
builder.add_node(call_model)
|
| 73 |
+
builder.add_node("tools", ToolNode(TOOLS))
|
| 74 |
+
|
| 75 |
+
# Set the entrypoint as `call_model`
|
| 76 |
+
# This means that this node is the first one called
|
| 77 |
+
builder.add_edge("__start__", "call_model")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def route_model_output(state: State) -> Literal["__end__", "tools"]:
|
| 81 |
+
"""Determine the next node based on the model's output.
|
| 82 |
+
|
| 83 |
+
This function checks if the model's last message contains tool calls.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
state (State): The current state of the conversation.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
str: The name of the next node to call ("__end__" or "tools").
|
| 90 |
+
"""
|
| 91 |
+
last_message = state.messages[-1]
|
| 92 |
+
if not isinstance(last_message, AIMessage):
|
| 93 |
+
raise ValueError(
|
| 94 |
+
f"Expected AIMessage in output edges, but got {type(last_message).__name__}"
|
| 95 |
+
)
|
| 96 |
+
# If there is no tool call, then we finish
|
| 97 |
+
if not last_message.tool_calls:
|
| 98 |
+
return "__end__"
|
| 99 |
+
# Otherwise we execute the requested actions
|
| 100 |
+
return "tools"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# Add a conditional edge to determine the next step after `call_model`
|
| 104 |
+
builder.add_conditional_edges(
|
| 105 |
+
"call_model",
|
| 106 |
+
# After call_model finishes running, the next node(s) are scheduled
|
| 107 |
+
# based on the output from route_model_output
|
| 108 |
+
route_model_output,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Add a normal edge from `tools` to `call_model`
|
| 112 |
+
# This creates a cycle: after using tools, we always return to the model
|
| 113 |
+
builder.add_edge("tools", "call_model")
|
| 114 |
+
|
| 115 |
+
# Compile the builder into an executable graph
|
| 116 |
+
graph = builder.compile(name="ReAct Agent")
|
src/react_agent/prompts.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Default prompts used by the agent."""
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a helpful AI assistant.
|
| 4 |
+
|
| 5 |
+
System time: {system_time}"""
|
src/react_agent/state.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Define the state structures for the agent."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Sequence
|
| 7 |
+
|
| 8 |
+
from langchain_core.messages import AnyMessage
|
| 9 |
+
from langgraph.graph import add_messages
|
| 10 |
+
from langgraph.managed import IsLastStep
|
| 11 |
+
from typing_extensions import Annotated
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class InputState:
|
| 16 |
+
"""Defines the input state for the agent, representing a narrower interface to the outside world.
|
| 17 |
+
|
| 18 |
+
This class is used to define the initial state and structure of incoming data.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
messages: Annotated[Sequence[AnyMessage], add_messages] = field(
|
| 22 |
+
default_factory=list
|
| 23 |
+
)
|
| 24 |
+
"""
|
| 25 |
+
Messages tracking the primary execution state of the agent.
|
| 26 |
+
|
| 27 |
+
Typically accumulates a pattern of:
|
| 28 |
+
1. HumanMessage - user input
|
| 29 |
+
2. AIMessage with .tool_calls - agent picking tool(s) to use to collect information
|
| 30 |
+
3. ToolMessage(s) - the responses (or errors) from the executed tools
|
| 31 |
+
4. AIMessage without .tool_calls - agent responding in unstructured format to the user
|
| 32 |
+
5. HumanMessage - user responds with the next conversational turn
|
| 33 |
+
|
| 34 |
+
Steps 2-5 may repeat as needed.
|
| 35 |
+
|
| 36 |
+
The `add_messages` annotation ensures that new messages are merged with existing ones,
|
| 37 |
+
updating by ID to maintain an "append-only" state unless a message with the same ID is provided.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class State(InputState):
|
| 43 |
+
"""Represents the complete state of the agent, extending InputState with additional attributes.
|
| 44 |
+
|
| 45 |
+
This class can be used to store any information needed throughout the agent's lifecycle.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
is_last_step: IsLastStep = field(default=False)
|
| 49 |
+
"""
|
| 50 |
+
Indicates whether the current step is the last one before the graph raises an error.
|
| 51 |
+
|
| 52 |
+
This is a 'managed' variable, controlled by the state machine rather than user code.
|
| 53 |
+
It is set to 'True' when the step count reaches recursion_limit - 1.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
# Additional attributes can be added here as needed.
|
| 57 |
+
# Common examples include:
|
| 58 |
+
# retrieved_documents: List[Document] = field(default_factory=list)
|
| 59 |
+
# extracted_entities: Dict[str, Any] = field(default_factory=dict)
|
| 60 |
+
# api_connections: Dict[str, Any] = field(default_factory=dict)
|
src/react_agent/tools.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""This module provides example tools for web scraping and search functionality.
|
| 2 |
+
|
| 3 |
+
It includes a basic Tavily search function (as an example)
|
| 4 |
+
|
| 5 |
+
These tools are intended as free examples to get started. For production use,
|
| 6 |
+
consider implementing more robust and specialized tools tailored to your needs.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Any, Callable, List, Optional, cast
|
| 10 |
+
|
| 11 |
+
from langchain_tavily import TavilySearch
|
| 12 |
+
from langgraph.runtime import get_runtime
|
| 13 |
+
|
| 14 |
+
from react_agent.context import Context
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
async def search(query: str) -> Optional[dict[str, Any]]:
|
| 18 |
+
"""Search for general web results.
|
| 19 |
+
|
| 20 |
+
This function performs a search using the Tavily search engine, which is designed
|
| 21 |
+
to provide comprehensive, accurate, and trusted results. It's particularly useful
|
| 22 |
+
for answering questions about current events.
|
| 23 |
+
"""
|
| 24 |
+
runtime = get_runtime(Context)
|
| 25 |
+
wrapped = TavilySearch(max_results=runtime.context.max_search_results)
|
| 26 |
+
return cast(dict[str, Any], await wrapped.ainvoke({"query": query}))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
TOOLS: List[Callable[..., Any]] = [search]
|
src/react_agent/utils.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility & helper functions."""
|
| 2 |
+
|
| 3 |
+
from langchain.chat_models import init_chat_model
|
| 4 |
+
from langchain_core.language_models import BaseChatModel
|
| 5 |
+
from langchain_core.messages import BaseMessage
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def get_message_text(msg: BaseMessage) -> str:
|
| 9 |
+
"""Get the text content of a message."""
|
| 10 |
+
content = msg.content
|
| 11 |
+
if isinstance(content, str):
|
| 12 |
+
return content
|
| 13 |
+
elif isinstance(content, dict):
|
| 14 |
+
return content.get("text", "")
|
| 15 |
+
else:
|
| 16 |
+
txts = [c if isinstance(c, str) else (c.get("text") or "") for c in content]
|
| 17 |
+
return "".join(txts).strip()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load_chat_model(fully_specified_name: str) -> BaseChatModel:
|
| 21 |
+
"""Load a chat model from a fully specified name.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
fully_specified_name (str): String in the format 'provider/model'.
|
| 25 |
+
"""
|
| 26 |
+
provider, model = fully_specified_name.split("/", maxsplit=1)
|
| 27 |
+
return init_chat_model(model, model_provider=provider)
|
static/studio_ui.png
ADDED
|
Git LFS Details
|
testeclone/docktent/.codespellignore
ADDED
|
File without changes
|
testeclone/docktent/.dockerignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
.git
|
| 5 |
+
.gitignore
|
| 6 |
+
static
|
| 7 |
+
tests
|
| 8 |
+
uv.lock
|
| 9 |
+
.codespellignore
|
| 10 |
+
Makefile
|
| 11 |
+
README.md
|
testeclone/docktent/.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TAVILY_API_KEY=...
|
| 2 |
+
|
| 3 |
+
# To separate your traces from other application
|
| 4 |
+
LANGSMITH_PROJECT=react-agent
|
| 5 |
+
|
| 6 |
+
# The following depend on your selected configuration
|
| 7 |
+
|
| 8 |
+
## LLM choice:
|
| 9 |
+
ANTHROPIC_API_KEY=....
|
| 10 |
+
FIREWORKS_API_KEY=...
|
| 11 |
+
OPENAI_API_KEY=...
|
testeclone/docktent/.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz 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 |
+
static/studio_ui.png filter=lfs diff=lfs merge=lfs -text
|
testeclone/docktent/.github/workflows/integration-tests.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This workflow will run integration tests for the current project once per day
|
| 2 |
+
|
| 3 |
+
name: Integration Tests
|
| 4 |
+
|
| 5 |
+
on:
|
| 6 |
+
schedule:
|
| 7 |
+
- cron: "37 14 * * *" # Run at 7:37 AM Pacific Time (14:37 UTC) every day
|
| 8 |
+
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
| 9 |
+
|
| 10 |
+
permissions:
|
| 11 |
+
contents: read
|
| 12 |
+
|
| 13 |
+
# If another scheduled run starts while this workflow is still running,
|
| 14 |
+
# cancel the earlier run in favor of the next run.
|
| 15 |
+
concurrency:
|
| 16 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 17 |
+
cancel-in-progress: true
|
| 18 |
+
|
| 19 |
+
jobs:
|
| 20 |
+
integration-tests:
|
| 21 |
+
name: Integration Tests
|
| 22 |
+
strategy:
|
| 23 |
+
matrix:
|
| 24 |
+
os: [ubuntu-latest]
|
| 25 |
+
python-version: ["3.11", "3.12"]
|
| 26 |
+
runs-on: ${{ matrix.os }}
|
| 27 |
+
steps:
|
| 28 |
+
- uses: actions/checkout@v4
|
| 29 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 30 |
+
uses: actions/setup-python@v4
|
| 31 |
+
with:
|
| 32 |
+
python-version: ${{ matrix.python-version }}
|
| 33 |
+
- name: Install dependencies
|
| 34 |
+
run: |
|
| 35 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 36 |
+
uv venv
|
| 37 |
+
uv pip install -r pyproject.toml
|
| 38 |
+
uv pip install -U pytest-asyncio vcrpy
|
| 39 |
+
- name: Run integration tests
|
| 40 |
+
env:
|
| 41 |
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
| 42 |
+
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
|
| 43 |
+
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
| 44 |
+
LANGSMITH_TRACING: true
|
| 45 |
+
LANGSMITH_TEST_CACHE: tests/cassettes
|
| 46 |
+
run: |
|
| 47 |
+
uv run pytest tests/integration_tests
|
testeclone/docktent/.github/workflows/unit-tests.yml
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This workflow will run unit tests for the current project
|
| 2 |
+
|
| 3 |
+
name: CI
|
| 4 |
+
|
| 5 |
+
on:
|
| 6 |
+
push:
|
| 7 |
+
branches: ["main"]
|
| 8 |
+
pull_request:
|
| 9 |
+
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
| 10 |
+
|
| 11 |
+
permissions:
|
| 12 |
+
contents: read
|
| 13 |
+
|
| 14 |
+
# If another push to the same PR or branch happens while this workflow is still running,
|
| 15 |
+
# cancel the earlier run in favor of the next run.
|
| 16 |
+
concurrency:
|
| 17 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 18 |
+
cancel-in-progress: true
|
| 19 |
+
|
| 20 |
+
jobs:
|
| 21 |
+
unit-tests:
|
| 22 |
+
name: Unit Tests
|
| 23 |
+
strategy:
|
| 24 |
+
matrix:
|
| 25 |
+
os: [ubuntu-latest]
|
| 26 |
+
python-version: ["3.11", "3.12"]
|
| 27 |
+
runs-on: ${{ matrix.os }}
|
| 28 |
+
steps:
|
| 29 |
+
- uses: actions/checkout@v4
|
| 30 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 31 |
+
uses: actions/setup-python@v4
|
| 32 |
+
with:
|
| 33 |
+
python-version: ${{ matrix.python-version }}
|
| 34 |
+
- name: Install dependencies
|
| 35 |
+
run: |
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 37 |
+
uv venv
|
| 38 |
+
uv pip install -r pyproject.toml
|
| 39 |
+
- name: Lint with ruff
|
| 40 |
+
run: |
|
| 41 |
+
uv pip install ruff
|
| 42 |
+
uv run ruff check .
|
| 43 |
+
- name: Lint with mypy
|
| 44 |
+
run: |
|
| 45 |
+
uv pip install mypy
|
| 46 |
+
uv run mypy --strict src/
|
| 47 |
+
- name: Check README spelling
|
| 48 |
+
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2
|
| 49 |
+
with:
|
| 50 |
+
ignore_words_file: .codespellignore
|
| 51 |
+
path: README.md
|
| 52 |
+
- name: Check code spelling
|
| 53 |
+
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2
|
| 54 |
+
with:
|
| 55 |
+
ignore_words_file: .codespellignore
|
| 56 |
+
path: src/
|
| 57 |
+
- name: Run tests with pytest
|
| 58 |
+
run: |
|
| 59 |
+
uv pip install pytest
|
| 60 |
+
uv run pytest tests/unit_tests
|
testeclone/docktent/.gitignore
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# poetry
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 102 |
+
#poetry.lock
|
| 103 |
+
|
| 104 |
+
# pdm
|
| 105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 106 |
+
#pdm.lock
|
| 107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 108 |
+
# in version control.
|
| 109 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 110 |
+
.pdm.toml
|
| 111 |
+
.pdm-python
|
| 112 |
+
.pdm-build/
|
| 113 |
+
|
| 114 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 115 |
+
__pypackages__/
|
| 116 |
+
|
| 117 |
+
# Celery stuff
|
| 118 |
+
celerybeat-schedule
|
| 119 |
+
celerybeat.pid
|
| 120 |
+
|
| 121 |
+
# SageMath parsed files
|
| 122 |
+
*.sage.py
|
| 123 |
+
|
| 124 |
+
# Environments
|
| 125 |
+
.env
|
| 126 |
+
.venv
|
| 127 |
+
env/
|
| 128 |
+
venv/
|
| 129 |
+
ENV/
|
| 130 |
+
env.bak/
|
| 131 |
+
venv.bak/
|
| 132 |
+
|
| 133 |
+
# Spyder project settings
|
| 134 |
+
.spyderproject
|
| 135 |
+
.spyproject
|
| 136 |
+
|
| 137 |
+
# Rope project settings
|
| 138 |
+
.ropeproject
|
| 139 |
+
|
| 140 |
+
# mkdocs documentation
|
| 141 |
+
/site
|
| 142 |
+
|
| 143 |
+
# mypy
|
| 144 |
+
.mypy_cache/
|
| 145 |
+
.dmypy.json
|
| 146 |
+
dmypy.json
|
| 147 |
+
|
| 148 |
+
# Pyre type checker
|
| 149 |
+
.pyre/
|
| 150 |
+
|
| 151 |
+
# pytype static type analyzer
|
| 152 |
+
.pytype/
|
| 153 |
+
|
| 154 |
+
# Cython debug symbols
|
| 155 |
+
cython_debug/
|
| 156 |
+
|
| 157 |
+
# PyCharm
|
| 158 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 159 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 160 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 161 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 162 |
+
#.idea/
|
| 163 |
+
|
| 164 |
+
.langgraph_api
|
testeclone/docktent/Dockerfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
WORKDIR /code
|
| 3 |
+
RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*
|
| 4 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 5 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 6 |
+
COPY . .
|
| 7 |
+
EXPOSE 7860
|
| 8 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
testeclone/docktent/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 LangChain
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
testeclone/docktent/Makefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: all format lint test tests test_watch integration_tests docker_tests help extended_tests
|
| 2 |
+
|
| 3 |
+
# Default target executed when no arguments are given to make.
|
| 4 |
+
all: help
|
| 5 |
+
|
| 6 |
+
# Define a variable for the test file path.
|
| 7 |
+
TEST_FILE ?= tests/unit_tests/
|
| 8 |
+
|
| 9 |
+
test:
|
| 10 |
+
python -m pytest $(TEST_FILE)
|
| 11 |
+
|
| 12 |
+
integration_tests:
|
| 13 |
+
python -m pytest tests/integration_tests
|
| 14 |
+
|
| 15 |
+
test_watch:
|
| 16 |
+
python -m ptw --snapshot-update --now . -- -vv tests/unit_tests
|
| 17 |
+
|
| 18 |
+
test_profile:
|
| 19 |
+
python -m pytest -vv tests/unit_tests/ --profile-svg
|
| 20 |
+
|
| 21 |
+
extended_tests:
|
| 22 |
+
python -m pytest --only-extended $(TEST_FILE)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
######################
|
| 26 |
+
# LINTING AND FORMATTING
|
| 27 |
+
######################
|
| 28 |
+
|
| 29 |
+
# Define a variable for Python and notebook files.
|
| 30 |
+
PYTHON_FILES=src/
|
| 31 |
+
MYPY_CACHE=.mypy_cache
|
| 32 |
+
lint format: PYTHON_FILES=.
|
| 33 |
+
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --diff-filter=d main | grep -E '\.py$$|\.ipynb$$')
|
| 34 |
+
lint_package: PYTHON_FILES=src
|
| 35 |
+
lint_tests: PYTHON_FILES=tests
|
| 36 |
+
lint_tests: MYPY_CACHE=.mypy_cache_test
|
| 37 |
+
|
| 38 |
+
lint lint_diff lint_package lint_tests:
|
| 39 |
+
python -m ruff check .
|
| 40 |
+
[ "$(PYTHON_FILES)" = "" ] || python -m ruff format $(PYTHON_FILES) --diff
|
| 41 |
+
[ "$(PYTHON_FILES)" = "" ] || python -m ruff check --select I $(PYTHON_FILES)
|
| 42 |
+
[ "$(PYTHON_FILES)" = "" ] || python -m mypy --strict $(PYTHON_FILES)
|
| 43 |
+
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) && python -m mypy --strict $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
| 44 |
+
|
| 45 |
+
format format_diff:
|
| 46 |
+
ruff format $(PYTHON_FILES)
|
| 47 |
+
ruff check --select I --fix $(PYTHON_FILES)
|
| 48 |
+
|
| 49 |
+
spell_check:
|
| 50 |
+
codespell --toml pyproject.toml
|
| 51 |
+
|
| 52 |
+
spell_fix:
|
| 53 |
+
codespell --toml pyproject.toml -w
|
| 54 |
+
|
| 55 |
+
######################
|
| 56 |
+
# HELP
|
| 57 |
+
######################
|
| 58 |
+
|
| 59 |
+
help:
|
| 60 |
+
@echo '----'
|
| 61 |
+
@echo 'format - run code formatters'
|
| 62 |
+
@echo 'lint - run linters'
|
| 63 |
+
@echo 'test - run unit tests'
|
| 64 |
+
@echo 'tests - run unit tests'
|
| 65 |
+
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
| 66 |
+
@echo 'test_watch - run unit tests in watch mode'
|
| 67 |
+
|
testeclone/docktent/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Stateless-Agent-Service
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
# Stateless Agent Engine
|
testeclone/docktent/agent.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from typing import Any, Dict, List
|
| 3 |
+
from langchain_openai import ChatOpenAI
|
| 4 |
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
| 5 |
+
from langchain_core.tools import tool
|
| 6 |
+
from langgraph.prebuilt import create_react_agent
|
| 7 |
+
|
| 8 |
+
def _make_file_tools(workspace: Path):
|
| 9 |
+
workspace = workspace.resolve()
|
| 10 |
+
|
| 11 |
+
@tool
|
| 12 |
+
def read_file(path: str) -> str:
|
| 13 |
+
"""Read the contents of a file from the workspace."""
|
| 14 |
+
full = (workspace / path).resolve()
|
| 15 |
+
if not str(full).startswith(str(workspace)):
|
| 16 |
+
return "Error: path outside workspace."
|
| 17 |
+
if not full.exists():
|
| 18 |
+
return f"Error: file not found: {path}"
|
| 19 |
+
return full.read_text(encoding="utf-8", errors="replace")
|
| 20 |
+
|
| 21 |
+
@tool
|
| 22 |
+
def write_file(path: str, content: str) -> str:
|
| 23 |
+
"""Write content to a file in the workspace (creates parent dirs)."""
|
| 24 |
+
full = (workspace / path).resolve()
|
| 25 |
+
if not str(full).startswith(str(workspace)):
|
| 26 |
+
return "Error: path outside workspace."
|
| 27 |
+
full.parent.mkdir(parents=True, exist_ok=True)
|
| 28 |
+
full.write_text(content, encoding="utf-8")
|
| 29 |
+
return f"Written: {path}"
|
| 30 |
+
|
| 31 |
+
@tool
|
| 32 |
+
def list_files(path: str = ".") -> str:
|
| 33 |
+
"""List files and directories under a path in the workspace."""
|
| 34 |
+
full = (workspace / path).resolve()
|
| 35 |
+
if not str(full).startswith(str(workspace)):
|
| 36 |
+
return "Error: path outside workspace."
|
| 37 |
+
if not full.exists():
|
| 38 |
+
return f"Error: path not found: {path}"
|
| 39 |
+
items = []
|
| 40 |
+
for p in sorted(full.iterdir()):
|
| 41 |
+
tag = "[DIR]" if p.is_dir() else "[FILE]"
|
| 42 |
+
items.append(f"{tag} {p.relative_to(workspace)}")
|
| 43 |
+
return "\n".join(items) if items else "(empty)"
|
| 44 |
+
|
| 45 |
+
@tool
|
| 46 |
+
def delete_file(path: str) -> str:
|
| 47 |
+
"""Delete a file or empty directory in the workspace."""
|
| 48 |
+
full = (workspace / path).resolve()
|
| 49 |
+
if not str(full).startswith(str(workspace)):
|
| 50 |
+
return "Error: path outside workspace."
|
| 51 |
+
if not full.exists():
|
| 52 |
+
return f"Error: not found: {path}"
|
| 53 |
+
if full.is_file():
|
| 54 |
+
full.unlink()
|
| 55 |
+
return f"Deleted file: {path}"
|
| 56 |
+
if full.is_dir():
|
| 57 |
+
full.rmdir()
|
| 58 |
+
return f"Deleted empty directory: {path}"
|
| 59 |
+
return f"Error: {path} is neither a file nor empty directory."
|
| 60 |
+
|
| 61 |
+
return [read_file, write_file, list_files, delete_file]
|
| 62 |
+
|
| 63 |
+
def _build_system_message(workspace: Path) -> SystemMessage:
|
| 64 |
+
return SystemMessage(
|
| 65 |
+
content=(
|
| 66 |
+
f"You are a helpful AI assistant. Your workspace is at {workspace}.\n"
|
| 67 |
+
"You have file tools to read, write, list, and delete files in this workspace.\n"
|
| 68 |
+
"You can search and process information safely using your integrated knowledge.\n\n"
|
| 69 |
+
"=== LEAN WORKSPACE RULES ===\n"
|
| 70 |
+
"1. KEEP ONLY HIGH-VALUE FILES: Retain only final artifacts the user would want\n"
|
| 71 |
+
" (reports, generated scripts, datasets, or files the user explicitly asked for).\n"
|
| 72 |
+
"2. AUTO-DELETE JUNK: Before finishing your response, use 'delete_file' to remove\n"
|
| 73 |
+
" all intermediate scratch files, .tmp files, raw HTML dumps, logs, or any file\n"
|
| 74 |
+
" that is NOT a final deliverable.\n"
|
| 75 |
+
"3. OVERWRITE — NEVER DUPLICATE: If you need to update a file, overwrite it in place\n"
|
| 76 |
+
" using 'write_file' with the same path. Never create versioned copies (e.g.,\n"
|
| 77 |
+
" report_v1.txt, report_v2.txt) unless the user explicitly asks for a new version.\n\n"
|
| 78 |
+
"When the user asks about their files or folder contents, use the file tools."
|
| 79 |
+
)
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
async def run_agent(
|
| 83 |
+
openrouter_key: str,
|
| 84 |
+
workspace: Path,
|
| 85 |
+
user_message: str,
|
| 86 |
+
chat_history: List[Dict[str, str]],
|
| 87 |
+
) -> Dict[str, Any]:
|
| 88 |
+
tools = _make_file_tools(workspace)
|
| 89 |
+
|
| 90 |
+
llm = ChatOpenAI(
|
| 91 |
+
model="openai/gpt-oss-20b:free",
|
| 92 |
+
openai_api_key=openrouter_key,
|
| 93 |
+
base_url="https://openrouter.ai/api/v1",
|
| 94 |
+
default_headers={
|
| 95 |
+
"HTTP-Referer": "https://huggingface.co/spaces/react-agent",
|
| 96 |
+
"X-Title": "React Agent Console",
|
| 97 |
+
},
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
agent = create_react_agent(
|
| 101 |
+
model=llm,
|
| 102 |
+
tools=tools
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
messages = [_build_system_message(workspace)]
|
| 106 |
+
|
| 107 |
+
for msg in chat_history:
|
| 108 |
+
role = msg.get("role", "")
|
| 109 |
+
content = msg.get("content", "")
|
| 110 |
+
if role == "user":
|
| 111 |
+
messages.append(HumanMessage(content=content))
|
| 112 |
+
elif role == "assistant":
|
| 113 |
+
messages.append(AIMessage(content=content))
|
| 114 |
+
|
| 115 |
+
messages.append(HumanMessage(content=user_message))
|
| 116 |
+
|
| 117 |
+
result = await agent.ainvoke({"messages": messages})
|
| 118 |
+
|
| 119 |
+
agent_response = ""
|
| 120 |
+
for msg in reversed(result.get("messages", [])):
|
| 121 |
+
if isinstance(msg, AIMessage) and msg.content:
|
| 122 |
+
agent_response = msg.content
|
| 123 |
+
break
|
| 124 |
+
|
| 125 |
+
updated_chat_history = []
|
| 126 |
+
for msg in result.get("messages", []):
|
| 127 |
+
if isinstance(msg, HumanMessage):
|
| 128 |
+
updated_chat_history.append({"role": "user", "content": msg.content})
|
| 129 |
+
elif isinstance(msg, AIMessage):
|
| 130 |
+
updated_chat_history.append({"role": "assistant", "content": msg.content})
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
"agent_response": agent_response,
|
| 134 |
+
"updated_chat_history": updated_chat_history,
|
| 135 |
+
}
|
testeclone/docktent/app.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import shutil
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
from contextlib import asynccontextmanager
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI, HTTPException
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from agent import run_agent
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ChatRequest(BaseModel):
|
| 14 |
+
user_id: str
|
| 15 |
+
user_message: str
|
| 16 |
+
openrouter_key: str
|
| 17 |
+
current_chat_history: Optional[List[Dict[str, str]]] = None
|
| 18 |
+
user_files: Optional[Dict[str, str]] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ChatResponse(BaseModel):
|
| 22 |
+
agent_response: str
|
| 23 |
+
updated_chat_history: List[Dict[str, str]]
|
| 24 |
+
updated_files: Dict[str, str]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@asynccontextmanager
|
| 28 |
+
async def lifespan(app: FastAPI):
|
| 29 |
+
yield
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
app = FastAPI(title="React Agent API", lifespan=lifespan)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.post("/chat", response_model=ChatResponse)
|
| 36 |
+
async def chat(request: ChatRequest):
|
| 37 |
+
if not request.openrouter_key:
|
| 38 |
+
raise HTTPException(status_code=400, detail="openrouter_key is required")
|
| 39 |
+
|
| 40 |
+
workspace = Path(f"/tmp/{request.user_id}")
|
| 41 |
+
workspace.mkdir(parents=True, exist_ok=True)
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
if request.user_files:
|
| 45 |
+
for filename, content_b64 in request.user_files.items():
|
| 46 |
+
filepath = workspace / filename
|
| 47 |
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
filepath.write_bytes(base64.b64decode(content_b64))
|
| 49 |
+
|
| 50 |
+
result = await run_agent(
|
| 51 |
+
openrouter_key=request.openrouter_key,
|
| 52 |
+
workspace=workspace,
|
| 53 |
+
user_message=request.user_message,
|
| 54 |
+
chat_history=request.current_chat_history or [],
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
updated_files = {}
|
| 58 |
+
if workspace.exists():
|
| 59 |
+
for filepath in sorted(workspace.rglob("*")):
|
| 60 |
+
if filepath.is_file():
|
| 61 |
+
rel_path = str(filepath.relative_to(workspace))
|
| 62 |
+
updated_files[rel_path] = base64.b64encode(
|
| 63 |
+
filepath.read_bytes()
|
| 64 |
+
).decode()
|
| 65 |
+
|
| 66 |
+
return ChatResponse(
|
| 67 |
+
agent_response=result["agent_response"],
|
| 68 |
+
updated_chat_history=result["updated_chat_history"],
|
| 69 |
+
updated_files=updated_files,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
finally:
|
| 73 |
+
if workspace.exists():
|
| 74 |
+
shutil.rmtree(workspace, ignore_errors=True)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
import uvicorn
|
| 79 |
+
|
| 80 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
testeclone/docktent/langgraph.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://langgra.ph/schema.json",
|
| 3 |
+
"dependencies": ["."],
|
| 4 |
+
"graphs": {
|
| 5 |
+
"agent": "./src/react_agent/graph.py:graph"
|
| 6 |
+
},
|
| 7 |
+
"env": ".env"
|
| 8 |
+
}
|
testeclone/docktent/pyproject.toml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "react-agent"
|
| 3 |
+
version = "0.0.1"
|
| 4 |
+
description = "Starter template for making a custom Reasoning and Action agent (using tool calling) in LangGraph."
|
| 5 |
+
authors = [
|
| 6 |
+
{ name = "William Fu-Hinthorn", email = "13333726+hinthornw@users.noreply.github.com" },
|
| 7 |
+
]
|
| 8 |
+
readme = "README.md"
|
| 9 |
+
license = { text = "MIT" }
|
| 10 |
+
requires-python = ">=3.11,<4.0"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"langgraph>=1.0.0",
|
| 13 |
+
"langchain-openai>=0.2.0",
|
| 14 |
+
"langchain-core>=0.3.0",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
[project.optional-dependencies]
|
| 19 |
+
dev = ["mypy>=1.11.1", "ruff>=0.6.1", "fastapi>=0.115.0", "uvicorn[standard]>=0.32.0"]
|
| 20 |
+
|
| 21 |
+
[build-system]
|
| 22 |
+
requires = ["setuptools>=73.0.0", "wheel"]
|
| 23 |
+
build-backend = "setuptools.build_meta"
|
| 24 |
+
|
| 25 |
+
[tool.setuptools]
|
| 26 |
+
packages = ["langgraph.templates.react_agent", "react_agent"]
|
| 27 |
+
[tool.setuptools.package-dir]
|
| 28 |
+
"langgraph.templates.react_agent" = "src/react_agent"
|
| 29 |
+
"react_agent" = "src/react_agent"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
[tool.setuptools.package-data]
|
| 33 |
+
"*" = ["py.typed"]
|
| 34 |
+
|
| 35 |
+
[tool.ruff]
|
| 36 |
+
lint.select = [
|
| 37 |
+
"E", # pycodestyle
|
| 38 |
+
"F", # pyflakes
|
| 39 |
+
"I", # isort
|
| 40 |
+
"D", # pydocstyle
|
| 41 |
+
"D401", # First line should be in imperative mood
|
| 42 |
+
"T201",
|
| 43 |
+
"UP",
|
| 44 |
+
]
|
| 45 |
+
lint.ignore = [
|
| 46 |
+
"UP006",
|
| 47 |
+
"UP007",
|
| 48 |
+
# We actually do want to import from typing_extensions
|
| 49 |
+
"UP035",
|
| 50 |
+
# Relax the convention by _not_ requiring documentation for every function parameter.
|
| 51 |
+
"D417",
|
| 52 |
+
"E501",
|
| 53 |
+
]
|
| 54 |
+
[tool.ruff.lint.per-file-ignores]
|
| 55 |
+
"tests/*" = ["D", "UP"]
|
| 56 |
+
[tool.ruff.lint.pydocstyle]
|
| 57 |
+
convention = "google"
|
| 58 |
+
|
| 59 |
+
[tool.uv]
|
| 60 |
+
# CVE-2026-32597: pyjwt < 2.12.0 accepts unknown `crit` header extensions.
|
| 61 |
+
# Transitive via langgraph-api -> pyjwt. Remove once langgraph-api requires pyjwt>=2.12.0.
|
| 62 |
+
constraint-dependencies = ["pyjwt>=2.12.0"]
|
| 63 |
+
|
| 64 |
+
[dependency-groups]
|
| 65 |
+
dev = [
|
| 66 |
+
"anyio>=4.7.0",
|
| 67 |
+
"pytest>=9.0.3",
|
| 68 |
+
]
|
testeclone/docktent/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.115.0
|
| 2 |
+
uvicorn[standard]>=0.32.0
|
| 3 |
+
langchain-openai>=0.2.0
|
| 4 |
+
langgraph>=1.0.0
|
| 5 |
+
langchain-core>=0.3.0
|
testeclone/docktent/src/react_agent/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""React Agent.
|
| 2 |
+
|
| 3 |
+
This module defines a custom reasoning and action agent graph.
|
| 4 |
+
It invokes tools in a simple loop.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from react_agent.graph import graph
|
| 8 |
+
|
| 9 |
+
__all__ = ["graph"]
|
testeclone/docktent/src/react_agent/context.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Define the configurable parameters for the agent."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from dataclasses import dataclass, field, fields
|
| 7 |
+
from typing import Annotated
|
| 8 |
+
|
| 9 |
+
from . import prompts
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(kw_only=True)
|
| 13 |
+
class Context:
|
| 14 |
+
"""The context for the agent."""
|
| 15 |
+
|
| 16 |
+
system_prompt: str = field(
|
| 17 |
+
default=prompts.SYSTEM_PROMPT,
|
| 18 |
+
metadata={
|
| 19 |
+
"description": "The system prompt to use for the agent's interactions. "
|
| 20 |
+
"This prompt sets the context and behavior for the agent."
|
| 21 |
+
},
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
|
| 25 |
+
default="anthropic/claude-sonnet-4-5-20250929",
|
| 26 |
+
metadata={
|
| 27 |
+
"description": "The name of the language model to use for the agent's main interactions. "
|
| 28 |
+
"Should be in the form: provider/model-name."
|
| 29 |
+
},
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
max_search_results: int = field(
|
| 33 |
+
default=10,
|
| 34 |
+
metadata={
|
| 35 |
+
"description": "The maximum number of search results to return for each search query."
|
| 36 |
+
},
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def __post_init__(self) -> None:
|
| 40 |
+
"""Fetch env vars for attributes that were not passed as args."""
|
| 41 |
+
for f in fields(self):
|
| 42 |
+
if not f.init:
|
| 43 |
+
continue
|
| 44 |
+
|
| 45 |
+
if getattr(self, f.name) == f.default:
|
| 46 |
+
setattr(self, f.name, os.environ.get(f.name.upper(), f.default))
|
testeclone/docktent/src/react_agent/graph.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Define a custom Reasoning and Action agent.
|
| 2 |
+
|
| 3 |
+
Works with a chat model with tool calling support.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import UTC, datetime
|
| 7 |
+
from typing import Dict, List, Literal, cast
|
| 8 |
+
|
| 9 |
+
from langchain_core.messages import AIMessage
|
| 10 |
+
from langgraph.graph import StateGraph
|
| 11 |
+
from langgraph.prebuilt import ToolNode
|
| 12 |
+
from langgraph.runtime import Runtime
|
| 13 |
+
|
| 14 |
+
from react_agent.context import Context
|
| 15 |
+
from react_agent.state import InputState, State
|
| 16 |
+
from react_agent.tools import TOOLS
|
| 17 |
+
from react_agent.utils import load_chat_model
|
| 18 |
+
|
| 19 |
+
# Define the function that calls the model
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
async def call_model(
|
| 23 |
+
state: State, runtime: Runtime[Context]
|
| 24 |
+
) -> Dict[str, List[AIMessage]]:
|
| 25 |
+
"""Call the LLM powering our "agent".
|
| 26 |
+
|
| 27 |
+
This function prepares the prompt, initializes the model, and processes the response.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
state (State): The current state of the conversation.
|
| 31 |
+
config (RunnableConfig): Configuration for the model run.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
dict: A dictionary containing the model's response message.
|
| 35 |
+
"""
|
| 36 |
+
# Initialize the model with tool binding. Change the model or add more tools here.
|
| 37 |
+
model = load_chat_model(runtime.context.model).bind_tools(TOOLS)
|
| 38 |
+
|
| 39 |
+
# Format the system prompt. Customize this to change the agent's behavior.
|
| 40 |
+
system_message = runtime.context.system_prompt.format(
|
| 41 |
+
system_time=datetime.now(tz=UTC).isoformat()
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Get the model's response
|
| 45 |
+
response = cast( # type: ignore[redundant-cast]
|
| 46 |
+
AIMessage,
|
| 47 |
+
await model.ainvoke(
|
| 48 |
+
[{"role": "system", "content": system_message}, *state.messages]
|
| 49 |
+
),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Handle the case when it's the last step and the model still wants to use a tool
|
| 53 |
+
if state.is_last_step and response.tool_calls:
|
| 54 |
+
return {
|
| 55 |
+
"messages": [
|
| 56 |
+
AIMessage(
|
| 57 |
+
id=response.id,
|
| 58 |
+
content="Sorry, I could not find an answer to your question in the specified number of steps.",
|
| 59 |
+
)
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# Return the model's response as a list to be added to existing messages
|
| 64 |
+
return {"messages": [response]}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Define a new graph
|
| 68 |
+
|
| 69 |
+
builder = StateGraph(State, input_schema=InputState, context_schema=Context)
|
| 70 |
+
|
| 71 |
+
# Define the two nodes we will cycle between
|
| 72 |
+
builder.add_node(call_model)
|
| 73 |
+
builder.add_node("tools", ToolNode(TOOLS))
|
| 74 |
+
|
| 75 |
+
# Set the entrypoint as `call_model`
|
| 76 |
+
# This means that this node is the first one called
|
| 77 |
+
builder.add_edge("__start__", "call_model")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def route_model_output(state: State) -> Literal["__end__", "tools"]:
|
| 81 |
+
"""Determine the next node based on the model's output.
|
| 82 |
+
|
| 83 |
+
This function checks if the model's last message contains tool calls.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
state (State): The current state of the conversation.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
str: The name of the next node to call ("__end__" or "tools").
|
| 90 |
+
"""
|
| 91 |
+
last_message = state.messages[-1]
|
| 92 |
+
if not isinstance(last_message, AIMessage):
|
| 93 |
+
raise ValueError(
|
| 94 |
+
f"Expected AIMessage in output edges, but got {type(last_message).__name__}"
|
| 95 |
+
)
|
| 96 |
+
# If there is no tool call, then we finish
|
| 97 |
+
if not last_message.tool_calls:
|
| 98 |
+
return "__end__"
|
| 99 |
+
# Otherwise we execute the requested actions
|
| 100 |
+
return "tools"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# Add a conditional edge to determine the next step after `call_model`
|
| 104 |
+
builder.add_conditional_edges(
|
| 105 |
+
"call_model",
|
| 106 |
+
# After call_model finishes running, the next node(s) are scheduled
|
| 107 |
+
# based on the output from route_model_output
|
| 108 |
+
route_model_output,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Add a normal edge from `tools` to `call_model`
|
| 112 |
+
# This creates a cycle: after using tools, we always return to the model
|
| 113 |
+
builder.add_edge("tools", "call_model")
|
| 114 |
+
|
| 115 |
+
# Compile the builder into an executable graph
|
| 116 |
+
graph = builder.compile(name="ReAct Agent")
|
testeclone/docktent/src/react_agent/prompts.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Default prompts used by the agent."""
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a helpful AI assistant.
|
| 4 |
+
|
| 5 |
+
System time: {system_time}"""
|
testeclone/docktent/src/react_agent/state.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Define the state structures for the agent."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Sequence
|
| 7 |
+
|
| 8 |
+
from langchain_core.messages import AnyMessage
|
| 9 |
+
from langgraph.graph import add_messages
|
| 10 |
+
from langgraph.managed import IsLastStep
|
| 11 |
+
from typing_extensions import Annotated
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class InputState:
|
| 16 |
+
"""Defines the input state for the agent, representing a narrower interface to the outside world.
|
| 17 |
+
|
| 18 |
+
This class is used to define the initial state and structure of incoming data.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
messages: Annotated[Sequence[AnyMessage], add_messages] = field(
|
| 22 |
+
default_factory=list
|
| 23 |
+
)
|
| 24 |
+
"""
|
| 25 |
+
Messages tracking the primary execution state of the agent.
|
| 26 |
+
|
| 27 |
+
Typically accumulates a pattern of:
|
| 28 |
+
1. HumanMessage - user input
|
| 29 |
+
2. AIMessage with .tool_calls - agent picking tool(s) to use to collect information
|
| 30 |
+
3. ToolMessage(s) - the responses (or errors) from the executed tools
|
| 31 |
+
4. AIMessage without .tool_calls - agent responding in unstructured format to the user
|
| 32 |
+
5. HumanMessage - user responds with the next conversational turn
|
| 33 |
+
|
| 34 |
+
Steps 2-5 may repeat as needed.
|
| 35 |
+
|
| 36 |
+
The `add_messages` annotation ensures that new messages are merged with existing ones,
|
| 37 |
+
updating by ID to maintain an "append-only" state unless a message with the same ID is provided.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class State(InputState):
|
| 43 |
+
"""Represents the complete state of the agent, extending InputState with additional attributes.
|
| 44 |
+
|
| 45 |
+
This class can be used to store any information needed throughout the agent's lifecycle.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
is_last_step: IsLastStep = field(default=False)
|
| 49 |
+
"""
|
| 50 |
+
Indicates whether the current step is the last one before the graph raises an error.
|
| 51 |
+
|
| 52 |
+
This is a 'managed' variable, controlled by the state machine rather than user code.
|
| 53 |
+
It is set to 'True' when the step count reaches recursion_limit - 1.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
# Additional attributes can be added here as needed.
|
| 57 |
+
# Common examples include:
|
| 58 |
+
# retrieved_documents: List[Document] = field(default_factory=list)
|
| 59 |
+
# extracted_entities: Dict[str, Any] = field(default_factory=dict)
|
| 60 |
+
# api_connections: Dict[str, Any] = field(default_factory=dict)
|
testeclone/docktent/src/react_agent/tools.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""This module provides example tools for web scraping and search functionality.
|
| 2 |
+
|
| 3 |
+
It includes a basic Tavily search function (as an example)
|
| 4 |
+
|
| 5 |
+
These tools are intended as free examples to get started. For production use,
|
| 6 |
+
consider implementing more robust and specialized tools tailored to your needs.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Any, Callable, List, Optional, cast
|
| 10 |
+
|
| 11 |
+
from langchain_tavily import TavilySearch
|
| 12 |
+
from langgraph.runtime import get_runtime
|
| 13 |
+
|
| 14 |
+
from react_agent.context import Context
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
async def search(query: str) -> Optional[dict[str, Any]]:
|
| 18 |
+
"""Search for general web results.
|
| 19 |
+
|
| 20 |
+
This function performs a search using the Tavily search engine, which is designed
|
| 21 |
+
to provide comprehensive, accurate, and trusted results. It's particularly useful
|
| 22 |
+
for answering questions about current events.
|
| 23 |
+
"""
|
| 24 |
+
runtime = get_runtime(Context)
|
| 25 |
+
wrapped = TavilySearch(max_results=runtime.context.max_search_results)
|
| 26 |
+
return cast(dict[str, Any], await wrapped.ainvoke({"query": query}))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
TOOLS: List[Callable[..., Any]] = [search]
|
testeclone/docktent/src/react_agent/utils.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility & helper functions."""
|
| 2 |
+
|
| 3 |
+
from langchain.chat_models import init_chat_model
|
| 4 |
+
from langchain_core.language_models import BaseChatModel
|
| 5 |
+
from langchain_core.messages import BaseMessage
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def get_message_text(msg: BaseMessage) -> str:
|
| 9 |
+
"""Get the text content of a message."""
|
| 10 |
+
content = msg.content
|
| 11 |
+
if isinstance(content, str):
|
| 12 |
+
return content
|
| 13 |
+
elif isinstance(content, dict):
|
| 14 |
+
return content.get("text", "")
|
| 15 |
+
else:
|
| 16 |
+
txts = [c if isinstance(c, str) else (c.get("text") or "") for c in content]
|
| 17 |
+
return "".join(txts).strip()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load_chat_model(fully_specified_name: str) -> BaseChatModel:
|
| 21 |
+
"""Load a chat model from a fully specified name.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
fully_specified_name (str): String in the format 'provider/model'.
|
| 25 |
+
"""
|
| 26 |
+
provider, model = fully_specified_name.split("/", maxsplit=1)
|
| 27 |
+
return init_chat_model(model, model_provider=provider)
|
testeclone/docktent/static/studio_ui.png
ADDED
|
Git LFS Details
|
testeclone/docktent/tests/cassettes/103fe67e-a040-4e4e-aadb-b20a7057f904.yaml
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
testeclone/docktent/tests/conftest.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
@pytest.fixture(scope="session")
|
| 5 |
+
def anyio_backend():
|
| 6 |
+
return "asyncio"
|
| 7 |
+
|