Skip to content

Commit 60c739e

Browse files
Working basic double agent handoff and streaming
1 parent 15a5636 commit 60c739e

File tree

15 files changed

+1103
-9
lines changed

15 files changed

+1103
-9
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,6 @@ env/
183183
**/__pycache__/
184184
dist/
185185
.coverage
186-
186+
.gradio
187+
.idea
188+
.DS_Store

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ help:
1717
@echo " make allci Run all CI steps (check, format, type, test coverage)"
1818

1919
dev:
20-
uv run opensymbiose
20+
uv run --env-file=.env gradio src/opensymbiose/gradio/app.py
2121

2222
prod:
23-
uv run opensymbiose
23+
uv run --env-file=.env opensymbiose
2424

2525
test:
26-
uv run pytest tests/
26+
uv run --env-file=.env pytest tests/
2727

2828
cov:
29-
uv run pytest --cov=src/opensymbiose tests/ --cov-report=term-missing
29+
uv run --env-file=.env pytest --cov=src/opensymbiose tests/ --cov-report=term-missing
3030

3131
check:
3232
uv run ruff check $$(git diff --name-only --cached -- '*.py')

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
---
2+
title: opensymbiose
3+
app_file: src/opensymbiose/gradio/app.py
4+
sdk: gradio
5+
sdk_version: 5.31.0
6+
---
17
# OpenSymbiose: Open Source Biotechnology AI Agent
28

3-
OpenSymbiose in an open-source biotechnology / biology research AI agent designed to support researcher.
9+
OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher.
410

511
Creator and Maintainer: [**Corentin Meyer**, PhD](https://cmeyer.fr/) - <[email protected]>
612

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ hide:
77

88
# OpenSymbiose: Open Source Biotechnology AI Agent
99

10-
OpenSymbiose in an open-source biotechnology / biology research AI agent designed to support researcher.
10+
OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher.
1111

1212
Creator and Maintainer: [**Corentin Meyer**, PhD](https://cmeyer.fr/) - <[email protected]>
1313

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "opensymbiose"
33
version = "0.0.0"
4-
description = "OpenSymbiose in an open-source biotechnology / biology research AI agent designed to support researcher."
4+
description = "OpenSymbiose is an open-source biotechnology / biology research AI agent designed to support researcher."
55
readme = "README.md"
66
requires-python = ">=3.13"
77
authors = [
@@ -11,8 +11,11 @@ maintainers = [
1111
{ name = "Corentin Meyer", email = "[email protected]" }
1212
]
1313
dependencies = [
14+
"biopython>=1.85",
15+
"bioservices>=1.12.1",
1416
"gradio>=5.31.0",
1517
"mistralai>=1.8.0",
18+
"python-dotenv>=1.1.0",
1619
]
1720

1821
[project.urls]
@@ -41,7 +44,7 @@ requires = ["hatchling"]
4144
build-backend = "hatchling.build"
4245

4346
[project.scripts]
44-
opensymbiose = "opensymbiose.main:hello_world"
47+
opensymbiose = "opensymbiose.gradio.app:demo.launch"
4548

4649
[tool.ruff]
4750
target-version = "py313"

requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from dotenv import load_dotenv
2+
3+
load_dotenv()

src/opensymbiose/agents/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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})")
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)