agno 2.0.7__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 (64) hide show
  1. agno/agent/agent.py +83 -51
  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 +322 -47
  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/knowledge/embedder/sentence_transformer.py +3 -3
  16. agno/knowledge/knowledge.py +152 -31
  17. agno/knowledge/types.py +8 -0
  18. agno/models/anthropic/claude.py +0 -20
  19. agno/models/cometapi/__init__.py +5 -0
  20. agno/models/cometapi/cometapi.py +57 -0
  21. agno/models/google/gemini.py +4 -8
  22. agno/models/huggingface/huggingface.py +2 -1
  23. agno/models/ollama/chat.py +52 -3
  24. agno/models/openai/chat.py +9 -7
  25. agno/models/openai/responses.py +21 -17
  26. agno/os/interfaces/agui/agui.py +2 -2
  27. agno/os/interfaces/agui/utils.py +81 -18
  28. agno/os/interfaces/base.py +2 -0
  29. agno/os/interfaces/slack/router.py +50 -10
  30. agno/os/interfaces/slack/slack.py +6 -4
  31. agno/os/interfaces/whatsapp/router.py +7 -4
  32. agno/os/interfaces/whatsapp/whatsapp.py +2 -2
  33. agno/os/router.py +18 -0
  34. agno/os/utils.py +10 -2
  35. agno/reasoning/azure_ai_foundry.py +2 -2
  36. agno/reasoning/deepseek.py +2 -2
  37. agno/reasoning/default.py +3 -1
  38. agno/reasoning/groq.py +2 -2
  39. agno/reasoning/ollama.py +2 -2
  40. agno/reasoning/openai.py +2 -2
  41. agno/run/base.py +15 -2
  42. agno/session/agent.py +8 -5
  43. agno/session/team.py +14 -10
  44. agno/team/team.py +218 -111
  45. agno/tools/function.py +43 -4
  46. agno/tools/mcp.py +60 -37
  47. agno/tools/mcp_toolbox.py +284 -0
  48. agno/tools/scrapegraph.py +58 -31
  49. agno/tools/whatsapp.py +1 -1
  50. agno/utils/gemini.py +147 -19
  51. agno/utils/models/claude.py +9 -0
  52. agno/utils/print_response/agent.py +18 -2
  53. agno/utils/print_response/team.py +22 -6
  54. agno/utils/reasoning.py +22 -1
  55. agno/utils/string.py +9 -0
  56. agno/vectordb/base.py +2 -2
  57. agno/vectordb/langchaindb/langchaindb.py +5 -7
  58. agno/vectordb/llamaindex/llamaindexdb.py +25 -6
  59. agno/workflow/workflow.py +30 -15
  60. {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/METADATA +4 -1
  61. {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/RECORD +64 -61
  62. {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/WHEEL +0 -0
  63. {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/licenses/LICENSE +0 -0
  64. {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/top_level.txt +0 -0
@@ -16,19 +16,15 @@ from agno.models.response import ModelResponse
16
16
  from agno.run.agent import RunOutput
17
17
  from agno.utils.log import log_debug, log_error, log_warning
18
18
  from agno.utils.openai import _format_file_for_message, audio_to_message, images_to_message
19
+ from agno.utils.reasoning import extract_thinking_content
19
20
 
20
21
  try:
21
22
  from openai import APIConnectionError, APIStatusError, RateLimitError
22
23
  from openai import AsyncOpenAI as AsyncOpenAIClient
23
24
  from openai import OpenAI as OpenAIClient
24
25
  from openai.types import CompletionUsage
25
- from openai.types.chat import ChatCompletionAudio
26
- from openai.types.chat.chat_completion import ChatCompletion
27
- from openai.types.chat.chat_completion_chunk import (
28
- ChatCompletionChunk,
29
- ChoiceDelta,
30
- ChoiceDeltaToolCall,
31
- )
26
+ from openai.types.chat import ChatCompletion, ChatCompletionAudio, ChatCompletionChunk
27
+ from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall
32
28
  except (ImportError, ModuleNotFoundError):
33
29
  raise ImportError("`openai` not installed. Please install using `pip install openai`")
34
30
 
@@ -716,6 +712,12 @@ class OpenAIChat(Model):
716
712
  if response_message.content is not None:
717
713
  model_response.content = response_message.content
718
714
 
715
+ # Extract thinking content before any structured parsing
716
+ if model_response.content:
717
+ reasoning_content, output_content = extract_thinking_content(model_response.content)
718
+ if reasoning_content:
719
+ model_response.reasoning_content = reasoning_content
720
+ model_response.content = output_content
719
721
  # Add tool calls
720
722
  if response_message.tool_calls is not None and len(response_message.tool_calls) > 0:
721
723
  try:
@@ -19,10 +19,7 @@ from agno.utils.models.schema_utils import get_response_schema_for_provider
19
19
 
20
20
  try:
21
21
  from openai import APIConnectionError, APIStatusError, AsyncOpenAI, OpenAI, RateLimitError
22
- from openai.types.responses.response import Response
23
- from openai.types.responses.response_reasoning_item import ResponseReasoningItem
24
- from openai.types.responses.response_stream_event import ResponseStreamEvent
25
- from openai.types.responses.response_usage import ResponseUsage
22
+ from openai.types.responses import Response, ResponseReasoningItem, ResponseStreamEvent, ResponseUsage
26
23
  except ImportError as e:
27
24
  raise ImportError("`openai` not installed. Please install using `pip install openai -U`") from e
28
25
 
@@ -407,21 +404,28 @@ class OpenAIResponses(Model):
407
404
  """
408
405
  formatted_messages: List[Union[Dict[str, Any], ResponseReasoningItem]] = []
409
406
 
410
- if self._using_reasoning_model():
407
+ messages_to_format = messages
408
+ previous_response_id: Optional[str] = None
409
+
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
- previous_response_id: Optional[str] = None
415
- if self.store is not False:
416
- for msg in reversed(messages):
417
- if (
418
- msg.role == "assistant"
419
- and hasattr(msg, "provider_data")
420
- and msg.provider_data
421
- and "response_id" in msg.provider_data
422
- ):
423
- previous_response_id = msg.provider_data["response_id"]
424
- break
414
+
415
+ for msg in reversed(messages):
416
+ if (
417
+ msg.role == "assistant"
418
+ and hasattr(msg, "provider_data")
419
+ and msg.provider_data
420
+ and "response_id" in msg.provider_data
421
+ ):
422
+ previous_response_id = msg.provider_data["response_id"]
423
+ msg_index = messages.index(msg)
424
+
425
+ # Include messages after this assistant message
426
+ messages_to_format = messages[msg_index + 1 :]
427
+
428
+ break
425
429
 
426
430
  # Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
427
431
  fc_id_to_call_id: Dict[str, str] = {}
@@ -434,7 +438,7 @@ class OpenAIResponses(Model):
434
438
  if isinstance(fc_id, str) and isinstance(call_id, str):
435
439
  fc_id_to_call_id[fc_id] = call_id
436
440
 
437
- for message in messages:
441
+ for message in messages_to_format:
438
442
  if message.role in ["user", "system"]:
439
443
  message_dict: Dict[str, Any] = {
440
444
  "role": self.role_map[message.role],
@@ -19,8 +19,8 @@ class AGUI(BaseInterface):
19
19
  self.agent = agent
20
20
  self.team = team
21
21
 
22
- if not self.agent and not self.team:
23
- raise ValueError("AGUI requires an agent and a team")
22
+ if not (self.agent or self.team):
23
+ raise ValueError("AGUI requires an agent or a team")
24
24
 
25
25
  def get_router(self, **kwargs) -> APIRouter:
26
26
  # Cannot be overridden
@@ -35,10 +35,16 @@ class EventBuffer:
35
35
 
36
36
  active_tool_call_ids: Set[str] # All currently active tool calls
37
37
  ended_tool_call_ids: Set[str] # All tool calls that have ended
38
+ current_text_message_id: str = "" # ID of the current text message context (for tool call parenting)
39
+ next_text_message_id: str = "" # Pre-generated ID for the next text message
40
+ pending_tool_calls_parent_id: str = "" # Parent message ID for pending tool calls
38
41
 
39
42
  def __init__(self):
40
43
  self.active_tool_call_ids = set()
41
44
  self.ended_tool_call_ids = set()
45
+ self.current_text_message_id = ""
46
+ self.next_text_message_id = str(uuid.uuid4())
47
+ self.pending_tool_calls_parent_id = ""
42
48
 
43
49
  def start_tool_call(self, tool_call_id: str) -> None:
44
50
  """Start a new tool call."""
@@ -49,6 +55,29 @@ class EventBuffer:
49
55
  self.active_tool_call_ids.discard(tool_call_id)
50
56
  self.ended_tool_call_ids.add(tool_call_id)
51
57
 
58
+ def start_text_message(self) -> str:
59
+ """Start a new text message and return its ID."""
60
+ # Use the pre-generated next ID as current, and generate a new next ID
61
+ self.current_text_message_id = self.next_text_message_id
62
+ self.next_text_message_id = str(uuid.uuid4())
63
+ return self.current_text_message_id
64
+
65
+ def get_parent_message_id_for_tool_call(self) -> str:
66
+ """Get the message ID to use as parent for tool calls."""
67
+ # If we have a pending parent ID set (from text message end), use that
68
+ if self.pending_tool_calls_parent_id:
69
+ return self.pending_tool_calls_parent_id
70
+ # Otherwise use current text message ID
71
+ return self.current_text_message_id
72
+
73
+ def set_pending_tool_calls_parent_id(self, parent_id: str) -> None:
74
+ """Set the parent message ID for upcoming tool calls."""
75
+ self.pending_tool_calls_parent_id = parent_id
76
+
77
+ def clear_pending_tool_calls_parent_id(self) -> None:
78
+ """Clear the pending parent ID when a new text message starts."""
79
+ self.pending_tool_calls_parent_id = ""
80
+
52
81
 
53
82
  def convert_agui_messages_to_agno_messages(messages: List[AGUIMessage]) -> List[Message]:
54
83
  """Convert AG-UI messages to Agno messages."""
@@ -113,10 +142,18 @@ def _create_events_from_chunk(
113
142
  message_id: str,
114
143
  message_started: bool,
115
144
  event_buffer: EventBuffer,
116
- ) -> Tuple[List[BaseEvent], bool]:
145
+ ) -> Tuple[List[BaseEvent], bool, str]:
117
146
  """
118
147
  Process a single chunk and return events to emit + updated message_started state.
119
- Returns: (events_to_emit, new_message_started_state)
148
+
149
+ Args:
150
+ chunk: The event chunk to process
151
+ message_id: Current message identifier
152
+ message_started: Whether a message is currently active
153
+ event_buffer: Event buffer for tracking tool call state
154
+
155
+ Returns:
156
+ Tuple of (events_to_emit, new_message_started_state, message_id)
120
157
  """
121
158
  events_to_emit: List[BaseEvent] = []
122
159
 
@@ -133,6 +170,11 @@ def _create_events_from_chunk(
133
170
  # Handle the message start event, emitted once per message
134
171
  if not message_started:
135
172
  message_started = True
173
+ message_id = event_buffer.start_text_message()
174
+
175
+ # Clear pending tool calls parent ID when starting new text message
176
+ event_buffer.clear_pending_tool_calls_parent_id()
177
+
136
178
  start_event = TextMessageStartEvent(
137
179
  type=EventType.TEXT_MESSAGE_START,
138
180
  message_id=message_id,
@@ -149,21 +191,37 @@ def _create_events_from_chunk(
149
191
  )
150
192
  events_to_emit.append(content_event) # type: ignore
151
193
 
152
- # Handle starting a new tool call
153
- elif chunk.event == RunEvent.tool_call_started:
154
- # End the current text message if one is active before starting tool calls
155
- if message_started:
156
- end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
157
- events_to_emit.append(end_message_event)
158
- message_started = False # Reset message_started state
159
-
194
+ # Handle starting a new tool
195
+ elif chunk.event == RunEvent.tool_call_started or chunk.event == TeamRunEvent.tool_call_started:
160
196
  if chunk.tool is not None: # type: ignore
161
197
  tool_call = chunk.tool # type: ignore
198
+
199
+ # End current text message and handle for tool calls
200
+ current_message_id = message_id
201
+ if message_started:
202
+ # End the current text message
203
+ end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=current_message_id)
204
+ events_to_emit.append(end_message_event)
205
+
206
+ # Set this message as the parent for any upcoming tool calls
207
+ # This ensures multiple sequential tool calls all use the same parent
208
+ event_buffer.set_pending_tool_calls_parent_id(current_message_id)
209
+
210
+ # Reset message started state and generate new message_id for future messages
211
+ message_started = False
212
+ message_id = str(uuid.uuid4())
213
+
214
+ # Get the parent message ID - this will use pending parent if set, ensuring multiple tool calls in sequence have the same parent
215
+ parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
216
+
217
+ if not parent_message_id:
218
+ parent_message_id = current_message_id
219
+
162
220
  start_event = ToolCallStartEvent(
163
221
  type=EventType.TOOL_CALL_START,
164
222
  tool_call_id=tool_call.tool_call_id, # type: ignore
165
223
  tool_call_name=tool_call.tool_name, # type: ignore
166
- parent_message_id=message_id,
224
+ parent_message_id=parent_message_id,
167
225
  )
168
226
  events_to_emit.append(start_event)
169
227
 
@@ -175,7 +233,7 @@ def _create_events_from_chunk(
175
233
  events_to_emit.append(args_event) # type: ignore
176
234
 
177
235
  # Handle tool call completion
178
- elif chunk.event == RunEvent.tool_call_completed:
236
+ elif chunk.event == RunEvent.tool_call_completed or chunk.event == TeamRunEvent.tool_call_completed:
179
237
  if chunk.tool is not None: # type: ignore
180
238
  tool_call = chunk.tool # type: ignore
181
239
  if tool_call.tool_call_id not in event_buffer.ended_tool_call_ids:
@@ -203,7 +261,7 @@ def _create_events_from_chunk(
203
261
  step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
204
262
  events_to_emit.append(step_finished_event)
205
263
 
206
- return events_to_emit, message_started
264
+ return events_to_emit, message_started, message_id
207
265
 
208
266
 
209
267
  def _create_completion_events(
@@ -237,11 +295,16 @@ def _create_completion_events(
237
295
  if tool.tool_call_id is None or tool.tool_name is None:
238
296
  continue
239
297
 
298
+ # Use the current text message ID from event buffer as parent
299
+ parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
300
+ if not parent_message_id:
301
+ parent_message_id = message_id # Fallback to the passed message_id
302
+
240
303
  start_event = ToolCallStartEvent(
241
304
  type=EventType.TOOL_CALL_START,
242
305
  tool_call_id=tool.tool_call_id,
243
306
  tool_call_name=tool.tool_name,
244
- parent_message_id=message_id,
307
+ parent_message_id=parent_message_id,
245
308
  )
246
309
  events_to_emit.append(start_event)
247
310
 
@@ -285,7 +348,7 @@ def stream_agno_response_as_agui_events(
285
348
  response_stream: Iterator[Union[RunOutputEvent, TeamRunOutputEvent]], thread_id: str, run_id: str
286
349
  ) -> Iterator[BaseEvent]:
287
350
  """Map the Agno response stream to AG-UI format, handling event ordering constraints."""
288
- message_id = str(uuid.uuid4())
351
+ message_id = "" # Will be set by EventBuffer when text message starts
289
352
  message_started = False
290
353
  event_buffer = EventBuffer()
291
354
  stream_completed = False
@@ -304,7 +367,7 @@ def stream_agno_response_as_agui_events(
304
367
  stream_completed = True
305
368
  else:
306
369
  # Process regular chunk immediately
307
- events_from_chunk, message_started = _create_events_from_chunk(
370
+ events_from_chunk, message_started, message_id = _create_events_from_chunk(
308
371
  chunk, message_id, message_started, event_buffer
309
372
  )
310
373
 
@@ -345,7 +408,7 @@ async def async_stream_agno_response_as_agui_events(
345
408
  run_id: str,
346
409
  ) -> AsyncIterator[BaseEvent]:
347
410
  """Map the Agno response stream to AG-UI format, handling event ordering constraints."""
348
- message_id = str(uuid.uuid4())
411
+ message_id = "" # Will be set by EventBuffer when text message starts
349
412
  message_started = False
350
413
  event_buffer = EventBuffer()
351
414
  stream_completed = False
@@ -364,7 +427,7 @@ async def async_stream_agno_response_as_agui_events(
364
427
  stream_completed = True
365
428
  else:
366
429
  # Process regular chunk immediately
367
- events_from_chunk, message_started = _create_events_from_chunk(
430
+ events_from_chunk, message_started, message_id = _create_events_from_chunk(
368
431
  chunk, message_id, message_started, event_buffer
369
432
  )
370
433
 
@@ -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 and not self.team:
24
- raise ValueError("Slack requires an agent and 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
@@ -17,8 +17,8 @@ class Whatsapp(BaseInterface):
17
17
  self.agent = agent
18
18
  self.team = team
19
19
 
20
- if not self.agent and not self.team:
21
- raise ValueError("Whatsapp requires an agent and a team")
20
+ if not (self.agent or self.team):
21
+ raise ValueError("Whatsapp requires an agent or a team")
22
22
 
23
23
  def get_router(self, **kwargs) -> APIRouter:
24
24
  # Cannot be overridden
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,12 +57,20 @@ 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
 
64
+ # Check the input field directly as final fallback
65
+ if run_dict.get("input") is not None:
66
+ input_value = run_dict.get("input")
67
+ if isinstance(input_value, str):
68
+ return input_value
69
+ else:
70
+ return str(input_value)
71
+
64
72
  if run_dict.get("messages") is not None:
65
- for message in run_dict["messages"]:
73
+ for message in reversed(run_dict["messages"]):
66
74
  if message.get("role") == "user":
67
75
  return message.get("content", "")
68
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/default.py CHANGED
@@ -14,6 +14,7 @@ def get_default_reasoning_agent(
14
14
  min_steps: int,
15
15
  max_steps: int,
16
16
  tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None,
17
+ tool_call_limit: Optional[int] = None,
17
18
  use_json_mode: bool = False,
18
19
  telemetry: bool = True,
19
20
  debug_mode: bool = False,
@@ -56,7 +57,7 @@ def get_default_reasoning_agent(
56
57
  - **validate**: When you reach a potential answer, signaling it's ready for validation.
57
58
  - **final_answer**: Only if you have confidently validated the solution.
58
59
  - **reset**: Immediately restart analysis if a critical error or incorrect result is identified.
59
- 6. **Confidence Score**: Provide a numeric confidence score (0.0–1.0) indicating your certainty in the steps correctness and its outcome.
60
+ 6. **Confidence Score**: Provide a numeric confidence score (0.0–1.0) indicating your certainty in the step's correctness and its outcome.
60
61
 
61
62
  Step 5 - Validation (mandatory before finalizing an answer):
62
63
  - Explicitly validate your solution by:
@@ -82,6 +83,7 @@ def get_default_reasoning_agent(
82
83
  - Only create a single instance of ReasoningSteps for your response.\
83
84
  """),
84
85
  tools=tools,
86
+ tool_call_limit=tool_call_limit,
85
87
  output_schema=ReasoningSteps,
86
88
  use_json_mode=use_json_mode,
87
89
  telemetry=telemetry,
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