agno 2.0.10__py3-none-any.whl → 2.1.0__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 (85) hide show
  1. agno/agent/agent.py +608 -175
  2. agno/db/in_memory/in_memory_db.py +42 -29
  3. agno/db/postgres/postgres.py +6 -4
  4. agno/exceptions.py +62 -1
  5. agno/guardrails/__init__.py +6 -0
  6. agno/guardrails/base.py +19 -0
  7. agno/guardrails/openai.py +144 -0
  8. agno/guardrails/pii.py +94 -0
  9. agno/guardrails/prompt_injection.py +51 -0
  10. agno/knowledge/embedder/aws_bedrock.py +9 -4
  11. agno/knowledge/embedder/azure_openai.py +54 -0
  12. agno/knowledge/embedder/base.py +2 -0
  13. agno/knowledge/embedder/cohere.py +184 -5
  14. agno/knowledge/embedder/google.py +79 -1
  15. agno/knowledge/embedder/huggingface.py +9 -4
  16. agno/knowledge/embedder/jina.py +63 -0
  17. agno/knowledge/embedder/mistral.py +78 -11
  18. agno/knowledge/embedder/ollama.py +5 -0
  19. agno/knowledge/embedder/openai.py +18 -54
  20. agno/knowledge/embedder/voyageai.py +69 -16
  21. agno/knowledge/knowledge.py +5 -4
  22. agno/knowledge/reader/pdf_reader.py +4 -3
  23. agno/knowledge/reader/website_reader.py +3 -2
  24. agno/models/base.py +125 -32
  25. agno/models/cerebras/cerebras.py +1 -0
  26. agno/models/cerebras/cerebras_openai.py +1 -0
  27. agno/models/dashscope/dashscope.py +1 -0
  28. agno/models/google/gemini.py +27 -5
  29. agno/models/litellm/chat.py +17 -0
  30. agno/models/openai/chat.py +13 -4
  31. agno/models/perplexity/perplexity.py +2 -3
  32. agno/models/requesty/__init__.py +5 -0
  33. agno/models/requesty/requesty.py +49 -0
  34. agno/models/vllm/vllm.py +1 -0
  35. agno/models/xai/xai.py +1 -0
  36. agno/os/app.py +167 -148
  37. agno/os/interfaces/whatsapp/router.py +2 -0
  38. agno/os/mcp.py +1 -1
  39. agno/os/middleware/__init__.py +7 -0
  40. agno/os/middleware/jwt.py +233 -0
  41. agno/os/router.py +181 -45
  42. agno/os/routers/home.py +2 -2
  43. agno/os/routers/memory/memory.py +23 -1
  44. agno/os/routers/memory/schemas.py +1 -1
  45. agno/os/routers/session/session.py +20 -3
  46. agno/os/utils.py +172 -8
  47. agno/run/agent.py +120 -77
  48. agno/run/team.py +115 -72
  49. agno/run/workflow.py +5 -15
  50. agno/session/summary.py +9 -10
  51. agno/session/team.py +2 -1
  52. agno/team/team.py +720 -168
  53. agno/tools/firecrawl.py +4 -4
  54. agno/tools/function.py +42 -2
  55. agno/tools/knowledge.py +3 -3
  56. agno/tools/searxng.py +2 -2
  57. agno/tools/serper.py +2 -2
  58. agno/tools/spider.py +2 -2
  59. agno/tools/workflow.py +4 -5
  60. agno/utils/events.py +66 -1
  61. agno/utils/hooks.py +57 -0
  62. agno/utils/media.py +11 -9
  63. agno/utils/print_response/agent.py +43 -5
  64. agno/utils/print_response/team.py +48 -12
  65. agno/vectordb/cassandra/cassandra.py +44 -4
  66. agno/vectordb/chroma/chromadb.py +79 -8
  67. agno/vectordb/clickhouse/clickhousedb.py +43 -6
  68. agno/vectordb/couchbase/couchbase.py +76 -5
  69. agno/vectordb/lancedb/lance_db.py +38 -3
  70. agno/vectordb/llamaindex/__init__.py +3 -0
  71. agno/vectordb/milvus/milvus.py +76 -4
  72. agno/vectordb/mongodb/mongodb.py +76 -4
  73. agno/vectordb/pgvector/pgvector.py +50 -6
  74. agno/vectordb/pineconedb/pineconedb.py +39 -2
  75. agno/vectordb/qdrant/qdrant.py +76 -26
  76. agno/vectordb/singlestore/singlestore.py +77 -4
  77. agno/vectordb/upstashdb/upstashdb.py +42 -2
  78. agno/vectordb/weaviate/weaviate.py +39 -3
  79. agno/workflow/types.py +1 -0
  80. agno/workflow/workflow.py +58 -2
  81. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/METADATA +4 -3
  82. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/RECORD +85 -75
  83. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/WHEEL +0 -0
  84. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/licenses/LICENSE +0 -0
  85. {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@ import math
3
3
  from typing import List, Optional
4
4
  from uuid import uuid4
5
5
 
6
- from fastapi import Depends, HTTPException, Path, Query
6
+ from fastapi import Depends, HTTPException, Path, Query, Request
7
7
  from fastapi.routing import APIRouter
8
8
 
9
9
  from agno.db.base import BaseDb
@@ -80,9 +80,17 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
80
80
  },
