agno 2.2.0__py3-none-any.whl → 2.2.2__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 (71) hide show
  1. agno/agent/agent.py +751 -575
  2. agno/culture/manager.py +22 -24
  3. agno/db/async_postgres/__init__.py +1 -1
  4. agno/db/dynamo/dynamo.py +0 -2
  5. agno/db/firestore/firestore.py +0 -2
  6. agno/db/gcs_json/gcs_json_db.py +0 -4
  7. agno/db/gcs_json/utils.py +0 -24
  8. agno/db/in_memory/in_memory_db.py +0 -3
  9. agno/db/json/json_db.py +4 -10
  10. agno/db/json/utils.py +0 -24
  11. agno/db/mongo/mongo.py +0 -2
  12. agno/db/mysql/mysql.py +0 -3
  13. agno/db/postgres/__init__.py +1 -1
  14. agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
  15. agno/db/postgres/postgres.py +7 -10
  16. agno/db/postgres/utils.py +106 -2
  17. agno/db/redis/redis.py +0 -2
  18. agno/db/singlestore/singlestore.py +0 -3
  19. agno/db/sqlite/__init__.py +2 -1
  20. agno/db/sqlite/async_sqlite.py +2269 -0
  21. agno/db/sqlite/sqlite.py +0 -2
  22. agno/db/sqlite/utils.py +96 -0
  23. agno/db/surrealdb/surrealdb.py +0 -6
  24. agno/knowledge/knowledge.py +14 -3
  25. agno/knowledge/reader/pptx_reader.py +101 -0
  26. agno/knowledge/reader/reader_factory.py +30 -0
  27. agno/knowledge/reader/tavily_reader.py +194 -0
  28. agno/knowledge/types.py +1 -0
  29. agno/memory/manager.py +28 -25
  30. agno/models/anthropic/claude.py +63 -6
  31. agno/models/base.py +255 -36
  32. agno/models/response.py +69 -0
  33. agno/os/router.py +7 -5
  34. agno/os/routers/memory/memory.py +2 -1
  35. agno/os/routers/memory/schemas.py +5 -2
  36. agno/os/schema.py +26 -20
  37. agno/os/utils.py +9 -2
  38. agno/run/agent.py +28 -30
  39. agno/run/base.py +17 -1
  40. agno/run/team.py +28 -29
  41. agno/run/workflow.py +32 -17
  42. agno/session/agent.py +3 -0
  43. agno/session/summary.py +4 -1
  44. agno/session/team.py +1 -1
  45. agno/team/team.py +620 -374
  46. agno/tools/dalle.py +2 -4
  47. agno/tools/eleven_labs.py +23 -25
  48. agno/tools/function.py +40 -0
  49. agno/tools/mcp/__init__.py +10 -0
  50. agno/tools/mcp/mcp.py +324 -0
  51. agno/tools/mcp/multi_mcp.py +347 -0
  52. agno/tools/mcp/params.py +24 -0
  53. agno/tools/slack.py +18 -3
  54. agno/tools/tavily.py +146 -0
  55. agno/utils/agent.py +366 -1
  56. agno/utils/mcp.py +92 -2
  57. agno/utils/media.py +166 -1
  58. agno/utils/message.py +60 -0
  59. agno/utils/print_response/workflow.py +17 -1
  60. agno/utils/team.py +89 -1
  61. agno/workflow/step.py +0 -1
  62. agno/workflow/types.py +10 -15
  63. agno/workflow/workflow.py +86 -1
  64. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/METADATA +31 -25
  65. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/RECORD +68 -64
  66. agno/db/async_postgres/schemas.py +0 -139
  67. agno/db/async_postgres/utils.py +0 -347
  68. agno/tools/mcp.py +0 -679
  69. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/WHEEL +0 -0
  70. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/licenses/LICENSE +0 -0
  71. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/top_level.txt +0 -0
agno/models/response.py CHANGED
@@ -123,6 +123,75 @@ class ModelResponse:
123
123
 
124
124
  updated_session_state: Optional[Dict[str, Any]] = None
125
125
 
