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/utils/message.py
CHANGED
|
@@ -1,8 +1,68 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
1
2
|
from typing import Dict, List, Union
|
|
2
3
|
|
|
3
4
|
from pydantic import BaseModel
|
|
4
5
|
|
|
5
6
|
from agno.models.message import Message
|
|
7
|
+
from agno.utils.log import log_debug
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def filter_tool_calls(messages: List[Message], max_tool_calls: int) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Filter messages (in-place) to keep only the most recent N tool calls.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
messages: List of messages to filter (modified in-place)
|
|
16
|
+
max_tool_calls: Number of recent tool calls to keep
|
|
17
|
+
"""
|
|
18
|
+
# Count total tool calls
|
|
19
|
+
tool_call_count = sum(1 for m in messages if m.role == "tool")
|
|
20
|
+
|
|
21
|
+
# No filtering needed
|
|
22
|
+
if tool_call_count <= max_tool_calls:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# Collect tool_call_ids to keep (most recent N)
|
|
26
|
+
tool_call_ids_list: List[str] = []
|
|
27
|
+
for msg in reversed(messages):
|
|
28
|
+
if msg.role == "tool" and len(tool_call_ids_list) < max_tool_calls:
|
|
29
|
+
if msg.tool_call_id:
|
|
30
|
+
tool_call_ids_list.append(msg.tool_call_id)
|
|
31
|
+
|
|
32
|
+
tool_call_ids_to_keep: set[str] = set(tool_call_ids_list)
|
|
33
|
+
|
|
34
|
+
# Filter messages in-place
|
|
35
|
+
filtered_messages = []
|
|
36
|
+
for msg in messages:
|
|
37
|
+
if msg.role == "tool":
|
|
38
|
+
# Keep only tool results in our window
|
|
39
|
+
if msg.tool_call_id in tool_call_ids_to_keep:
|
|
40
|
+
filtered_messages.append(msg)
|
|
41
|
+
elif msg.role == "assistant" and msg.tool_calls:
|
|
42
|
+
# Filter tool_calls within the assistant message
|
|
43
|
+
# Use deepcopy to ensure complete isolation of the filtered message
|
|
44
|
+
filtered_msg = deepcopy(msg)
|
|
45
|
+
# Filter tool_calls
|
|
46
|
+
if filtered_msg.tool_calls is not None:
|
|
47
|
+
filtered_msg.tool_calls = [
|
|
48
|
+
tc for tc in filtered_msg.tool_calls if tc.get("id") in tool_call_ids_to_keep
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
if filtered_msg.tool_calls:
|
|
52
|
+
# Has tool_calls remaining, keep it
|
|
53
|
+
filtered_messages.append(filtered_msg)
|
|
54
|
+
# skip empty messages
|
|
55
|
+
elif filtered_msg.content:
|
|
56
|
+
filtered_msg.tool_calls = None
|
|
57
|
+
filtered_messages.append(filtered_msg)
|
|
58
|
+
else:
|
|
59
|
+
filtered_messages.append(msg)
|
|
60
|
+
|
|
61
|
+
messages[:] = filtered_messages
|
|
62
|
+
|
|
63
|
+
# Log filtering information
|
|
64
|
+
num_filtered = tool_call_count - len(tool_call_ids_to_keep)
|
|
65
|
+
log_debug(f"Filtered {num_filtered} tool calls, kept {len(tool_call_ids_to_keep)}")
|
|
6
66
|
|
|
7
67
|
|
|
8
68
|
def get_text_from_message(message: Union[List, Dict, str, Message, BaseModel]) -> str:
|
|
@@ -7,7 +7,7 @@ from rich.markdown import Markdown
|
|
|
7
7
|
from rich.status import Status
|
|
8
8
|
from rich.text import Text
|
|
9
9
|
|
|
10
|
-
from agno.media import Audio, Image, Video
|
|
10
|
+
from agno.media import Audio, File, Image, Video
|
|
11
11
|
from agno.models.message import Message
|
|
12
12
|
from agno.run.workflow import (
|
|
13
13
|
ConditionExecutionCompletedEvent,
|
|
@@ -48,6 +48,7 @@ def print_response(
|
|
|
48
48
|
audio: Optional[List[Audio]] = None,
|
|
49
49
|
images: Optional[List[Image]] = None,
|
|
50
50
|
videos: Optional[List[Video]] = None,
|
|
51
|
+
files: Optional[List[File]] = None,
|
|
51
52
|
markdown: bool = True,
|
|
52
53
|
show_time: bool = True,
|
|
53
54
|
show_step_details: bool = True,
|
|
@@ -76,6 +77,8 @@ def print_response(
|
|
|
76
77
|
media_info.append(f"Images: {len(images)}")
|
|
77
78
|
if videos:
|
|
78
79
|
media_info.append(f"Videos: {len(videos)}")
|
|
80
|
+
if files:
|
|
81
|
+
media_info.append(f"Files: {len(files)}")
|
|
79
82
|
|
|
80
83
|
workflow_info = f"""**Workflow:** {workflow.name}"""
|
|
81
84
|
if workflow.description:
|
|
@@ -126,6 +129,7 @@ def print_response(
|
|
|
126
129
|
audio=audio,
|
|
127
130
|
images=images,
|
|
128
131
|
videos=videos,
|
|
132
|
+
files=files,
|
|
129
133
|
**kwargs,
|
|
130
134
|
) # type: ignore
|
|
131
135
|
|
|
@@ -186,6 +190,7 @@ def print_response_stream(
|
|
|
186
190
|
audio: Optional[List[Audio]] = None,
|
|
187
191
|
images: Optional[List[Image]] = None,
|
|
188
192
|
videos: Optional[List[Video]] = None,
|
|
193
|
+
files: Optional[List[File]] = None,
|
|
189
194
|
stream_events: bool = False,
|
|
190
195
|
stream_intermediate_steps: bool = False,
|
|
191
196
|
markdown: bool = True,
|
|
@@ -210,6 +215,8 @@ def print_response_stream(
|
|
|
210
215
|
media_info.append(f"Images: {len(images)}")
|
|
211
216
|
if videos:
|
|
212
217
|
media_info.append(f"Videos: {len(videos)}")
|
|
218
|
+
if files:
|
|
219
|
+
media_info.append(f"Files: {len(files)}")
|
|
213
220
|
|
|
214
221
|
workflow_info = f"""**Workflow:** {workflow.name}"""
|
|
215
222
|
if workflow.description:
|
|
@@ -314,6 +321,7 @@ def print_response_stream(
|
|
|
314
321
|
audio=audio,
|
|
315
322
|
images=images,
|
|
316
323
|
videos=videos,
|
|
324
|
+
files=files,
|
|
317
325
|
stream=True,
|
|
318
326
|
stream_events=stream_events,
|
|
319
327
|
**kwargs,
|
|
@@ -832,6 +840,7 @@ async def aprint_response(
|
|
|
832
840
|
audio: Optional[List[Audio]] = None,
|
|
833
841
|
images: Optional[List[Image]] = None,
|
|
834
842
|
videos: Optional[List[Video]] = None,
|
|
843
|
+
files: Optional[List[File]] = None,
|
|
835
844
|
markdown: bool = True,
|
|
836
845
|
show_time: bool = True,
|
|
837
846
|
show_step_details: bool = True,
|
|
@@ -860,6 +869,8 @@ async def aprint_response(
|
|
|
860
869
|
media_info.append(f"Images: {len(images)}")
|
|
861
870
|
if videos:
|
|
862
871
|
media_info.append(f"Videos: {len(videos)}")
|
|
872
|
+
if files:
|
|
873
|
+
media_info.append(f"Files: {len(files)}")
|
|
863
874
|
|
|
864
875
|
workflow_info = f"""**Workflow:** {workflow.name}"""
|
|
865
876
|
if workflow.description:
|
|
@@ -910,6 +921,7 @@ async def aprint_response(
|
|
|
910
921
|
audio=audio,
|
|
911
922
|
images=images,
|
|
912
923
|
videos=videos,
|
|
924
|
+
files=files,
|
|
913
925
|
**kwargs,
|
|
914
926
|
) # type: ignore
|
|
915
927
|
|
|
@@ -970,6 +982,7 @@ async def aprint_response_stream(
|
|
|
970
982
|
audio: Optional[List[Audio]] = None,
|
|
971
983
|
images: Optional[List[Image]] = None,
|
|
972
984
|
videos: Optional[List[Video]] = None,
|
|
985
|
+
files: Optional[List[File]] = None,
|
|
973
986
|
stream_events: bool = False,
|
|
974
987
|
stream_intermediate_steps: bool = False,
|
|
975
988
|
markdown: bool = True,
|
|
@@ -994,6 +1007,8 @@ async def aprint_response_stream(
|
|
|
994
1007
|
media_info.append(f"Images: {len(images)}")
|
|
995
1008
|
if videos:
|
|
996
1009
|
media_info.append(f"Videos: {len(videos)}")
|
|
1010
|
+
if files:
|
|
1011
|
+
media_info.append(f"Files: {len(files)}")
|
|
997
1012
|
|
|
998
1013
|
workflow_info = f"""**Workflow:** {workflow.name}"""
|
|
999
1014
|
if workflow.description:
|
|
@@ -1098,6 +1113,7 @@ async def aprint_response_stream(
|
|
|
1098
1113
|
audio=audio,
|
|
1099
1114
|
images=images,
|
|
1100
1115
|
videos=videos,
|
|
1116
|
+
files=files,
|
|
1101
1117
|
stream=True,
|
|
1102
1118
|
stream_events=stream_events,
|
|
1103
1119
|
**kwargs,
|
agno/utils/team.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
2
2
|
|
|
3
3
|
from agno.agent import Agent
|
|
4
|
+
from agno.media import Audio, File, Image, Video
|
|
5
|
+
from agno.run.agent import RunOutput
|
|
6
|
+
from agno.run.team import TeamRunOutput
|
|
7
|
+
from agno.utils.log import log_debug
|
|
4
8
|
from agno.utils.string import is_valid_uuid, url_safe_string
|
|
5
9
|
|
|
6
10
|
if TYPE_CHECKING:
|
|
@@ -49,3 +53,87 @@ def get_member_id(member: Union[Agent, "Team"]) -> str:
|
|
|
49
53
|
else:
|
|
50
54
|
url_safe_member_id = None
|
|
51
55
|
return url_safe_member_id
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def add_interaction_to_team_run_context(
|
|
59
|
+
team_run_context: Dict[str, Any],
|
|
60
|
+
member_name: str,
|
|
61
|
+
task: str,
|
|
62
|
+
run_response: Union[RunOutput, TeamRunOutput],
|
|
63
|
+
) -> None:
|
|
64
|
+
if "member_responses" not in team_run_context:
|
|
65
|
+
team_run_context["member_responses"] = []
|
|
66
|
+
team_run_context["member_responses"].append(
|
|
67
|
+
{
|
|
68
|
+
"member_name": member_name,
|
|
69
|
+
"task": task,
|
|
70
|
+
"run_response": run_response,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
log_debug(f"Updated team run context with member name: {member_name}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_team_member_interactions_str(team_run_context: Dict[str, Any]) -> str:
|
|
77
|
+
if not team_run_context:
|
|
78
|
+
return ""
|
|
79
|
+
team_member_interactions_str = ""
|
|
80
|
+
if "member_responses" in team_run_context:
|
|
81
|
+
team_member_interactions_str += "<member_interaction_context>\nSee below interactions wit other team members.\n"
|
|
82
|
+
|
|
83
|
+
for interaction in team_run_context["member_responses"]:
|
|
84
|
+
response_dict = interaction["run_response"].to_dict()
|
|
85
|
+
response_content = (
|
|
86
|
+
response_dict.get("content")
|
|
87
|
+
or ",".join([tool.get("content", "") for tool in response_dict.get("tools", [])])
|
|
88
|
+
or ""
|
|
89
|
+
)
|
|
90
|
+
team_member_interactions_str += f"Member: {interaction['member_name']}\n"
|
|
91
|
+
team_member_interactions_str += f"Task: {interaction['task']}\n"
|
|
92
|
+
team_member_interactions_str += f"Response: {response_content}\n"
|
|
93
|
+
team_member_interactions_str += "\n"
|
|
94
|
+
team_member_interactions_str += "</member_interaction_context>\n"
|
|
95
|
+
return team_member_interactions_str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_team_run_context_images(team_run_context: Dict[str, Any]) -> List[Image]:
|
|
99
|
+
if not team_run_context:
|
|
100
|
+
return []
|
|
101
|
+
images = []
|
|
102
|
+
if "member_responses" in team_run_context:
|
|
103
|
+
for interaction in team_run_context["member_responses"]:
|
|
104
|
+
if interaction["run_response"].images:
|
|
105
|
+
images.extend(interaction["run_response"].images)
|
|
106
|
+
return images
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_team_run_context_videos(team_run_context: Dict[str, Any]) -> List[Video]:
|
|
110
|
+
if not team_run_context:
|
|
111
|
+
return []
|
|
112
|
+
videos = []
|
|
113
|
+
if "member_responses" in team_run_context:
|
|
114
|
+
for interaction in team_run_context["member_responses"]:
|
|
115
|
+
if interaction["run_response"].videos:
|
|
116
|
+
videos.extend(interaction["run_response"].videos)
|
|
117
|
+
return videos
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_team_run_context_audio(team_run_context: Dict[str, Any]) -> List[Audio]:
|
|
121
|
+
if not team_run_context:
|
|
122
|
+
return []
|
|
123
|
+
audio = []
|
|
124
|
+
if "member_responses" in team_run_context:
|
|
125
|
+
for interaction in team_run_context["member_responses"]:
|
|
126
|
+
if interaction["run_response"].audio:
|
|
127
|
+
audio.extend(interaction["run_response"].audio)
|
|
128
|
+
return audio
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_team_run_context_files(team_run_context: Dict[str, Any]) -> List[File]:
|
|
132
|
+
if not team_run_context:
|
|
133
|
+
return []
|
|
134
|
+
files = []
|
|
135
|
+
if "member_responses" in team_run_context:
|
|
136
|
+
for interaction in team_run_context["member_responses"]:
|
|
137
|
+
if interaction["run_response"].files:
|
|
138
|
+
files.extend(interaction["run_response"].files)
|
|
139
|
+
return files
|
agno/workflow/step.py
CHANGED
|
@@ -382,7 +382,6 @@ class Step:
|
|
|
382
382
|
"""Enrich event with step and workflow context information"""
|
|
383
383
|
if workflow_run_response is None:
|
|
384
384
|
return event
|
|
385
|
-
|
|
386
385
|
if hasattr(event, "workflow_id"):
|
|
387
386
|
event.workflow_id = workflow_run_response.workflow_id
|
|
388
387
|
if hasattr(event, "workflow_run_id"):
|
agno/workflow/types.py
CHANGED
|
@@ -10,6 +10,12 @@ from agno.media import Audio, File, Image, Video
|
|
|
10
10
|
from agno.models.metrics import Metrics
|
|
11
11
|
from agno.session.workflow import WorkflowSession
|
|
12
12
|
from agno.utils.log import log_warning
|
|
13
|
+
from agno.utils.media import (
|
|
14
|
+
reconstruct_audio_list,
|
|
15
|
+
reconstruct_files,
|
|
16
|
+
reconstruct_images,
|
|
17
|
+
reconstruct_videos,
|
|
18
|
+
)
|
|
13
19
|
from agno.utils.serialize import json_serializer
|
|
14
20
|
|
|
15
21
|
|
|
@@ -308,21 +314,10 @@ class StepOutput:
|
|
|
308
314
|
def from_dict(cls, data: Dict[str, Any]) -> "StepOutput":
|
|
309
315
|
"""Create StepOutput from dictionary"""
|
|
310
316
|
# Reconstruct media artifacts
|
|
311
|
-
images = data.get("images")
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
videos = data.get("videos")
|
|
316
|
-
if videos:
|
|
317
|
-
videos = [Video.model_validate(vid) for vid in videos]
|
|
318
|
-
|
|
319
|
-
audio = data.get("audio")
|
|
320
|
-
if audio:
|
|
321
|
-
audio = [Audio.model_validate(aud) for aud in audio]
|
|
322
|
-
|
|
323
|
-
files = data.get("files")
|
|
324
|
-
if files:
|
|
325
|
-
files = [File.model_validate(file) for file in files]
|
|
317
|
+
images = reconstruct_images(data.get("images"))
|
|
318
|
+
videos = reconstruct_videos(data.get("videos"))
|
|
319
|
+
audio = reconstruct_audio_list(data.get("audio"))
|
|
320
|
+
files = reconstruct_files(data.get("files"))
|
|
326
321
|
|
|
327
322
|
metrics_data = data.get("metrics")
|
|
328
323
|
metrics = None
|
agno/workflow/workflow.py
CHANGED
|
@@ -29,7 +29,7 @@ from agno.exceptions import InputCheckError, OutputCheckError, RunCancelledExcep
|
|
|
29
29
|
from agno.media import Audio, File, Image, Video
|
|
30
30
|
from agno.models.message import Message
|
|
31
31
|
from agno.models.metrics import Metrics
|
|
32
|
-
from agno.run.agent import RunEvent
|
|
32
|
+
from agno.run.agent import RunContentEvent, RunEvent
|
|
33
33
|
from agno.run.base import RunStatus
|
|
34
34
|
from agno.run.cancel import (
|
|
35
35
|
cancel_run as cancel_run_global,
|
|
@@ -39,6 +39,7 @@ from agno.run.cancel import (
|
|
|
39
39
|
raise_if_cancelled,
|
|
40
40
|
register_run,
|
|
41
41
|
)
|
|
42
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
42
43
|
from agno.run.team import TeamRunEvent
|
|
43
44
|
from agno.run.workflow import (
|
|
44
45
|
StepOutputEvent,
|
|
@@ -1177,6 +1178,15 @@ class Workflow:
|
|
|
1177
1178
|
logger.error(f"Function signature inspection failed: {e}. Falling back to original calling convention.")
|
|
1178
1179
|
return func(**kwargs)
|
|
1179
1180
|
|
|
1181
|
+
def _accumulate_partial_step_data(
|
|
1182
|
+
self, event: Union[RunContentEvent, TeamRunContentEvent], partial_step_content: str
|
|
1183
|
+
) -> str:
|
|
1184
|
+
"""Accumulate partial step data from streaming events"""
|
|
1185
|
+
if isinstance(event, (RunContentEvent, TeamRunContentEvent)) and event.content:
|
|
1186
|
+
if isinstance(event.content, str):
|
|
1187
|
+
partial_step_content += event.content
|
|
1188
|
+
return partial_step_content
|
|
1189
|
+
|
|
1180
1190
|
def _execute(
|
|
1181
1191
|
self,
|
|
1182
1192
|
session: WorkflowSession,
|
|
@@ -1400,11 +1410,22 @@ class Workflow:
|
|
|
1400
1410
|
|
|
1401
1411
|
early_termination = False
|
|
1402
1412
|
|
|
1413
|
+
# Track partial step data in case of cancellation
|
|
1414
|
+
current_step_name = ""
|
|
1415
|
+
current_step = None
|
|
1416
|
+
partial_step_content = ""
|
|
1417
|
+
|
|
1403
1418
|
for i, step in enumerate(self.steps): # type: ignore[arg-type]
|
|
1404
1419
|
raise_if_cancelled(workflow_run_response.run_id) # type: ignore
|
|
1405
1420
|
step_name = getattr(step, "name", f"step_{i + 1}")
|
|
1406
1421
|
log_debug(f"Streaming step {i + 1}/{self._get_step_count()}: {step_name}")
|
|
1407
1422
|
|
|
1423
|
+
# Track current step for cancellation handler
|
|
1424
|
+
current_step_name = step_name
|
|
1425
|
+
current_step = step
|
|
1426
|
+
# Reset partial data for this step
|
|
1427
|
+
partial_step_content = ""
|
|
1428
|
+
|
|
1408
1429
|
# Create enhanced StepInput
|
|
1409
1430
|
step_input = self._create_step_input(
|
|
1410
1431
|
execution_input=execution_input,
|
|
@@ -1433,6 +1454,10 @@ class Workflow:
|
|
|
1433
1454
|
num_history_runs=self.num_history_runs,
|
|
1434
1455
|
):
|
|
1435
1456
|
raise_if_cancelled(workflow_run_response.run_id) # type: ignore
|
|
1457
|
+
|
|
1458
|
+
# Accumulate partial data from streaming events
|
|
1459
|
+
partial_step_content = self._accumulate_partial_step_data(event, partial_step_content) # type: ignore
|
|
1460
|
+
|
|
1436
1461
|
# Handle events
|
|
1437
1462
|
if isinstance(event, StepOutput):
|
|
1438
1463
|
step_output = event
|
|
@@ -1547,6 +1572,29 @@ class Workflow:
|
|
|
1547
1572
|
logger.info(f"Workflow run {workflow_run_response.run_id} was cancelled during streaming")
|
|
1548
1573
|
workflow_run_response.status = RunStatus.cancelled
|
|
1549
1574
|
workflow_run_response.content = str(e)
|
|
1575
|
+
|
|
1576
|
+
# Capture partial progress from the step that was cancelled mid-stream
|
|
1577
|
+
if partial_step_content:
|
|
1578
|
+
logger.info(
|
|
1579
|
+
f"Step with name '{current_step_name}' was cancelled. Setting its partial progress as step output."
|
|
1580
|
+
)
|
|
1581
|
+
partial_step_output = StepOutput(
|
|
1582
|
+
step_name=current_step_name,
|
|
1583
|
+
step_id=getattr(current_step, "step_id", None) if current_step else None,
|
|
1584
|
+
step_type=StepType.STEP,
|
|
1585
|
+
executor_type=getattr(current_step, "executor_type", None) if current_step else None,
|
|
1586
|
+
executor_name=getattr(current_step, "executor_name", None) if current_step else None,
|
|
1587
|
+
content=partial_step_content,
|
|
1588
|
+
success=False,
|
|
1589
|
+
error="Cancelled during execution",
|
|
1590
|
+
)
|
|
1591
|
+
collected_step_outputs.append(partial_step_output)
|
|
1592
|
+
|
|
1593
|
+
# Preserve all progress (completed steps + partial step) before cancellation
|
|
1594
|
+
if collected_step_outputs:
|
|
1595
|
+
workflow_run_response.step_results = collected_step_outputs
|
|
1596
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(collected_step_outputs)
|
|
1597
|
+
|
|
1550
1598
|
cancelled_event = WorkflowCancelledEvent(
|
|
1551
1599
|
run_id=workflow_run_response.run_id or "",
|
|
1552
1600
|
workflow_id=self.id,
|
|
@@ -1922,12 +1970,22 @@ class Workflow:
|
|
|
1922
1970
|
|
|
1923
1971
|
early_termination = False
|
|
1924
1972
|
|
|
1973
|
+
# Track partial step data in case of cancellation
|
|
1974
|
+
current_step_name = ""
|
|
1975
|
+
current_step = None
|
|
1976
|
+
partial_step_content = ""
|
|
1977
|
+
|
|
1925
1978
|
for i, step in enumerate(self.steps): # type: ignore[arg-type]
|
|
1926
1979
|
if workflow_run_response.run_id:
|
|
1927
1980
|
raise_if_cancelled(workflow_run_response.run_id)
|
|
1928
1981
|
step_name = getattr(step, "name", f"step_{i + 1}")
|
|
1929
1982
|
log_debug(f"Async streaming step {i + 1}/{self._get_step_count()}: {step_name}")
|
|
1930
1983
|
|
|
1984
|
+
current_step_name = step_name
|
|
1985
|
+
current_step = step
|
|
1986
|
+
# Reset partial data for this step
|
|
1987
|
+
partial_step_content = ""
|
|
1988
|
+
|
|
1931
1989
|
# Create enhanced StepInput
|
|
1932
1990
|
step_input = self._create_step_input(
|
|
1933
1991
|
execution_input=execution_input,
|
|
@@ -1957,6 +2015,10 @@ class Workflow:
|
|
|
1957
2015
|
):
|
|
1958
2016
|
if workflow_run_response.run_id:
|
|
1959
2017
|
raise_if_cancelled(workflow_run_response.run_id)
|
|
2018
|
+
|
|
2019
|
+
# Accumulate partial data from streaming events
|
|
2020
|
+
partial_step_content = self._accumulate_partial_step_data(event, partial_step_content) # type: ignore
|
|
2021
|
+
|
|
1960
2022
|
if isinstance(event, StepOutput):
|
|
1961
2023
|
step_output = event
|
|
1962
2024
|
collected_step_outputs.append(step_output)
|
|
@@ -2073,6 +2135,29 @@ class Workflow:
|
|
|
2073
2135
|
logger.info(f"Workflow run {workflow_run_response.run_id} was cancelled during streaming")
|
|
2074
2136
|
workflow_run_response.status = RunStatus.cancelled
|
|
2075
2137
|
workflow_run_response.content = str(e)
|
|
2138
|
+
|
|
2139
|
+
# Capture partial progress from the step that was cancelled mid-stream
|
|
2140
|
+
if partial_step_content:
|
|
2141
|
+
logger.info(
|
|
2142
|
+
f"Step with name '{current_step_name}' was cancelled. Setting its partial progress as step output."
|
|
2143
|
+
)
|
|
2144
|
+
partial_step_output = StepOutput(
|
|
2145
|
+
step_name=current_step_name,
|
|
2146
|
+
step_id=getattr(current_step, "step_id", None) if current_step else None,
|
|
2147
|
+
step_type=StepType.STEP,
|
|
2148
|
+
executor_type=getattr(current_step, "executor_type", None) if current_step else None,
|
|
2149
|
+
executor_name=getattr(current_step, "executor_name", None) if current_step else None,
|
|
2150
|
+
content=partial_step_content,
|
|
2151
|
+
success=False,
|
|
2152
|
+
error="Cancelled during execution",
|
|
2153
|
+
)
|
|
2154
|
+
collected_step_outputs.append(partial_step_output)
|
|
2155
|
+
|
|
2156
|
+
# Preserve all progress (completed steps + partial step) before cancellation
|
|
2157
|
+
if collected_step_outputs:
|
|
2158
|
+
workflow_run_response.step_results = collected_step_outputs
|
|
2159
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(collected_step_outputs)
|
|
2160
|
+
|
|
2076
2161
|
cancelled_event = WorkflowCancelledEvent(
|
|
2077
2162
|
run_id=workflow_run_response.run_id or "",
|
|
2078
2163
|
workflow_id=self.id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agno
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Agno: a lightweight library for building Multi-Agent Systems
|
|
5
5
|
Author-email: Ashpreet Bedi <ashpreet@agno.com>
|
|
6
6
|
Project-URL: homepage, https://agno.com
|
|
@@ -135,6 +135,8 @@ Provides-Extra: fal
|
|
|
135
135
|
Requires-Dist: fal_client; extra == "fal"
|
|
136
136
|
Provides-Extra: firecrawl
|
|
137
137
|
Requires-Dist: firecrawl-py; extra == "firecrawl"
|
|
138
|
+
Provides-Extra: tavily
|
|
139
|
+
Requires-Dist: tavily-python; extra == "tavily"
|
|
138
140
|
Provides-Extra: crawl4ai
|
|
139
141
|
Requires-Dist: crawl4ai; extra == "crawl4ai"
|
|
140
142
|
Provides-Extra: github
|
|
@@ -238,6 +240,8 @@ Requires-Dist: pypdf; extra == "pdf"
|
|
|
238
240
|
Requires-Dist: rapidocr_onnxruntime; extra == "pdf"
|
|
239
241
|
Provides-Extra: docx
|
|
240
242
|
Requires-Dist: python-docx; extra == "docx"
|
|
243
|
+
Provides-Extra: pptx
|
|
244
|
+
Requires-Dist: python-pptx; extra == "pptx"
|
|
241
245
|
Provides-Extra: text
|
|
242
246
|
Requires-Dist: aiofiles; extra == "text"
|
|
243
247
|
Provides-Extra: csv
|
|
@@ -294,6 +298,7 @@ Requires-Dist: agno[duckdb]; extra == "tools"
|
|
|
294
298
|
Requires-Dist: agno[newspaper]; extra == "tools"
|
|
295
299
|
Requires-Dist: agno[youtube]; extra == "tools"
|
|
296
300
|
Requires-Dist: agno[firecrawl]; extra == "tools"
|
|
301
|
+
Requires-Dist: agno[tavily]; extra == "tools"
|
|
297
302
|
Requires-Dist: agno[crawl4ai]; extra == "tools"
|
|
298
303
|
Requires-Dist: agno[github]; extra == "tools"
|
|
299
304
|
Requires-Dist: agno[gmail]; extra == "tools"
|
|
@@ -346,6 +351,7 @@ Requires-Dist: agno[upstash]; extra == "vectordbs"
|
|
|
346
351
|
Provides-Extra: knowledge
|
|
347
352
|
Requires-Dist: agno[pdf]; extra == "knowledge"
|
|
348
353
|
Requires-Dist: agno[docx]; extra == "knowledge"
|
|
354
|
+
Requires-Dist: agno[pptx]; extra == "knowledge"
|
|
349
355
|
Requires-Dist: agno[text]; extra == "knowledge"
|
|
350
356
|
Requires-Dist: agno[csv]; extra == "knowledge"
|
|
351
357
|
Requires-Dist: agno[markdown]; extra == "knowledge"
|
|
@@ -446,6 +452,30 @@ if __name__ == "__main__":
|
|
|
446
452
|
agent_os.serve(app="agno_agent:app", reload=True)
|
|
447
453
|
```
|
|
448
454
|
|
|
455
|
+
## AgentOS - Production Runtime for Multi-Agent Systems
|
|
456
|
+
|
|
457
|
+
AgentOS is Agno's high-performance runtime for serving multi-agent systems in production. Key features include:
|
|
458
|
+
|
|
459
|
+
1. **Pre-built FastAPI Runtime**: AgentOS ships with a ready-to-use FastAPI app for orchestrating your agents, teams, and workflows. This provides a major head start when building an AI product.
|
|
460
|
+
|
|
461
|
+
2. **Integrated Control Plane**: The [AgentOS UI](https://os.agno.com) connects directly to your runtime, letting you test, monitor, and manage your system in real time. This gives you unmatched visibility and control over your system.
|
|
462
|
+
|
|
463
|
+
3. **Private by Design**: AgentOS runs entirely in your cloud, ensuring complete data privacy. No data ever leaves your system. This is ideal for security-conscious enterprises.
|
|
464
|
+
|
|
465
|
+
Here's what the [AgentOS UI](https://os.agno.com) looks like in action:
|
|
466
|
+
|
|
467
|
+
https://github.com/user-attachments/assets/feb23db8-15cc-4e88-be7c-01a21a03ebf6
|
|
468
|
+
|
|
469
|
+
## The Complete Agentic Solution
|
|
470
|
+
|
|
471
|
+
For companies building agents, Agno provides the complete agentic solution:
|
|
472
|
+
|
|
473
|
+
- The fastest framework for building agents, multi-agent teams and agentic workflows.
|
|
474
|
+
- A ready-to-use FastAPI app that gets you building AI products on day one.
|
|
475
|
+
- A control plane for testing, monitoring and managing your system.
|
|
476
|
+
|
|
477
|
+
Agno brings a novel architecture that no other framework provides, your AgentOS runs securely in your cloud, and the control plane connects directly to it from your browser. You don't need to send data to any external services or pay retention costs, you get complete privacy and control.
|
|
478
|
+
|
|
449
479
|
## Designed for Agent Engineering
|
|
450
480
|
|
|
451
481
|
Agno is a remarkably feature-rich framework, purpose-built for large-scale multi-agent deployments.
|
|
@@ -474,30 +504,6 @@ Agno is a remarkably feature-rich framework, purpose-built for large-scale multi
|
|
|
474
504
|
|
|
475
505
|
Every part of Agno is built for real-world deployment — where developer experience meets production performance.
|
|
476
506
|
|
|
477
|
-
## AgentOS - Production Runtime for Multi-Agent Systems
|
|
478
|
-
|
|
479
|
-
AgentOS is Agno's high-performance runtime for serving multi-agent systems in production. Key features include:
|
|
480
|
-
|
|
481
|
-
1. **Pre-built FastAPI Runtime**: AgentOS ships with a ready-to-use FastAPI app for orchestrating your agents, teams, and workflows. This provides a major head start when building an AI product.
|
|
482
|
-
|
|
483
|
-
2. **Integrated Control Plane**: The [AgentOS UI](https://os.agno.com) connects directly to your runtime, letting you test, monitor, and manage your system in real time. This gives you unmatched visibility and control over your system.
|
|
484
|
-
|
|
485
|
-
3. **Private by Design**: AgentOS runs entirely in your cloud, ensuring complete data privacy. No data ever leaves your system. This is ideal for security-conscious enterprises.
|
|
486
|
-
|
|
487
|
-
Here's what the [AgentOS UI](https://os.agno.com) looks like in action:
|
|
488
|
-
|
|
489
|
-
https://github.com/user-attachments/assets/feb23db8-15cc-4e88-be7c-01a21a03ebf6
|
|
490
|
-
|
|
491
|
-
## The Complete Agentic Solution
|
|
492
|
-
|
|
493
|
-
For companies building agents, Agno provides the complete agentic solution:
|
|
494
|
-
|
|
495
|
-
- The fastest framework for building agents, multi-agent teams and agentic workflows.
|
|
496
|
-
- A ready-to-use FastAPI app that gets you building AI products on day one.
|
|
497
|
-
- A control plane for testing, monitoring and managing your system.
|
|
498
|
-
|
|
499
|
-
Agno brings a novel architecture that no other framework provides, your AgentOS runs securely in your cloud, and the control plane connects directly to it from your browser. You don't need to send data to any external services or pay retention costs, you get complete privacy and control.
|
|
500
|
-
|
|
501
507
|
## Setup Your Coding Agent to Use Agno
|
|
502
508
|
|
|
503
509
|
For LLMs and AI assistants to understand and navigate Agno's documentation, we provide an [llms.txt](https://docs.agno.com/llms.txt) or [llms-full.txt](https://docs.agno.com/llms-full.txt) file. This file is built for AI systems to efficiently parse and reference our documentation.
|