agno 2.0.8__py3-none-any.whl → 2.0.9__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.
Files changed (45) hide show
  1. agno/agent/agent.py +2 -2
  2. agno/db/base.py +14 -0
  3. agno/db/dynamo/dynamo.py +107 -27
  4. agno/db/firestore/firestore.py +109 -33
  5. agno/db/gcs_json/gcs_json_db.py +100 -20
  6. agno/db/in_memory/in_memory_db.py +95 -20
  7. agno/db/json/json_db.py +101 -21
  8. agno/db/migrations/v1_to_v2.py +181 -35
  9. agno/db/mongo/mongo.py +251 -26
  10. agno/db/mysql/mysql.py +307 -6
  11. agno/db/postgres/postgres.py +279 -33
  12. agno/db/redis/redis.py +99 -22
  13. agno/db/singlestore/singlestore.py +319 -38
  14. agno/db/sqlite/sqlite.py +339 -23
  15. agno/models/anthropic/claude.py +0 -20
  16. agno/models/huggingface/huggingface.py +2 -1
  17. agno/models/ollama/chat.py +28 -2
  18. agno/models/openai/chat.py +7 -0
  19. agno/models/openai/responses.py +8 -8
  20. agno/os/interfaces/base.py +2 -0
  21. agno/os/interfaces/slack/router.py +50 -10
  22. agno/os/interfaces/slack/slack.py +6 -4
  23. agno/os/interfaces/whatsapp/router.py +7 -4
  24. agno/os/router.py +18 -0
  25. agno/os/utils.py +2 -2
  26. agno/reasoning/azure_ai_foundry.py +2 -2
  27. agno/reasoning/deepseek.py +2 -2
  28. agno/reasoning/groq.py +2 -2
  29. agno/reasoning/ollama.py +2 -2
  30. agno/reasoning/openai.py +2 -2
  31. agno/run/base.py +15 -2
  32. agno/team/team.py +0 -7
  33. agno/tools/mcp_toolbox.py +284 -0
  34. agno/tools/scrapegraph.py +58 -31
  35. agno/tools/whatsapp.py +1 -1
  36. agno/utils/print_response/agent.py +2 -2
  37. agno/utils/print_response/team.py +6 -6
  38. agno/utils/reasoning.py +22 -1
  39. agno/utils/string.py +9 -0
  40. agno/workflow/workflow.py +0 -1
  41. {agno-2.0.8.dist-info → agno-2.0.9.dist-info}/METADATA +4 -1
  42. {agno-2.0.8.dist-info → agno-2.0.9.dist-info}/RECORD +45 -44
  43. {agno-2.0.8.dist-info → agno-2.0.9.dist-info}/WHEEL +0 -0
  44. {agno-2.0.8.dist-info → agno-2.0.9.dist-info}/licenses/LICENSE +0 -0
  45. {agno-2.0.8.dist-info → agno-2.0.9.dist-info}/top_level.txt +0 -0
@@ -406,25 +406,25 @@ class OpenAIResponses(Model):
406
406
 
407
407
  messages_to_format = messages
408
408
  previous_response_id: Optional[str] = None
409
-
409
+
410
410
  if self._using_reasoning_model() and self.store is not False:
411
411
  # Detect whether we're chaining via previous_response_id. If so, we should NOT
412
412
  # re-send prior function_call items; the Responses API already has the state and
413
413
  # expects only the corresponding function_call_output items.
414
-
414
+
415
415
  for msg in reversed(messages):
416
416
  if (
417
- msg.role == "assistant"
418
- and hasattr(msg, "provider_data")
419
- and msg.provider_data
417
+ msg.role == "assistant"
418
+ and hasattr(msg, "provider_data")
419
+ and msg.provider_data
420
420
  and "response_id" in msg.provider_data
421
421
  ):
422
422
  previous_response_id = msg.provider_data["response_id"]
423
423
  msg_index = messages.index(msg)
424
-
424
+
425
425
  # Include messages after this assistant message
426
- messages_to_format = messages[msg_index + 1:]
427
-
426
+ messages_to_format = messages[msg_index + 1 :]
427
+
428
428
  break
429
429
 
430
430
  # Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
@@ -5,6 +5,7 @@ from fastapi import APIRouter
5
5
 
6
6
  from agno.agent import Agent
