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.
@@ -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 # type: ignore
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 # type: ignore
312
+ final_lifespan = combined_lifespan
259
313
 
260
314
  fastapi_app = self._make_app(lifespan=final_lifespan)
261
315
  else:
@@ -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.run(message)
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.run(message)
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.run(message)
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