agno 2.0.11__py3-none-any.whl → 2.1.1__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 (93) hide show
  1. agno/agent/agent.py +607 -176
  2. agno/db/in_memory/in_memory_db.py +42 -29
  3. agno/db/mongo/mongo.py +65 -66
  4. agno/db/postgres/postgres.py +6 -4
  5. agno/db/utils.py +50 -22
  6. agno/exceptions.py +62 -1
  7. agno/guardrails/__init__.py +6 -0
  8. agno/guardrails/base.py +19 -0
  9. agno/guardrails/openai.py +144 -0
  10. agno/guardrails/pii.py +94 -0
  11. agno/guardrails/prompt_injection.py +51 -0
  12. agno/knowledge/embedder/aws_bedrock.py +9 -4
  13. agno/knowledge/embedder/azure_openai.py +54 -0
  14. agno/knowledge/embedder/base.py +2 -0
  15. agno/knowledge/embedder/cohere.py +184 -5
  16. agno/knowledge/embedder/google.py +79 -1
  17. agno/knowledge/embedder/huggingface.py +9 -4
  18. agno/knowledge/embedder/jina.py +63 -0
  19. agno/knowledge/embedder/mistral.py +78 -11
  20. agno/knowledge/embedder/ollama.py +5 -0
  21. agno/knowledge/embedder/openai.py +18 -54
  22. agno/knowledge/embedder/voyageai.py +69 -16
  23. agno/knowledge/knowledge.py +11 -4
  24. agno/knowledge/reader/pdf_reader.py +4 -3
  25. agno/knowledge/reader/website_reader.py +3 -2
  26. agno/models/base.py +125 -32
  27. agno/models/cerebras/cerebras.py +1 -0
  28. agno/models/cerebras/cerebras_openai.py +1 -0
  29. agno/models/dashscope/dashscope.py +1 -0
  30. agno/models/google/gemini.py +27 -5
  31. agno/models/openai/chat.py +13 -4
  32. agno/models/openai/responses.py +1 -1
  33. agno/models/perplexity/perplexity.py +2 -3
  34. agno/models/requesty/__init__.py +5 -0
  35. agno/models/requesty/requesty.py +49 -0
  36. agno/models/vllm/vllm.py +1 -0
  37. agno/models/xai/xai.py +1 -0
  38. agno/os/app.py +98 -126
  39. agno/os/interfaces/__init__.py +1 -0
  40. agno/os/interfaces/agui/agui.py +21 -5
  41. agno/os/interfaces/base.py +4 -2
  42. agno/os/interfaces/slack/slack.py +13 -8
  43. agno/os/interfaces/whatsapp/router.py +2 -0
  44. agno/os/interfaces/whatsapp/whatsapp.py +12 -5
  45. agno/os/mcp.py +2 -2
  46. agno/os/middleware/__init__.py +7 -0
  47. agno/os/middleware/jwt.py +233 -0
  48. agno/os/router.py +182 -46
  49. agno/os/routers/home.py +2 -2
  50. agno/os/routers/memory/memory.py +23 -1
  51. agno/os/routers/memory/schemas.py +1 -1
  52. agno/os/routers/session/session.py +20 -3
  53. agno/os/utils.py +74 -8
  54. agno/run/agent.py +120 -77
  55. agno/run/base.py +2 -13
  56. agno/run/team.py +115 -72
  57. agno/run/workflow.py +5 -15
  58. agno/session/summary.py +9 -10
  59. agno/session/team.py +2 -1
  60. agno/team/team.py +721 -169
  61. agno/tools/firecrawl.py +4 -4
  62. agno/tools/function.py +42 -2
  63. agno/tools/knowledge.py +3 -3
  64. agno/tools/searxng.py +2 -2
  65. agno/tools/serper.py +2 -2
  66. agno/tools/spider.py +2 -2
  67. agno/tools/workflow.py +4 -5
  68. agno/utils/events.py +66 -1
  69. agno/utils/hooks.py +57 -0
  70. agno/utils/media.py +11 -9
  71. agno/utils/print_response/agent.py +43 -5
  72. agno/utils/print_response/team.py +48 -12
  73. agno/utils/serialize.py +32 -0
  74. agno/vectordb/cassandra/cassandra.py +44 -4
  75. agno/vectordb/chroma/chromadb.py +79 -8
  76. agno/vectordb/clickhouse/clickhousedb.py +43 -6
  77. agno/vectordb/couchbase/couchbase.py +76 -5
  78. agno/vectordb/lancedb/lance_db.py +38 -3
  79. agno/vectordb/milvus/milvus.py +76 -4
  80. agno/vectordb/mongodb/mongodb.py +76 -4
  81. agno/vectordb/pgvector/pgvector.py +50 -6
  82. agno/vectordb/pineconedb/pineconedb.py +39 -2
  83. agno/vectordb/qdrant/qdrant.py +76 -26
  84. agno/vectordb/singlestore/singlestore.py +77 -4
  85. agno/vectordb/upstashdb/upstashdb.py +42 -2
  86. agno/vectordb/weaviate/weaviate.py +39 -3
  87. agno/workflow/types.py +5 -6
  88. agno/workflow/workflow.py +58 -2
  89. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/METADATA +4 -3
  90. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/RECORD +93 -82
  91. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/WHEEL +0 -0
  92. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/licenses/LICENSE +0 -0
  93. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/top_level.txt +0 -0