7
7
  from agno.team import Team
8
+ from agno.workflow.workflow import Workflow
8
9
 
9
10
 
10
11
  class BaseInterface(ABC):
@@ -13,6 +14,7 @@ class BaseInterface(ABC):
13
14
  router_prefix: str = ""
14
15
  agent: Optional[Agent] = None
15
16
  team: Optional[Team] = None
17
+ workflow: Optional[Workflow] = None
16
18
 
17
19
  router: APIRouter
18
20
 
@@ -1,16 +1,49 @@
1
1
  from typing import Optional
2
2
 
3
3
  from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
4
+ from pydantic import BaseModel, Field
4
5
 
5
6
  from agno.agent.agent import Agent
6
7
  from agno.os.interfaces.slack.security import verify_slack_signature
7
8
  from agno.team.team import Team
8
9
  from agno.tools.slack import SlackTools
9
10
  from agno.utils.log import log_info
11
+ from agno.workflow.workflow import Workflow
10
12
 
11
13
 
12
- def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Optional[Team] = None) -> APIRouter:
13
- @router.post("/events")
14
+ class SlackEventResponse(BaseModel):
15
+ """Response model for Slack event processing"""
16
+
17
+ status: str = Field(default="ok", description="Processing status")
18
+
19
+
20
+ class SlackChallengeResponse(BaseModel):
21
+ """Response model for Slack URL verification challenge"""
22
+
23
+ challenge: str = Field(description="Challenge string to echo back to Slack")
24
+
25
+
26
+ def attach_routes(
27
+ router: APIRouter, agent: Optional[Agent] = None, team: Optional[Team] = None, workflow: Optional[Workflow] = None
28
+ ) -> APIRouter:
29
+ # Determine entity type for documentation
30
+ entity_type = "agent" if agent else "team" if team else "workflow" if workflow else "unknown"
31
+ entity_name = getattr(agent or team or workflow, "name", f"Unnamed {entity_type}")
32
+
33
+ @router.post(
34
+ "/events",
35
+ operation_id=f"slack_events_{entity_type}",
36
+ summary=f"Process Slack Events for {entity_type.title()}",
37
+ description=f"Process incoming Slack events and route them to the configured {entity_type}: {entity_name}",
38
+ tags=["Slack", f"Slack-{entity_type.title()}"],
39
+ response_model=SlackEventResponse,
40
+ response_model_exclude_none=True,
41
+ responses={
42
+ 200: {"description": "Event processed successfully"},
43
+ 400: {"description": "Missing Slack headers"},
44
+ 403: {"description": "Invalid Slack signature"},
45
+ },
46
+ )
14
47
  async def slack_events(request: Request, background_tasks: BackgroundTasks):
15
48
  body = await request.body()
16
49
  timestamp = request.headers.get("X-Slack-Request-Timestamp")
@@ -26,7 +59,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
26
59
 
27
60
  # Handle URL verification
28
61
  if data.get("type") == "url_verification":
29
- return {"challenge": data.get("challenge")}
62
+ return SlackChallengeResponse(challenge=data.get("challenge"))
30
63
 
31
64
  # Process other event types (e.g., message events) asynchronously
32
65
  if "event" in data:
@@ -37,7 +70,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
37
70
  else:
38
71
  background_tasks.add_task(_process_slack_event, event)
39
72
 
40
- return {"status": "ok"}
73
+ return SlackEventResponse(status="ok")
41
74
 
42
75
  async def _process_slack_event(event: dict):
43
76
  if event.get("type") == "message":
@@ -57,12 +90,19 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
57
90
  response = await agent.arun(message_text, user_id=user if user else None, session_id=session_id)
58
91
  elif team:
59
92
  response = await team.arun(message_text, user_id=user if user else None, session_id=session_id) # type: ignore
93
+ elif workflow:
94
+ response = await workflow.arun(message_text, user_id=user if user else None, session_id=session_id) # type: ignore
60
95
 