126
+ def to_dict(self) -> Dict[str, Any]:
127
+ """Serialize ModelResponse to dictionary for caching."""
128
+ _dict = asdict(self)
129
+
130
+ # Handle special serialization for audio
131
+ if self.audio is not None:
132
+ _dict["audio"] = self.audio.to_dict()
133
+
134
+ # Handle lists of media objects
135
+ if self.images is not None:
136
+ _dict["images"] = [img.to_dict() for img in self.images]
137
+ if self.videos is not None:
138
+ _dict["videos"] = [vid.to_dict() for vid in self.videos]
139
+ if self.audios is not None:
140
+ _dict["audios"] = [aud.to_dict() for aud in self.audios]
141
+ if self.files is not None:
142
+ _dict["files"] = [f.to_dict() for f in self.files]
143
+
144
+ # Handle tool executions
145
+ if self.tool_executions is not None:
146
+ _dict["tool_executions"] = [tool_execution.to_dict() for tool_execution in self.tool_executions]
147
+
148
+ # Handle response usage which might be a Pydantic BaseModel
149
+ response_usage = _dict.pop("response_usage", None)
150
+ if response_usage is not None:
151
+ try:
152
+ from pydantic import BaseModel
153
+
154
+ if isinstance(response_usage, BaseModel):
155
+ _dict["response_usage"] = response_usage.model_dump()
156
+ else:
157
+ _dict["response_usage"] = response_usage
158
+ except ImportError:
159
+ _dict["response_usage"] = response_usage
160
+
161
+ return _dict
162
+
163
+ @classmethod
164
+ def from_dict(cls, data: Dict[str, Any]) -> "ModelResponse":
165
+ """Reconstruct ModelResponse from cached dictionary."""
166
+ # Reconstruct media objects
167
+ if data.get("audio"):
168
+ data["audio"] = Audio(**data["audio"])
169
+
170
+ if data.get("images"):
171
+ data["images"] = [Image(**img) for img in data["images"]]
172
+ if data.get("videos"):
173
+ data["videos"] = [Video(**vid) for vid in data["videos"]]
174
+ if data.get("audios"):
175
+ data["audios"] = [Audio(**aud) for aud in data["audios"]]
176
+ if data.get("files"):
177
+ data["files"] = [File(**f) for f in data["files"]]
178
+
179
+ # Reconstruct tool executions
180
+ if data.get("tool_executions"):
181
+ data["tool_executions"] = [ToolExecution.from_dict(te) for te in data["tool_executions"]]
182
+
183
+ # Reconstruct citations
184
+ if data.get("citations") and isinstance(data["citations"], dict):
185
+ data["citations"] = Citations(**data["citations"])
186
+
187
+ # Reconstruct response usage (Metrics)
188
+ if data.get("response_usage") and isinstance(data["response_usage"], dict):
189
+ from agno.models.metrics import Metrics
190
+
191
+ data["response_usage"] = Metrics(**data["response_usage"])
192
+
193
+ return cls(**data)
194
+
126
195
 
127
196
  class FileType(str, Enum):
128
197
  MP4 = "mp4"