agno/os/utils.py CHANGED
@@ -1,6 +1,7 @@
1
- from typing import Any, Callable, Dict, List, Optional, Union
1
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
2
2
 
3
3
  from fastapi import FastAPI, HTTPException, UploadFile
4
+ from fastapi.routing import APIRoute, APIRouter
4
5
  from starlette.middleware.cors import CORSMiddleware
5
6
 
6
7
  from agno.agent.agent import Agent
@@ -8,6 +9,7 @@ from agno.db.base import BaseDb
8
9
  from agno.knowledge.knowledge import Knowledge
9
10
  from agno.media import Audio, Image, Video
10
11
  from agno.media import File as FileMedia
12
+ from agno.os.config import AgentOSConfig
11
13
  from agno.team.team import Team
12
14
  from agno.tools import Toolkit
13
15
  from agno.tools.function import Function
@@ -149,16 +151,16 @@ def process_document(file: UploadFile) -> Optional[FileMedia]:
149
151
 
150
152
 
151
153
  def extract_format(file: UploadFile):
152
- format = None
154
+ _format = None
153
155
  if file.filename and "." in file.filename:
154
- format = file.filename.split(".")[-1].lower()
156
+ _format = file.filename.split(".")[-1].lower()
155
157
  elif file.content_type:
156
- format = file.content_type.split("/")[-1]
157
- return format
158
+ _format = file.content_type.split("/")[-1]
159
+ return _format
158
160
 
159
161
 
160
162
  def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
161
- formatted_tools = []
163
+ formatted_tools: List[Dict] = []
162
164
  if agent_tools is not None:
163
165
  for tool in agent_tools:
164
166
  if isinstance(tool, dict):
@@ -284,7 +286,11 @@ def update_cors_middleware(app: FastAPI, new_origins: list):
284
286
  for middleware in app.user_middleware:
285
287
  if middleware.cls == CORSMiddleware:
286
288
  if hasattr(middleware, "kwargs"):
287
- existing_origins = middleware.kwargs.get("allow_origins", [])
289
+ origins_value = middleware.kwargs.get("allow_origins", [])
290
+ if isinstance(origins_value, list):
291
+ existing_origins = origins_value
292
+ else:
293
+ existing_origins = []
288
294
  break
289
295
  # Merge origins
290
296
  merged_origins = list(set(new_origins + existing_origins))
@@ -296,7 +302,7 @@ def update_cors_middleware(app: FastAPI, new_origins: list):
296
302
 
297
303
  # Add updated CORS
298
304
  app.add_middleware(
299
- CORSMiddleware,
305
+ CORSMiddleware, # type: ignore
300
306
  allow_origins=final_origins,
301
307
  allow_credentials=True,
302
308
  allow_methods=["*"],
@@ -305,6 +311,66 @@ def update_cors_middleware(app: FastAPI, new_origins: list):
305
311
  )
306
312
 
307
313
 
