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.
Files changed (62) hide show
  1. agno/agent/agent.py +134 -82
  2. agno/db/mysql/__init__.py +2 -1
  3. agno/db/mysql/async_mysql.py +2888 -0
  4. agno/db/mysql/mysql.py +17 -8
  5. agno/db/mysql/utils.py +139 -6
  6. agno/db/postgres/async_postgres.py +10 -5
  7. agno/db/postgres/postgres.py +7 -2
  8. agno/db/schemas/evals.py +1 -0
  9. agno/db/singlestore/singlestore.py +5 -1
  10. agno/db/sqlite/async_sqlite.py +2 -2
  11. agno/eval/__init__.py +10 -0
  12. agno/eval/agent_as_judge.py +860 -0
  13. agno/eval/base.py +29 -0
  14. agno/eval/utils.py +2 -1
  15. agno/exceptions.py +7 -0
  16. agno/knowledge/embedder/openai.py +8 -8
  17. agno/knowledge/knowledge.py +1142 -176
  18. agno/media.py +22 -6
  19. agno/models/aws/claude.py +8 -7
  20. agno/models/base.py +27 -1
  21. agno/models/deepseek/deepseek.py +67 -0
  22. agno/models/google/gemini.py +65 -11
  23. agno/models/google/utils.py +22 -0
  24. agno/models/message.py +2 -0
  25. agno/models/openai/chat.py +4 -0
  26. agno/os/app.py +64 -74
  27. agno/os/interfaces/a2a/router.py +3 -4
  28. agno/os/interfaces/agui/router.py +2 -0
  29. agno/os/router.py +3 -1607
  30. agno/os/routers/agents/__init__.py +3 -0
  31. agno/os/routers/agents/router.py +581 -0
  32. agno/os/routers/agents/schema.py +261 -0
  33. agno/os/routers/evals/evals.py +26 -6
  34. agno/os/routers/evals/schemas.py +34 -2
  35. agno/os/routers/evals/utils.py +101 -20
  36. agno/os/routers/knowledge/knowledge.py +1 -1
  37. agno/os/routers/teams/__init__.py +3 -0
  38. agno/os/routers/teams/router.py +496 -0
  39. agno/os/routers/teams/schema.py +257 -0
  40. agno/os/routers/workflows/__init__.py +3 -0
  41. agno/os/routers/workflows/router.py +545 -0
  42. agno/os/routers/workflows/schema.py +75 -0
  43. agno/os/schema.py +1 -559
  44. agno/os/utils.py +139 -2
  45. agno/team/team.py +73 -16
  46. agno/tools/file_generation.py +12 -6
  47. agno/tools/firecrawl.py +15 -7
  48. agno/utils/hooks.py +64 -5
  49. agno/utils/http.py +2 -2
  50. agno/utils/media.py +11 -1
  51. agno/utils/print_response/agent.py +8 -0
  52. agno/utils/print_response/team.py +8 -0
  53. agno/vectordb/pgvector/pgvector.py +88 -51
  54. agno/workflow/parallel.py +3 -3
  55. agno/workflow/step.py +14 -2
  56. agno/workflow/types.py +38 -2
  57. agno/workflow/workflow.py +12 -4
  58. {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/METADATA +7 -2
  59. {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/RECORD +62 -49
  60. {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/WHEEL +0 -0
  61. {agno-2.3.8.dist-info → agno-2.3.9.dist-info}/licenses/LICENSE +0 -0
  62. {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 copy_args_for_background, filter_hook_args, normalize_hooks, should_run_hook_in_background
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
- # Create a run_id for this specific run and register immediately for cancellation tracking
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 = normalize_hooks(self.pre_hooks) # type: ignore
2099
+ self.pre_hooks = normalize_pre_hooks(self.pre_hooks) # type: ignore
2083
2100
  if self.post_hooks:
2084
- self.post_hooks = normalize_hooks(self.post_hooks) # type: ignore
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
- # Create a run_id for this specific run and register immediately for cancellation tracking
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 = normalize_hooks(self.pre_hooks, async_mode=True) # type: ignore
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 = normalize_hooks(self.post_hooks, async_mode=True) # type: ignore
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:
@@ -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=json_content,
116
+ content=content_bytes,
115
117
  mime_type="application/json",
116
118
  file_type="json",
117
119
  filename=filename,
118
- size=len(json_content.encode("utf-8")),
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=csv_content,
205
+ content=content_bytes,
202
206
  mime_type="text/csv",
203
207
  file_type="csv",
204
208
  filename=filename,
205
- size=len(csv_content.encode("utf-8")),
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=content,
337
+ content=content_bytes,
332
338
  mime_type="text/plain",
333
339
  file_type="txt",
334
340
  filename=filename,
335
- size=len(content.encode("utf-8")),
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 or limit:
105
- params["limit"] = self.limit or 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 or limit:
133
- params["limit"] = self.limit or 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
- if search_result.success:
141
- return json.dumps(search_result.data, cls=CustomJSONEncoder)
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 "Error searching with the Firecrawl tool: " + search_result.error
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 normalize_hooks(
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 hook instances
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(f"Request failed (attempt {attempt + 1}), retrying in {wait_time} seconds...")
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(f"Request failed (attempt {attempt + 1}), retrying in {wait_time} seconds...")
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
- return File.from_base64(
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)