agno 2.3.13__py3-none-any.whl → 2.3.15__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 (49) hide show
  1. agno/agent/agent.py +1149 -1392
  2. agno/db/migrations/manager.py +3 -3
  3. agno/eval/__init__.py +21 -8
  4. agno/knowledge/embedder/azure_openai.py +0 -1
  5. agno/knowledge/embedder/google.py +1 -1
  6. agno/models/anthropic/claude.py +9 -4
  7. agno/models/base.py +8 -4
  8. agno/models/metrics.py +12 -0
  9. agno/models/openai/chat.py +2 -0
  10. agno/models/openai/responses.py +2 -2
  11. agno/os/app.py +59 -2
  12. agno/os/auth.py +40 -3
  13. agno/os/interfaces/a2a/router.py +619 -9
  14. agno/os/interfaces/a2a/utils.py +31 -32
  15. agno/os/middleware/jwt.py +5 -5
  16. agno/os/router.py +1 -57
  17. agno/os/routers/agents/schema.py +14 -1
  18. agno/os/routers/database.py +150 -0
  19. agno/os/routers/teams/schema.py +14 -1
  20. agno/os/settings.py +3 -0
  21. agno/os/utils.py +61 -53
  22. agno/reasoning/anthropic.py +85 -1
  23. agno/reasoning/azure_ai_foundry.py +93 -1
  24. agno/reasoning/deepseek.py +91 -1
  25. agno/reasoning/gemini.py +81 -1
  26. agno/reasoning/groq.py +103 -1
  27. agno/reasoning/manager.py +1244 -0
  28. agno/reasoning/ollama.py +93 -1
  29. agno/reasoning/openai.py +113 -1
  30. agno/reasoning/vertexai.py +85 -1
  31. agno/run/agent.py +21 -0
  32. agno/run/base.py +20 -1
  33. agno/run/team.py +21 -0
  34. agno/session/team.py +0 -3
  35. agno/team/team.py +1211 -1445
  36. agno/tools/toolkit.py +119 -8
  37. agno/utils/events.py +99 -4
  38. agno/utils/hooks.py +4 -10
  39. agno/utils/print_response/agent.py +26 -0
  40. agno/utils/print_response/team.py +11 -0
  41. agno/utils/prompts.py +8 -6
  42. agno/utils/string.py +46 -0
  43. agno/utils/team.py +1 -1
  44. agno/vectordb/milvus/milvus.py +32 -3
  45. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/METADATA +3 -2
  46. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/RECORD +49 -47
  47. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/WHEEL +0 -0
  48. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/licenses/LICENSE +0 -0
  49. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/top_level.txt +0 -0
agno/tools/toolkit.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from collections import OrderedDict
2
- from typing import Any, Callable, Dict, List, Optional
2
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
3
3
 
4
4
  from agno.tools.function import Function
5
5
  from agno.utils.log import log_debug, log_warning, logger