314
+ def get_existing_route_paths(fastapi_app: FastAPI) -> Dict[str, List[str]]:
315
+ """Get all existing route paths and methods from the FastAPI app.
316
+
317
+ Returns:
318
+ Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
319
+ """
320
+ existing_paths: Dict[str, Any] = {}
321
+ for route in fastapi_app.routes:
322
+ if isinstance(route, APIRoute):
323
+ path = route.path
324
+ methods = list(route.methods) if route.methods else []
325
+ if path in existing_paths:
326
+ existing_paths[path].extend(methods)
327
+ else:
328
+ existing_paths[path] = methods
329
+ return existing_paths
330
+
331
+
332
+ def find_conflicting_routes(fastapi_app: FastAPI, router: APIRouter) -> List[Dict[str, Any]]:
333
+ """Find conflicting routes in the FastAPI app.
334
+
335
+ Args:
336
+ fastapi_app: The FastAPI app with all existing routes
337
+ router: The APIRouter to add
338
+
339
+ Returns:
340
+ List[Dict[str, Any]]: List of conflicting routes
341
+ """
342
+ existing_paths = get_existing_route_paths(fastapi_app)
343
+
344
+ conflicts = []
345
+
346
+ for route in router.routes:
347
+ if isinstance(route, APIRoute):
348
+ full_path = route.path
349
+ route_methods = list(route.methods) if route.methods else []
350
+
351
+ if full_path in existing_paths:
352
+ conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
353
+ if conflicting_methods:
354
+ conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
355
+ return conflicts
356
+
357
+
358
+ def load_yaml_config(config_file_path: str) -> AgentOSConfig:
359
+ """Load a YAML config file and return the configuration as an AgentOSConfig instance."""
360
+ from pathlib import Path
361
+
362
+ import yaml
363
+
364
+ # Validate that the path points to a YAML file
365
+ path = Path(config_file_path)
366
+ if path.suffix.lower() not in [".yaml", ".yml"]:
367
+ raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
368
+
369
+ # Load the YAML file
370
+ with open(config_file_path, "r") as f:
371
+ return AgentOSConfig.model_validate(yaml.safe_load(f))
372
+
373
+
308
374
  def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
309
375
  """Recursively collect MCP tools from a team and its members."""
310
376
  # Check the team tools
agno/run/agent.py CHANGED
@@ -14,6 +14,96 @@ from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
14
14
  from agno.utils.log import logger
15
15
 
16
16
 
17
+ @dataclass
18
+ class RunInput:
19
+ """Container for the raw input data passed to Agent.run().
20
+
21
+ This captures the original input exactly as provided by the user,
22
+ separate from the processed messages that go to the model.
23
+
24
+ Attributes:
25
+ input_content: The literal input message/content passed to run()
26
+ images: Images directly passed to run()
27
+ videos: Videos directly passed to run()
28
+ audios: Audio files directly passed to run()
29
+ files: Files directly passed to run()
30
+ """
31
+
32
+ input_content: Union[str, List, Dict, Message, BaseModel, List[Message]]
33
+ images: Optional[Sequence[Image]] = None
34
+ videos: Optional[Sequence[Video]] = None
35
+ audios: Optional[Sequence[Audio]] = None
36
+ files: Optional[Sequence[File]] = None
37
+
38
+ def input_content_string(self) -> str:
39
+ import json
40
+
41
+ if isinstance(self.input_content, (str)):
42
+ return self.input_content
43
+ elif isinstance(self.input_content, BaseModel):
44
+ return self.input_content.model_dump_json(exclude_none=True)
45
+ elif isinstance(self.input_content, Message):
46
+ return json.dumps(self.input_content.to_dict())
47
+ elif isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], Message):
48
+ return json.dumps([m.to_dict() for m in self.input_content])
49
+ else:
50
+ return str(self.input_content)
51
+
52
+ def to_dict(self) -> Dict[str, Any]:
53
+ """Convert to dictionary representation"""
54
+ result: Dict[str, Any] = {}
55
+
56
+ if self.input_content is not None:
57
+ if isinstance(self.input_content, (str)):
58
+ result["input_content"] = self.input_content
59
+ elif isinstance(self.input_content, BaseModel):
60
+ result["input_content"] = self.input_content.model_dump(exclude_none=True)
61
+ elif isinstance(self.input_content, Message):
62
+ result["input_content"] = self.input_content.to_dict()
63
+ elif (
64
+ isinstance(self.input_content, list)
65
+ and self.input_content
66
+ and isinstance(self.input_content[0], Message)
67
+ ):
68
+ result["input_content"] = [m.to_dict() for m in self.input_content]
69
+ else:
70
+ result["input_content"] = self.input_content
71
+
72
+ if self.images:
73
+ result["images"] = [img.to_dict() for img in self.images]
74
+ if self.videos:
75
+ result["videos"] = [vid.to_dict() for vid in self.videos]
76
+ if self.audios:
77
+ result["audios"] = [aud.to_dict() for aud in self.audios]
78
+ if self.files:
79
+ result["files"] = [file.to_dict() for file in self.files]
80
+
81
+ return result
82
+
83
+ @classmethod
84
+ def from_dict(cls, data: Dict[str, Any]) -> "RunInput":
85
+ """Create RunInput from dictionary"""
86
+ images = None
87
+ if data.get("images"):
88
+ images = [Image.model_validate(img_data) for img_data in data["images"]]
89
+
90
+ videos = None
91
+ if data.get("videos"):
92
+ videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
93
+
94
+ audios = None
95
+ if data.get("audios"):
96
+ audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
97
+
98
+ files = None
99
+ if data.get("files"):
100
+ files = [File.model_validate(file_data) for file_data in data["files"]]
101
+
102
+ return cls(
103
+ input_content=data.get("input_content", ""), images=images, videos=videos, audios=audios, files=files
104
+ )
105
+
106
+
17
107
  class RunEvent(str, Enum):