61
- if response.reasoning_content:
62
- _send_slack_message(
63
- channel=channel_id, message=f"Reasoning: \n{response.reasoning_content}", thread_ts=ts, italics=True
64
- )
65
- _send_slack_message(channel=channel_id, message=response.content or "", thread_ts=ts)
96
+ if response:
97
+ if hasattr(response, "reasoning_content") and response.reasoning_content:
98
+ _send_slack_message(
99
+ channel=channel_id,
100
+ message=f"Reasoning: \n{response.reasoning_content}",
101
+ thread_ts=ts,
102
+ italics=True,
103
+ )
104
+
105
+ _send_slack_message(channel=channel_id, message=response.content or "", thread_ts=ts)
66
106
 
67
107
  def _send_slack_message(channel: str, thread_ts: str, message: str, italics: bool = False):
68
108
  if len(message) <= 40000:
@@ -85,6 +125,6 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
85
125
  formatted_batch = "\n".join([f"_{line}_" for line in batch_message.split("\n")])
86
126
  SlackTools().send_message_thread(channel=channel, text=formatted_batch or "", thread_ts=thread_ts)
87
127
  else:
88
- SlackTools().send_message_thread(channel=channel, text=message or "", thread_ts=thread_ts)
128
+ SlackTools().send_message_thread(channel=channel, text=batch_message or "", thread_ts=thread_ts)
89
129
 
90
130
  return router
@@ -7,6 +7,7 @@ from agno.agent.agent import Agent
7
7
  from agno.os.interfaces.base import BaseInterface
8
8
  from agno.os.interfaces.slack.router import attach_routes
9
9
  from agno.team.team import Team
10
+ from agno.workflow.workflow import Workflow
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
@@ -16,17 +17,18 @@ class Slack(BaseInterface):
16
17
 
17
18
  router: APIRouter
18
19
 
19
- def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None):
20
+ def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None, workflow: Optional[Workflow] = None):
20
21
  self.agent = agent
21
22
  self.team = team
23
+ self.workflow = workflow
22
24
 
23
- if not (self.agent or self.team):
24
- raise ValueError("Slack requires an agent or a team")
25
+ if not (self.agent or self.team or self.workflow):
26
+ raise ValueError("Slack requires an agent, team or workflow")
25
27
 
26
28
  def get_router(self, **kwargs) -> APIRouter:
27
29
  # Cannot be overridden
28
30
  self.router = APIRouter(prefix="/slack", tags=["Slack"])
29
31
 
30
- self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
32
+ self.router = attach_routes(router=self.router, agent=self.agent, team=self.team, workflow=self.workflow)
31
33
 
32
34
  return self.router
@@ -19,6 +19,9 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
19
19
  if agent is None and team is None:
20
20
  raise ValueError("Either agent or team must be provided.")
21
21
 
22
+ # Create WhatsApp tools instance once for reuse
23
+ whatsapp_tools = WhatsAppTools(async_mode=True)
24
+
22
25
  @router.get("/status")
23
26
  async def status():
24
27
  return {"status": "available"}
@@ -185,9 +188,9 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
185
188
  if italics:
186
189
  # Handle multi-line messages by making each line italic
187
190
  formatted_message = "\n".join([f"_{line}_" for line in message.split("\n")])
188
- await WhatsAppTools().send_text_message_async(recipient=recipient, text=formatted_message)
191
+ await whatsapp_tools.send_text_message_async(recipient=recipient, text=formatted_message)
189
192
  else:
190
- await WhatsAppTools().send_text_message_async(recipient=recipient, text=message)
193
+ await whatsapp_tools.send_text_message_async(recipient=recipient, text=message)
191
194
  return
192
195
 
193
196
  # Split message into batches of 4000 characters (WhatsApp message limit is 4096)
@@ -199,8 +202,8 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
199
202
  if italics:
200
203
  # Handle multi-line messages by making each line italic
201
204
  formatted_batch = "\n".join([f"_{line}_" for line in batch_message.split("\n")])
202
- await WhatsAppTools().send_text_message_async(recipient=recipient, text=formatted_batch)
205
+ await whatsapp_tools.send_text_message_async(recipient=recipient, text=formatted_batch)
203
206
  else:
204
- await WhatsAppTools().send_text_message_async(recipient=recipient, text=batch_message)
207
+ await whatsapp_tools.send_text_message_async(recipient=recipient, text=batch_message)
205
208
 
206
209
  return router
agno/os/router.py CHANGED
@@ -72,6 +72,24 @@ async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict
72
72
  sig = inspect.signature(endpoint_func)