agno/os/router.py CHANGED
@@ -1082,7 +1082,8 @@ def get_base_router(
1082
1082
 
1083
1083
  agents = []
1084
1084
  for agent in os.agents:
1085
- agents.append(AgentResponse.from_agent(agent=agent))
1085
+ agent_response = await AgentResponse.from_agent(agent=agent)
1086
+ agents.append(agent_response)
1086
1087
 
1087
1088
  return agents
1088
1089
 
@@ -1129,7 +1130,7 @@ def get_base_router(
1129
1130
  if agent is None:
1130
1131
  raise HTTPException(status_code=404, detail="Agent not found")
1131
1132
 
1132
- return AgentResponse.from_agent(agent)
1133
+ return await AgentResponse.from_agent(agent)
1133
1134
 
1134
1135
  # -- Team routes ---
1135
1136
 
@@ -1414,7 +1415,8 @@ def get_base_router(
1414
1415
 
1415
1416
  teams = []
1416
1417
  for team in os.teams:
1417
- teams.append(TeamResponse.from_team(team=team))
1418
+ team_response = await TeamResponse.from_team(team=team)
1419
+ teams.append(team_response)
1418
1420
 
1419
1421
  return teams
1420
1422
 
@@ -1507,7 +1509,7 @@ def get_base_router(
1507
1509
  if team is None:
1508
1510
  raise HTTPException(status_code=404, detail="Team not found")
1509
1511
 
1510
- return TeamResponse.from_team(team)
1512
+ return await TeamResponse.from_team(team)
1511
1513
 
1512
1514
  # -- Workflow routes ---
1513
1515
 
@@ -1580,7 +1582,7 @@ def get_base_router(
1580
1582
  if workflow is None:
1581
1583
  raise HTTPException(status_code=404, detail="Workflow not found")
1582
1584
 
1583
- return WorkflowResponse.from_workflow(workflow)
1585
+ return await WorkflowResponse.from_workflow(workflow)
1584
1586
 
1585
1587
  @router.post(
1586
1588
  "/workflows/{workflow_id}/runs",
@@ -251,8 +251,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]])
251
251
  deserialize=False,
252
252
  )
253
253
 
254
+ memories = [UserMemorySchema.from_dict(user_memory) for user_memory in user_memories] # type: ignore
254
255
  return PaginatedResponse(
255
- data=[UserMemorySchema.from_dict(user_memory) for user_memory in user_memories], # type: ignore
256
+ data=[memory for memory in memories if memory is not None],
256
257
  meta=PaginationInfo(
257
258
  page=page,
258
259
  limit=limit,
@@ -11,7 +11,7 @@ class DeleteMemoriesRequest(BaseModel):
11
11
 
12
12
  class UserMemorySchema(BaseModel):
13
13
  memory_id: str = Field(..., description="Unique identifier for the memory")
14
- memory: str = Field(..., description="Memory content text", min_length=1)
14
+ memory: str = Field(..., description="Memory content text")
15
15
  topics: Optional[List[str]] = Field(None, description="Topics or tags associated with the memory")
16
16
 
17
17
  agent_id: Optional[str] = Field(None, description="Agent ID associated with this memory")
@@ -21,7 +21,10 @@ class UserMemorySchema(BaseModel):
21
21
  updated_at: Optional[datetime] = Field(None, description="Timestamp when memory was last updated")
22
22
 
23
23
  @classmethod
24
- def from_dict(cls, memory_dict: Dict[str, Any]) -> "UserMemorySchema":
24
+ def from_dict(cls, memory_dict: Dict[str, Any]) -> Optional["UserMemorySchema"]:
25
+ if memory_dict["memory"] == "":
26
+ return None
27
+
25
28
  return cls(
26
29
  memory_id=memory_dict["memory_id"],
27
30
  user_id=str(memory_dict["user_id"]),
agno/os/schema.py CHANGED
@@ -183,7 +183,7 @@ class AgentResponse(BaseModel):
183
183
  metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata")
184
184
 
185
185
  @classmethod
186
- def from_agent(cls, agent: Agent) -> "AgentResponse":
186
+ async def from_agent(cls, agent: Agent) -> "AgentResponse":
187
187
  def filter_meaningful_config(d: Dict[str, Any], defaults: Dict[str, Any]) -> Optional[Dict[str, Any]]:
188
188
  """Filter out fields that match their default values, keeping only meaningful user configurations"""
189
189
  filtered = {}
@@ -243,10 +243,10 @@ class AgentResponse(BaseModel):
243
243
  "stream_intermediate_steps": False,
244
244
  }
245
245
 
246
- agent_tools = agent.get_tools(
246
+ agent_tools = await agent.aget_tools(
247
247
  session=AgentSession(session_id=str(uuid4()), session_data={}),
248
248
  run_response=RunOutput(run_id=str(uuid4())),
249
- async_mode=True,
249
+ check_mcp_tools=False,
250
250
  )
251
251
  formatted_tools = format_tools(agent_tools) if agent_tools else None
252
252
 
@@ -415,7 +415,7 @@ class TeamResponse(BaseModel):
415
415
  metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata")
416
416
 
417
417
  @classmethod
418
- def from_team(cls, team: Team) -> "TeamResponse":
418
+ async def from_team(cls, team: Team) -> "TeamResponse":
419
419
  def filter_meaningful_config(d: Dict[str, Any], defaults: Dict[str, Any]) -> Optional[Dict[str, Any]]:
420
420
  """Filter out fields that match their default values, keeping only meaningful user configurations"""
421
421
  filtered = {}
@@ -466,15 +466,17 @@ class TeamResponse(BaseModel):
466
466
  "stream_member_events": False,
467
467
  }
468
468
 
469
- team.determine_tools_for_model(
469
+ _tools = team._determine_tools_for_model(
470
470
  model=team.model, # type: ignore
471
471
  session=TeamSession(session_id=str(uuid4()), session_data={}),
472
472
  run_response=TeamRunOutput(run_id=str(uuid4())),
473
473
  async_mode=True,
474
474
  session_state={},
475
475
  team_run_context={},
476
+ check_mcp_tools=False,
476
477
  )
477
- team_tools = list(team._functions_for_model.values()) if team._functions_for_model else []
478
+ print(team.tools, _tools)
479
+ team_tools = _tools
478
480
  formatted_tools = format_team_tools(team_tools) if team_tools else None
479
481
 
480
482
  model_name = team.model.name or team.model.__class__.__name__ if team.model else None
@@ -595,6 +597,15 @@ class TeamResponse(BaseModel):
595
597
  if team.model and team.model.provider is not None:
596
598
  _team_model_data["provider"] = team.model.provider
597
599
 
600
+ members: List[Union[AgentResponse, TeamResponse]] = []
601
+ for member in team.members:
602
+ if isinstance(member, Agent):
603
+ agent_response = await AgentResponse.from_agent(member)
604
+ members.append(agent_response)
605
+ if isinstance(member, Team):
606
+ team_response = await TeamResponse.from_team(member)
607
+ members.append(team_response)
608
+
598
609
  return TeamResponse(
599
610
  id=team.id,
600
611
  name=team.name,
@@ -609,14 +620,7 @@ class TeamResponse(BaseModel):
609
620
  system_message=filter_meaningful_config(system_message_info, team_defaults),
610
621
  response_settings=filter_meaningful_config(response_settings_info, team_defaults),
611
622
  streaming=filter_meaningful_config(streaming_info, team_defaults),
612
- members=[ # type: ignore
613
- AgentResponse.from_agent(member)
614
- if isinstance(member, Agent)
615
- else TeamResponse.from_team(member)
616
- if isinstance(member, Team)
617
- else None
618
- for member in team.members
619
- ],
623
+ members=members if members else None,
620
624
  metadata=team.metadata,
621
625
  )
622
626
 
@@ -633,7 +637,7 @@ class WorkflowResponse(BaseModel):
633
637
  metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata")
634
638
 
635
639
  @classmethod
636
- def _resolve_agents_and_teams_recursively(cls, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
640
+ async def _resolve_agents_and_teams_recursively(cls, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
637
641
  """Parse Agents and Teams into AgentResponse and TeamResponse objects.
638
642
 
639
643
  If the given steps have nested steps, recursively work on those."""
@@ -651,13 +655,15 @@ class WorkflowResponse(BaseModel):
651
655
  for idx, step in enumerate(steps):
652
656
  if step.get("agent"):
653
657
  # Convert to dict and exclude fields that are None
654
- step["agent"] = AgentResponse.from_agent(step["agent"]).model_dump(exclude_none=True)
658
+ agent_response = await AgentResponse.from_agent(step.get("agent")) # type: ignore
659
+ step["agent"] = agent_response.model_dump(exclude_none=True)
655
660
 
656
661
  if step.get("team"):
657
- step["team"] = TeamResponse.from_team(step["team"]).model_dump(exclude_none=True)
662
+ team_response = await TeamResponse.from_team(step.get("team")) # type: ignore
663
+ step["team"] = team_response.model_dump(exclude_none=True)
658
664
 
659
665
  if step.get("steps"):
660
- step["steps"] = cls._resolve_agents_and_teams_recursively(step["steps"])
666
+ step["steps"] = await cls._resolve_agents_and_teams_recursively(step["steps"])
661
667
 
662
668
  # Prune None values in the entire step
663
669
  steps[idx] = _prune_none(step)
@@ -665,12 +671,12 @@ class WorkflowResponse(BaseModel):
665
671
  return steps
666
672
 
667
673
  @classmethod
668
- def from_workflow(cls, workflow: Workflow) -> "WorkflowResponse":
674
+ async def from_workflow(cls, workflow: Workflow) -> "WorkflowResponse":
669
675
  workflow_dict = workflow.to_dict()
670
676
  steps = workflow_dict.get("steps")
671
677
 
672
678
  if steps:
673
- steps = cls._resolve_agents_and_teams_recursively(steps)
679
+ steps = await cls._resolve_agents_and_teams_recursively(steps)
674
680
 
675
681
  return cls(
676
682
  id=workflow.id,
agno/os/utils.py CHANGED
@@ -233,8 +233,15 @@ def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Call
233
233
  return formatted_tools
234
234
 
235
235
 
236
- def format_team_tools(team_tools: List[Function]):
237
- return [tool.to_dict() for tool in team_tools]
236
+ def format_team_tools(team_tools: List[Union[Function, dict]]):
237
+ formatted_tools: List[Dict] = []
238
+ if team_tools is not None:
239
+ for tool in team_tools:
240
+ if isinstance(tool, dict):
241
+ formatted_tools.append(tool)
242
+ elif isinstance(tool, Function):
243
+ formatted_tools.append(tool.to_dict())
244
+ return formatted_tools
238
245
 
239
246
 
240
247
  def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Optional[Agent]:
agno/run/agent.py CHANGED
@@ -12,6 +12,13 @@ from agno.models.response import ToolExecution
12
12
  from agno.reasoning.step import ReasoningStep
13
13
  from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
14
14
  from agno.utils.log import logger
15
+ from agno.utils.media import (
16
+ reconstruct_audio_list,
17
+ reconstruct_files,
18
+ reconstruct_images,
19
+ reconstruct_response_audio,
20
+ reconstruct_videos,
21
+ )
15
22
 
16
23
  if TYPE_CHECKING:
17
24
  from agno.session.summary import SessionSummary
@@ -86,21 +93,10 @@ class RunInput:
86
93
  @classmethod
87
94
  def from_dict(cls, data: Dict[str, Any]) -> "RunInput":
88
95
  """Create RunInput from dictionary"""
89
- images = None
90
- if data.get("images"):
91
- images = [Image.model_validate(img_data) for img_data in data["images"]]
92
-
93
- videos = None
94
- if data.get("videos"):
95
- videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
96
-
97
- audios = None
98
- if data.get("audios"):
99
- audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
100
-
101
- files = None
102
- if data.get("files"):
103
- files = [File.model_validate(file_data) for file_data in data["files"]]
96
+ images = reconstruct_images(data.get("images"))
97
+ videos = reconstruct_videos(data.get("videos"))
98
+ audios = reconstruct_audio_list(data.get("audios"))
99
+ files = reconstruct_files(data.get("files"))
104
100
 
105
101
  return cls(
106
102
  input_content=data.get("input_content", ""), images=images, videos=videos, audios=audios, files=files
@@ -385,6 +381,11 @@ class OutputModelResponseCompletedEvent(BaseAgentRunEvent):
385
381
  class CustomEvent(BaseAgentRunEvent):
386
382
  event: str = RunEvent.custom_event.value
387
383
 
384
+ def __init__(self, **kwargs):
385
+ # Store arbitrary attributes directly on the instance
386
+ for key, value in kwargs.items():
387
+ setattr(self, key, value)
388
+
388
389
 
389
390
  RunOutputEvent = Union[
390
391
  RunStartedEvent,
@@ -674,20 +675,11 @@ class RunOutput:
674
675
  tools = data.pop("tools", [])
675
676
  tools = [ToolExecution.from_dict(tool) for tool in tools] if tools else None
676
677
 
677
- images = data.pop("images", [])
678
- images = [Image.model_validate(image) for image in images] if images else None
679
-
680
- videos = data.pop("videos", [])
681
- videos = [Video.model_validate(video) for video in videos] if videos else None
682
-
683
- audio = data.pop("audio", [])
684
- audio = [Audio.model_validate(audio) for audio in audio] if audio else None
685
-
686
- files = data.pop("files", [])
687
- files = [File.model_validate(file) for file in files] if files else None
688
-
689
- response_audio = data.pop("response_audio", None)
690
- response_audio = Audio.model_validate(response_audio) if response_audio else None
678
+ images = reconstruct_images(data.pop("images", []))
679
+ videos = reconstruct_videos(data.pop("videos", []))
680
+ audio = reconstruct_audio_list(data.pop("audio", []))
681
+ files = reconstruct_files(data.pop("files", []))
682
+ response_audio = reconstruct_response_audio(data.pop("response_audio", None))
691
683
 
692
684
  input_data = data.pop("input", None)
693
685
  input_obj = None
@@ -715,6 +707,12 @@ class RunOutput:
715
707
  if references is not None:
716
708
  references = [MessageReferences.model_validate(reference) for reference in references]
717
709
 
710
+ # Filter data to only include fields that are actually defined in the RunOutput dataclass
711
+ from dataclasses import fields
712
+
713
+ supported_fields = {f.name for f in fields(cls)}
714
+ filtered_data = {k: v for k, v in data.items() if k in supported_fields}
715
+
718
716
  return cls(
719
717
  messages=messages,
720
718
  metrics=metrics,
@@ -731,7 +729,7 @@ class RunOutput:
731
729
  reasoning_steps=reasoning_steps,
732
730
  reasoning_messages=reasoning_messages,
733
731
  references=references,
734
- **data,
732
+ **filtered_data,
735
733
  )
736
734
 
737
735
  def get_content_as_string(self, **kwargs) -> str:
agno/run/base.py CHANGED
@@ -35,6 +35,7 @@ class BaseRunOutputEvent:
35
35
  "reasoning_steps",
36
36
  "references",
37
37
  "additional_input",
38
+ "session_summary",
38
39
  "metrics",
39
40
  ]
40
41
  }
@@ -113,6 +114,9 @@ class BaseRunOutputEvent:
113
114
  if hasattr(self, "metrics") and self.metrics is not None:
114
115
  _dict["metrics"] = self.metrics.to_dict()
115
116
 
117
+ if hasattr(self, "session_summary") and self.session_summary is not None:
118
+ _dict["session_summary"] = self.session_summary.to_dict()
119
+
116
120
  return _dict
117
121
 
118
122
  def to_json(self, separators=(", ", ": "), indent: Optional[int] = 2) -> str:
@@ -173,7 +177,19 @@ class BaseRunOutputEvent:
173
177
  if metrics:
174
178
  data["metrics"] = Metrics(**metrics)
175
179
 
176
- return cls(**data)
180
+ session_summary = data.pop("session_summary", None)
181
+ if session_summary:
182
+ from agno.session.summary import SessionSummary
183
+
184
+ data["session_summary"] = SessionSummary.from_dict(session_summary)
185
+
186
+ # Filter data to only include fields that are actually defined in the target class
187
+ from dataclasses import fields
188
+
189
+ supported_fields = {f.name for f in fields(cls)}
190
+ filtered_data = {k: v for k, v in data.items() if k in supported_fields}
191
+
192
+ return cls(**filtered_data)
177
193
 
178
194
  @property
179
195
  def is_paused(self):
agno/run/team.py CHANGED
@@ -13,6 +13,13 @@ from agno.reasoning.step import ReasoningStep
13
13
  from agno.run.agent import RunEvent, RunOutput, RunOutputEvent, run_output_event_from_dict
14
14
  from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
15
15
  from agno.utils.log import log_error
16
+ from agno.utils.media import (
17
+ reconstruct_audio_list,
18
+ reconstruct_files,
19
+ reconstruct_images,
20
+ reconstruct_response_audio,
21
+ reconstruct_videos,
22
+ )
16
23
 
17
24
 
18
25
  @dataclass
@@ -82,21 +89,10 @@ class TeamRunInput:
82
89
  @classmethod
83
90
  def from_dict(cls, data: Dict[str, Any]) -> "TeamRunInput":
84
91
  """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"]]
92
+ images = reconstruct_images(data.get("images"))
93
+ videos = reconstruct_videos(data.get("videos"))
94
+ audios = reconstruct_audio_list(data.get("audios"))
95
+ files = reconstruct_files(data.get("files"))
100
96
 
101
97
  return cls(
102
98
  input_content=data.get("input_content", ""), images=images, videos=videos, audios=audios, files=files
@@ -368,6 +364,11 @@ class OutputModelResponseCompletedEvent(BaseTeamRunEvent):
368
364
  class CustomEvent(BaseTeamRunEvent):
369
365
  event: str = TeamRunEvent.custom_event.value
370
366
 
367
+ def __init__(self, **kwargs):
368
+ # Store arbitrary attributes directly on the instance
369
+ for key, value in kwargs.items():
370
+ setattr(self, key, value)
371
+
371
372
 
372
373
  TeamRunOutputEvent = Union[
373
374
  RunStartedEvent,
@@ -641,23 +642,15 @@ class TeamRunOutput:
641
642
  if references is not None:
642
643
  references = [MessageReferences.model_validate(reference) for reference in references]
643
644
 
644
- images = data.pop("images", [])
645
- images = [Image.model_validate(image) for image in images] if images else None
646
-
647
- videos = data.pop("videos", [])
648
- videos = [Video.model_validate(video) for video in videos] if videos else None
649
-
650
- audio = data.pop("audio", [])
651
- audio = [Audio.model_validate(audio) for audio in audio] if audio else None
652
-
653
- files = data.pop("files", [])
654
- files = [File.model_validate(file) for file in files] if files else None
645
+ images = reconstruct_images(data.pop("images", []))
646
+ videos = reconstruct_videos(data.pop("videos", []))
647
+ audio = reconstruct_audio_list(data.pop("audio", []))
648
+ files = reconstruct_files(data.pop("files", []))
655
649
 
656
650
  tools = data.pop("tools", [])
657
651
  tools = [ToolExecution.from_dict(tool) for tool in tools] if tools else None
658
652
 
659
- response_audio = data.pop("response_audio", None)
660
- response_audio = Audio.model_validate(response_audio) if response_audio else None
653
+ response_audio = reconstruct_response_audio(data.pop("response_audio", None))
661
654
 
662
655
  input_data = data.pop("input", None)
663
656
  input_obj = None
@@ -671,6 +664,12 @@ class TeamRunOutput:
671
664
  citations = data.pop("citations", None)
672
665
  citations = Citations.model_validate(citations) if citations else None
673
666
 
667
+ # Filter data to only include fields that are actually defined in the TeamRunOutput dataclass
668
+ from dataclasses import fields
669
+
670
+ supported_fields = {f.name for f in fields(cls)}
671
+ filtered_data = {k: v for k, v in data.items() if k in supported_fields}
672
+
674
673
  return cls(
675
674
  messages=messages,
676
675
  metrics=metrics,
@@ -688,7 +687,7 @@ class TeamRunOutput:
688
687
  citations=citations,
689
688
  tools=tools,
690
689
  events=events,
691
- **data,
690
+ **filtered_data,
692
691
  )
693
692
 
694
693
  def get_content_as_string(self, **kwargs) -> str:
agno/run/workflow.py CHANGED
@@ -6,9 +6,15 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
6
6
  from pydantic import BaseModel
7
7
 
8
8
  from agno.media import Audio, Image, Video
9
- from agno.run.agent import RunOutput
9
+ from agno.run.agent import RunEvent, RunOutput, run_output_event_from_dict
10
10
  from agno.run.base import BaseRunOutputEvent, RunStatus
11
- from agno.run.team import TeamRunOutput
11
+ from agno.run.team import TeamRunEvent, TeamRunOutput, team_run_output_event_from_dict
12
+ from agno.utils.media import (
13
+ reconstruct_audio_list,
14
+ reconstruct_images,
15
+ reconstruct_response_audio,
16
+ reconstruct_videos,
17
+ )
12
18
 
13
19
  if TYPE_CHECKING:
14
20
  from agno.workflow.types import StepOutput, WorkflowMetrics
@@ -388,6 +394,11 @@ class CustomEvent(BaseWorkflowRunOutputEvent):
388
394
 
389
395
  event: str = WorkflowRunEvent.custom_event.value
390
396
 
397
+ def __init__(self, **kwargs):
398
+ # Store arbitrary attributes directly on the instance
399
+ for key, value in kwargs.items():
400
+ setattr(self, key, value)
401
+
391
402
 
392
403
  # Union type for all workflow run response events
393
404
  WorkflowRunOutputEvent = Union[
@@ -442,10 +453,15 @@ WORKFLOW_RUN_EVENT_TYPE_REGISTRY = {
442
453
 
443
454
  def workflow_run_output_event_from_dict(data: dict) -> BaseWorkflowRunOutputEvent:
444
455
  event_type = data.get("event", "")
445
- cls = WORKFLOW_RUN_EVENT_TYPE_REGISTRY.get(event_type)
446
- if not cls:
456
+ if event_type in {e.value for e in RunEvent}:
457
+ return run_output_event_from_dict(data) # type: ignore
458
+ elif event_type in {e.value for e in TeamRunEvent}:
459
+ return team_run_output_event_from_dict(data) # type: ignore
460
+ else:
461
+ event_class = WORKFLOW_RUN_EVENT_TYPE_REGISTRY.get(event_type)
462
+ if not event_class:
447
463
  raise ValueError(f"Unknown workflow event type: {event_type}")
448
- return cls.from_dict(data) # type: ignore
464
+ return event_class.from_dict(data) # type: ignore
449
465
 
450
466
 
451
467
  @dataclass
@@ -590,17 +606,10 @@ class WorkflowRunOutput:
590
606
 
591
607
  metadata = data.pop("metadata", None)
592
608
 
593
- images = data.pop("images", [])
594
- images = [Image.model_validate(image) for image in images] if images else None
595
-
596
- videos = data.pop("videos", [])
597
- videos = [Video.model_validate(video) for video in videos] if videos else None
598
-
599
- audio = data.pop("audio", [])
600
- audio = [Audio.model_validate(audio) for audio in audio] if audio else None
601
-
602
- response_audio = data.pop("response_audio", None)
603
- response_audio = Audio.model_validate(response_audio) if response_audio else None
609
+ images = reconstruct_images(data.pop("images", []))
610
+ videos = reconstruct_videos(data.pop("videos", []))
611
+ audio = reconstruct_audio_list(data.pop("audio", []))
612
+ response_audio = reconstruct_response_audio(data.pop("response_audio", None))
604
613
 
605
614
  events_data = data.pop("events", [])
606
615
  final_events = []
@@ -623,6 +632,12 @@ class WorkflowRunOutput:
623
632
 
624
633
  input_data = data.pop("input", None)
625
634
 
635
+ # Filter data to only include fields that are actually defined in the WorkflowRunOutput dataclass
636
+ from dataclasses import fields
637
+
638
+ supported_fields = {f.name for f in fields(cls)}
639
+ filtered_data = {k: v for k, v in data.items() if k in supported_fields}
640
+
626
641
  return cls(
627
642
  step_results=parsed_step_results,
628
643
  metadata=metadata,
@@ -634,7 +649,7 @@ class WorkflowRunOutput:
634
649
  metrics=workflow_metrics,
635
650
  step_executor_runs=step_executor_runs,
636
651
  input=input_data,
637
- **data,
652
+ **filtered_data,
638
653
  )
639
654
 
640
655
  def get_content_as_string(self, **kwargs) -> str:
agno/session/agent.py CHANGED
@@ -140,6 +140,9 @@ class AgentSession:
140
140
  if team_id:
141
141
  session_runs = [run for run in session_runs if hasattr(run, "team_id") and run.team_id == team_id] # type: ignore
142
142
 
143
+ # Skip any messages that might be part of members of teams (for session re-use)
144
+ session_runs = [run for run in session_runs if run.parent_run_id is None] # type: ignore
145
+
143
146
  # Filter by status
144
147
  session_runs = [run for run in session_runs if hasattr(run, "status") and run.status not in skip_status] # type: ignore
145
148
 
agno/session/summary.py CHANGED
@@ -66,6 +66,9 @@ class SessionSummaryManager:
66
66
  # Prompt used for session summary generation
67
67
  session_summary_prompt: Optional[str] = None
68
68
 
69
+ # User message prompt for requesting the summary
70
+ summary_request_message: str = "Provide the summary of the conversation."
71
+
69
72
  # Whether session summaries were created in the last run
70
73
  summaries_updated: bool = False
71
74
 
@@ -152,7 +155,7 @@ class SessionSummaryManager:
152
155
 
153
156
  return [
154
157
  system_message,
155
- Message(role="user", content="Provide the summary of the conversation."),
158
+ Message(role="user", content=self.summary_request_message),
156
159
  ]
157
160
 
158
161
  def _process_summary_response(self, summary_response, session_summary_model: "Model") -> Optional[SessionSummary]: # type: ignore
agno/session/team.py CHANGED
@@ -150,7 +150,7 @@ class TeamSession:
150
150
  session_runs = [run for run in session_runs if hasattr(run, "team_id") and run.team_id == team_id] # type: ignore
151
151
 
152
152
  if not member_runs:
153
- # Filter for the main team runs
153
+ # Filter for the top-level runs (main team runs or agent runs when sharing session)
154
154
  session_runs = [run for run in session_runs if run.parent_run_id is None] # type: ignore
155
155
  # Filter by status
156
156
  session_runs = [run for run in session_runs if hasattr(run, "status") and run.status not in skip_status] # type: ignore