18
108
  """Events that can be sent by the run() functions"""
19
109
 
@@ -27,6 +117,9 @@ class RunEvent(str, Enum):
27
117
  run_paused = "RunPaused"
28
118
  run_continued = "RunContinued"
29
119
 
120
+ pre_hook_started = "PreHookStarted"
121
+ pre_hook_completed = "PreHookCompleted"
122
+
30
123
  tool_call_started = "ToolCallStarted"
31
124
  tool_call_completed = "ToolCallCompleted"
32
125
 
@@ -53,6 +146,7 @@ class BaseAgentRunEvent(BaseRunOutputEvent):
53
146
  agent_id: str = ""
54
147
  agent_name: str = ""
55
148
  run_id: Optional[str] = None
149
+ parent_run_id: Optional[str] = None
56
150
  session_id: Optional[str] = None
57
151
 
58
152
  # Step context for workflow execution
@@ -153,6 +247,11 @@ class RunErrorEvent(BaseAgentRunEvent):
153
247
  event: str = RunEvent.run_error.value
154
248
  content: Optional[str] = None
155
249
 
250
+ # From exceptions
251
+ error_type: Optional[str] = None
252
+ error_id: Optional[str] = None
253
+ additional_data: Optional[Dict[str, Any]] = None
254
+
156
255
 
157
256
  @dataclass
158
257
  class RunCancelledEvent(BaseAgentRunEvent):
@@ -164,6 +263,20 @@ class RunCancelledEvent(BaseAgentRunEvent):
164
263
  return True
165
264
 
166
265
 
266
+ @dataclass
267
+ class PreHookStartedEvent(BaseAgentRunEvent):
268
+ event: str = RunEvent.pre_hook_started.value
269
+ pre_hook_name: Optional[str] = None
270
+ run_input: Optional[RunInput] = None
271
+
272
+
273
+ @dataclass
274
+ class PreHookCompletedEvent(BaseAgentRunEvent):
275
+ event: str = RunEvent.pre_hook_completed.value
276
+ pre_hook_name: Optional[str] = None
277
+ run_input: Optional[RunInput] = None
278
+
279
+
167
280
  @dataclass
168
281
  class MemoryUpdateStartedEvent(BaseAgentRunEvent):
169
282
  event: str = RunEvent.memory_update_started.value