73
73
  known_fields = set(sig.parameters.keys())
74
74
  kwargs = {key: value for key, value in form_data.items() if key not in known_fields}
75
+
76
+ # Handle JSON parameters. They are passed as strings and need to be deserialized.
77
+
78
+ if session_state := kwargs.get("session_state"):
79
+ try:
80
+ session_state_dict = json.loads(session_state) # type: ignore
81
+ kwargs["session_state"] = session_state_dict
82
+ except json.JSONDecodeError:
83
+ kwargs.pop("session_state")
84
+ log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
85
+ if dependencies := kwargs.get("dependencies"):
86
+ try:
87
+ dependencies_dict = json.loads(dependencies) # type: ignore
88
+ kwargs["dependencies"] = dependencies_dict
89
+ except json.JSONDecodeError:
90
+ kwargs.pop("dependencies")
91
+ log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
92
+
75
93
  return kwargs
76
94
 
77
95
 
agno/os/utils.py CHANGED
@@ -57,7 +57,7 @@ def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> st
57
57
  if is_workflow_run:
58
58
  step_executor_runs = run_dict.get("step_executor_runs", [])
59
59
  if step_executor_runs:
60
- for message in step_executor_runs[0].get("messages", []):
60
+ for message in reversed(step_executor_runs[0].get("messages", [])):
61
61
  if message.get("role") == "user":
62
62
  return message.get("content", "")
63
63
 
@@ -70,7 +70,7 @@ def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> st
70
70
  return str(input_value)
71
71
 
72
72
  if run_dict.get("messages") is not None:
73
- for message in run_dict["messages"]:
73
+ for message in reversed(run_dict["messages"]):
74
74
  if message.get("role") == "user":
75
75
  return message.get("content", "")
76
76
 
@@ -20,7 +20,7 @@ def get_ai_foundry_reasoning(reasoning_agent: "Agent", messages: List[Message])
20
20
  from agno.run.agent import RunOutput
21
21
 
22
22
  try:
23
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
23
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
24
24
  except Exception as e:
25
25
  logger.warning(f"Reasoning error: {e}")
26
26
  return None
@@ -46,7 +46,7 @@ async def aget_ai_foundry_reasoning(reasoning_agent: "Agent", messages: List[Mes
46
46
  from agno.run.agent import RunOutput
47
47
 
48
48
  try:
49
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
49
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
50
50
  except Exception as e:
51
51
  logger.warning(f"Reasoning error: {e}")
52
52
  return None
@@ -20,7 +20,7 @@ def get_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Message]) ->
20
20
  message.role = "system"
21
21
 
22
22
  try:
23
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
23
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
24
24
  except Exception as e:
25
25
  logger.warning(f"Reasoning error: {e}")
26
26
  return None
@@ -46,7 +46,7 @@ async def aget_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Messa
46
46
  message.role = "system"
47
47
 
48
48
  try:
49
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
49
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
50
50
  except Exception as e:
51
51
  logger.warning(f"Reasoning error: {e}")
52
52
  return None
agno/reasoning/groq.py CHANGED
@@ -20,7 +20,7 @@ def get_groq_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Opt
20
20
  message.role = "system"
21
21
 
22
22
  try:
23
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
23
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
24
24
  except Exception as e:
25
25
  logger.warning(f"Reasoning error: {e}")
26
26
  return None
@@ -50,7 +50,7 @@ async def aget_groq_reasoning(reasoning_agent: "Agent", messages: List[Message])
50
50
  message.role = "system"
51
51
 
52
52
  try:
53
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
53
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
54
54
  except Exception as e:
55
55
  logger.warning(f"Reasoning error: {e}")
56
56
  return None
agno/reasoning/ollama.py CHANGED
@@ -20,7 +20,7 @@ def get_ollama_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> O
20
20
  from agno.run.agent import RunOutput
21
21
 
22
22
  try:
23
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
23
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
24
24
  except Exception as e:
25
25
  logger.warning(f"Reasoning error: {e}")
26
26
  return None
