nulltron commited on
Commit
c2df2b9
·
verified ·
1 Parent(s): d1566f5

Upload 62 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .codespellignore +0 -0
  2. .dockerignore +11 -0
  3. .env.example +11 -0
  4. .gitattributes +37 -35
  5. .github/workflows/integration-tests.yml +47 -0
  6. .github/workflows/unit-tests.yml +60 -0
  7. .gitignore +164 -0
  8. Dockerfile +8 -0
  9. LICENSE +21 -0
  10. Makefile +67 -0
  11. README.md +10 -10
  12. agent.py +148 -0
  13. app.py +78 -0
  14. langgraph.json +8 -0
  15. pyproject.toml +68 -0
  16. requirements.txt +7 -0
  17. src/react_agent/__init__.py +9 -0
  18. src/react_agent/context.py +46 -0
  19. src/react_agent/graph.py +116 -0
  20. src/react_agent/prompts.py +5 -0
  21. src/react_agent/state.py +60 -0
  22. src/react_agent/tools.py +29 -0
  23. src/react_agent/utils.py +27 -0
  24. static/studio_ui.png +3 -0
  25. testeclone/docktent/.codespellignore +0 -0
  26. testeclone/docktent/.dockerignore +11 -0
  27. testeclone/docktent/.env.example +11 -0
  28. testeclone/docktent/.gitattributes +36 -0
  29. testeclone/docktent/.github/workflows/integration-tests.yml +47 -0
  30. testeclone/docktent/.github/workflows/unit-tests.yml +60 -0
  31. testeclone/docktent/.gitignore +164 -0
  32. testeclone/docktent/Dockerfile +8 -0
  33. testeclone/docktent/LICENSE +21 -0
  34. testeclone/docktent/Makefile +67 -0
  35. testeclone/docktent/README.md +10 -0
  36. testeclone/docktent/agent.py +135 -0
  37. testeclone/docktent/app.py +80 -0
  38. testeclone/docktent/langgraph.json +8 -0
  39. testeclone/docktent/pyproject.toml +68 -0
  40. testeclone/docktent/requirements.txt +5 -0
  41. testeclone/docktent/src/react_agent/__init__.py +9 -0
  42. testeclone/docktent/src/react_agent/context.py +46 -0
  43. testeclone/docktent/src/react_agent/graph.py +116 -0
  44. testeclone/docktent/src/react_agent/prompts.py +5 -0
  45. testeclone/docktent/src/react_agent/state.py +60 -0
  46. testeclone/docktent/src/react_agent/tools.py +29 -0
  47. testeclone/docktent/src/react_agent/utils.py +27 -0
  48. testeclone/docktent/static/studio_ui.png +3 -0
  49. testeclone/docktent/tests/cassettes/103fe67e-a040-4e4e-aadb-b20a7057f904.yaml +0 -0
  50. 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: Newtrone
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
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

  • SHA256: ea9387cbb55dc2dcd6f580084afa447d223fae0e3eda476e19a6b384b3b3911d
  • Pointer size: 131 Bytes
  • Size of remote file: 582 kB
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

  • SHA256: ea9387cbb55dc2dcd6f580084afa447d223fae0e3eda476e19a6b384b3b3911d
  • Pointer size: 131 Bytes
  • Size of remote file: 582 kB
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
+