@@ -244,6 +357,8 @@ RunOutputEvent = Union[
244
357
  RunCancelledEvent,
245
358
  RunPausedEvent,
246
359
  RunContinuedEvent,
360
+ PreHookStartedEvent,
361
+ PreHookCompletedEvent,
247
362
  ReasoningStartedEvent,
248
363
  ReasoningStepEvent,
249
364
  ReasoningCompletedEvent,
@@ -269,6 +384,8 @@ RUN_EVENT_TYPE_REGISTRY = {
269
384
  RunEvent.run_cancelled.value: RunCancelledEvent,
270
385
  RunEvent.run_paused.value: RunPausedEvent,
271
386
  RunEvent.run_continued.value: RunContinuedEvent,
387
+ RunEvent.pre_hook_started.value: PreHookStartedEvent,
388
+ RunEvent.pre_hook_completed.value: PreHookCompletedEvent,
272
389
  RunEvent.reasoning_started.value: ReasoningStartedEvent,
273
390
  RunEvent.reasoning_step.value: ReasoningStepEvent,
274
391
  RunEvent.reasoning_completed.value: ReasoningCompletedEvent,
@@ -292,80 +409,6 @@ def run_output_event_from_dict(data: dict) -> BaseRunOutputEvent:
292
409
  return cls.from_dict(data) # type: ignore
293
410
 
294
411
 
295
- @dataclass
296
- class RunInput:
297
- """Container for the raw input data passed to Agent.run().
298
-
299
- This captures the original input exactly as provided by the user,
300
- separate from the processed messages that go to the model.
301
-
302
- Attributes:
303
- input_content: The literal input message/content passed to run()
304
- images: Images directly passed to run()
305
- videos: Videos directly passed to run()
306
- audios: Audio files directly passed to run()
307
- files: Files directly passed to run()
308
- """
309
-
310
- input_content: Optional[Union[str, List, Dict, Message, BaseModel, List[Message]]] = None
311
- images: Optional[Sequence[Image]] = None
312
- videos: Optional[Sequence[Video]] = None
313
- audios: Optional[Sequence[Audio]] = None
314
- files: Optional[Sequence[File]] = None
315
-
316
- def to_dict(self) -> Dict[str, Any]:
317
- """Convert to dictionary representation"""
318
- result: Dict[str, Any] = {}
319
-
320
- if self.input_content is not None:
321
- if isinstance(self.input_content, (str)):
322
- result["input_content"] = self.input_content
323
- elif isinstance(self.input_content, BaseModel):
324
- result["input_content"] = self.input_content.model_dump(exclude_none=True)
325
- elif isinstance(self.input_content, Message):
326
- result["input_content"] = self.input_content.to_dict()
327
- elif (
328
- isinstance(self.input_content, list)
329
- and self.input_content
330
- and isinstance(self.input_content[0], Message)
331
- ):
332
- result["input_content"] = [m.to_dict() for m in self.input_content]
333
- else:
334
- result["input_content"] = self.input_content
335
-
336
- if self.images:
337
- result["images"] = [img.to_dict() for img in self.images]
338
- if self.videos:
339
- result["videos"] = [vid.to_dict() for vid in self.videos]
340
- if self.audios:
341
- result["audios"] = [aud.to_dict() for aud in self.audios]
342
- if self.files:
343
- result["files"] = [file.to_dict() for file in self.files]
344
-
345
- return result
346
-
347
- @classmethod
348
- def from_dict(cls, data: Dict[str, Any]) -> "RunInput":
349
- """Create RunInput from dictionary"""
350
- images = None
351
- if data.get("images"):
352
- images = [Image.model_validate(img_data) for img_data in data["images"]]
353
-
354
- videos = None
355
- if data.get("videos"):
356
- videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
357
-
358
- audios = None
359
- if data.get("audios"):
360
- audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
361
-
362
- files = None
363
- if data.get("files"):
364
- files = [File.model_validate(file_data) for file_data in data["files"]]
365
-
366
- return cls(input_content=data.get("input_content"), images=images, videos=videos, audios=audios, files=files)
367
-
368
-
369
412
  @dataclass
370
413
  class RunOutput:
371
414
  """Response returned by Agent.run() or Workflow.run() functions"""
@@ -378,6 +421,9 @@ class RunOutput:
378
421
  workflow_id: Optional[str] = None
379
422
  user_id: Optional[str] = None
380
423
 
424
+ # Input media and messages from user
425
+ input: Optional[RunInput] = None
426
+
381
427
  content: Optional[Any] = None
382
428
  content_type: str = "str"
383
429
 
@@ -401,9 +447,6 @@ class RunOutput:
401
447
  files: Optional[List[File]] = None # Files attached to the response
402
448
  response_audio: Optional[Audio] = None # Model audio response
403
449
 
404
- # Input media and messages from user
405
- input: Optional[RunInput] = None
406
-
407
450
  citations: Optional[Citations] = None
408
451
  references: Optional[List[MessageReferences]] = None
409
452
 
agno/run/base.py CHANGED
@@ -117,19 +117,8 @@ class BaseRunOutputEvent:
117
117
 
118
118
  def to_json(self, separators=(", ", ": "), indent: Optional[int] = 2) -> str:
119
119
  import json
120
- from datetime import date, datetime, time
121
- from enum import Enum
122
-
123
- def json_serializer(obj):
124
- # Datetime like
125
- if isinstance(obj, (datetime, date, time)):
126
- return obj.isoformat()
127
- # Enums
128
- if isinstance(obj, Enum):
129
- v = obj.value
130
- return v if isinstance(v, (str, int, float, bool, type(None))) else obj.name
131
- # Fallback to string
132
- return str(obj)
120
+
121
+ from agno.utils.serialize import json_serializer
133
122
 
134
123
  try:
135
124
  _dict = self.to_dict()
agno/run/team.py CHANGED
@@ -15,6 +15,94 @@ from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
15
15
  from agno.utils.log import log_error
16
16
 
17
17
 
18
+ @dataclass
19
+ class TeamRunInput:
20
+ """Container for the raw input data passed to Agent.run().
21
+ This captures the original input exactly as provided by the user,
22
+ separate from the processed messages that go to the model.
23
+ Attributes:
24
+ input_content: The literal input message/content passed to run()
25
+ images: Images directly passed to run()
26
+ videos: Videos directly passed to run()
27
+ audios: Audio files directly passed to run()
28
+ files: Files directly passed to run()
29
+ """
30
+
31
+ input_content: Union[str, List, Dict, Message, BaseModel, List[Message]]
32
+ images: Optional[Sequence[Image]] = None
33
+ videos: Optional[Sequence[Video]] = None
34
+ audios: Optional[Sequence[Audio]] = None
35
+ files: Optional[Sequence[File]] = None
36
+
37
+ def input_content_string(self) -> str:
38
+ import json
39
+
40
+ if isinstance(self.input_content, (str)):
41
+ return self.input_content
42
+ elif isinstance(self.input_content, BaseModel):
43
+ return self.input_content.model_dump_json(exclude_none=True)
44
+ elif isinstance(self.input_content, Message):
45
+ return json.dumps(self.input_content.to_dict())
46
+ elif isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], Message):
47
+ return json.dumps([m.to_dict() for m in self.input_content])
48
+ else:
49
+ return str(self.input_content)
50
+
51
+ def to_dict(self) -> Dict[str, Any]:
52
+ """Convert to dictionary representation"""
53
+ result: Dict[str, Any] = {}
54
+
55
+ if self.input_content is not None:
56
+ if isinstance(self.input_content, (str)):
57
+ result["input_content"] = self.input_content
58
+ elif isinstance(self.input_content, BaseModel):
59
+ result["input_content"] = self.input_content.model_dump(exclude_none=True)
60
+ elif isinstance(self.input_content, Message):
61
+ result["input_content"] = self.input_content.to_dict()
62
+ elif (
63
+ isinstance(self.input_content, list)
64
+ and self.input_content
65
+ and isinstance(self.input_content[0], Message)
66
+ ):
67
+ result["input_content"] = [m.to_dict() for m in self.input_content]
68
+ else:
69
+ result["input_content"] = self.input_content
70
+
71
+ if self.images:
72
+ result["images"] = [img.to_dict() for img in self.images]
73
+ if self.videos:
74
+ result["videos"] = [vid.to_dict() for vid in self.videos]
75
+ if self.audios:
76
+ result["audios"] = [aud.to_dict() for aud in self.audios]
77
+ if self.files:
78
+ result["files"] = [file.to_dict() for file in self.files]
79
+
80
+ return result
81
+
82
+ @classmethod
83
+ def from_dict(cls, data: Dict[str, Any]) -> "TeamRunInput":
84
+ """Create TeamRunInput from dictionary"""
85
+ images = None
86
+ if data.get("images"):
87
+ images = [Image.model_validate(img_data) for img_data in data["images"]]
88
+
89
+ videos = None
90
+ if data.get("videos"):
91
+ videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
92
+
93
+ audios = None
94
+ if data.get("audios"):
95
+ audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
96
+
97
+ files = None
98
+ if data.get("files"):
99
+ files = [File.model_validate(file_data) for file_data in data["files"]]
100
+
101
+ return cls(
102
+ input_content=data.get("input_content", ""), images=images, videos=videos, audios=audios, files=files
103
+ )
104
+
105
+
18
106
  class TeamRunEvent(str, Enum):
