agno 2.3.8__py3-none-any.whl → 2.3.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.
- agno/agent/agent.py +134 -82
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2888 -0
- agno/db/mysql/mysql.py +17 -8
- agno/db/mysql/utils.py +139 -6
- agno/db/postgres/async_postgres.py +10 -5
- agno/db/postgres/postgres.py +7 -2
- agno/db/schemas/evals.py +1 -0
- agno/db/singlestore/singlestore.py +5 -1
- agno/db/sqlite/async_sqlite.py +2 -2
- agno/eval/__init__.py +10 -0
- agno/eval/agent_as_judge.py +860 -0
- agno/eval/base.py +29 -0
- agno/eval/utils.py +2 -1
- agno/exceptions.py +7 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/knowledge.py +1142 -176
- agno/media.py +22 -6
- agno/models/aws/claude.py +8 -7
- agno/models/base.py +27 -1
- agno/models/deepseek/deepseek.py +67 -0
- agno/models/google/gemini.py +65 -11
- agno/models/google/utils.py +22 -0
- agno/models/message.py +2 -0
- agno/models/openai/chat.py +4 -0
- agno/os/app.py +64 -74
- agno/os/interfaces/a2a/router.py +3 -4
- agno/os/interfaces/agui/router.py +2 -0
- agno/os/router.py +3 -1607
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +581 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +26 -6
- agno/os/routers/evals/schemas.py +34 -2
- agno/os/routers/evals/utils.py +101 -20
- agno/os/routers/knowledge/knowledge.py +1 -1
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +496 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +545 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +1 -559
- agno/os/utils.py +139 -2
- agno/team/team.py +73 -16
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/utils/hooks.py +64 -5
- agno/utils/http.py +2 -2
- agno/utils/media.py +11 -1
- agno/utils/print_response/agent.py +8 -0
- agno/utils/print_response/team.py +8 -0
- agno/vectordb/pgvector/pgvector.py +88 -51
- agno/workflow/parallel.py +3 -3
- agno/workflow/step.py +14 -2
- agno/workflow/types.py +38 -2
- agno/workflow/workflow.py +12 -4
- {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/METADATA +7 -2
- {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/RECORD +62 -49
- {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/WHEEL +0 -0
- {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/top_level.txt +0 -0
agno/os/utils.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from datetime import datetime, timezone
|
|
2
3
|
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
|
|
3
4
|
|
|
4
|
-
from fastapi import FastAPI, HTTPException, UploadFile
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request, UploadFile
|
|
5
6
|
from fastapi.routing import APIRoute, APIRouter
|
|
6
7
|
from pydantic import BaseModel, create_model
|
|
7
8
|
from starlette.middleware.cors import CORSMiddleware
|
|
@@ -13,13 +14,149 @@ from agno.media import Audio, Image, Video
|
|
|
13
14
|
from agno.media import File as FileMedia
|
|
14
15
|
from agno.models.message import Message
|
|
15
16
|
from agno.os.config import AgentOSConfig
|
|
17
|
+
from agno.run.agent import RunOutputEvent
|
|
18
|
+
from agno.run.team import TeamRunOutputEvent
|
|
19
|
+
from agno.run.workflow import WorkflowRunOutputEvent
|
|
16
20
|
from agno.team.team import Team
|
|
17
21
|
from agno.tools import Toolkit
|
|
18
22
|
from agno.tools.function import Function
|
|
19
|
-
from agno.utils.log import logger
|
|
23
|
+
from agno.utils.log import log_warning, logger
|
|
20
24
|
from agno.workflow.workflow import Workflow
|
|
21
25
|
|
|
22
26
|
|
|
27
|
+
async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
|
|
28
|
+
"""Given a Request and an endpoint function, return a dictionary with all extra form data fields.
|
|
29
|
+
Args:
|
|
30
|
+
request: The FastAPI Request object
|
|
31
|
+
endpoint_func: The function exposing the endpoint that received the request
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary of kwargs
|
|
35
|
+
"""
|
|
36
|
+
import inspect
|
|
37
|
+
|
|
38
|
+
form_data = await request.form()
|
|
39
|
+
sig = inspect.signature(endpoint_func)
|
|
40
|
+
known_fields = set(sig.parameters.keys())
|
|
41
|
+
kwargs: Dict[str, Any] = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
42
|
+
|
|
43
|
+
# Handle JSON parameters. They are passed as strings and need to be deserialized.
|
|
44
|
+
if session_state := kwargs.get("session_state"):
|
|
45
|
+
try:
|
|
46
|
+
if isinstance(session_state, str):
|
|
47
|
+
session_state_dict = json.loads(session_state) # type: ignore
|
|
48
|
+
kwargs["session_state"] = session_state_dict
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
kwargs.pop("session_state")
|
|
51
|
+
log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
|
|
52
|
+
|
|
53
|
+
if dependencies := kwargs.get("dependencies"):
|
|
54
|
+
try:
|
|
55
|
+
if isinstance(dependencies, str):
|
|
56
|
+
dependencies_dict = json.loads(dependencies) # type: ignore
|
|
57
|
+
kwargs["dependencies"] = dependencies_dict
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
kwargs.pop("dependencies")
|
|
60
|
+
log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
|
|
61
|
+
|
|
62
|
+
if metadata := kwargs.get("metadata"):
|
|
63
|
+
try:
|
|
64
|
+
if isinstance(metadata, str):
|
|
65
|
+
metadata_dict = json.loads(metadata) # type: ignore
|
|
66
|
+
kwargs["metadata"] = metadata_dict
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
kwargs.pop("metadata")
|
|
69
|
+
log_warning(f"Invalid metadata parameter couldn't be loaded: {metadata}")
|
|
70
|
+
|
|
71
|
+
if knowledge_filters := kwargs.get("knowledge_filters"):
|
|
72
|
+
try:
|
|
73
|
+
if isinstance(knowledge_filters, str):
|
|
74
|
+
knowledge_filters_dict = json.loads(knowledge_filters) # type: ignore
|
|
75
|
+
|
|
76
|
+
# Try to deserialize FilterExpr objects
|
|
77
|
+
from agno.filters import from_dict
|
|
78
|
+
|
|
79
|
+
# Check if it's a single FilterExpr dict or a list of FilterExpr dicts
|
|
80
|
+
if isinstance(knowledge_filters_dict, dict) and "op" in knowledge_filters_dict:
|
|
81
|
+
# Single FilterExpr - convert to list format
|
|
82
|
+
kwargs["knowledge_filters"] = [from_dict(knowledge_filters_dict)]
|
|
83
|
+
elif isinstance(knowledge_filters_dict, list):
|
|
84
|
+
# List of FilterExprs or mixed content
|
|
85
|
+
deserialized = []
|
|
86
|
+
for item in knowledge_filters_dict:
|
|
87
|
+
if isinstance(item, dict) and "op" in item:
|
|
88
|
+
deserialized.append(from_dict(item))
|
|
89
|
+
else:
|
|
90
|
+
# Keep non-FilterExpr items as-is
|
|
91
|
+
deserialized.append(item)
|
|
92
|
+
kwargs["knowledge_filters"] = deserialized
|
|
93
|
+
else:
|
|
94
|
+
# Regular dict filter
|
|
95
|
+
kwargs["knowledge_filters"] = knowledge_filters_dict
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
kwargs.pop("knowledge_filters")
|
|
98
|
+
log_warning(f"Invalid knowledge_filters parameter couldn't be loaded: {knowledge_filters}")
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
# Filter deserialization failed
|
|
101
|
+
kwargs.pop("knowledge_filters")
|
|
102
|
+
log_warning(f"Invalid FilterExpr in knowledge_filters: {e}")
|
|
103
|
+
|
|
104
|
+
# Handle output_schema - convert JSON schema to dynamic Pydantic model
|
|
105
|
+
if output_schema := kwargs.get("output_schema"):
|
|
106
|
+
try:
|
|
107
|
+
if isinstance(output_schema, str):
|
|
108
|
+
from agno.os.utils import json_schema_to_pydantic_model
|
|
109
|
+
|
|
110
|
+
schema_dict = json.loads(output_schema)
|
|
111
|
+
dynamic_model = json_schema_to_pydantic_model(schema_dict)
|
|
112
|
+
kwargs["output_schema"] = dynamic_model
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
kwargs.pop("output_schema")
|
|
115
|
+
log_warning(f"Invalid output_schema JSON: {output_schema}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
kwargs.pop("output_schema")
|
|
118
|
+
log_warning(f"Failed to create output_schema model: {e}")
|
|
119
|
+
|
|
120
|
+
# Parse boolean and null values
|
|
121
|
+
for key, value in kwargs.items():
|
|
122
|
+
if isinstance(value, str) and value.lower() in ["true", "false"]:
|
|
123
|
+
kwargs[key] = value.lower() == "true"
|
|
124
|
+
elif isinstance(value, str) and value.lower() in ["null", "none"]:
|
|
125
|
+
kwargs[key] = None
|
|
126
|
+
|
|
127
|
+
return kwargs
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRunOutputEvent]) -> str:
|
|
131
|
+
"""Parse JSON data into SSE-compliant format.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
event_dict: Dictionary containing the event data
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
SSE-formatted response:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
event: EventName
|
|
141
|
+
data: { ... }
|
|
142
|
+
|
|
143
|
+
event: AnotherEventName
|
|
144
|
+
data: { ... }
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
# Parse the JSON to extract the event type
|
|
149
|
+
event_type = event.event or "message"
|
|
150
|
+
|
|
151
|
+
# Serialize to valid JSON with double quotes and no newlines
|
|
152
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
153
|
+
|
|
154
|
+
return f"event: {event_type}\ndata: {clean_json}\n\n"
|
|
155
|
+
except json.JSONDecodeError:
|
|
156
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
157
|
+
return f"event: message\ndata: {clean_json}\n\n"
|
|
158
|
+
|
|
159
|
+
|
|
23
160
|
async def get_db(
|
|
24
161
|
dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], db_id: Optional[str] = None, table: Optional[str] = None
|
|
25
162
|
) -> Union[BaseDb, AsyncBaseDb]:
|
agno/team/team.py
CHANGED
|
@@ -11,6 +11,7 @@ from dataclasses import dataclass
|
|
|
11
11
|
from os import getenv
|
|
12
12
|
from textwrap import dedent
|
|
13
13
|
from typing import (
|
|
14
|
+
TYPE_CHECKING,
|
|
14
15
|
Any,
|
|
15
16
|
AsyncIterator,
|
|
16
17
|
Callable,
|
|
@@ -32,6 +33,9 @@ from uuid import uuid4
|
|
|
32
33
|
|
|
33
34
|
from pydantic import BaseModel
|
|
34
35
|
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from agno.eval.base import BaseEval
|
|
38
|
+
|
|
35
39
|
from agno.agent import Agent
|
|
36
40
|
from agno.compression.manager import CompressionManager
|
|
37
41
|
from agno.db.base import AsyncBaseDb, BaseDb, SessionType, UserMemory
|
|
@@ -123,7 +127,13 @@ from agno.utils.events import (
|
|
|
123
127
|
create_team_tool_call_started_event,
|
|
124
128
|
handle_event,
|
|
125
129
|
)
|
|
126
|
-
from agno.utils.hooks import
|
|
130
|
+
from agno.utils.hooks import (
|
|
131
|
+
copy_args_for_background,
|
|
132
|
+
filter_hook_args,
|
|
133
|
+
normalize_post_hooks,
|
|
134
|
+
normalize_pre_hooks,
|
|
135
|
+
should_run_hook_in_background,
|
|
136
|
+
)
|
|
127
137
|
from agno.utils.knowledge import get_agentic_or_user_search_filters
|
|
128
138
|
from agno.utils.log import (
|
|
129
139
|
log_debug,
|
|
@@ -266,6 +276,8 @@ class Team:
|
|
|
266
276
|
system_message: Optional[Union[str, Callable, Message]] = None
|
|
267
277
|
# Role for the system message
|
|
268
278
|
system_message_role: str = "system"
|
|
279
|
+
# Introduction for the team
|
|
280
|
+
introduction: Optional[str] = None
|
|
269
281
|
|
|
270
282
|
# If True, resolve the session_state, dependencies, and metadata in the user and system messages
|
|
271
283
|
resolve_in_context: bool = True
|
|
@@ -342,9 +354,9 @@ class Team:
|
|
|
342
354
|
|
|
343
355
|
# --- Team Hooks ---
|
|
344
356
|
# Functions called right after team session is loaded, before processing starts
|
|
345
|
-
pre_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]] = None
|
|
357
|
+
pre_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]] = None
|
|
346
358
|
# Functions called after output is generated but before the response is returned
|
|
347
|
-
post_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]] = None
|
|
359
|
+
post_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]] = None
|
|
348
360
|
# If True, run hooks as FastAPI background tasks (non-blocking). Set by AgentOS.
|
|
349
361
|
_run_hooks_in_background: Optional[bool] = None
|
|
350
362
|
|
|
@@ -486,6 +498,7 @@ class Team:
|
|
|
486
498
|
add_member_tools_to_context: bool = False,
|
|
487
499
|
system_message: Optional[Union[str, Callable, Message]] = None,
|
|
488
500
|
system_message_role: str = "system",
|
|
501
|
+
introduction: Optional[str] = None,
|
|
489
502
|
additional_input: Optional[List[Union[str, Dict, BaseModel, Message]]] = None,
|
|
490
503
|
dependencies: Optional[Dict[str, Any]] = None,
|
|
491
504
|
add_dependencies_to_context: bool = False,
|
|
@@ -512,8 +525,8 @@ class Team:
|
|
|
512
525
|
tool_call_limit: Optional[int] = None,
|
|
513
526
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
514
527
|
tool_hooks: Optional[List[Callable]] = None,
|
|
515
|
-
pre_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]] = None,
|
|
516
|
-
post_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]] = None,
|
|
528
|
+
pre_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]] = None,
|
|
529
|
+
post_hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]] = None,
|
|
517
530
|
input_schema: Optional[Type[BaseModel]] = None,
|
|
518
531
|
output_schema: Optional[Type[BaseModel]] = None,
|
|
519
532
|
parser_model: Optional[Union[Model, str]] = None,
|
|
@@ -611,6 +624,7 @@ class Team:
|
|
|
611
624
|
self.add_member_tools_to_context = add_member_tools_to_context
|
|
612
625
|
self.system_message = system_message
|
|
613
626
|
self.system_message_role = system_message_role
|
|
627
|
+
self.introduction = introduction
|
|
614
628
|
self.additional_input = additional_input
|
|
615
629
|
|
|
616
630
|
self.dependencies = dependencies
|
|
@@ -1972,6 +1986,7 @@ class Team:
|
|
|
1972
1986
|
session_id: Optional[str] = None,
|
|
1973
1987
|
session_state: Optional[Dict[str, Any]] = None,
|
|
1974
1988
|
user_id: Optional[str] = None,
|
|
1989
|
+
run_id: Optional[str] = None,
|
|
1975
1990
|
audio: Optional[Sequence[Audio]] = None,
|
|
1976
1991
|
images: Optional[Sequence[Image]] = None,
|
|
1977
1992
|
videos: Optional[Sequence[Video]] = None,
|
|
@@ -1999,6 +2014,7 @@ class Team:
|
|
|
1999
2014
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2000
2015
|
run_context: Optional[RunContext] = None,
|
|
2001
2016
|
user_id: Optional[str] = None,
|
|
2017
|
+
run_id: Optional[str] = None,
|
|
2002
2018
|
audio: Optional[Sequence[Audio]] = None,
|
|
2003
2019
|
images: Optional[Sequence[Image]] = None,
|
|
2004
2020
|
videos: Optional[Sequence[Video]] = None,
|
|
@@ -2026,6 +2042,7 @@ class Team:
|
|
|
2026
2042
|
session_id: Optional[str] = None,
|
|
2027
2043
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2028
2044
|
run_context: Optional[RunContext] = None,
|
|
2045
|
+
run_id: Optional[str] = None,
|
|
2029
2046
|
user_id: Optional[str] = None,
|
|
2030
2047
|
audio: Optional[Sequence[Audio]] = None,
|
|
2031
2048
|
images: Optional[Sequence[Image]] = None,
|
|
@@ -2047,8 +2064,8 @@ class Team:
|
|
|
2047
2064
|
if self._has_async_db():
|
|
2048
2065
|
raise Exception("run() is not supported with an async DB. Please use arun() instead.")
|
|
2049
2066
|
|
|
2050
|
-
#
|
|
2051
|
-
run_id = str(uuid4())
|
|
2067
|
+
# Set the id for the run and register it immediately for cancellation tracking
|
|
2068
|
+
run_id = run_id or str(uuid4())
|
|
2052
2069
|
register_run(run_id)
|
|
2053
2070
|
|
|
2054
2071
|
# Initialize Team
|
|
@@ -2079,9 +2096,9 @@ class Team:
|
|
|
2079
2096
|
# Normalise hook & guardails
|
|
2080
2097
|
if not self._hooks_normalised:
|
|
2081
2098
|
if self.pre_hooks:
|
|
2082
|
-
self.pre_hooks =
|
|
2099
|
+
self.pre_hooks = normalize_pre_hooks(self.pre_hooks) # type: ignore
|
|
2083
2100
|
if self.post_hooks:
|
|
2084
|
-
self.post_hooks =
|
|
2101
|
+
self.post_hooks = normalize_post_hooks(self.post_hooks) # type: ignore
|
|
2085
2102
|
self._hooks_normalised = True
|
|
2086
2103
|
|
|
2087
2104
|
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
@@ -2866,6 +2883,7 @@ class Team:
|
|
|
2866
2883
|
stream_intermediate_steps: Optional[bool] = None,
|
|
2867
2884
|
session_id: Optional[str] = None,
|
|
2868
2885
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2886
|
+
run_id: Optional[str] = None,
|
|
2869
2887
|
run_context: Optional[RunContext] = None,
|
|
2870
2888
|
user_id: Optional[str] = None,
|
|
2871
2889
|
audio: Optional[Sequence[Audio]] = None,
|
|
@@ -2893,6 +2911,7 @@ class Team:
|
|
|
2893
2911
|
stream_intermediate_steps: Optional[bool] = None,
|
|
2894
2912
|
session_id: Optional[str] = None,
|
|
2895
2913
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2914
|
+
run_id: Optional[str] = None,
|
|
2896
2915
|
run_context: Optional[RunContext] = None,
|
|
2897
2916
|
user_id: Optional[str] = None,
|
|
2898
2917
|
audio: Optional[Sequence[Audio]] = None,
|
|
@@ -2921,6 +2940,7 @@ class Team:
|
|
|
2921
2940
|
stream_intermediate_steps: Optional[bool] = None,
|
|
2922
2941
|
session_id: Optional[str] = None,
|
|
2923
2942
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2943
|
+
run_id: Optional[str] = None,
|
|
2924
2944
|
run_context: Optional[RunContext] = None,
|
|
2925
2945
|
user_id: Optional[str] = None,
|
|
2926
2946
|
audio: Optional[Sequence[Audio]] = None,
|
|
@@ -2941,8 +2961,8 @@ class Team:
|
|
|
2941
2961
|
) -> Union[TeamRunOutput, AsyncIterator[Union[RunOutputEvent, TeamRunOutputEvent]]]:
|
|
2942
2962
|
"""Run the Team asynchronously and return the response."""
|
|
2943
2963
|
|
|
2944
|
-
#
|
|
2945
|
-
run_id = str(uuid4())
|
|
2964
|
+
# Set the id for the run and register it immediately for cancellation tracking
|
|
2965
|
+
run_id = run_id or str(uuid4())
|
|
2946
2966
|
register_run(run_id)
|
|
2947
2967
|
|
|
2948
2968
|
if (add_history_to_context or self.add_history_to_context) and not self.db and not self.parent_team_id:
|
|
@@ -2971,9 +2991,9 @@ class Team:
|
|
|
2971
2991
|
# Normalise hook & guardails
|
|
2972
2992
|
if not self._hooks_normalised:
|
|
2973
2993
|
if self.pre_hooks:
|
|
2974
|
-
self.pre_hooks =
|
|
2994
|
+
self.pre_hooks = normalize_pre_hooks(self.pre_hooks, async_mode=True) # type: ignore
|
|
2975
2995
|
if self.post_hooks:
|
|
2976
|
-
self.post_hooks =
|
|
2996
|
+
self.post_hooks = normalize_post_hooks(self.post_hooks, async_mode=True) # type: ignore
|
|
2977
2997
|
self._hooks_normalised = True
|
|
2978
2998
|
|
|
2979
2999
|
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
@@ -3090,8 +3110,6 @@ class Team:
|
|
|
3090
3110
|
num_attempts = self.retries + 1
|
|
3091
3111
|
|
|
3092
3112
|
for attempt in range(num_attempts):
|
|
3093
|
-
log_debug(f"Retrying Team run {run_id}. Attempt {attempt + 1} of {num_attempts}...")
|
|
3094
|
-
|
|
3095
3113
|
# Run the team
|
|
3096
3114
|
try:
|
|
3097
3115
|
if stream:
|
|
@@ -4244,6 +4262,7 @@ class Team:
|
|
|
4244
4262
|
session_id: Optional[str] = None,
|
|
4245
4263
|
session_state: Optional[Dict[str, Any]] = None,
|
|
4246
4264
|
user_id: Optional[str] = None,
|
|
4265
|
+
run_id: Optional[str] = None,
|
|
4247
4266
|
audio: Optional[Sequence[Audio]] = None,
|
|
4248
4267
|
images: Optional[Sequence[Image]] = None,
|
|
4249
4268
|
videos: Optional[Sequence[Video]] = None,
|
|
@@ -4316,6 +4335,7 @@ class Team:
|
|
|
4316
4335
|
session_id=session_id,
|
|
4317
4336
|
session_state=session_state,
|
|
4318
4337
|
user_id=user_id,
|
|
4338
|
+
run_id=run_id,
|
|
4319
4339
|
audio=audio,
|
|
4320
4340
|
images=images,
|
|
4321
4341
|
videos=videos,
|
|
@@ -4344,6 +4364,7 @@ class Team:
|
|
|
4344
4364
|
session_id=session_id,
|
|
4345
4365
|
session_state=session_state,
|
|
4346
4366
|
user_id=user_id,
|
|
4367
|
+
run_id=run_id,
|
|
4347
4368
|
audio=audio,
|
|
4348
4369
|
images=images,
|
|
4349
4370
|
videos=videos,
|
|
@@ -4367,6 +4388,7 @@ class Team:
|
|
|
4367
4388
|
session_id: Optional[str] = None,
|
|
4368
4389
|
session_state: Optional[Dict[str, Any]] = None,
|
|
4369
4390
|
user_id: Optional[str] = None,
|
|
4391
|
+
run_id: Optional[str] = None,
|
|
4370
4392
|
audio: Optional[Sequence[Audio]] = None,
|
|
4371
4393
|
images: Optional[Sequence[Image]] = None,
|
|
4372
4394
|
videos: Optional[Sequence[Video]] = None,
|
|
@@ -4434,6 +4456,7 @@ class Team:
|
|
|
4434
4456
|
session_id=session_id,
|
|
4435
4457
|
session_state=session_state,
|
|
4436
4458
|
user_id=user_id,
|
|
4459
|
+
run_id=run_id,
|
|
4437
4460
|
audio=audio,
|
|
4438
4461
|
images=images,
|
|
4439
4462
|
videos=videos,
|
|
@@ -4462,6 +4485,7 @@ class Team:
|
|
|
4462
4485
|
session_id=session_id,
|
|
4463
4486
|
session_state=session_state,
|
|
4464
4487
|
user_id=user_id,
|
|
4488
|
+
run_id=run_id,
|
|
4465
4489
|
audio=audio,
|
|
4466
4490
|
images=images,
|
|
4467
4491
|
videos=videos,
|
|
@@ -8207,6 +8231,20 @@ class Team:
|
|
|
8207
8231
|
metadata=self.metadata,
|
|
8208
8232
|
created_at=int(time()),
|
|
8209
8233
|
)
|
|
8234
|
+
if self.introduction is not None:
|
|
8235
|
+
from uuid import uuid4
|
|
8236
|
+
|
|
8237
|
+
team_session.upsert_run(
|
|
8238
|
+
TeamRunOutput(
|
|
8239
|
+
run_id=str(uuid4()),
|
|
8240
|
+
team_id=self.id,
|
|
8241
|
+
session_id=session_id,
|
|
8242
|
+
user_id=user_id,
|
|
8243
|
+
team_name=self.name,
|
|
8244
|
+
content=self.introduction,
|
|
8245
|
+
messages=[Message(role=self.model.assistant_message_role, content=self.introduction)], # type: ignore
|
|
8246
|
+
)
|
|
8247
|
+
)
|
|
8210
8248
|
|
|
8211
8249
|
# Cache the session if relevant
|
|
8212
8250
|
if team_session is not None and self.cache_session:
|
|
@@ -8239,15 +8277,34 @@ class Team:
|
|
|
8239
8277
|
# Create new session if none found
|
|
8240
8278
|
if team_session is None:
|
|
8241
8279
|
log_debug(f"Creating new TeamSession: {session_id}")
|
|
8280
|
+
session_data = {}
|
|
8281
|
+
if self.session_state is not None:
|
|
8282
|
+
from copy import deepcopy
|
|
8283
|
+
|
|
8284
|
+
session_data["session_state"] = deepcopy(self.session_state)
|
|
8242
8285
|
team_session = TeamSession(
|
|
8243
8286
|
session_id=session_id,
|
|
8244
8287
|
team_id=self.id,
|
|
8245
8288
|
user_id=user_id,
|
|
8246
8289
|
team_data=self._get_team_data(),
|
|
8247
|
-
session_data=
|
|
8290
|
+
session_data=session_data,
|
|
8248
8291
|
metadata=self.metadata,
|
|
8249
8292
|
created_at=int(time()),
|
|
8250
8293
|
)
|
|
8294
|
+
if self.introduction is not None:
|
|
8295
|
+
from uuid import uuid4
|
|
8296
|
+
|
|
8297
|
+
team_session.upsert_run(
|
|
8298
|
+
TeamRunOutput(
|
|
8299
|
+
run_id=str(uuid4()),
|
|
8300
|
+
team_id=self.id,
|
|
8301
|
+
session_id=session_id,
|
|
8302
|
+
user_id=user_id,
|
|
8303
|
+
team_name=self.name,
|
|
8304
|
+
content=self.introduction,
|
|
8305
|
+
messages=[Message(role=self.model.assistant_message_role, content=self.introduction)], # type: ignore
|
|
8306
|
+
)
|
|
8307
|
+
)
|
|
8251
8308
|
|
|
8252
8309
|
# Cache the session if relevant
|
|
8253
8310
|
if team_session is not None and self.cache_session:
|
agno/tools/file_generation.py
CHANGED
|
@@ -108,14 +108,16 @@ class FileGenerationTools(Toolkit):
|
|
|
108
108
|
# Save file to disk (if output_directory is set)
|
|
109
109
|
file_path = self._save_file_to_disk(json_content, filename)
|
|
110
110
|
|
|
111
|
+
content_bytes = json_content.encode("utf-8")
|
|
112
|
+
|
|
111
113
|
# Create FileArtifact
|
|
112
114
|
file_artifact = File(
|
|
113
115
|
id=str(uuid4()),
|
|
114
|
-
content=
|
|
116
|
+
content=content_bytes,
|
|
115
117
|
mime_type="application/json",
|
|
116
118
|
file_type="json",
|
|
117
119
|
filename=filename,
|
|
118
|
-
size=len(
|
|
120
|
+
size=len(content_bytes),
|
|
119
121
|
filepath=file_path if file_path else None,
|
|
120
122
|
)
|
|
121
123
|
|
|
@@ -195,14 +197,16 @@ class FileGenerationTools(Toolkit):
|
|
|
195
197
|
# Save file to disk (if output_directory is set)
|
|
196
198
|
file_path = self._save_file_to_disk(csv_content, filename)
|
|
197
199
|
|
|
200
|
+
content_bytes = csv_content.encode("utf-8")
|
|
201
|
+
|
|
198
202
|
# Create FileArtifact
|
|
199
203
|
file_artifact = File(
|
|
200
204
|
id=str(uuid4()),
|
|
201
|
-
content=
|
|
205
|
+
content=content_bytes,
|
|
202
206
|
mime_type="text/csv",
|
|
203
207
|
file_type="csv",
|
|
204
208
|
filename=filename,
|
|
205
|
-
size=len(
|
|
209
|
+
size=len(content_bytes),
|
|
206
210
|
filepath=file_path if file_path else None,
|
|
207
211
|
)
|
|
208
212
|
|
|
@@ -325,14 +329,16 @@ class FileGenerationTools(Toolkit):
|
|
|
325
329
|
# Save file to disk (if output_directory is set)
|
|
326
330
|
file_path = self._save_file_to_disk(content, filename)
|
|
327
331
|
|
|
332
|
+
content_bytes = content.encode("utf-8")
|
|
333
|
+
|
|
328
334
|
# Create FileArtifact
|
|
329
335
|
file_artifact = File(
|
|
330
336
|
id=str(uuid4()),
|
|
331
|
-
content=
|
|
337
|
+
content=content_bytes,
|
|
332
338
|
mime_type="text/plain",
|
|
333
339
|
file_type="txt",
|
|
334
340
|
filename=filename,
|
|
335
|
-
size=len(
|
|
341
|
+
size=len(content_bytes),
|
|
336
342
|
filepath=file_path if file_path else None,
|
|
337
343
|
)
|
|
338
344
|
|
agno/tools/firecrawl.py
CHANGED
|
@@ -101,8 +101,10 @@ class FirecrawlTools(Toolkit):
|
|
|
101
101
|
The results of the crawling.
|
|
102
102
|
"""
|
|
103
103
|
params: Dict[str, Any] = {}
|
|
104
|
-
if self.limit
|
|
105
|
-
params["limit"] = self.limit
|
|
104
|
+
if self.limit is not None:
|
|
105
|
+
params["limit"] = self.limit
|
|
106
|
+
elif limit is not None:
|
|
107
|
+
params["limit"] = limit
|
|
106
108
|
if self.formats:
|
|
107
109
|
params["scrape_options"] = ScrapeOptions(formats=self.formats) # type: ignore
|
|
108
110
|
|
|
@@ -129,15 +131,21 @@ class FirecrawlTools(Toolkit):
|
|
|
129
131
|
limit (int): The maximum number of results to return.
|
|
130
132
|
"""
|
|
131
133
|
params: Dict[str, Any] = {}
|
|
132
|
-
if self.limit
|
|
133
|
-
params["limit"] = self.limit
|
|
134
|
+
if self.limit is not None:
|
|
135
|
+
params["limit"] = self.limit
|
|
136
|
+
elif limit is not None:
|
|
137
|
+
params["limit"] = limit
|
|
134
138
|
if self.formats:
|
|
135
139
|
params["scrape_options"] = ScrapeOptions(formats=self.formats) # type: ignore
|
|
136
140
|
if self.search_params:
|
|
137
141
|
params.update(self.search_params)
|
|
138
142
|
|
|
139
143
|
search_result = self.app.search(query, **params)
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
|
|
145
|
+
if hasattr(search_result, "success"):
|
|
146
|
+
if search_result.success:
|
|
147
|
+
return json.dumps(search_result.data, cls=CustomJSONEncoder)
|
|
148
|
+
else:
|
|
149
|
+
return f"Error searching with the Firecrawl tool: {search_result.error}"
|
|
142
150
|
else:
|
|
143
|
-
return
|
|
151
|
+
return json.dumps(search_result.model_dump(), cls=CustomJSONEncoder)
|
agno/utils/hooks.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from copy import deepcopy
|
|
2
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from agno.eval.base import BaseEval
|
|
3
6
|
|
|
4
7
|
from agno.guardrails.base import BaseGuardrail
|
|
5
8
|
from agno.hooks.decorator import HOOK_RUN_IN_BACKGROUND_ATTR
|
|
@@ -53,16 +56,17 @@ def should_run_hook_in_background(hook: Callable[..., Any]) -> bool:
|
|
|
53
56
|
return getattr(hook, HOOK_RUN_IN_BACKGROUND_ATTR, False)
|
|
54
57
|
|
|
55
58
|
|
|
56
|
-
def
|
|
57
|
-
hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]],
|
|
59
|
+
def normalize_pre_hooks(
|
|
60
|
+
hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]],
|
|
58
61
|
async_mode: bool = False,
|
|
59
62
|
) -> Optional[List[Callable[..., Any]]]:
|
|
60
|
-
"""Normalize hooks to a list format
|
|
63
|
+
"""Normalize pre-hooks to a list format.
|
|
61
64
|
|
|
62
65
|
Args:
|
|
63
|
-
hooks: List of hook functions or
|
|
66
|
+
hooks: List of hook functions, guardrails, or eval instances
|
|
64
67
|
async_mode: Whether to use async versions of methods
|
|
65
68
|
"""
|
|
69
|
+
from agno.eval.base import BaseEval
|
|
66
70
|
|
|
67
71
|
result_hooks: List[Callable[..., Any]] = []
|
|
68
72
|
|
|
@@ -73,6 +77,61 @@ def normalize_hooks(
|
|
|
73
77
|
result_hooks.append(hook.async_check)
|
|
74
78
|
else:
|
|
75
79
|
result_hooks.append(hook.check)
|
|
80
|
+
elif isinstance(hook, BaseEval):
|
|
81
|
+
# Extract pre_check method
|
|
82
|
+
method = hook.async_pre_check if async_mode else hook.pre_check
|
|
83
|
+
|
|
84
|
+
from functools import partial
|
|
85
|
+
|
|
86
|
+
wrapped = partial(method)
|
|
87
|
+
wrapped.__name__ = method.__name__ # type: ignore
|
|
88
|
+
setattr(wrapped, HOOK_RUN_IN_BACKGROUND_ATTR, getattr(hook, "run_in_background", False))
|
|
89
|
+
result_hooks.append(wrapped)
|
|
90
|
+
else:
|
|
91
|
+
# Check if the hook is async and used within sync methods
|
|
92
|
+
if not async_mode:
|
|
93
|
+
import asyncio
|
|
94
|
+
|
|
95
|
+
if asyncio.iscoroutinefunction(hook):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Cannot use {hook.__name__} (an async hook) with `run()`. Use `arun()` instead."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
result_hooks.append(hook)
|
|
101
|
+
return result_hooks if result_hooks else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def normalize_post_hooks(
|
|
105
|
+
hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]],
|
|
106
|
+
async_mode: bool = False,
|
|
107
|
+
) -> Optional[List[Callable[..., Any]]]:
|
|
108
|
+
"""Normalize post-hooks to a list format.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
hooks: List of hook functions, guardrails, or eval instances
|
|
112
|
+
async_mode: Whether to use async versions of methods
|
|
113
|
+
"""
|
|
114
|
+
from agno.eval.base import BaseEval
|
|
115
|
+
|
|
116
|
+
result_hooks: List[Callable[..., Any]] = []
|
|
117
|
+
|
|
118
|
+
if hooks is not None:
|
|
119
|
+
for hook in hooks:
|
|
120
|
+
if isinstance(hook, BaseGuardrail):
|
|
121
|
+
if async_mode:
|
|
122
|
+
result_hooks.append(hook.async_check)
|
|
123
|
+
else:
|
|
124
|
+
result_hooks.append(hook.check)
|
|
125
|
+
elif isinstance(hook, BaseEval):
|
|
126
|
+
# Extract post_check method
|
|
127
|
+
method = hook.async_post_check if async_mode else hook.post_check # type: ignore[assignment]
|
|
128
|
+
|
|
129
|
+
from functools import partial
|
|
130
|
+
|
|
131
|
+
wrapped = partial(method)
|
|
132
|
+
wrapped.__name__ = method.__name__ # type: ignore
|
|
133
|
+
setattr(wrapped, HOOK_RUN_IN_BACKGROUND_ATTR, getattr(hook, "run_in_background", False))
|
|
134
|
+
result_hooks.append(wrapped)
|
|
76
135
|
else:
|
|
77
136
|
# Check if the hook is async and used within sync methods
|
|
78
137
|
if not async_mode:
|
agno/utils/http.py
CHANGED
|
@@ -140,7 +140,7 @@ def fetch_with_retry(
|
|
|
140
140
|
logger.error(f"Failed to fetch {url} after {max_retries} attempts: {e}")
|
|
141
141
|
raise
|
|
142
142
|
wait_time = backoff_factor**attempt
|
|
143
|
-
logger.warning(
|
|
143
|
+
logger.warning("Connection error.")
|
|
144
144
|
sleep(wait_time)
|
|
145
145
|
except httpx.HTTPStatusError as e:
|
|
146
146
|
logger.error(f"HTTP error for {url}: {e.response.status_code} - {e.response.text}")
|
|
@@ -176,7 +176,7 @@ async def async_fetch_with_retry(
|
|
|
176
176
|
logger.error(f"Failed to fetch {url} after {max_retries} attempts: {e}")
|
|
177
177
|
raise
|
|
178
178
|
wait_time = backoff_factor**attempt
|
|
179
|
-
logger.warning(
|
|
179
|
+
logger.warning("Connection error.")
|
|
180
180
|
await asyncio.sleep(wait_time)
|
|
181
181
|
except httpx.HTTPStatusError as e:
|
|
182
182
|
logger.error(f"HTTP error for {url}: {e.response.status_code} - {e.response.text}")
|
agno/utils/media.py
CHANGED
|
@@ -291,7 +291,7 @@ def reconstruct_file_from_dict(file_data):
|
|
|
291
291
|
if isinstance(file_data, dict):
|
|
292
292
|
# If content is base64 string, decode it back to bytes
|
|
293
293
|
if "content" in file_data and isinstance(file_data["content"], str):
|
|
294
|
-
|
|
294
|
+
file_obj = File.from_base64(
|
|
295
295
|
file_data["content"],
|
|
296
296
|
id=file_data.get("id"),
|
|
297
297
|
mime_type=file_data.get("mime_type"),
|
|
@@ -299,6 +299,16 @@ def reconstruct_file_from_dict(file_data):
|
|
|
299
299
|
name=file_data.get("name"),
|
|
300
300
|
format=file_data.get("format"),
|
|
301
301
|
)
|
|
302
|
+
# Preserve additional fields that from_base64 doesn't handle
|
|
303
|
+
if file_data.get("size") is not None:
|
|
304
|
+
file_obj.size = file_data.get("size")
|
|
305
|
+
if file_data.get("file_type") is not None:
|
|
306
|
+
file_obj.file_type = file_data.get("file_type")
|
|
307
|
+
if file_data.get("filepath") is not None:
|
|
308
|
+
file_obj.filepath = file_data.get("filepath")
|
|
309
|
+
if file_data.get("url") is not None:
|
|
310
|
+
file_obj.url = file_data.get("url")
|
|
311
|
+
return file_obj
|
|
302
312
|
else:
|
|
303
313
|
# Regular file (filepath/url)
|
|
304
314
|
return File(**file_data)
|