agno 2.1.2__py3-none-any.whl → 2.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +39 -69
- agno/db/dynamo/dynamo.py +8 -6
- agno/db/dynamo/schemas.py +1 -10
- agno/db/dynamo/utils.py +2 -2
- agno/db/firestore/firestore.py +1 -3
- agno/db/gcs_json/gcs_json_db.py +5 -3
- agno/db/in_memory/in_memory_db.py +7 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/models/aws/bedrock.py +1 -1
- agno/models/openrouter/openrouter.py +39 -1
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +74 -0
- agno/os/app.py +59 -5
- agno/os/interfaces/agui/utils.py +6 -0
- agno/os/mcp.py +3 -3
- agno/os/schema.py +2 -0
- agno/os/utils.py +4 -2
- agno/session/workflow.py +69 -1
- agno/team/team.py +29 -79
- agno/tools/file.py +4 -2
- agno/tools/function.py +36 -18
- agno/tools/google_drive.py +270 -0
- agno/utils/merge_dict.py +3 -3
- agno/utils/print_response/workflow.py +112 -12
- agno/workflow/condition.py +25 -0
- agno/workflow/loop.py +25 -0
- agno/workflow/parallel.py +142 -118
- agno/workflow/router.py +25 -0
- agno/workflow/step.py +138 -25
- agno/workflow/steps.py +25 -0
- agno/workflow/types.py +26 -1
- agno/workflow/workflow.py +234 -12
- {agno-2.1.2.dist-info → agno-2.1.4.dist-info}/METADATA +1 -1
- {agno-2.1.2.dist-info → agno-2.1.4.dist-info}/RECORD +37 -34
- {agno-2.1.2.dist-info → agno-2.1.4.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.1.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.anthropic import Claude as AnthropicClaude
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from anthropic import AnthropicVertex as AnthropicClient
|
|
9
|
+
from anthropic import (
|
|
10
|
+
AsyncAnthropicVertex as AsyncAnthropicClient,
|
|
11
|
+
)
|
|
12
|
+
except ImportError as e:
|
|
13
|
+
raise ImportError("`anthropic` not installed. Please install it with `pip install anthropic`") from e
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Claude(AnthropicClaude):
|
|
18
|
+
"""
|
|
19
|
+
A class representing Anthropic Claude model.
|
|
20
|
+
|
|
21
|
+
For more information, see: https://docs.anthropic.com/en/api/messages
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
id: str = "claude-sonnet-4@20250514"
|
|
25
|
+
name: str = "Claude"
|
|
26
|
+
provider: str = "VertexAI"
|
|
27
|
+
|
|
28
|
+
# Client parameters
|
|
29
|
+
region: Optional[str] = None
|
|
30
|
+
project_id: Optional[str] = None
|
|
31
|
+
base_url: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
# Anthropic clients
|
|
34
|
+
client: Optional[AnthropicClient] = None
|
|
35
|
+
async_client: Optional[AsyncAnthropicClient] = None
|
|
36
|
+
|
|
37
|
+
def _get_client_params(self) -> Dict[str, Any]:
|
|
38
|
+
client_params: Dict[str, Any] = {}
|
|
39
|
+
|
|
40
|
+
# Add API key to client parameters
|
|
41
|
+
client_params["region"] = self.region or getenv("CLOUD_ML_REGION")
|
|
42
|
+
client_params["project_id"] = self.project_id or getenv("ANTHROPIC_VERTEX_PROJECT_ID")
|
|
43
|
+
client_params["base_url"] = self.base_url or getenv("ANTHROPIC_VERTEX_BASE_URL")
|
|
44
|
+
if self.timeout is not None:
|
|
45
|
+
client_params["timeout"] = self.timeout
|
|
46
|
+
|
|
47
|
+
# Add additional client parameters
|
|
48
|
+
if self.client_params is not None:
|
|
49
|
+
client_params.update(self.client_params)
|
|
50
|
+
if self.default_headers is not None:
|
|
51
|
+
client_params["default_headers"] = self.default_headers
|
|
52
|
+
return client_params
|
|
53
|
+
|
|
54
|
+
def get_client(self) -> AnthropicClient:
|
|
55
|
+
"""
|
|
56
|
+
Returns an instance of the Anthropic client.
|
|
57
|
+
"""
|
|
58
|
+
if self.client and not self.client.is_closed():
|
|
59
|
+
return self.client
|
|
60
|
+
|
|
61
|
+
_client_params = self._get_client_params()
|
|
62
|
+
self.client = AnthropicClient(**_client_params)
|
|
63
|
+
return self.client
|
|
64
|
+
|
|
65
|
+
def get_async_client(self) -> AsyncAnthropicClient:
|
|
66
|
+
"""
|
|
67
|
+
Returns an instance of the async Anthropic client.
|
|
68
|
+
"""
|
|
69
|
+
if self.async_client:
|
|
70
|
+
return self.async_client
|
|
71
|
+
|
|
72
|
+
_client_params = self._get_client_params()
|
|
73
|
+
self.async_client = AsyncAnthropicClient(**_client_params)
|
|
74
|
+
return self.async_client
|
agno/os/app.py
CHANGED
|
@@ -64,6 +64,30 @@ async def mcp_lifespan(_, mcp_tools):
|
|
|
64
64
|
await tool.close()
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def _combine_app_lifespans(lifespans: list) -> Any:
|
|
68
|
+
"""Combine multiple FastAPI app lifespan context managers into one."""
|
|
69
|
+
if len(lifespans) == 1:
|
|
70
|
+
return lifespans[0]
|
|
71
|
+
|
|
72
|
+
from contextlib import asynccontextmanager
|
|
73
|
+
|
|
74
|
+
@asynccontextmanager
|
|
75
|
+
async def combined_lifespan(app):
|
|
76
|
+
async def _run_nested(index: int):
|
|
77
|
+
if index >= len(lifespans):
|
|
78
|
+
yield
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
async with lifespans[index](app):
|
|
82
|
+
async for _ in _run_nested(index + 1):
|
|
83
|
+
yield
|
|
84
|
+
|
|
85
|
+
async for _ in _run_nested(0):
|
|
86
|
+
yield
|
|
87
|
+
|
|
88
|
+
return combined_lifespan
|
|
89
|
+
|
|
90
|
+
|
|
67
91
|
class AgentOS:
|
|
68
92
|
def __init__(
|
|
69
93
|
self,
|
|
@@ -183,9 +207,6 @@ class AgentOS:
|
|
|
183
207
|
|
|
184
208
|
team.initialize_team()
|
|
185
209
|
|
|
186
|
-
# Required for the built-in routes to work
|
|
187
|
-
team.store_events = True
|
|
188
|
-
|
|
189
210
|
for member in team.members:
|
|
190
211
|
if isinstance(member, Agent):
|
|
191
212
|
member.team_id = None
|
|
@@ -193,13 +214,20 @@ class AgentOS:
|
|
|
193
214
|
elif isinstance(member, Team):
|
|
194
215
|
member.initialize_team()
|
|
195
216
|
|
|
217
|
+
# Required for the built-in routes to work
|
|
218
|
+
team.store_events = True
|
|
219
|
+
|
|
196
220
|
if self.workflows:
|
|
197
221
|
for workflow in self.workflows:
|
|
198
222
|
# Track MCP tools recursively in workflow members
|
|
199
223
|
collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
|
|
224
|
+
|
|
200
225
|
if not workflow.id:
|
|
201
226
|
workflow.id = generate_id_from_name(workflow.name)
|
|
202
227
|
|
|
228
|
+
# Required for the built-in routes to work
|
|
229
|
+
workflow.store_events = True
|
|
230
|
+
|
|
203
231
|
if self.telemetry:
|
|
204
232
|
from agno.api.os import OSLaunch, log_os_telemetry
|
|
205
233
|
|
|
@@ -220,7 +248,7 @@ class AgentOS:
|
|
|
220
248
|
async with mcp_tools_lifespan(app): # type: ignore
|
|
221
249
|
yield
|
|
222
250
|
|
|
223
|
-
app_lifespan = combined_lifespan
|
|
251
|
+
app_lifespan = combined_lifespan
|
|
224
252
|
else:
|
|
225
253
|
app_lifespan = mcp_tools_lifespan
|
|
226
254
|
|
|
@@ -237,6 +265,32 @@ class AgentOS:
|
|
|
237
265
|
def get_app(self) -> FastAPI:
|
|
238
266
|
if self.base_app:
|
|
239
267
|
fastapi_app = self.base_app
|
|
268
|
+
|
|
269
|
+
# Initialize MCP server if enabled
|
|
270
|
+
if self.enable_mcp_server:
|
|
271
|
+
from agno.os.mcp import get_mcp_server
|
|
272
|
+
|
|
273
|
+
self._mcp_app = get_mcp_server(self)
|
|
274
|
+
|
|
275
|
+
# Collect all lifespans that need to be combined
|
|
276
|
+
lifespans = []
|
|
277
|
+
|
|
278
|
+
if fastapi_app.router.lifespan_context:
|
|
279
|
+
lifespans.append(fastapi_app.router.lifespan_context)
|
|
280
|
+
|
|
281
|
+
if self.mcp_tools:
|
|
282
|
+
lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
|
|
283
|
+
|
|
284
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
285
|
+
lifespans.append(self._mcp_app.lifespan)
|
|
286
|
+
|
|
287
|
+
if self.lifespan:
|
|
288
|
+
lifespans.append(self.lifespan)
|
|
289
|
+
|
|
290
|
+
# Combine lifespans and set them in the app
|
|
291
|
+
if lifespans:
|
|
292
|
+
fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
|
|
293
|
+
|
|
240
294
|
else:
|
|
241
295
|
if self.enable_mcp_server:
|
|
242
296
|
from contextlib import asynccontextmanager
|
|
@@ -255,7 +309,7 @@ class AgentOS:
|
|
|
255
309
|
async with self._mcp_app.lifespan(app): # type: ignore
|
|
256
310
|
yield
|
|
257
311
|
|
|
258
|
-
final_lifespan = combined_lifespan
|
|
312
|
+
final_lifespan = combined_lifespan
|
|
259
313
|
|
|
260
314
|
fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
261
315
|
else:
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import AsyncIterator, List, Set, Tuple, Union
|
|
|
8
8
|
|
|
9
9
|
from ag_ui.core import (
|
|
10
10
|
BaseEvent,
|
|
11
|
+
CustomEvent,
|
|
11
12
|
EventType,
|
|
12
13
|
RunFinishedEvent,
|
|
13
14
|
StepFinishedEvent,
|
|
@@ -261,6 +262,11 @@ def _create_events_from_chunk(
|
|
|
261
262
|
step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
|
|
262
263
|
events_to_emit.append(step_finished_event)
|
|
263
264
|
|
|
265
|
+
# Handle custom events
|
|
266
|
+
elif chunk.event == RunEvent.custom_event:
|
|
267
|
+
custom_event = CustomEvent(name=chunk.event, value=chunk.content)
|
|
268
|
+
events_to_emit.append(custom_event)
|
|
269
|
+
|
|
264
270
|
return events_to_emit, message_started, message_id
|
|
265
271
|
|
|
266
272
|
|
agno/os/mcp.py
CHANGED
|
@@ -78,21 +78,21 @@ def get_mcp_server(
|
|
|
78
78
|
agent = get_agent_by_id(agent_id, os.agents)
|
|
79
79
|
if agent is None:
|
|
80
80
|
raise Exception(f"Agent {agent_id} not found")
|
|
81
|
-
return agent.
|
|
81
|
+
return await agent.arun(message)
|
|
82
82
|
|
|
83
83
|
@mcp.tool(name="run_team", description="Run a team", tags={"core"}) # type: ignore
|
|
84
84
|
async def run_team(team_id: str, message: str) -> TeamRunOutput:
|
|
85
85
|
team = get_team_by_id(team_id, os.teams)
|
|
86
86
|
if team is None:
|
|
87
87
|
raise Exception(f"Team {team_id} not found")
|
|
88
|
-
return team.
|
|
88
|
+
return await team.arun(message)
|
|
89
89
|
|
|
90
90
|
@mcp.tool(name="run_workflow", description="Run a workflow", tags={"core"}) # type: ignore
|
|
91
91
|
async def run_workflow(workflow_id: str, message: str) -> WorkflowRunOutput:
|
|
92
92
|
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
93
93
|
if workflow is None:
|
|
94
94
|
raise Exception(f"Workflow {workflow_id} not found")
|
|
95
|
-
return workflow.
|
|
95
|
+
return await workflow.arun(message)
|
|
96
96
|
|
|
97
97
|
# Session Management Tools
|
|
98
98
|
@mcp.tool(name="get_sessions_for_agent", description="Get list of sessions for an agent", tags={"session"}) # type: ignore
|
agno/os/schema.py
CHANGED
|
@@ -913,6 +913,7 @@ class TeamRunSchema(BaseModel):
|
|
|
913
913
|
class WorkflowRunSchema(BaseModel):
|
|
914
914
|
run_id: str
|
|
915
915
|
run_input: Optional[str]
|
|
916
|
+
events: Optional[List[dict]]
|
|
916
917
|
workflow_id: Optional[str]
|
|
917
918
|
user_id: Optional[str]
|
|
918
919
|
content: Optional[Union[str, dict]]
|
|
@@ -933,6 +934,7 @@ class WorkflowRunSchema(BaseModel):
|
|
|
933
934
|
return cls(
|
|
934
935
|
run_id=run_response.get("run_id", ""),
|
|
935
936
|
run_input=run_input,
|
|
937
|
+
events=run_response.get("events", []),
|
|
936
938
|
workflow_id=run_response.get("workflow_id", ""),
|
|
937
939
|
user_id=run_response.get("user_id", ""),
|
|
938
940
|
content=run_response.get("content", ""),
|
agno/os/utils.py
CHANGED
|
@@ -89,16 +89,17 @@ def get_session_name(session: Dict[str, Any]) -> str:
|
|
|
89
89
|
|
|
90
90
|
# Otherwise use the original user message
|
|
91
91
|
else:
|
|
92
|
-
runs = session.get("runs", [])
|
|
92
|
+
runs = session.get("runs", []) or []
|
|
93
93
|
|
|
94
94
|
# For teams, identify the first Team run and avoid using the first member's run
|
|
95
95
|
if session.get("session_type") == "team":
|
|
96
96
|
run = None
|
|
97
97
|
for r in runs:
|
|
98
98
|
# If agent_id is not present, it's a team run
|
|
99
|
-
if not r.get("agent_id"):
|
|
99
|
+
if not r.get("agent_id"):
|
|
100
100
|
run = r
|
|
101
101
|
break
|
|
102
|
+
|
|
102
103
|
# Fallback to first run if no team run found
|
|
103
104
|
if run is None and runs:
|
|
104
105
|
run = runs[0]
|
|
@@ -112,6 +113,7 @@ def get_session_name(session: Dict[str, Any]) -> str:
|
|
|
112
113
|
elif isinstance(workflow_input, dict):
|
|
113
114
|
try:
|
|
114
115
|
import json
|
|
116
|
+
|
|
115
117
|
return json.dumps(workflow_input)
|
|
116
118
|
except (TypeError, ValueError):
|
|
117
119
|
pass
|
agno/session/workflow.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Any, Dict, List, Mapping, Optional
|
|
5
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
6
6
|
|
|
7
7
|
from agno.run.workflow import WorkflowRunOutput
|
|
8
8
|
from agno.utils.log import logger
|
|
@@ -75,6 +75,74 @@ class WorkflowSession:
|
|
|
75
75
|
else:
|
|
76
76
|
self.runs.append(run)
|
|
77
77
|
|
|
78
|
+
def get_workflow_history(self, num_runs: Optional[int] = None) -> List[Tuple[str, str]]:
|
|
79
|
+
"""Get workflow history as structured data (input, response pairs)
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
num_runs: Number of recent runs to include. If None, returns all available history.
|
|
83
|
+
"""
|
|
84
|
+
if not self.runs:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
from agno.run.base import RunStatus
|
|
88
|
+
|
|
89
|
+
# Get completed runs only (exclude current/pending run)
|
|
90
|
+
completed_runs = [run for run in self.runs if run.status == RunStatus.completed]
|
|
91
|
+
|
|
92
|
+
if num_runs is not None and len(completed_runs) > num_runs:
|
|
93
|
+
recent_runs = completed_runs[-num_runs:]
|
|
94
|
+
else:
|
|
95
|
+
recent_runs = completed_runs
|
|
96
|
+
|
|
97
|
+
if not recent_runs:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
# Return structured data as list of (input, response) tuples
|
|
101
|
+
history_data = []
|
|
102
|
+
for run in recent_runs:
|
|
103
|
+
# Get input
|
|
104
|
+
input_str = ""
|
|
105
|
+
if run.input:
|
|
106
|
+
input_str = str(run.input) if not isinstance(run.input, str) else run.input
|
|
107
|
+
|
|
108
|
+
# Get response
|
|
109
|
+
response_str = ""
|
|
110
|
+
if run.content:
|
|
111
|
+
response_str = str(run.content) if not isinstance(run.content, str) else run.content
|
|
112
|
+
|
|
113
|
+
history_data.append((input_str, response_str))
|
|
114
|
+
|
|
115
|
+
return history_data
|
|
116
|
+
|
|
117
|
+
def get_workflow_history_context(self, num_runs: Optional[int] = None) -> Optional[str]:
|
|
118
|
+
"""Get formatted workflow history context for steps
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
num_runs: Number of recent runs to include. If None, returns all available history.
|
|
122
|
+
"""
|
|
123
|
+
history_data = self.get_workflow_history(num_runs)
|
|
124
|
+
|
|
125
|
+
if not history_data:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# Format as workflow context using the structured data
|
|
129
|
+
context_parts = ["<workflow_history_context>"]
|
|
130
|
+
|
|
131
|
+
for i, (input_str, response_str) in enumerate(history_data, 1):
|
|
132
|
+
context_parts.append(f"[run-{i}]")
|
|
133
|
+
|
|
134
|
+
if input_str:
|
|
135
|
+
context_parts.append(f"input: {input_str}")
|
|
136
|
+
if response_str:
|
|
137
|
+
context_parts.append(f"response: {response_str}")
|
|
138
|
+
|
|
139
|
+
context_parts.append("") # Empty line between runs
|
|
140
|
+
|
|
141
|
+
context_parts.append("</workflow_history_context>")
|
|
142
|
+
context_parts.append("") # Empty line before current input
|
|
143
|
+
|
|
144
|
+
return "\n".join(context_parts)
|
|
145
|
+
|
|
78
146
|
def to_dict(self) -> Dict[str, Any]:
|
|
79
147
|
"""Convert to dictionary for storage, serializing runs to dicts"""
|
|
80
148
|
|