@@ -46,7 +46,7 @@ async def aget_ollama_reasoning(reasoning_agent: "Agent", messages: List[Message
46
46
  from agno.run.agent import RunOutput
47
47
 
48
48
  try:
49
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
49
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
50
50
  except Exception as e:
51
51
  logger.warning(f"Reasoning error: {e}")
52
52
  return None
agno/reasoning/openai.py CHANGED
@@ -29,7 +29,7 @@ def get_openai_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> O
29
29
  from agno.run.agent import RunOutput
30
30
 
31
31
  try:
32
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
32
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
33
33
  except Exception as e:
34
34
  logger.warning(f"Reasoning error: {e}")
35
35
  return None
@@ -60,7 +60,7 @@ async def aget_openai_reasoning(reasoning_agent: "Agent", messages: List[Message
60
60
  message.role = "system"
61
61
 
62
62
  try:
63
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
63
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
64
64
  except Exception as e:
65
65
  logger.warning(f"Reasoning error: {e}")
66
66
  return None
agno/run/base.py CHANGED
@@ -117,6 +117,19 @@ class BaseRunOutputEvent:
117
117
 
118
118
  def to_json(self, separators=(", ", ": "), indent: Optional[int] = 2) -> str:
119
119
  import json
120
+ from datetime import date, datetime, time
121
+ from enum import Enum
122
+
123
+ def json_serializer(obj):
124
+ # Datetime like
125
+ if isinstance(obj, (datetime, date, time)):
126
+ return obj.isoformat()
127
+ # Enums
128
+ if isinstance(obj, Enum):
129
+ v = obj.value
130
+ return v if isinstance(v, (str, int, float, bool, type(None))) else obj.name
131
+ # Fallback to string
132
+ return str(obj)
120
133
 
121
134
  try:
122
135
  _dict = self.to_dict()
@@ -125,9 +138,9 @@ class BaseRunOutputEvent:
125
138
  raise
126
139
 
127
140
  if indent is None:
128
- return json.dumps(_dict, separators=separators)
141
+ return json.dumps(_dict, separators=separators, default=json_serializer, ensure_ascii=False)
129
142
  else:
130
- return json.dumps(_dict, indent=indent, separators=separators)
143
+ return json.dumps(_dict, indent=indent, separators=separators, default=json_serializer, ensure_ascii=False)
131
144
 
132
145
  @classmethod
133
146
  def from_dict(cls, data: Dict[str, Any]):
agno/team/team.py CHANGED
@@ -2037,9 +2037,6 @@ class Team:
2037
2037
  if model_response.audio is not None:
2038
2038
  run_response.response_audio = model_response.audio
2039
2039
 
2040
- # Update the run_response created_at with the model response created_at
2041
- run_response.created_at = model_response.created_at
2042
-
2043
2040
  # Build a list of messages that should be added to the RunOutput
2044
2041
  messages_for_run_response = [m for m in run_messages.messages if m.add_to_agent_memory]
2045
2042
 
@@ -2099,7 +2096,6 @@ class Team:
2099
2096
  )
2100
2097
 
2101
2098
  # 3. Update TeamRunOutput
2102
- run_response.created_at = full_model_response.created_at
2103
2099
  if full_model_response.content is not None:
2104
2100
  run_response.content = full_model_response.content
2105
2101
  if full_model_response.reasoning_content is not None:
@@ -2189,7 +2185,6 @@ class Team:
2189
2185
  run_response.content = full_model_response.parsed
2190
2186
 
2191
2187
  # Update TeamRunOutput
2192
- run_response.created_at = full_model_response.created_at
2193
2188
  if full_model_response.content is not None:
2194
2189
  run_response.content = full_model_response.content
2195
2190
  if full_model_response.reasoning_content is not None:
@@ -2851,7 +2846,6 @@ class Team:
2851
2846
 
2852
2847
  # Update the TeamRunResponse content
2853
2848
  run_response.content = model_response.content
2854
- run_response.created_at = model_response.created_at
2855
2849
 
2856
2850
  if stream_intermediate_steps:
2857
2851
  yield self._handle_event(create_team_output_model_response_completed_event(run_response), run_response)
@@ -2909,7 +2903,6 @@ class Team:
2909
2903
 
2910
2904
  # Update the TeamRunResponse content
2911
2905
  run_response.content = model_response.content
2912
- run_response.created_at = model_response.created_at
2913
2906
 
2914
2907
  if stream_intermediate_steps:
2915
2908
  yield self._handle_event(create_team_output_model_response_completed_event(run_response), run_response)