19
107
  """Events that can be sent by the run() functions"""
20
108
 
@@ -25,6 +113,9 @@ class TeamRunEvent(str, Enum):
25
113
  run_error = "TeamRunError"
26
114
  run_cancelled = "TeamRunCancelled"
27
115
 
116
+ pre_hook_started = "TeamPreHookStarted"
117
+ pre_hook_completed = "TeamPreHookCompleted"
118
+
28
119
  tool_call_started = "TeamToolCallStarted"
29
120
  tool_call_completed = "TeamToolCallCompleted"
30
121
 
@@ -51,6 +142,7 @@ class BaseTeamRunEvent(BaseRunOutputEvent):
51
142
  team_id: str = ""
52
143
  team_name: str = ""
53
144
  run_id: Optional[str] = None
145
+ parent_run_id: Optional[str] = None
54
146
  session_id: Optional[str] = None
55
147
 
56
148
  workflow_id: Optional[str] = None
@@ -141,6 +233,11 @@ class RunErrorEvent(BaseTeamRunEvent):
141
233
  event: str = TeamRunEvent.run_error.value
142
234
  content: Optional[str] = None
143
235
 
236
+ # From exceptions
237
+ error_type: Optional[str] = None
238
+ error_id: Optional[str] = None
239
+ additional_data: Optional[Dict[str, Any]] = None
240
+
144
241
 
