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.
- agno/agent/agent.py +1149 -1392
- agno/db/migrations/manager.py +3 -3
- agno/eval/__init__.py +21 -8
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/models/anthropic/claude.py +9 -4
- agno/models/base.py +8 -4
- agno/models/metrics.py +12 -0
- agno/models/openai/chat.py +2 -0
- agno/models/openai/responses.py +2 -2
- agno/os/app.py +59 -2
- agno/os/auth.py +40 -3
- agno/os/interfaces/a2a/router.py +619 -9
- agno/os/interfaces/a2a/utils.py +31 -32
- agno/os/middleware/jwt.py +5 -5
- agno/os/router.py +1 -57
- agno/os/routers/agents/schema.py +14 -1
- agno/os/routers/database.py +150 -0
- agno/os/routers/teams/schema.py +14 -1
- agno/os/settings.py +3 -0
- agno/os/utils.py +61 -53
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +91 -1
- agno/reasoning/gemini.py +81 -1
- agno/reasoning/groq.py +103 -1
- agno/reasoning/manager.py +1244 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +113 -1
- agno/reasoning/vertexai.py +85 -1
- agno/run/agent.py +21 -0
- agno/run/base.py +20 -1
- agno/run/team.py +21 -0
- agno/session/team.py +0 -3
- agno/team/team.py +1211 -1445
- agno/tools/toolkit.py +119 -8
- agno/utils/events.py +99 -4
- agno/utils/hooks.py +4 -10
- agno/utils/print_response/agent.py +26 -0
- agno/utils/print_response/team.py +11 -0
- agno/utils/prompts.py +8 -6
- agno/utils/string.py +46 -0
- agno/utils/team.py +1 -1
- agno/vectordb/milvus/milvus.py +32 -3
- {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/METADATA +3 -2
- {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/RECORD +49 -47
- {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/WHEEL +0 -0
- {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/licenses/LICENSE +0 -0
- {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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
696
|
-
if store_events and event.event not in
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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"] = []
|