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.
- agno/agent/agent.py +751 -575
- agno/culture/manager.py +22 -24
- agno/db/async_postgres/__init__.py +1 -1
- agno/db/dynamo/dynamo.py +0 -2
- agno/db/firestore/firestore.py +0 -2
- agno/db/gcs_json/gcs_json_db.py +0 -4
- agno/db/gcs_json/utils.py +0 -24
- agno/db/in_memory/in_memory_db.py +0 -3
- agno/db/json/json_db.py +4 -10
- agno/db/json/utils.py +0 -24
- agno/db/mongo/mongo.py +0 -2
- agno/db/mysql/mysql.py +0 -3
- agno/db/postgres/__init__.py +1 -1
- agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
- agno/db/postgres/postgres.py +7 -10
- agno/db/postgres/utils.py +106 -2
- agno/db/redis/redis.py +0 -2
- agno/db/singlestore/singlestore.py +0 -3
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2269 -0
- agno/db/sqlite/sqlite.py +0 -2
- agno/db/sqlite/utils.py +96 -0
- agno/db/surrealdb/surrealdb.py +0 -6
- agno/knowledge/knowledge.py +14 -3
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +30 -0
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/types.py +1 -0
- agno/memory/manager.py +28 -25
- agno/models/anthropic/claude.py +63 -6
- agno/models/base.py +255 -36
- agno/models/response.py +69 -0
- agno/os/router.py +7 -5
- agno/os/routers/memory/memory.py +2 -1
- agno/os/routers/memory/schemas.py +5 -2
- agno/os/schema.py +26 -20
- agno/os/utils.py +9 -2
- agno/run/agent.py +28 -30
- agno/run/base.py +17 -1
- agno/run/team.py +28 -29
- agno/run/workflow.py +32 -17
- agno/session/agent.py +3 -0
- agno/session/summary.py +4 -1
- agno/session/team.py +1 -1
- agno/team/team.py +620 -374
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/function.py +40 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +324 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/slack.py +18 -3
- agno/tools/tavily.py +146 -0
- agno/utils/agent.py +366 -1
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +166 -1
- agno/utils/message.py +60 -0
- agno/utils/print_response/workflow.py +17 -1
- agno/utils/team.py +89 -1
- agno/workflow/step.py +0 -1
- agno/workflow/types.py +10 -15
- agno/workflow/workflow.py +86 -1
- {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/METADATA +31 -25
- {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/RECORD +68 -64
- agno/db/async_postgres/schemas.py +0 -139
- agno/db/async_postgres/utils.py +0 -347
- agno/tools/mcp.py +0 -679
- {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/WHEEL +0 -0
- {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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",
|
agno/os/routers/memory/memory.py
CHANGED
|
@@ -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=[
|
|
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"
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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 =
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
446
|
-
|
|
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
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
**
|
|
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=
|
|
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
|