@@ -13,7 +13,7 @@ class Toolkit:
13
13
  def __init__(
14
14
  self,
15
15
  name: str = "toolkit",
16
- tools: List[Callable] = [],
16
+ tools: Sequence[Union[Callable[..., Any], Function]] = [],
17
17
  instructions: Optional[str] = None,
18
18
  add_instructions: bool = False,
19
19
  include_tools: Optional[list[str]] = None,
@@ -31,7 +31,7 @@ class Toolkit:
31
31
 
32
32
  Args:
33
33
  name: A descriptive name for the toolkit
34
- tools: List of tools to include in the toolkit
34
+ tools: List of tools to include in the toolkit (can be callables or Function objects from @tool decorator)
35
35
  instructions: Instructions for the toolkit
36
36
  add_instructions: Whether to add instructions to the toolkit
37
37
  include_tools: List of tool names to include in the toolkit
@@ -46,7 +46,7 @@ class Toolkit:
46
46
  show_result_tools (Optional[List[str]]): List of function names whose results should be shown.
47
47
  """
48
48
  self.name: str = name
49
- self.tools: List[Callable] = tools
49
+ self.tools: Sequence[Union[Callable[..., Any], Function]] = tools
50
50
  self.functions: Dict[str, Function] = OrderedDict()
51
51
  self.instructions: Optional[str] = instructions
52
52
  self.add_instructions: bool = add_instructions
@@ -58,7 +58,9 @@ class Toolkit:
58
58
  self.show_result_tools: list[str] = show_result_tools or []
59
59
 
60
60
  self._check_tools_filters(
61
- available_tools=[tool.__name__ for tool in tools], include_tools=include_tools, exclude_tools=exclude_tools
61
+ available_tools=[self._get_tool_name(tool) for tool in tools],
62
+ include_tools=include_tools,
63
+ exclude_tools=exclude_tools,
62
64
  )
63
65
 
64
66
  self.include_tools = include_tools
@@ -72,6 +74,12 @@ class Toolkit:
72
74
  if auto_register and self.tools:
73
75
  self._register_tools()
74
76
 
77
+ def _get_tool_name(self, tool: Union[Callable[..., Any], Function]) -> str:
78
+ """Get the name of a tool, whether it's a Function or callable."""
79
+ if isinstance(tool, Function):
80
+ return tool.name
81
+ return tool.__name__
82
+
75
83
  def _check_tools_filters(
76
84
  self,
77
85
  available_tools: List[str],
@@ -104,22 +112,45 @@ class Toolkit:
104
112
  f"External execution required tool(s) not present in the toolkit: {', '.join(missing_external_execution_required)}"
105
113
  )
106
114
 
115
+ if self.stop_after_tool_call_tools:
116
+ missing_stop_after_tool_call = set(self.stop_after_tool_call_tools) - set(available_tools)
117
+ if missing_stop_after_tool_call:
118
+ log_warning(
119
+ f"Stop after tool call tool(s) not present in the toolkit: {', '.join(missing_stop_after_tool_call)}"
120
+ )
121
+
122
+ if self.show_result_tools:
123
+ missing_show_result = set(self.show_result_tools) - set(available_tools)
124
+ if missing_show_result:
125
+ log_warning(f"Show result tool(s) not present in the toolkit: {', '.join(missing_show_result)}")
126
+
107
127
  def _register_tools(self) -> None:
108
128
  """Register all tools."""
109
129
  for tool in self.tools:
110
130
  self.register(tool)
111
131
 
112
- def register(self, function: Callable[..., Any], name: Optional[str] = None):
132
+ def register(self, function: Union[Callable[..., Any], Function], name: Optional[str] = None) -> None:
113
133
  """Register a function with the toolkit.
114
134
 
135
+ This method supports both regular callables and Function objects (from @tool decorator).
136
+ When a Function object is passed (e.g., from a @tool decorated method), it will:
137
+ 1. Extract the configuration from the Function object
138
+ 2. Look for a bound method with the same name on `self`
139
+ 3. Create a new Function with the bound method as entrypoint, preserving decorator settings
140
+
115
141
  Args:
116
- function: The callable to register
142
+ function: The callable or Function object to register
117
143
  name: Optional custom name for the function
118
144
 
119
145
  Returns:
120
146
  The registered function
121
147
  """
122
148
  try:
149
+ # Handle Function objects (from @tool decorator)
150
+ if isinstance(function, Function):
151
+ return self._register_decorated_tool(function, name)
152
+
153
+ # Handle regular callables
123
154
  tool_name = name or function.__name__
124
155
  if self.include_tools is not None and tool_name not in self.include_tools:
125
156
  return
@@ -140,9 +171,89 @@ class Toolkit:
140
171
  self.functions[f.name] = f
141
172
  log_debug(f"Function: {f.name} registered with {self.name}")
142
173
  except Exception as e:
143
- logger.warning(f"Failed to create Function for: {function.__name__}")
174
+ func_name = self._get_tool_name(function)
175
+ logger.warning(f"Failed to create Function for: {func_name}")
144
176
  raise e
145
177
 
178
+ def _register_decorated_tool(self, function: Function, name: Optional[str] = None) -> None:
179
+ """Register a Function object from @tool decorator, binding it to self.
180
+
181
+ When @tool decorator is used on a class method, it creates a Function with an unbound
182
+ method as entrypoint. This method creates a bound version of the entrypoint that
183
+ includes `self`, preserving all decorator settings.
184
+
185
+ Args:
186
+ function: The Function object from @tool decorator
187
+ name: Optional custom name override
188
+ """
189
+ import inspect
190
+
191
+ tool_name = name or function.name
192
+ if self.include_tools is not None and len(self.include_tools) > 0 and tool_name not in self.include_tools:
193
+ return
194
+ if self.exclude_tools is not None and len(self.exclude_tools) > 0 and tool_name in self.exclude_tools:
195
+ return
196
+
197
+ # Get the original entrypoint from the Function
198
+ if function.entrypoint is None:
199
+ log_warning(f"Function '{tool_name}' has no entrypoint, skipping registration")
200
+ return
201
+
202
+ original_func = function.entrypoint
203
+
204
+ # Check if the function expects 'self' as first argument (i.e., it's an unbound method)
205
+ sig = inspect.signature(original_func)
206
+ params = list(sig.parameters.keys())
207
+
208
+ if params and params[0] == "self":
209
+ # Create a bound method by wrapping the function to include self
210
+ def make_bound_method(func, instance):
211
+ def bound(*args, **kwargs):
212
+ return func(instance, *args, **kwargs)
213
+
214
+ # Preserve function metadata for debugging
215
+ bound.__name__ = getattr(func, "__name__", tool_name)
216
+ bound.__doc__ = getattr(func, "__doc__", None)
217
+ return bound
218
+
219
+ bound_method = make_bound_method(original_func, self)
220
+ else:
221
+ # Function doesn't expect self (e.g., static method or already bound)
222
+ bound_method = original_func
223
+
224
+ # decorator settings take precedence, then toolkit settings
225
+ stop_after = function.stop_after_tool_call or tool_name in self.stop_after_tool_call_tools
226
+ show_result = function.show_result or tool_name in self.show_result_tools or stop_after
227
+ requires_confirmation = function.requires_confirmation or tool_name in self.requires_confirmation_tools
228
+ external_execution = function.external_execution or tool_name in self.external_execution_required_tools
229
+
230
+ # Create new Function with bound method, preserving decorator settings
231
+ f = Function(
232
+ name=tool_name,
233
+ description=function.description,
234
+ parameters=function.parameters,
235
+ strict=function.strict,
236
+ instructions=function.instructions,
237
+ add_instructions=function.add_instructions,
238
+ entrypoint=bound_method,
239
+ skip_entrypoint_processing=True, # Parameters already processed by decorator
240
+ show_result=show_result,
241
+ stop_after_tool_call=stop_after,
242
+ pre_hook=function.pre_hook,
243
+ post_hook=function.post_hook,
244
+ tool_hooks=function.tool_hooks,
245
+ requires_confirmation=requires_confirmation,
246
+ requires_user_input=function.requires_user_input,
247
+ user_input_fields=function.user_input_fields,
248
+ user_input_schema=function.user_input_schema,
249
+ external_execution=external_execution,
250
+ cache_results=function.cache_results if function.cache_results else self.cache_results,
251
+ cache_dir=function.cache_dir if function.cache_dir else self.cache_dir,
252
+ cache_ttl=function.cache_ttl if function.cache_ttl != 3600 else self.cache_ttl,
253
+ )
254
+ self.functions[f.name] = f
255
+ log_debug(f"Function: {f.name} registered with {self.name} (from @tool decorator)")
256
+
146
257
  @property
147
258
  def requires_connect(self) -> bool:
148
259
  """Whether the toolkit requires connection management."""
agno/utils/events.py CHANGED
@@ -16,6 +16,7 @@ from agno.run.agent import (
16
16
  PreHookCompletedEvent,
17
17
  PreHookStartedEvent,
18
18
  ReasoningCompletedEvent,
19
+ ReasoningContentDeltaEvent,
19
20
  ReasoningStartedEvent,
20
21
  ReasoningStepEvent,
21
22
  RunCancelledEvent,
@@ -33,6 +34,7 @@ from agno.run.agent import (
33
34
  SessionSummaryCompletedEvent,
34
35
  SessionSummaryStartedEvent,
35
36
  ToolCallCompletedEvent,
37
+ ToolCallErrorEvent,
36
38
  ToolCallStartedEvent,
37
39
  )
38
40
  from agno.run.requirement import RunRequirement
@@ -47,6 +49,7 @@ from agno.run.team import PostHookStartedEvent as TeamPostHookStartedEvent
47
49
  from agno.run.team import PreHookCompletedEvent as TeamPreHookCompletedEvent
48
50
  from agno.run.team import PreHookStartedEvent as TeamPreHookStartedEvent
49
51
  from agno.run.team import ReasoningCompletedEvent as TeamReasoningCompletedEvent
52
+ from agno.run.team import ReasoningContentDeltaEvent as TeamReasoningContentDeltaEvent
50
53
  from agno.run.team import ReasoningStartedEvent as TeamReasoningStartedEvent
51
54
  from agno.run.team import ReasoningStepEvent as TeamReasoningStepEvent
52
55
  from agno.run.team import RunCancelledEvent as TeamRunCancelledEvent
@@ -59,6 +62,7 @@ from agno.run.team import SessionSummaryCompletedEvent as TeamSessionSummaryComp
59
62
  from agno.run.team import SessionSummaryStartedEvent as TeamSessionSummaryStartedEvent
60
63
  from agno.run.team import TeamRunEvent, TeamRunInput, TeamRunOutput, TeamRunOutputEvent
61
64
  from agno.run.team import ToolCallCompletedEvent as TeamToolCallCompletedEvent
65
+ from agno.run.team import ToolCallErrorEvent as TeamToolCallErrorEvent
62
66
  from agno.run.team import ToolCallStartedEvent as TeamToolCallStartedEvent
63
67
  from agno.session.summary import SessionSummary
64
68
 
@@ -161,23 +165,41 @@ def create_run_continued_event(from_run_response: RunOutput) -> RunContinuedEven
161
165
  )
162
166
 
163
167
 
164
- def create_team_run_error_event(from_run_response: TeamRunOutput, error: str) -> TeamRunErrorEvent:
168
+ def create_team_run_error_event(
169
+ from_run_response: TeamRunOutput,
170
+ error: str,
171
+ error_type: Optional[str] = None,
172
+ error_id: Optional[str] = None,
173
+ additional_data: Optional[Dict[str, Any]] = None,
174
+ ) -> TeamRunErrorEvent:
165
175
  return TeamRunErrorEvent(
166
176
  session_id=from_run_response.session_id,
167
177
  team_id=from_run_response.team_id, # type: ignore
168
178
  team_name=from_run_response.team_name, # type: ignore
169
179
  run_id=from_run_response.run_id,
170
180
  content=error,
181
+ error_type=error_type,
182
+ error_id=error_id,
183
+ additional_data=additional_data,
171
184
  )
172
185
 
173
186
 
174
- def create_run_error_event(from_run_response: RunOutput, error: str) -> RunErrorEvent:
187
+ def create_run_error_event(
188
+ from_run_response: RunOutput,
189
+ error: str,
190
+ error_type: Optional[str] = None,
191
+ error_id: Optional[str] = None,
192
+ additional_data: Optional[Dict[str, Any]] = None,
193
+ ) -> RunErrorEvent:
175
194
  return RunErrorEvent(
176
195
  session_id=from_run_response.session_id,
177
196
  agent_id=from_run_response.agent_id, # type: ignore
178
197
  agent_name=from_run_response.agent_name, # type: ignore
179
198
  run_id=from_run_response.run_id,
180
199
  content=error,
200
+ error_type=error_type,
201
+ error_id=error_id,
202
+ additional_data=additional_data,
181
203
  )
182
204
 
183
205
 
@@ -421,6 +443,19 @@ def create_reasoning_step_event(
421
443
  )
422
444
 
423
445
 
446
+ def create_reasoning_content_delta_event(
447
+ from_run_response: RunOutput, reasoning_content: str
448
+ ) -> ReasoningContentDeltaEvent:
449
+ """Create an event for streaming reasoning content chunks."""
450
+ return ReasoningContentDeltaEvent(
451
+ session_id=from_run_response.session_id,
452
+ agent_id=from_run_response.agent_id, # type: ignore
453
+ agent_name=from_run_response.agent_name, # type: ignore
454
+ run_id=from_run_response.run_id,
455
+ reasoning_content=reasoning_content,
456
+ )
457
+
458
+
424
459
  def create_team_reasoning_step_event(
425
460
  from_run_response: TeamRunOutput, reasoning_step: ReasoningStep, reasoning_content: str
426
461
  ) -> TeamReasoningStepEvent:
@@ -435,6 +470,19 @@ def create_team_reasoning_step_event(
435
470
  )
436
471
 
437
472
 
473
+ def create_team_reasoning_content_delta_event(
474
+ from_run_response: TeamRunOutput, reasoning_content: str
475
+ ) -> TeamReasoningContentDeltaEvent:
476
+ """Create an event for streaming reasoning content chunks for Team."""
477
+ return TeamReasoningContentDeltaEvent(
478
+ session_id=from_run_response.session_id,
479
+ team_id=from_run_response.team_id, # type: ignore
480
+ team_name=from_run_response.team_name, # type: ignore
481
+ run_id=from_run_response.run_id,
482
+ reasoning_content=reasoning_content,
483
+ )
484
+
485
+
438
486
  def create_reasoning_completed_event(
439
487
  from_run_response: RunOutput, content: Optional[Any] = None, content_type: Optional[str] = None
440
488
  ) -> ReasoningCompletedEvent:
@@ -515,6 +563,32 @@ def create_team_tool_call_completed_event(
515
563
  )
516
564
 
517
565
 
566
+ def create_tool_call_error_event(
567
+ from_run_response: RunOutput, tool: ToolExecution, error: Optional[str] = None
568
+ ) -> ToolCallErrorEvent:
569
+ return ToolCallErrorEvent(
570
+ session_id=from_run_response.session_id,
571
+ agent_id=from_run_response.agent_id, # type: ignore
572
+ agent_name=from_run_response.agent_name, # type: ignore
573
+ run_id=from_run_response.run_id,
574
+ tool=tool,
575
+ error=error,
576
+ )
577
+
578
+
579
+ def create_team_tool_call_error_event(
580
+ from_run_response: TeamRunOutput, tool: ToolExecution, error: Optional[str] = None
581
+ ) -> TeamToolCallErrorEvent:
582
+ return TeamToolCallErrorEvent(
583
+ session_id=from_run_response.session_id,
584
+ team_id=from_run_response.team_id, # type: ignore
585
+ team_name=from_run_response.team_name, # type: ignore
586
+ run_id=from_run_response.run_id,
587
+ tool=tool,
588
+ error=error,
589
+ )
590
+
591
+
518
592
  def create_run_output_content_event(
519
593
  from_run_response: RunOutput,
520
594
  content: Optional[Any] = None,
@@ -692,9 +766,30 @@ def handle_event(
692
766
  store_events: bool = False,
693
767
  ) -> Union[RunOutputEvent, TeamRunOutputEvent]:
694
768
  # We only store events that are not run_response_content events
695
- events_to_skip = [event.value for event in events_to_skip] if events_to_skip else []
696
- if store_events and event.event not in events_to_skip:
769
+ _events_to_skip: List[str] = [event.value for event in events_to_skip] if events_to_skip else []
770
+ if store_events and event.event not in _events_to_skip:
697
771
  if run_response.events is None:
698
772
  run_response.events = []
699
773
  run_response.events.append(event) # type: ignore
700
774
  return event
775
+
776
+
777
+ def add_error_event(
778
+ error: RunErrorEvent,
779
+ events: Optional[List[RunOutputEvent]],
780
+ ):
781
+ if events is None:
782
+ events = []
783
+ events.append(error)
784
+
785
+ return events
786
+
787
+
788
+ def add_team_error_event(
789
+ error: TeamRunErrorEvent,
790
+ events: Optional[List[Union[RunOutputEvent, TeamRunOutputEvent]]],
791
+ ):
792
+ if events is None:
793
+ events = []
794
+ events.append(error)
795
+ return events
agno/utils/hooks.py CHANGED
@@ -1,9 +1,7 @@
1
1
  from copy import deepcopy
2
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
3
-
4
- if TYPE_CHECKING:
5
- from agno.eval.base import BaseEval
2
+ from typing import Any, Callable, Dict, List, Optional, Union
6
3
 
4
+ from agno.eval.base import BaseEval
7
5
  from agno.guardrails.base import BaseGuardrail
8
6
  from agno.hooks.decorator import HOOK_RUN_IN_BACKGROUND_ATTR
9
7
  from agno.utils.log import log_warning
@@ -57,7 +55,7 @@ def should_run_hook_in_background(hook: Callable[..., Any]) -> bool:
57
55
 
58
56
 
59
57
  def normalize_pre_hooks(
60
- hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]],
58
+ hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, BaseEval]]],
61
59
  async_mode: bool = False,