81
81
  )
82
82
  async def create_memory(
83
+ request: Request,
83
84
  payload: UserMemoryCreateSchema,
84
85
  db_id: Optional[str] = Query(default=None, description="Database ID to use for memory storage"),
85
86
  ) -> UserMemorySchema:
87
+ if hasattr(request.state, "user_id"):
88
+ user_id = request.state.user_id
89
+ payload.user_id = user_id
90
+
91
+ if payload.user_id is None:
92
+ raise HTTPException(status_code=400, detail="User ID is required")
93
+
86
94
  db = get_db(dbs, db_id)
87
95
  user_memory = db.upsert_user_memory(
88
96
  memory=UserMemory(
@@ -173,6 +181,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
173
181
  },
174
182
  )
175
183
  async def get_memories(
184
+ request: Request,
176
185
  user_id: Optional[str] = Query(default=None, description="Filter memories by user ID"),
177
186
  agent_id: Optional[str] = Query(default=None, description="Filter memories by agent ID"),
178
187
  team_id: Optional[str] = Query(default=None, description="Filter memories by team ID"),
@@ -185,6 +194,10 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
185
194
  db_id: Optional[str] = Query(default=None, description="Database ID to query memories from"),
186
195
  ) -> PaginatedResponse[UserMemorySchema]:
187
196
  db = get_db(dbs, db_id)
197
+
198
+ if hasattr(request.state, "user_id"):
199
+ user_id = request.state.user_id
200
+
188
201
  user_memories, total_count = db.get_user_memories(
189
202
  limit=limit,
190
203
  page=page,
@@ -316,11 +329,20 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
316
329
  },
317
330
  )
318
331
  async def update_memory(
332
+ request: Request,
319
333
  payload: UserMemoryCreateSchema,
320
334
  memory_id: str = Path(description="Memory ID to update"),
321
335
  db_id: Optional[str] = Query(default=None, description="Database ID to use for update"),
322
336
  ) -> UserMemorySchema:
323
337
  db = get_db(dbs, db_id)
338
+
339
+ if hasattr(request.state, "user_id"):
340
+ user_id = request.state.user_id
341
+ payload.user_id = user_id
342
+
343
+ if payload.user_id is None:
344
+ raise HTTPException(status_code=400, detail="User ID is required")
345
+
324
346
  user_memory = db.upsert_user_memory(
325
347
  memory=UserMemory(
326
348
  memory_id=memory_id,
@@ -36,7 +36,7 @@ class UserMemoryCreateSchema(BaseModel):
36
36
  """Define the payload expected for creating a new user memory"""
37
37
 
38
38
  memory: str
39
- user_id: str
39
+ user_id: Optional[str] = None
40
40
  topics: Optional[List[str]] = None
41
41
 
42
42
 
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import List, Optional, Union
3
3
 
4
- from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
4
+ from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
5
5
 
6
6
  from agno.db.base import BaseDb, SessionType
7
7
  from agno.os.auth import get_authentication_dependency
@@ -86,6 +86,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
86
86
  },
87
87
  )
88
88
  async def get_sessions(
89
+ request: Request,
89
90
  session_type: SessionType = Query(
90
91
  default=SessionType.AGENT,
91
92
  alias="type",
@@ -103,6 +104,10 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
103
104
  db_id: Optional[str] = Query(default=None, description="Database ID to query sessions from"),
104
105
  ) -> PaginatedResponse[SessionSchema]:
105
106
  db = get_db(dbs, db_id)
107
+
108
+ if hasattr(request.state, "user_id"):
109
+ user_id = request.state.user_id
110
+
106
111
  sessions, total_count = db.get_sessions(
107
112
  session_type=session_type,
108
113
  component_id=component_id,
@@ -213,14 +218,20 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
213
218
  },
214
219
  )
215
220
  async def get_session_by_id(
221
+ request: Request,
216
222
  session_id: str = Path(description="Session ID to retrieve"),
217
223
  session_type: SessionType = Query(
218
224
  default=SessionType.AGENT, description="Session type (agent, team, or workflow)", alias="type"
219
225
  ),
226
+ user_id: Optional[str] = Query(default=None, description="User ID to query session from"),
220
227
  db_id: Optional[str] = Query(default=None, description="Database ID to query session from"),
221
228
  ) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
222
229
  db = get_db(dbs, db_id)
223
- session = db.get_session(session_id=session_id, session_type=session_type)
230
+
231
+ if hasattr(request.state, "user_id"):
232
+ user_id = request.state.user_id
233
+
234
+ session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id)
224
235
  if not session:
225
236
  raise HTTPException(
226
237
  status_code=404, detail=f"{session_type.value.title()} Session with id '{session_id}' not found"
@@ -349,14 +360,20 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
349
360
  },
350
361
  )
351
362
  async def get_session_runs(
363
+ request: Request,
352
364
  session_id: str = Path(description="Session ID to get runs from"),
353
365
  session_type: SessionType = Query(
354
366
  default=SessionType.AGENT, description="Session type (agent, team, or workflow)", alias="type"
355
367
  ),
368
+ user_id: Optional[str] = Query(default=None, description="User ID to query runs from"),
356
369
  db_id: Optional[str] = Query(default=None, description="Database ID to query runs from"),
357
370
  ) -> List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]]:
358
371
  db = get_db(dbs, db_id)
359
- session = db.get_session(session_id=session_id, session_type=session_type, deserialize=False)
372
+
373
+ if hasattr(request.state, "user_id"):
374
+ user_id = request.state.user_id
375
+
376
+ session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id, deserialize=False)
360
377
  if not session:
361
378
  raise HTTPException(status_code=404, detail=f"Session with ID {session_id} not found")
362
379
 
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,10 +302,168 @@ 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=["*"],
303
309
  allow_headers=["*"],
304
310
  expose_headers=["*"],
305
311
  )
312
+
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
+
374
+ def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
375
+ """Recursively collect MCP tools from a team and its members."""
376
+ # Check the team tools
377
+ if team.tools:
378
+ for tool in team.tools:
379
+ type_name = type(tool).__name__
380
+ if type_name in ("MCPTools", "MultiMCPTools"):
381
+ if tool not in mcp_tools:
382
+ mcp_tools.append(tool)
383
+
384
+ # Recursively check team members
385
+ if team.members:
386
+ for member in team.members:
387
+ if isinstance(member, Agent):
388
+ if member.tools:
389
+ for tool in member.tools:
390
+ type_name = type(tool).__name__
391
+ if type_name in ("MCPTools", "MultiMCPTools"):
392
+ if tool not in mcp_tools:
393
+ mcp_tools.append(tool)
394
+
395
+ elif isinstance(member, Team):
396
+ # Recursively check nested team
397
+ collect_mcp_tools_from_team(member, mcp_tools)
398
+
399
+
400
+ def collect_mcp_tools_from_workflow(workflow: Workflow, mcp_tools: List[Any]) -> None:
401
+ """Recursively collect MCP tools from a workflow and its steps."""
402
+ from agno.workflow.steps import Steps
403
+
404
+ # Recursively check workflow steps
405
+ if workflow.steps:
406
+ if isinstance(workflow.steps, list):
407
+ # Handle list of steps
408
+ for step in workflow.steps:
409
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
410
+
411
+ elif isinstance(workflow.steps, Steps):
412
+ # Handle Steps container
413
+ if steps := workflow.steps.steps:
414
+ for step in steps:
415
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
416
+
417
+ elif callable(workflow.steps):
418
+ pass
419
+
420
+
421
+ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> None:
422
+ """Collect MCP tools from a single workflow step."""
423
+ from agno.workflow.condition import Condition
424
+ from agno.workflow.loop import Loop
425
+ from agno.workflow.parallel import Parallel
426
+ from agno.workflow.router import Router
427
+ from agno.workflow.step import Step
428
+ from agno.workflow.steps import Steps
429
+
430
+ if isinstance(step, Step):
431
+ # Check step's agent
432
+ if step.agent:
433
+ if step.agent.tools:
434
+ for tool in step.agent.tools:
435
+ type_name = type(tool).__name__
436
+ if type_name in ("MCPTools", "MultiMCPTools"):
437
+ if tool not in mcp_tools:
438
+ mcp_tools.append(tool)
439
+ # Check step's team
440
+ if step.team:
441
+ collect_mcp_tools_from_team(step.team, mcp_tools)
442
+
443
+ elif isinstance(step, Steps):
444
+ if steps := step.steps:
445
+ for step in steps:
446
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
447
+
448
+ elif isinstance(step, (Parallel, Loop, Condition, Router)):
449
+ # These contain other steps - recursively check them
450
+ if hasattr(step, "steps") and step.steps:
451
+ for sub_step in step.steps:
452
+ collect_mcp_tools_from_workflow_step(sub_step, mcp_tools)
453
+
454
+ elif isinstance(step, Agent):
455
+ # Direct agent in workflow steps
456
+ if step.tools:
457
+ for tool in step.tools:
458
+ type_name = type(tool).__name__
459
+ if type_name in ("MCPTools", "MultiMCPTools"):
460
+ if tool not in mcp_tools:
461
+ mcp_tools.append(tool)
462
+
463
+ elif isinstance(step, Team):
464
+ # Direct team in workflow steps
465
+ collect_mcp_tools_from_team(step, mcp_tools)
466
+
467
+ elif isinstance(step, Workflow):
468
+ # Nested workflow
469
+ collect_mcp_tools_from_workflow(step, mcp_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