145
242
  @dataclass
146
243
  class RunCancelledEvent(BaseTeamRunEvent):
@@ -152,6 +249,20 @@ class RunCancelledEvent(BaseTeamRunEvent):
152
249
  return True
153
250
 
154
251
 
252
+ @dataclass
253
+ class PreHookStartedEvent(BaseTeamRunEvent):
254
+ event: str = TeamRunEvent.pre_hook_started.value
255
+ pre_hook_name: Optional[str] = None
256
+ run_input: Optional[TeamRunInput] = None
257
+
258
+
259
+ @dataclass
260
+ class PreHookCompletedEvent(BaseTeamRunEvent):
261
+ event: str = TeamRunEvent.pre_hook_completed.value
262
+ pre_hook_name: Optional[str] = None
263
+ run_input: Optional[TeamRunInput] = None
264
+
265
+
155
266
  @dataclass
156
267
  class MemoryUpdateStartedEvent(BaseTeamRunEvent):
157
268
  event: str = TeamRunEvent.memory_update_started.value
@@ -230,6 +341,8 @@ TeamRunOutputEvent = Union[
230
341
  RunCompletedEvent,
231
342
  RunErrorEvent,
232
343
  RunCancelledEvent,
344
+ PreHookStartedEvent,
345
+ PreHookCompletedEvent,
233
346
  ReasoningStartedEvent,
234
347
  ReasoningStepEvent,
235
348
  ReasoningCompletedEvent,
@@ -252,6 +365,8 @@ TEAM_RUN_EVENT_TYPE_REGISTRY = {
252
365
  TeamRunEvent.run_completed.value: RunCompletedEvent,
253
366
  TeamRunEvent.run_error.value: RunErrorEvent,
254
367
  TeamRunEvent.run_cancelled.value: RunCancelledEvent,
368
+ TeamRunEvent.pre_hook_started.value: PreHookStartedEvent,
369
+ TeamRunEvent.pre_hook_completed.value: PreHookCompletedEvent,
255
370
  TeamRunEvent.reasoning_started.value: ReasoningStartedEvent,
256
371
  TeamRunEvent.reasoning_step.value: ReasoningStepEvent,
257
372
  TeamRunEvent.reasoning_completed.value: ReasoningCompletedEvent,
@@ -278,78 +393,6 @@ def team_run_output_event_from_dict(data: dict) -> BaseTeamRunEvent:
278
393
  return event_class.from_dict(data) # type: ignore
279
394
 
280
395
 
281
- @dataclass
282
- class TeamRunInput:
283
- """Container for the raw input data passed to Agent.run().
284
- This captures the original input exactly as provided by the user,
285
- separate from the processed messages that go to the model.
286
- Attributes:
287
- input_content: The literal input message/content passed to run()
288
- images: Images directly passed to run()
289
- videos: Videos directly passed to run()
290
- audios: Audio files directly passed to run()
291
- files: Files directly passed to run()
292
- """
293
-
294
- input_content: Optional[Union[str, List, Dict, Message, BaseModel, List[Message]]] = None
295
- images: Optional[Sequence[Image]] = None
296
- videos: Optional[Sequence[Video]] = None
297
- audios: Optional[Sequence[Audio]] = None
298
- files: Optional[Sequence[File]] = None
299
-
300
- def to_dict(self) -> Dict[str, Any]:
301
- """Convert to dictionary representation"""
302
- result: Dict[str, Any] = {}
303
-
304
- if self.input_content is not None:
305
- if isinstance(self.input_content, (str)):
306
- result["input_content"] = self.input_content
307
- elif isinstance(self.input_content, BaseModel):
308
- result["input_content"] = self.input_content.model_dump(exclude_none=True)
309
- elif isinstance(self.input_content, Message):
310
- result["input_content"] = self.input_content.to_dict()
311
- elif (
312
- isinstance(self.input_content, list)
313
- and self.input_content
314
- and isinstance(self.input_content[0], Message)
315
- ):
316
- result["input_content"] = [m.to_dict() for m in self.input_content]
317
- else:
318
- result["input_content"] = self.input_content
319
-
320
- if self.images:
321
- result["images"] = [img.to_dict() for img in self.images]
322
- if self.videos:
323
- result["videos"] = [vid.to_dict() for vid in self.videos]
324
- if self.audios:
325
- result["audios"] = [aud.to_dict() for aud in self.audios]
326
- if self.files:
327
- result["files"] = [file.to_dict() for file in self.files]
328
-
329
- return result
330
-
331
- @classmethod
332
- def from_dict(cls, data: Dict[str, Any]) -> "TeamRunInput":
333
- """Create TeamRunInput from dictionary"""
334
- images = None
335
- if data.get("images"):
336
- images = [Image.model_validate(img_data) for img_data in data["images"]]
337
-
338
- videos = None
339
- if data.get("videos"):
340
- videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
341
-
342
- audios = None
343
- if data.get("audios"):
344
- audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
345
-
346
- files = None
347
- if data.get("files"):
348
- files = [File.model_validate(file_data) for file_data in data["files"]]
349
-
350
- return cls(input_content=data.get("input_content"), images=images, videos=videos, audios=audios, files=files)
351
-
352
-
353
396
  @dataclass
354
397
  class TeamRunOutput:
355
398
  """Response returned by Team.run() functions"""
agno/run/workflow.py CHANGED
@@ -9,7 +9,6 @@ from agno.media import Audio, Image, Video
9
9
  from agno.run.agent import RunOutput
10
10
  from agno.run.base import BaseRunOutputEvent, RunStatus
11
11
  from agno.run.team import TeamRunOutput
12
- from agno.utils.log import log_error
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from agno.workflow.types import StepOutput, WorkflowMetrics
@@ -95,20 +94,6 @@ class BaseWorkflowRunOutputEvent(BaseRunOutputEvent):
95
94
 
96
95
  return _dict
97
96
 
98
- def to_json(self, separators=(", ", ": "), indent: Optional[int] = 2) -> str:
99
- import json
100
-
101
- try:
102
- _dict = self.to_dict()
103
- except Exception:
104
- log_error("Failed to convert response to json", exc_info=True)
105
- raise
106
-
107
- if indent is None:
108
- return json.dumps(_dict, separators=separators)
109
- else:
110
- return json.dumps(_dict, indent=indent, separators=separators)
111
-
112
97
  @property
113
98
  def is_cancelled(self):
114
99
  return False
@@ -155,6 +140,11 @@ class WorkflowErrorEvent(BaseWorkflowRunOutputEvent):
155
140
  event: str = WorkflowRunEvent.workflow_error.value
156
141
  error: Optional[str] = None
157
142
 
143
+ # From exceptions
144
+ error_type: Optional[str] = None
145
+ error_id: Optional[str] = None
146
+ additional_data: Optional[Dict[str, Any]] = None
147
+
158
148
 
159
149
  @dataclass
160
150
  class WorkflowCancelledEvent(BaseWorkflowRunOutputEvent):