62
60
  ) -> Optional[List[Callable[..., Any]]]:
63
61
  """Normalize pre-hooks to a list format.
@@ -66,8 +64,6 @@ def normalize_pre_hooks(
66
64
  hooks: List of hook functions, guardrails, or eval instances
67
65
  async_mode: Whether to use async versions of methods
68
66
  """
69
- from agno.eval.base import BaseEval
70
-
71
67
  result_hooks: List[Callable[..., Any]] = []
72
68
 
73
69
  if hooks is not None:
@@ -102,7 +98,7 @@ def normalize_pre_hooks(
102
98
 
103
99
 
104
100
  def normalize_post_hooks(
105
- hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]],
101
+ hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, BaseEval]]],
106
102
  async_mode: bool = False,
107
103
  ) -> Optional[List[Callable[..., Any]]]:
108
104
  """Normalize post-hooks to a list format.
@@ -111,8 +107,6 @@ def normalize_post_hooks(
111
107
  hooks: List of hook functions, guardrails, or eval instances
112
108
  async_mode: Whether to use async versions of methods
113
109
  """
114
- from agno.eval.base import BaseEval
115
-
116
110
  result_hooks: List[Callable[..., Any]] = []
117
111
 
118
112
  if hooks is not None:
@@ -134,6 +134,11 @@ def print_response_stream(
134
134
  )
