corentinm7 commited on
Commit
1dc3303
·
verified ·
1 Parent(s): be3b887

Upload folder using huggingface_hub

Browse files
.devcontainer/Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
2
+
3
+ ENV UV_COMPILE_BYTECODE=1
4
+ ENV UV_NO_CACHE=1
5
+
6
+ RUN apt update && \
7
+ apt install -y --no-install-recommends \
8
+ build-essential \
9
+ cmake \
10
+ curl \
11
+ gcc \
12
+ git \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ WORKDIR /workspaces/opensymbiose
.devcontainer/devcontainer.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "OpenSymbiose DevContainer",
3
+ "build": {
4
+ "dockerfile": "Dockerfile",
5
+ "context": "."
6
+ },
7
+ "forwardPorts": [
8
+ 8000
9
+ ],
10
+ "postCreateCommand": "uv sync && make commit",
11
+ "remoteUser": "root",
12
+ "features": {
13
+ "ghcr.io/devcontainers/features/git:1": {}
14
+ },
15
+ "workspaceFolder": "/workspaces/opensymbiose"
16
+ }
.env.sample ADDED
@@ -0,0 +1 @@
 
 
1
+ MISTRAL_API_KEY=azerty
.github/workflows/ci.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy Documentation
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+ - main
7
+ permissions:
8
+ contents: write
9
+ jobs:
10
+ deploy:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Configure Git Credentials
15
+ run: |
16
+ git config user.name github-actions[bot]
17
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: 3.x
21
+ - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
22
+ - uses: actions/cache@v4
23
+ with:
24
+ key: mkdocs-material-${{ env.cache_id }}
25
+ path: .cache
26
+ restore-keys: |
27
+ mkdocs-material-
28
+ - run: pip install mkdocs-material mkdocstrings mkdocstrings-python mkdocs-include-markdown-plugin
29
+ - run: mkdocs gh-deploy --force
.github/workflows/update_space.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to HuggingFace
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v2
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v2
19
+ with:
20
+ python-version: '3.13'
21
+
22
+ - name: Install Gradio
23
+ run: python -m pip install gradio
24
+
25
+ - name: Log in to Hugging Face
26
+ run: python -c 'import huggingface_hub; huggingface_hub.login(token="${{ secrets.hf_token }}")'
27
+
28
+ - name: Deploy to Spaces
29
+ run: gradio deploy
.gitignore ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ # Boiletplate
177
+ venv/
178
+ env/
179
+ .env/
180
+ .venv/
181
+ .ruff_cache/
182
+ **/.pytest_cache/
183
+ **/__pycache__/
184
+ dist/
185
+ .coverage
186
+ .gradio
187
+ .idea
188
+ .DS_Store
.pre-commit-config.yaml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.11.10
4
+ hooks:
5
+ - id: ruff-check
6
+ args: [ --fix, --config=pyproject.toml ]
7
+ - id: ruff-format
8
+ args: [ --config=pyproject.toml ]
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # v0.0.1
2
+
3
+ - Prototype with two agents with tool calling, handoff, streaming of answer and state.
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
2
+
3
+ ENV UV_COMPILE_BYTECODE=1
4
+ ENV UV_NO_CACHE=1
5
+
6
+ RUN apt update && \
7
+ apt install -y --no-install-recommends \
8
+ build-essential \
9
+ cmake \
10
+ curl \
11
+ gcc \
12
+ git \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ WORKDIR /app
16
+ COPY README.md /app/
17
+ COPY pyproject.toml /app/
18
+ RUN uv sync --no-install-project --no-dev
19
+ COPY src/opensymbiose /app/opensymbiose
20
+
21
+ EXPOSE 8000
22
+
23
+ CMD ["uv", "run", "opensymbiose/main.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Corentin Meyer
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,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: help
2
+
3
+ help:
4
+ @echo "Usage:"
5
+ @echo " make dev Run the package with developer settings"
6
+ @echo " make prod Run the pacakge with production settings"
7
+ @echo " make test CI: Run tests"
8
+ @echo " make cov CI: Run test and calculate coverage"
9
+ @echo " make check CI: Lint the code"
10
+ @echo " make format CI: Format the code"
11
+ @echo " make type CI: Check typing"
12
+ @echo " make doc Run local documentation server"
13
+ @echo " make build Build the package wheel before publishing to Pypi"
14
+ @echo " make publish Publish package to Pypi"
15
+ @echo " make dockerbuild Build the docker image"
16
+ @echo " make dockerrun Run the docker image"
17
+ @echo " make allci Run all CI steps (check, format, type, test coverage)"
18
+
19
+ dev:
20
+ uv run --env-file=.env gradio src/opensymbiose/gradio/app.py
21
+
22
+ prod:
23
+ uv run --env-file=.env opensymbiose
24
+
25
+ test:
26
+ uv run --env-file=.env pytest tests/
27
+
28
+ cov:
29
+ uv run --env-file=.env pytest --cov=src/opensymbiose tests/ --cov-report=term-missing
30
+
31
+ check:
32
+ uv run ruff check $$(git diff --name-only --cached -- '*.py')
33
+
34
+ format:
35
+ uv run ruff format $$(git diff --name-only --cached -- '*.py')
36
+
37
+ type:
38
+ uv run ty check $$(git diff --name-only --cached -- '*.py')
39
+
40
+ doc:
41
+ uvx --with mkdocstrings --with mkdocs-material --with mkdocstrings-python --with mkdocs-include-markdown-plugin mkdocs serve
42
+
43
+ build:
44
+ uv build
45
+
46
+ publish:
47
+ uv publish
48
+
49
+ commit:
50
+ uv run pre-commit
51
+
52
+ dockerbuild:
53
+ docker build -t opensymbiose:latest .
54
+
55
+ dockerrun:
56
+ docker run --rm opensymbiose:latest
57
+
58
+ allci:
59
+ $(MAKE) check
60
+ $(MAKE) format
61
+ $(MAKE) type
62
+ $(MAKE) cov
README.md CHANGED
@@ -1,12 +1,20 @@
1
  ---
2
- title: Opensymbiose
3
- emoji: 🦀
4
- colorFrom: pink
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 5.31.0
8
- app_file: app.py
9
- pinned: false
10
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: opensymbiose
3
+ app_file: src/opensymbiose/gradio/app.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.31.0
 
 
6
  ---
7
+ # OpenSymbiose: Open Source Biotechnology AI Agent
8
+
9
+ OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher.
10
+
11
+ Creator and Maintainer: [**Corentin Meyer**, PhD](https://cmeyer.fr/) - <[email protected]>
12
+
13
+ ## Installation
14
+
15
+ From PyPi: `pip install opensymbiose`
16
+
17
+ ## Usage
18
+
19
+ Placeholder
20
 
 
docs/index.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Home
3
+ description: OpenSymbiose
4
+ hide:
5
+ - navigation
6
+ ---
7
+
8
+ # OpenSymbiose: Open Source Biotechnology AI Agent
9
+
10
+ OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher.
11
+
12
+ Creator and Maintainer: [**Corentin Meyer**, PhD](https://cmeyer.fr/) - <[email protected]>
13
+
14
+ ## Installation
15
+
16
+ From PyPi: `pip install opensymbiose`
17
+
18
+ ## Usage
19
+
20
+ Placeholder
docs/reference.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # API Reference
2
+
3
+ ::: opensymbiose.main
4
+ handler: python
5
+ options:
6
+ show_root_heading: true
7
+ show_source: true
mkdocs.yml ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ site_name: OpenSymbiose
2
+ site_url: https://github.com/lambda-science/OpenSymbiose
3
+ site_author: Corentin Meyer
4
+ site_description: OpenSymbiose
5
+ theme:
6
+ font:
7
+ text: Inter
8
+ code: JetBrains Mono
9
+ icon:
10
+ logo: material/file-document-multiple-outline
11
+ palette:
12
+ # Palette toggle for light mode
13
+ - scheme: default
14
+ primary: white
15
+ toggle:
16
+ icon: material/lightbulb
17
+ name: Switch to dark mode
18
+
19
+ # Palette toggle for dark mode
20
+ - scheme: slate
21
+ primary: black
22
+ toggle:
23
+ icon: material/lightbulb-outline
24
+ name: Switch to light mode
25
+ name: material
26
+ features:
27
+ - navigation.instant
28
+ - navigation.top
29
+ - navigation.tabs
30
+ - search.suggest
31
+ - search.highlight
32
+
33
+ plugins:
34
+ - search
35
+ - tags
36
+ - mkdocstrings:
37
+ default_handler: python
38
+ handlers:
39
+ python:
40
+ paths: [ "src" ]
41
+ options:
42
+ annotations_path: brief
43
+ docstring_style: google
44
+ - include-markdown:
45
+ preserve_includer_indent: true
46
+ nav:
47
+ - Home: index.md
48
+ - API Reference: reference.md
49
+ markdown_extensions:
50
+ - attr_list
51
+ - admonition
52
+ - pymdownx.details
53
+ - pymdownx.superfences
54
+ - tables
55
+ - md_in_html
56
+ - pymdownx.highlight:
57
+ anchor_linenums: true
58
+ - pymdownx.inlinehilite
59
+ - pymdownx.snippets
60
+ - pymdownx.superfences
61
+ - pymdownx.emoji:
62
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
63
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
64
+
65
+ repo_url: https://github.com/lambda-science/OpenSymbiose
66
+ extra:
67
+ social:
68
+ - icon: material/web
69
+ link: https://cmeyer.fr/
70
+ - icon: fontawesome/brands/x-twitter
71
+ link: https://x.com/corentinm_py
pyproject.toml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "opensymbiose"
3
+ version = "0.0.1"
4
+ description = "OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ authors = [
8
+ { name = "Corentin Meyer", email = "[email protected]" },
9
+ ]
10
+ maintainers = [
11
+ { name = "Corentin Meyer", email = "[email protected]" }
12
+ ]
13
+ dependencies = [
14
+ "biopython>=1.85",
15
+ "bioservices>=1.12.1",
16
+ "gradio>=5.31.0",
17
+ "mistralai>=1.8.0",
18
+ "python-dotenv>=1.1.0",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/lambda-science/OpenSymbiose"
23
+ Documentation = "https://lambda-science.github.io/OpenSymbiose/"
24
+ Repository = "https://github.com/lambda-science/OpenSymbiose"
25
+ Issues = "https://github.com/lambda-science/OpenSymbiose/issues"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "ruff",
30
+ "pytest",
31
+ "pytest-cov",
32
+ "ty",
33
+ "pre-commit",
34
+ ]
35
+
36
+ [tool.hatch.metadata]
37
+ allow-direct-references = true
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/opensymbiose"]
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [project.scripts]
47
+ opensymbiose = "opensymbiose.gradio.app:demo.launch"
48
+
49
+ [tool.ruff]
50
+ target-version = "py313"
51
+
52
+ [tool.coverage.run]
53
+ omit = ["tests/*"]
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # For HuggingFace Space Gradio App
2
+ opensymbiose
3
+ mistralai
4
+ python-dotenv
5
+ gradio
6
+ bioservices
7
+ biopython
src/opensymbiose/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from dotenv import load_dotenv
2
+
3
+ load_dotenv()
src/opensymbiose/agents/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Opensymbiose Agents package.
3
+
4
+ This package provides classes and functions for managing Mistral AI agents.
5
+ """
6
+
7
+ from opensymbiose.agents.agent import Agent
8
+ from opensymbiose.agents.agent_manager import AgentManager
9
+ from opensymbiose.agents.create_agents import setup_agents, get_agents
10
+
11
+ __all__ = ['Agent', 'AgentManager', 'setup_agents', 'get_agents']
src/opensymbiose/agents/agent.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent class to represent individual Mistral AI agents.
3
+ """
4
+ from typing import Any
5
+
6
+ from mistralai import Mistral
7
+
8
+
9
+ class Agent:
10
+ """
11
+ Represents a Mistral AI agent with its properties and capabilities.
12
+ """
13
+
14
+ def __init__(self, agent_data: Any):
15
+ """
16
+ Initialize an Agent object from Mistral API agent data.
17
+
18
+ Args:
19
+ agent_data: The agent data returned from Mistral API
20
+ """
21
+ self.id = agent_data.id
22
+ self.name = agent_data.name
23
+ self.description = agent_data.description
24
+ self.model = agent_data.model
25
+ self.tools = agent_data.tools
26
+ self.handoffs = agent_data.handoffs if hasattr(agent_data, 'handoffs') else []
27
+ self._raw_data = agent_data
28
+
29
+ @property
30
+ def raw_data(self) -> Any:
31
+ """
32
+ Get the raw agent data from Mistral API.
33
+
34
+ Returns:
35
+ The raw agent data
36
+ """
37
+ return self._raw_data
38
+
39
+ def add_handoff(self, agent_id: str, client: Mistral) -> None:
40
+ """
41
+ Add a handoff to another agent.
42
+
43
+ Args:
44
+ agent_id: The ID of the agent to handoff to
45
+ client: The Mistral client instance
46
+ """
47
+ if agent_id not in self.handoffs:
48
+ self.handoffs.append(agent_id)
49
+ updated_agent = client.beta.agents.update(
50
+ agent_id=self.id,
51
+ handoffs=self.handoffs
52
+ )
53
+ # Update the raw data with the updated agent
54
+ self._raw_data = updated_agent
55
+
56
+ def remove_handoff(self, agent_id: str, client: Mistral) -> None:
57
+ """
58
+ Remove a handoff to another agent.
59
+
60
+ Args:
61
+ agent_id: The ID of the agent to remove handoff from
62
+ client: The Mistral client instance
63
+ """
64
+ if agent_id in self.handoffs:
65
+ self.handoffs.remove(agent_id)
66
+ updated_agent = client.beta.agents.update(
67
+ agent_id=self.id,
68
+ handoffs=self.handoffs
69
+ )
70
+ # Update the raw data with the updated agent
71
+ self._raw_data = updated_agent
72
+
73
+ def __str__(self) -> str:
74
+ """
75
+ String representation of the agent.
76
+
77
+ Returns:
78
+ A string representation of the agent
79
+ """
80
+ return f"Agent(id={self.id}, name={self.name}, model={self.model})"
81
+
82
+ def __repr__(self) -> str:
83
+ """
84
+ Detailed representation of the agent.
85
+
86
+ Returns:
87
+ A detailed representation of the agent
88
+ """
89
+ return (f"Agent(id={self.id}, name={self.name}, "
90
+ f"description={self.description}, model={self.model}, "
91
+ f"tools={self.tools}, handoffs={self.handoffs})")
src/opensymbiose/agents/agent_manager.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AgentManager class to manage Mistral AI agents.
3
+ """
4
+ import os
5
+ from typing import Dict, List, Optional
6
+
7
+ from mistralai import Mistral
8
+
9
+ from opensymbiose.agents.agent import Agent
10
+
11
+
12
+ class AgentManager:
13
+ """
14
+ Manages Mistral AI agents, including creation, retrieval, and handoffs.
15
+ Implements the Singleton pattern to ensure only one instance exists.
16
+ """
17
+
18
+ _instance = None
19
+
20
+ def __new__(cls, api_key: Optional[str] = None):
21
+ """
22
+ Create a new instance of AgentManager or return the existing one (Singleton pattern).
23
+
24
+ Args:
25
+ api_key: The Mistral API key. If not provided, it will be read from the environment.
26
+
27
+ Returns:
28
+ The AgentManager instance
29
+ """
30
+ if cls._instance is None:
31
+ cls._instance = super(AgentManager, cls).__new__(cls)
32
+ cls._instance._initialized = False
33
+ return cls._instance
34
+
35
+ def __init__(self, api_key: Optional[str] = None):
36
+ """
37
+ Initialize the AgentManager with the Mistral API key.
38
+
39
+ Args:
40
+ api_key: The Mistral API key. If not provided, it will be read from the environment.
41
+ """
42
+ # Skip initialization if already initialized (part of Singleton pattern)
43
+ if self._initialized:
44
+ return
45
+
46
+ self.api_key = api_key or os.environ.get("MISTRAL_API_KEY")
47
+ if not self.api_key:
48
+ raise ValueError(
49
+ "Mistral API key is required. Provide it as an argument or set the MISTRAL_API_KEY environment variable.")
50
+
51
+ self.client = Mistral(self.api_key)
52
+ self.agents: Dict[str, Agent] = {}
53
+ self._initialized = True
54
+
55
+ def list_agents(self) -> List[Agent]:
56
+ """
57
+ List all agents from the Mistral API.
58
+
59
+ Returns:
60
+ A list of Agent objects
61
+ """
62
+ agent_list = self.client.beta.agents.list()
63
+ return [Agent(agent) for agent in agent_list]
64
+
65
+ def refresh_agents(self) -> None:
66
+ """
67
+ Refresh the local cache of agents from the Mistral API.
68
+ """
69
+ self.agents = {}
70
+ agent_list = self.client.beta.agents.list()
71
+ for agent_data in agent_list:
72
+ agent = Agent(agent_data)
73
+ self.agents[agent.name] = agent
74
+
75
+ def get_agent(self, agent_name: str) -> Optional[Agent]:
76
+ """
77
+ Get an agent by name.
78
+
79
+ Args:
80
+ agent_name: The name of the agent
81
+
82
+ Returns:
83
+ The Agent object if found, None otherwise
84
+ """
85
+ # Refresh agents if not already loaded
86
+ if not self.agents:
87
+ self.refresh_agents()
88
+
89
+ return self.agents.get(agent_name)
90
+
91
+ def get_or_create_agent(
92
+ self,
93
+ agent_name: str,
94
+ model: str,
95
+ description: str,
96
+ tools: List[Dict[str, str]]
97
+ ) -> Agent:
98
+ """
99
+ Get an agent by name or create it if it doesn't exist.
100
+
101
+ Args:
102
+ agent_name: The name of the agent
103
+ model: The model to use for the agent
104
+ description: The description of the agent
105
+ tools: The tools to enable for the agent
106
+
107
+ Returns:
108
+ The Agent object
109
+ """
110
+ # Refresh agents if not already loaded
111
+ if not self.agents:
112
+ self.refresh_agents()
113
+
114
+ # Check if agent exists
115
+ agent = self.agents.get(agent_name)
116
+
117
+ # Create agent if it doesn't exist
118
+ if not agent:
119
+ agent_data = self.client.beta.agents.create(
120
+ model=model,
121
+ description=description,
122
+ name=agent_name,
123
+ tools=tools
124
+ )
125
+ agent = Agent(agent_data)
126
+ self.agents[agent_name] = agent
127
+ print(f"Created new agent: {agent}")
128
+ else:
129
+ print(f"Using existing agent: {agent}")
130
+
131
+ return agent
132
+
133
+ def create_handoff(self, from_agent_name: str, to_agent_name: str) -> None:
134
+ """
135
+ Create a handoff from one agent to another.
136
+
137
+ Args:
138
+ from_agent_name: The name of the agent to handoff from
139
+ to_agent_name: The name of the agent to handoff to
140
+ """
141
+ from_agent = self.get_agent(from_agent_name)
142
+ to_agent = self.get_agent(to_agent_name)
143
+
144
+ if not from_agent:
145
+ raise ValueError(f"Agent '{from_agent_name}' not found")
146
+ if not to_agent:
147
+ raise ValueError(f"Agent '{to_agent_name}' not found")
148
+
149
+ from_agent.add_handoff(to_agent.id, self.client)
150
+
151
+ # Update the local cache
152
+ self.agents[from_agent_name] = from_agent
src/opensymbiose/agents/create_agents.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for creating and managing Mistral AI agents.
3
+ This module provides a simple interface to create and manage agents using the AgentManager class.
4
+ """
5
+ from opensymbiose.agents.agent_manager import AgentManager
6
+
7
+
8
+ def setup_agents():
9
+ """
10
+ Set up the agents for the application.
11
+
12
+ Returns:
13
+ A dictionary of agent names to Agent objects
14
+ """
15
+ # Initialize the agent manager (Singleton pattern ensures only one instance)
16
+ agent_manager = AgentManager()
17
+
18
+ # Get or create the test agent with web search tool
19
+ test_agent = agent_manager.get_or_create_agent(
20
+ agent_name="Test Agent Tools",
21
+ model="mistral-medium-latest",
22
+ description="An agent with tools",
23
+ tools=[{"type": "web_search"}]
24
+ )
25
+
26
+ # Get or create the calculating agent with code interpreter tool
27
+ calculating_agent = agent_manager.get_or_create_agent(
28
+ agent_name="Calculating Agent",
29
+ model="mistral-medium-latest",
30
+ description="A calculating agent with tools",
31
+ tools=[{"type": "code_interpreter"}]
32
+ )
33
+
34
+ # Create handoff from test agent to calculating agent
35
+ agent_manager.create_handoff("Test Agent Tools", "Calculating Agent")
36
+
37
+ # Return a dictionary of agent names to Agent objects
38
+ return {
39
+ "test_agent_tools": test_agent,
40
+ "calculating_agent": calculating_agent
41
+ }
42
+
43
+
44
+ # Create a convenience function to get the agents
45
+ def get_agents():
46
+ """
47
+ Get the agents for the application.
48
+
49
+ Returns:
50
+ A dictionary of agent names to Agent objects
51
+ """
52
+ return setup_agents()
53
+
54
+
55
+ # For backwards compatibility and direct script execution
56
+ symbiose_agents = setup_agents() if __name__ == "__main__" else {}
src/opensymbiose/agents/test_agent_manager.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for the AgentManager and Agent classes.
3
+ """
4
+ import os
5
+ import sys
6
+
7
+ # Add the parent directory to the path to allow importing the modules
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
9
+
10
+ from opensymbiose.agents.agent_manager import AgentManager
11
+ from opensymbiose.agents.create_agents import setup_agents, get_agents
12
+
13
+
14
+ def test_agent_manager():
15
+ """
16
+ Test the AgentManager class.
17
+ """
18
+ print("Testing AgentManager...")
19
+
20
+ # Test singleton pattern
21
+ manager1 = AgentManager()
22
+ manager2 = AgentManager()
23
+ assert manager1 is manager2, "Singleton pattern failed"
24
+ print("✓ Singleton pattern works")
25
+
26
+ # Test listing agents
27
+ agents = manager1.list_agents()
28
+ print(f"Found {len(agents)} agents")
29
+
30
+ # Test refreshing agents
31
+ manager1.refresh_agents()
32
+ print(f"Refreshed agents, found {len(manager1.agents)} agents")
33
+
34
+ # Test getting an agent
35
+ if manager1.agents:
36
+ agent_name = list(manager1.agents.keys())[0]
37
+ agent = manager1.get_agent(agent_name)
38
+ print(f"Got agent: {agent}")
39
+
40
+ print("AgentManager tests completed successfully")
41
+
42
+
43
+ def test_create_agents():
44
+ """
45
+ Test the create_agents module.
46
+ """
47
+ print("\nTesting create_agents module...")
48
+
49
+ # Test setup_agents function
50
+ agents = setup_agents()
51
+ assert "test_agent_tools" in agents, "Test agent not created"
52
+ assert "calculating_agent" in agents, "Calculating agent not created"
53
+
54
+ print(f"✓ setup_agents created {len(agents)} agents")
55
+
56
+ # Test get_agents function
57
+ agents2 = get_agents()
58
+ assert agents2["test_agent_tools"].id == agents["test_agent_tools"].id, "get_agents returned different agents"
59
+
60
+ print("✓ get_agents works correctly")
61
+
62
+ # Test handoffs
63
+ test_agent = agents["test_agent_tools"]
64
+ calc_agent = agents["calculating_agent"]
65
+
66
+ assert calc_agent.id in test_agent.handoffs, "Handoff not created"
67
+
68
+ print(f"✓ Handoff from {test_agent.name} to {calc_agent.name} created successfully")
69
+
70
+ print("create_agents tests completed successfully")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ # Check if MISTRAL_API_KEY is set
75
+ if not os.environ.get("MISTRAL_API_KEY"):
76
+ print("Error: MISTRAL_API_KEY environment variable is not set")
77
+ print("Please set it before running this test script")
78
+ sys.exit(1)
79
+
80
+ test_agent_manager()
81
+ test_create_agents()
82
+
83
+ print("\nAll tests completed successfully!")
src/opensymbiose/config.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import os
2
+
3
+ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
src/opensymbiose/gradio/app.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import time
3
+ from dataclasses import asdict
4
+
5
+ import gradio as gr
6
+ from gradio import ChatMessage
7
+ from mistralai import Mistral, ToolReferenceChunk
8
+
9
+ import opensymbiose.config
10
+ from opensymbiose.agents.create_agents import get_agents
11
+
12
+ logger = logging.getLogger(__name__)
13
+ client = Mistral(api_key=opensymbiose.config.MISTRAL_API_KEY)
14
+ model = "mistral-medium-latest"
15
+
16
+
17
+ def get_mistral_answer(message: str, history):
18
+ # Start global execution timer
19
+ global_start_time = time.time()
20
+
21
+ # Dictionary to track tool execution times
22
+ tool_timers = {}
23
+
24
+ agents = get_agents()
25
+ main_agent = agents["test_agent_tools"]
26
+ stream_response = client.beta.conversations.start_stream(
27
+ agent_id=main_agent.id, inputs=message, store=False
28
+ )
29
+ messages = []
30
+ # Add an initial empty message for the assistant's response
31
+ messages.append(asdict(ChatMessage(role="assistant", content="")))
32
+ yield messages
33
+ full_response = ""
34
+ for chunk in stream_response:
35
+ print(chunk)
36
+ if chunk.event == "message.output.delta":
37
+ if isinstance(chunk.data.content, str):
38
+ full_response += chunk.data.content
39
+ if isinstance(chunk.data.content, ToolReferenceChunk):
40
+ full_response += f"([{chunk.data.content.title}]({chunk.data.content.url})) "
41
+ # Update the last message with the current full response
42
+ messages[-1] = asdict(ChatMessage(role="assistant", content=full_response))
43
+ yield messages
44
+ elif chunk.event == "tool.execution.started":
45
+ # Start timer for this tool
46
+ tool_timers[chunk.data.name] = time.time()
47
+ # Add a new message for tool execution start
48
+ messages.append(asdict(ChatMessage(role="assistant", content="",
49
+ metadata={"title": f"🛠️ Using tool {chunk.data.name}..."})))
50
+ yield messages
51
+ elif chunk.event == "tool.execution.done":
52
+ # Calculate tool execution duration
53
+ tool_duration = time.time() - tool_timers.get(chunk.data.name, time.time())
54
+ # Add a new message for tool execution completion
55
+ messages.append(asdict(ChatMessage(role="assistant", content="",
56
+ metadata={"title": f"🛠️ Finished using {chunk.data.name}.",
57
+ "duration": round(tool_duration, 2)})))
58
+ # Add a new empty message for the continuing assistant response
59
+ messages.append(asdict(ChatMessage(role="assistant", content=full_response)))
60
+ yield messages
61
+ elif chunk.event == "agent.handoff.started":
62
+ # Start timer for agent handoff
63
+ tool_timers["agent_handoff"] = time.time()
64
+ # Add a new message for agent handoff start
65
+ messages.append(asdict(ChatMessage(role="assistant", content="",
66
+ metadata={
67
+ "title": f"🔄 Handing off from agent {chunk.data.previous_agent_name}..."})))
68
+ yield messages
69
+ elif chunk.event == "agent.handoff.done":
70
+ # Calculate handoff duration
71
+ handoff_duration = time.time() - tool_timers.get("agent_handoff", time.time())
72
+ # Add a new message for agent handoff completion
73
+ messages.append(asdict(ChatMessage(role="assistant", content="",
74
+ metadata={
75
+ "title": f"✅ Handoff complete. Now using agent {chunk.data.next_agent_name}.",
76
+ "duration": round(handoff_duration, 2)})))
77
+ # Add a new empty message for the continuing assistant response
78
+ messages.append(asdict(ChatMessage(role="assistant", content=full_response)))
79
+ yield messages
80
+ elif chunk.event == "function.call.delta":
81
+ # Start timer for function call
82
+ function_start_time = time.time()
83
+ # Add function call information to the response
84
+ function_info = f"Calling function: {chunk.data.name} with arguments: {chunk.data.arguments}"
85
+ # Store the function name for potential future duration calculation
86
+ tool_timers[f"function_{chunk.data.name}"] = function_start_time
87
+ messages.append(asdict(ChatMessage(role="assistant", content="",
88
+ metadata={"title": f"📞 {function_info}"})))
89
+ yield messages
90
+ elif chunk.event == "conversation.response.started":
91
+ # Add a new message for conversation response start
92
+ messages.append(asdict(ChatMessage(role="assistant", content="",
93
+ metadata={"title": f"🚀 Symbiose agent is starting...",
94
+ "log": f"{chunk.data.conversation_id}"})))
95
+ yield messages
96
+ elif chunk.event == "conversation.response.done":
97
+ # Calculate global execution duration
98
+ global_duration = time.time() - global_start_time
99
+ # Add a new message for conversation response completion
100
+ messages.append(asdict(ChatMessage(role="assistant", content="",
101
+ metadata={"title": f"✅ Conversation response complete.",
102
+ "log": f"Tokens: Prompt: {chunk.data.usage.prompt_tokens}, Completion: {chunk.data.usage.completion_tokens}, Total: {chunk.data.usage.total_tokens} Connectors: {chunk.data.usage.connector_tokens}",
103
+ "duration": round(global_duration, 2)})))
104
+ yield messages
105
+ elif chunk.event == "conversation.response.error":
106
+ # Add a new message for conversation response error
107
+ error_message = f"Error: {chunk.data.message} (Code: {chunk.data.code})"
108
+ messages.append(asdict(ChatMessage(role="assistant", content="",
109
+ metadata={"title": f"❌ {error_message}"})))
110
+ yield messages
111
+ else:
112
+ # For other events, just update the last message
113
+ messages[-1] = asdict(ChatMessage(role="assistant", content=full_response))
114
+ yield messages
115
+
116
+
117
+ with gr.Blocks() as demo:
118
+ gr.ChatInterface(
119
+ fn=get_mistral_answer,
120
+ type="messages",
121
+ title="Open-Symbiose🧬",
122
+ description="OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher",
123
+ examples=["search internet: CEO of google", "Use your calculator agent to do 1273*3/12^2"],
124
+ save_history=True,
125
+ flagging_mode="manual"
126
+
127
+ )
128
+
129
+ if __name__ == "__main__":
130
+ demo.launch()
src/opensymbiose/main.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ def hello_world() -> str:
2
+ """Prints a hello world message."""
3
+ return "Hello from OpenSymbiose!"
4
+
5
+
6
+ if __name__ == "__main__":
7
+ hello_world()
tests/test_main.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from opensymbiose.main import hello_world
2
+
3
+
4
+ def test_hello_world():
5
+ """Test that hello_world prints the expected message."""
6
+ output = hello_world()
7
+ assert output == "Hello from OpenSymbiose!"
uv.lock ADDED
The diff for this file is too large to render. See raw diff