135
135
  except Exception as e:
136
136
  log_warning(f"Failed to convert response to JSON: {e}")
137
+ elif agent.output_schema is not None and isinstance(response_event.content, dict):
138
+ try:
139
+ response_content_batch = JSON(json.dumps(response_event.content), indent=2) # type: ignore
140
+ except Exception as e:
141
+ log_warning(f"Failed to convert response to JSON: {e}")
137
142
  else:
138
143
  try:
139
144
  response_content_batch = JSON(json.dumps(response_event.content), indent=4)
@@ -141,6 +146,12 @@ def print_response_stream(
141
146
  log_warning(f"Failed to convert response to JSON: {e}")
142
147
  if hasattr(response_event, "reasoning_content") and response_event.reasoning_content is not None: # type: ignore
143
148
  _response_reasoning_content += response_event.reasoning_content # type: ignore
149
+
150
+ # Handle streaming reasoning content delta events
151
+ if response_event.event == RunEvent.reasoning_content_delta: # type: ignore
152
+ if hasattr(response_event, "reasoning_content") and response_event.reasoning_content is not None: # type: ignore
153
+ _response_reasoning_content += response_event.reasoning_content # type: ignore
154
+
144
155
  if hasattr(response_event, "reasoning_steps") and response_event.reasoning_steps is not None: # type: ignore
145
156
  reasoning_steps = response_event.reasoning_steps # type: ignore
146
157
 
@@ -325,6 +336,11 @@ async def aprint_response_stream(
325
336
  response_content_batch = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
326
337
  except Exception as e:
327
338
  log_warning(f"Failed to convert response to JSON: {e}")
339
+ elif agent.output_schema is not None and isinstance(resp.content, dict):
340
+ try:
341
+ response_content_batch = JSON(json.dumps(resp.content), indent=2) # type: ignore
342
+ except Exception as e:
343
+ log_warning(f"Failed to convert response to JSON: {e}")
328
344
  else:
329
345
  try:
330
346
  response_content_batch = JSON(json.dumps(resp.content), indent=4)
@@ -333,6 +349,11 @@ async def aprint_response_stream(
333
349
  if resp.reasoning_content is not None: # type: ignore
334
350
  _response_reasoning_content += resp.reasoning_content # type: ignore
335
351
 
352
+ # Handle streaming reasoning content delta events
353
+ if resp.event == RunEvent.reasoning_content_delta: # type: ignore
354
+ if hasattr(resp, "reasoning_content") and resp.reasoning_content is not None: # type: ignore
355
+ _response_reasoning_content += resp.reasoning_content # type: ignore
356
+
336
357
  if hasattr(resp, "reasoning_steps") and resp.reasoning_steps is not None: # type: ignore
337
358
  reasoning_steps = resp.reasoning_steps # type: ignore
338
359
 
@@ -883,6 +904,11 @@ def build_panels(
883
904
  response_content_batch = JSON(run_response.content.model_dump_json(exclude_none=True), indent=2)
884
905
  except Exception as e:
885
906
  log_warning(f"Failed to convert response to JSON: {e}")
907
+ elif output_schema is not None and isinstance(run_response.content, dict):
908
+ try:
909
+ response_content_batch = JSON(json.dumps(run_response.content), indent=2)
910
+ except Exception as e:
911
+ log_warning(f"Failed to convert response to JSON: {e}")
886
912
  else:
887
913
  try:
888
914
  response_content_batch = JSON(json.dumps(run_response.content), indent=4)
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Set, Union, get_args
2
3
 
3
4
  from pydantic import BaseModel
@@ -488,6 +489,11 @@ def print_response_stream(
488
489
  _response_content = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
489
490
  except Exception as e:
490
491
  log_warning(f"Failed to convert response to JSON: {e}")
492
+ elif team.output_schema is not None and isinstance(resp.content, dict):
493
+ try:
494
+ _response_content = JSON(json.dumps(resp.content), indent=2) # type: ignore
495
+ except Exception as e:
496
+ log_warning(f"Failed to convert response to JSON: {e}")
491
497
  if hasattr(resp, "reasoning_content") and resp.reasoning_content is not None: # type: ignore
492
498
  _response_reasoning_content += resp.reasoning_content # type: ignore
493
499
  if hasattr(resp, "reasoning_steps") and resp.reasoning_steps is not None: # type: ignore
@@ -1412,6 +1418,11 @@ async def aprint_response_stream(
1412
1418
  _response_content = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
1413
1419
  except Exception as e:
1414
1420
  log_warning(f"Failed to convert response to JSON: {e}")
1421
+ elif team.output_schema is not None and isinstance(resp.content, dict):
1422
+ try:
1423
+ _response_content = JSON(json.dumps(resp.content), indent=2) # type: ignore
1424
+ except Exception as e:
1425
+ log_warning(f"Failed to convert response to JSON: {e}")
1415
1426
  if hasattr(resp, "reasoning_content") and resp.reasoning_content is not None: # type: ignore
1416
1427
  _response_reasoning_content += resp.reasoning_content # type: ignore
1417
1428
  if hasattr(resp, "reasoning_steps") and resp.reasoning_steps is not None: # type: ignore
agno/utils/prompts.py CHANGED
@@ -6,7 +6,7 @@ from pydantic import BaseModel
6
6
  from agno.utils.log import log_warning
7
7
 
8
8
 
9
- def get_json_output_prompt(output_schema: Union[str, list, BaseModel]) -> str:
9
+ def get_json_output_prompt(output_schema: Union[str, list, dict, BaseModel]) -> str:
10
10
  """Return the JSON output prompt for the Agent.
11
11
 
12
12
  This is added to the system prompt when the output_schema is set and structured_outputs is False.
@@ -22,11 +22,13 @@ def get_json_output_prompt(output_schema: Union[str, list, BaseModel]) -> str:
22
22
  json_output_prompt += "\n<json_fields>"
23
23
  json_output_prompt += f"\n{json.dumps(output_schema)}"
24
24
  json_output_prompt += "\n</json_fields>"
25
- elif (
26
- issubclass(type(output_schema), BaseModel)
27
- or issubclass(output_schema, BaseModel) # type: ignore
28
- or isinstance(output_schema, BaseModel)
29
- ): # type: ignore
25
+ elif isinstance(output_schema, dict):
26
+ json_output_prompt += "\n<json_fields>"
27
+ json_output_prompt += f"\n{json.dumps(output_schema)}"
28
+ json_output_prompt += "\n</json_fields>"
29
+ elif (isinstance(output_schema, type) and issubclass(output_schema, BaseModel)) or isinstance(
30
+ output_schema, BaseModel
31
+ ):
30
32
  json_schema = output_schema.model_json_schema()
31
33
  if json_schema is not None:
32
34
  response_model_properties = {}
agno/utils/string.py CHANGED
@@ -201,6 +201,52 @@ def parse_response_model_str(content: str, output_schema: Type[BaseModel]) -> Op
201
201
  return structured_output
202
202
 
203
203
 
204
+ def parse_response_dict_str(content: str) -> Optional[dict]:
205
+ """Parse dict from string content, extracting JSON if needed"""
206
+ from agno.utils.reasoning import extract_thinking_content
207
+
208
+ # Handle thinking content b/w <think> tags
209
+ if "</think>" in content:
210
+ reasoning_content, output_content = extract_thinking_content(content)
211
+ if reasoning_content:
212
+ content = output_content
213
+
214
+ # Clean content first to simplify all parsing attempts
215
+ cleaned_content = _clean_json_content(content)
216
+
217
+ try:
218
+ # First attempt: direct JSON parsing on cleaned content
219
+ return json.loads(cleaned_content)
220
+ except json.JSONDecodeError as e:
221
+ logger.warning(f"Failed to parse cleaned JSON: {e}")
222
+
223
+ # Second attempt: Extract individual JSON objects
224
+ candidate_jsons = _extract_json_objects(cleaned_content)
225
+
226
+ if len(candidate_jsons) == 1:
227
+ # Single JSON object - try to parse it directly
228
+ try:
229
+ return json.loads(candidate_jsons[0])
230
+ except json.JSONDecodeError:
231
+ pass
232
+
233
+ if len(candidate_jsons) > 1:
234
+ # Final attempt: Merge multiple JSON objects
235
+ merged_data: dict = {}
236
+ for candidate in candidate_jsons:
237
+ try:
238
+ obj = json.loads(candidate)
239
+ if isinstance(obj, dict):
240
+ merged_data.update(obj)
241
+ except json.JSONDecodeError:
242
+ continue
243
+ if merged_data:
244
+ return merged_data
245
+
246
+ logger.warning("All parsing attempts failed.")
247
+ return None
248
+
249
+
204
250
  def generate_id(seed: Optional[str] = None) -> str:
205
251
  """
206
252
  Generate a deterministic UUID5 based on a seed string.
agno/utils/team.py CHANGED
@@ -59,7 +59,7 @@ def add_interaction_to_team_run_context(
59
59
  team_run_context: Dict[str, Any],
60
60
  member_name: str,
61
61
  task: str,
62
- run_response: Union[RunOutput, TeamRunOutput],
62
+ run_response: Optional[Union[RunOutput, TeamRunOutput]],
63
63
  ) -> None:
64
64
  if "member_responses" not in team_run_context:
65
65
  team_run_context["member_responses"] = []