agno 2.0.10__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +608 -175
- agno/db/in_memory/in_memory_db.py +42 -29
- agno/db/postgres/postgres.py +6 -4
- agno/exceptions.py +62 -1
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +51 -0
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/ollama.py +5 -0
- agno/knowledge/embedder/openai.py +18 -54
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +5 -4
- agno/knowledge/reader/pdf_reader.py +4 -3
- agno/knowledge/reader/website_reader.py +3 -2
- agno/models/base.py +125 -32
- agno/models/cerebras/cerebras.py +1 -0
- agno/models/cerebras/cerebras_openai.py +1 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/google/gemini.py +27 -5
- agno/models/litellm/chat.py +17 -0
- agno/models/openai/chat.py +13 -4
- agno/models/perplexity/perplexity.py +2 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +49 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +1 -0
- agno/os/app.py +167 -148
- agno/os/interfaces/whatsapp/router.py +2 -0
- agno/os/mcp.py +1 -1
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +181 -45
- agno/os/routers/home.py +2 -2
- agno/os/routers/memory/memory.py +23 -1
- agno/os/routers/memory/schemas.py +1 -1
- agno/os/routers/session/session.py +20 -3
- agno/os/utils.py +172 -8
- agno/run/agent.py +120 -77
- agno/run/team.py +115 -72
- agno/run/workflow.py +5 -15
- agno/session/summary.py +9 -10
- agno/session/team.py +2 -1
- agno/team/team.py +720 -168
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +42 -2
- agno/tools/knowledge.py +3 -3
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/spider.py +2 -2
- agno/tools/workflow.py +4 -5
- agno/utils/events.py +66 -1
- agno/utils/hooks.py +57 -0
- agno/utils/media.py +11 -9
- agno/utils/print_response/agent.py +43 -5
- agno/utils/print_response/team.py +48 -12
- agno/vectordb/cassandra/cassandra.py +44 -4
- agno/vectordb/chroma/chromadb.py +79 -8
- agno/vectordb/clickhouse/clickhousedb.py +43 -6
- agno/vectordb/couchbase/couchbase.py +76 -5
- agno/vectordb/lancedb/lance_db.py +38 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/milvus/milvus.py +76 -4
- agno/vectordb/mongodb/mongodb.py +76 -4
- agno/vectordb/pgvector/pgvector.py +50 -6
- agno/vectordb/pineconedb/pineconedb.py +39 -2
- agno/vectordb/qdrant/qdrant.py +76 -26
- agno/vectordb/singlestore/singlestore.py +77 -4
- agno/vectordb/upstashdb/upstashdb.py +42 -2
- agno/vectordb/weaviate/weaviate.py +39 -3
- agno/workflow/types.py +1 -0
- agno/workflow/workflow.py +58 -2
- {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/METADATA +4 -3
- {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/RECORD +85 -75
- {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/WHEEL +0 -0
- {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.10.dist-info → agno-2.1.0.dist-info}/top_level.txt +0 -0
agno/os/routers/memory/memory.py
CHANGED
|
@@ -3,7 +3,7 @@ import math
|
|
|
3
3
|
from typing import List, Optional
|
|
4
4
|
from uuid import uuid4
|
|
5
5
|
|
|
6
|
-
from fastapi import Depends, HTTPException, Path, Query
|
|
6
|
+
from fastapi import Depends, HTTPException, Path, Query, Request
|
|
7
7
|
from fastapi.routing import APIRouter
|
|
8
8
|
|
|
9
9
|
from agno.db.base import BaseDb
|
|
@@ -80,9 +80,17 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
80
80
|
},
|
|
81
81
|
)
|
|
82
82
|
async def create_memory(
|
|
83
|
+
request: Request,
|
|
83
84
|
payload: UserMemoryCreateSchema,
|
|
84
85
|
db_id: Optional[str] = Query(default=None, description="Database ID to use for memory storage"),
|
|
85
86
|
) -> UserMemorySchema:
|
|
87
|
+
if hasattr(request.state, "user_id"):
|
|
88
|
+
user_id = request.state.user_id
|
|
89
|
+
payload.user_id = user_id
|
|
90
|
+
|
|
91
|
+
if payload.user_id is None:
|
|
92
|
+
raise HTTPException(status_code=400, detail="User ID is required")
|
|
93
|
+
|
|
86
94
|
db = get_db(dbs, db_id)
|
|
87
95
|
user_memory = db.upsert_user_memory(
|
|
88
96
|
memory=UserMemory(
|
|
@@ -173,6 +181,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
173
181
|
},
|
|
174
182
|
)
|
|
175
183
|
async def get_memories(
|
|
184
|
+
request: Request,
|
|
176
185
|
user_id: Optional[str] = Query(default=None, description="Filter memories by user ID"),
|
|
177
186
|
agent_id: Optional[str] = Query(default=None, description="Filter memories by agent ID"),
|
|
178
187
|
team_id: Optional[str] = Query(default=None, description="Filter memories by team ID"),
|
|
@@ -185,6 +194,10 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
185
194
|
db_id: Optional[str] = Query(default=None, description="Database ID to query memories from"),
|
|
186
195
|
) -> PaginatedResponse[UserMemorySchema]:
|
|
187
196
|
db = get_db(dbs, db_id)
|
|
197
|
+
|
|
198
|
+
if hasattr(request.state, "user_id"):
|
|
199
|
+
user_id = request.state.user_id
|
|
200
|
+
|
|
188
201
|
user_memories, total_count = db.get_user_memories(
|
|
189
202
|
limit=limit,
|
|
190
203
|
page=page,
|
|
@@ -316,11 +329,20 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
316
329
|
},
|
|
317
330
|
)
|
|
318
331
|
async def update_memory(
|
|
332
|
+
request: Request,
|
|
319
333
|
payload: UserMemoryCreateSchema,
|
|
320
334
|
memory_id: str = Path(description="Memory ID to update"),
|
|
321
335
|
db_id: Optional[str] = Query(default=None, description="Database ID to use for update"),
|
|
322
336
|
) -> UserMemorySchema:
|
|
323
337
|
db = get_db(dbs, db_id)
|
|
338
|
+
|
|
339
|
+
if hasattr(request.state, "user_id"):
|
|
340
|
+
user_id = request.state.user_id
|
|
341
|
+
payload.user_id = user_id
|
|
342
|
+
|
|
343
|
+
if payload.user_id is None:
|
|
344
|
+
raise HTTPException(status_code=400, detail="User ID is required")
|
|
345
|
+
|
|
324
346
|
user_memory = db.upsert_user_memory(
|
|
325
347
|
memory=UserMemory(
|
|
326
348
|
memory_id=memory_id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import List, Optional, Union
|
|
3
3
|
|
|
4
|
-
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
|
4
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
|
5
5
|
|
|
6
6
|
from agno.db.base import BaseDb, SessionType
|
|
7
7
|
from agno.os.auth import get_authentication_dependency
|
|
@@ -86,6 +86,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
86
86
|
},
|
|
87
87
|
)
|
|
88
88
|
async def get_sessions(
|
|
89
|
+
request: Request,
|
|
89
90
|
session_type: SessionType = Query(
|
|
90
91
|
default=SessionType.AGENT,
|
|
91
92
|
alias="type",
|
|
@@ -103,6 +104,10 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
103
104
|
db_id: Optional[str] = Query(default=None, description="Database ID to query sessions from"),
|
|
104
105
|
) -> PaginatedResponse[SessionSchema]:
|
|
105
106
|
db = get_db(dbs, db_id)
|
|
107
|
+
|
|
108
|
+
if hasattr(request.state, "user_id"):
|
|
109
|
+
user_id = request.state.user_id
|
|
110
|
+
|
|
106
111
|
sessions, total_count = db.get_sessions(
|
|
107
112
|
session_type=session_type,
|
|
108
113
|
component_id=component_id,
|
|
@@ -213,14 +218,20 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
213
218
|
},
|
|
214
219
|
)
|
|
215
220
|
async def get_session_by_id(
|
|
221
|
+
request: Request,
|
|
216
222
|
session_id: str = Path(description="Session ID to retrieve"),
|
|
217
223
|
session_type: SessionType = Query(
|
|
218
224
|
default=SessionType.AGENT, description="Session type (agent, team, or workflow)", alias="type"
|
|
219
225
|
),
|
|
226
|
+
user_id: Optional[str] = Query(default=None, description="User ID to query session from"),
|
|
220
227
|
db_id: Optional[str] = Query(default=None, description="Database ID to query session from"),
|
|
221
228
|
) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
|
|
222
229
|
db = get_db(dbs, db_id)
|
|
223
|
-
|
|
230
|
+
|
|
231
|
+
if hasattr(request.state, "user_id"):
|
|
232
|
+
user_id = request.state.user_id
|
|
233
|
+
|
|
234
|
+
session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id)
|
|
224
235
|
if not session:
|
|
225
236
|
raise HTTPException(
|
|
226
237
|
status_code=404, detail=f"{session_type.value.title()} Session with id '{session_id}' not found"
|
|
@@ -349,14 +360,20 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
349
360
|
},
|
|
350
361
|
)
|
|
351
362
|
async def get_session_runs(
|
|
363
|
+
request: Request,
|
|
352
364
|
session_id: str = Path(description="Session ID to get runs from"),
|
|
353
365
|
session_type: SessionType = Query(
|
|
354
366
|
default=SessionType.AGENT, description="Session type (agent, team, or workflow)", alias="type"
|
|
355
367
|
),
|
|
368
|
+
user_id: Optional[str] = Query(default=None, description="User ID to query runs from"),
|
|
356
369
|
db_id: Optional[str] = Query(default=None, description="Database ID to query runs from"),
|
|
357
370
|
) -> List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]]:
|
|
358
371
|
db = get_db(dbs, db_id)
|
|
359
|
-
|
|
372
|
+
|
|
373
|
+
if hasattr(request.state, "user_id"):
|
|
374
|
+
user_id = request.state.user_id
|
|
375
|
+
|
|
376
|
+
session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id, deserialize=False)
|
|
360
377
|
if not session:
|
|
361
378
|
raise HTTPException(status_code=404, detail=f"Session with ID {session_id} not found")
|
|
362
379
|
|
agno/os/utils.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
2
2
|
|
|
3
3
|
from fastapi import FastAPI, HTTPException, UploadFile
|
|
4
|
+
from fastapi.routing import APIRoute, APIRouter
|
|
4
5
|
from starlette.middleware.cors import CORSMiddleware
|
|
5
6
|
|
|
6
7
|
from agno.agent.agent import Agent
|
|
@@ -8,6 +9,7 @@ from agno.db.base import BaseDb
|
|
|
8
9
|
from agno.knowledge.knowledge import Knowledge
|
|
9
10
|
from agno.media import Audio, Image, Video
|
|
10
11
|
from agno.media import File as FileMedia
|
|
12
|
+
from agno.os.config import AgentOSConfig
|
|
11
13
|
from agno.team.team import Team
|
|
12
14
|
from agno.tools import Toolkit
|
|
13
15
|
from agno.tools.function import Function
|
|
@@ -149,16 +151,16 @@ def process_document(file: UploadFile) -> Optional[FileMedia]:
|
|
|
149
151
|
|
|
150
152
|
|
|
151
153
|
def extract_format(file: UploadFile):
|
|
152
|
-
|
|
154
|
+
_format = None
|
|
153
155
|
if file.filename and "." in file.filename:
|
|
154
|
-
|
|
156
|
+
_format = file.filename.split(".")[-1].lower()
|
|
155
157
|
elif file.content_type:
|
|
156
|
-
|
|
157
|
-
return
|
|
158
|
+
_format = file.content_type.split("/")[-1]
|
|
159
|
+
return _format
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
|
|
161
|
-
formatted_tools = []
|
|
163
|
+
formatted_tools: List[Dict] = []
|
|
162
164
|
if agent_tools is not None:
|
|
163
165
|
for tool in agent_tools:
|
|
164
166
|
if isinstance(tool, dict):
|
|
@@ -284,7 +286,11 @@ def update_cors_middleware(app: FastAPI, new_origins: list):
|
|
|
284
286
|
for middleware in app.user_middleware:
|
|
285
287
|
if middleware.cls == CORSMiddleware:
|
|
286
288
|
if hasattr(middleware, "kwargs"):
|
|
287
|
-
|
|
289
|
+
origins_value = middleware.kwargs.get("allow_origins", [])
|
|
290
|
+
if isinstance(origins_value, list):
|
|
291
|
+
existing_origins = origins_value
|
|
292
|
+
else:
|
|
293
|
+
existing_origins = []
|
|
288
294
|
break
|
|
289
295
|
# Merge origins
|
|
290
296
|
merged_origins = list(set(new_origins + existing_origins))
|
|
@@ -296,10 +302,168 @@ def update_cors_middleware(app: FastAPI, new_origins: list):
|
|
|
296
302
|
|
|
297
303
|
# Add updated CORS
|
|
298
304
|
app.add_middleware(
|
|
299
|
-
CORSMiddleware,
|
|
305
|
+
CORSMiddleware, # type: ignore
|
|
300
306
|
allow_origins=final_origins,
|
|
301
307
|
allow_credentials=True,
|
|
302
308
|
allow_methods=["*"],
|
|
303
309
|
allow_headers=["*"],
|
|
304
310
|
expose_headers=["*"],
|
|
305
311
|
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_existing_route_paths(fastapi_app: FastAPI) -> Dict[str, List[str]]:
|
|
315
|
+
"""Get all existing route paths and methods from the FastAPI app.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
|
|
319
|
+
"""
|
|
320
|
+
existing_paths: Dict[str, Any] = {}
|
|
321
|
+
for route in fastapi_app.routes:
|
|
322
|
+
if isinstance(route, APIRoute):
|
|
323
|
+
path = route.path
|
|
324
|
+
methods = list(route.methods) if route.methods else []
|
|
325
|
+
if path in existing_paths:
|
|
326
|
+
existing_paths[path].extend(methods)
|
|
327
|
+
else:
|
|
328
|
+
existing_paths[path] = methods
|
|
329
|
+
return existing_paths
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def find_conflicting_routes(fastapi_app: FastAPI, router: APIRouter) -> List[Dict[str, Any]]:
|
|
333
|
+
"""Find conflicting routes in the FastAPI app.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
fastapi_app: The FastAPI app with all existing routes
|
|
337
|
+
router: The APIRouter to add
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List[Dict[str, Any]]: List of conflicting routes
|
|
341
|
+
"""
|
|
342
|
+
existing_paths = get_existing_route_paths(fastapi_app)
|
|
343
|
+
|
|
344
|
+
conflicts = []
|
|
345
|
+
|
|
346
|
+
for route in router.routes:
|
|
347
|
+
if isinstance(route, APIRoute):
|
|
348
|
+
full_path = route.path
|
|
349
|
+
route_methods = list(route.methods) if route.methods else []
|
|
350
|
+
|
|
351
|
+
if full_path in existing_paths:
|
|
352
|
+
conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
|
|
353
|
+
if conflicting_methods:
|
|
354
|
+
conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
|
|
355
|
+
return conflicts
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def load_yaml_config(config_file_path: str) -> AgentOSConfig:
|
|
359
|
+
"""Load a YAML config file and return the configuration as an AgentOSConfig instance."""
|
|
360
|
+
from pathlib import Path
|
|
361
|
+
|
|
362
|
+
import yaml
|
|
363
|
+
|
|
364
|
+
# Validate that the path points to a YAML file
|
|
365
|
+
path = Path(config_file_path)
|
|
366
|
+
if path.suffix.lower() not in [".yaml", ".yml"]:
|
|
367
|
+
raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
|
|
368
|
+
|
|
369
|
+
# Load the YAML file
|
|
370
|
+
with open(config_file_path, "r") as f:
|
|
371
|
+
return AgentOSConfig.model_validate(yaml.safe_load(f))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
|
|
375
|
+
"""Recursively collect MCP tools from a team and its members."""
|
|
376
|
+
# Check the team tools
|
|
377
|
+
if team.tools:
|
|
378
|
+
for tool in team.tools:
|
|
379
|
+
type_name = type(tool).__name__
|
|
380
|
+
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
381
|
+
if tool not in mcp_tools:
|
|
382
|
+
mcp_tools.append(tool)
|
|
383
|
+
|
|
384
|
+
# Recursively check team members
|
|
385
|
+
if team.members:
|
|
386
|
+
for member in team.members:
|
|
387
|
+
if isinstance(member, Agent):
|
|
388
|
+
if member.tools:
|
|
389
|
+
for tool in member.tools:
|
|
390
|
+
type_name = type(tool).__name__
|
|
391
|
+
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
392
|
+
if tool not in mcp_tools:
|
|
393
|
+
mcp_tools.append(tool)
|
|
394
|
+
|
|
395
|
+
elif isinstance(member, Team):
|
|
396
|
+
# Recursively check nested team
|
|
397
|
+
collect_mcp_tools_from_team(member, mcp_tools)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def collect_mcp_tools_from_workflow(workflow: Workflow, mcp_tools: List[Any]) -> None:
|
|
401
|
+
"""Recursively collect MCP tools from a workflow and its steps."""
|
|
402
|
+
from agno.workflow.steps import Steps
|
|
403
|
+
|
|
404
|
+
# Recursively check workflow steps
|
|
405
|
+
if workflow.steps:
|
|
406
|
+
if isinstance(workflow.steps, list):
|
|
407
|
+
# Handle list of steps
|
|
408
|
+
for step in workflow.steps:
|
|
409
|
+
collect_mcp_tools_from_workflow_step(step, mcp_tools)
|
|
410
|
+
|
|
411
|
+
elif isinstance(workflow.steps, Steps):
|
|
412
|
+
# Handle Steps container
|
|
413
|
+
if steps := workflow.steps.steps:
|
|
414
|
+
for step in steps:
|
|
415
|
+
collect_mcp_tools_from_workflow_step(step, mcp_tools)
|
|
416
|
+
|
|
417
|
+
elif callable(workflow.steps):
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> None:
|
|
422
|
+
"""Collect MCP tools from a single workflow step."""
|
|
423
|
+
from agno.workflow.condition import Condition
|
|
424
|
+
from agno.workflow.loop import Loop
|
|
425
|
+
from agno.workflow.parallel import Parallel
|
|
426
|
+
from agno.workflow.router import Router
|
|
427
|
+
from agno.workflow.step import Step
|
|
428
|
+
from agno.workflow.steps import Steps
|
|
429
|
+
|
|
430
|
+
if isinstance(step, Step):
|
|
431
|
+
# Check step's agent
|
|
432
|
+
if step.agent:
|
|
433
|
+
if step.agent.tools:
|
|
434
|
+
for tool in step.agent.tools:
|
|
435
|
+
type_name = type(tool).__name__
|
|
436
|
+
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
437
|
+
if tool not in mcp_tools:
|
|
438
|
+
mcp_tools.append(tool)
|
|
439
|
+
# Check step's team
|
|
440
|
+
if step.team:
|
|
441
|
+
collect_mcp_tools_from_team(step.team, mcp_tools)
|
|
442
|
+
|
|
443
|
+
elif isinstance(step, Steps):
|
|
444
|
+
if steps := step.steps:
|
|
445
|
+
for step in steps:
|
|
446
|
+
collect_mcp_tools_from_workflow_step(step, mcp_tools)
|
|
447
|
+
|
|
448
|
+
elif isinstance(step, (Parallel, Loop, Condition, Router)):
|
|
449
|
+
# These contain other steps - recursively check them
|
|
450
|
+
if hasattr(step, "steps") and step.steps:
|
|
451
|
+
for sub_step in step.steps:
|
|
452
|
+
collect_mcp_tools_from_workflow_step(sub_step, mcp_tools)
|
|
453
|
+
|
|
454
|
+
elif isinstance(step, Agent):
|
|
455
|
+
# Direct agent in workflow steps
|
|
456
|
+
if step.tools:
|
|
457
|
+
for tool in step.tools:
|
|
458
|
+
type_name = type(tool).__name__
|
|
459
|
+
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
460
|
+
if tool not in mcp_tools:
|
|
461
|
+
mcp_tools.append(tool)
|
|
462
|
+
|
|
463
|
+
elif isinstance(step, Team):
|
|
464
|
+
# Direct team in workflow steps
|
|
465
|
+
collect_mcp_tools_from_team(step, mcp_tools)
|
|
466
|
+
|
|
467
|
+
elif isinstance(step, Workflow):
|
|
468
|
+
# Nested workflow
|
|
469
|
+
collect_mcp_tools_from_workflow(step, mcp_tools)
|
agno/run/agent.py
CHANGED
|
@@ -14,6 +14,96 @@ from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
|
|
|
14
14
|
from agno.utils.log import logger
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
@dataclass
|
|
18
|
+
class RunInput:
|
|
19
|
+
"""Container for the raw input data passed to Agent.run().
|
|
20
|
+
|
|
21
|
+
This captures the original input exactly as provided by the user,
|
|
22
|
+
separate from the processed messages that go to the model.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
input_content: The literal input message/content passed to run()
|
|
26
|
+
images: Images directly passed to run()
|
|
27
|
+
videos: Videos directly passed to run()
|
|
28
|
+
audios: Audio files directly passed to run()
|
|
29
|
+
files: Files directly passed to run()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
input_content: Union[str, List, Dict, Message, BaseModel, List[Message]]
|
|
33
|
+
images: Optional[Sequence[Image]] = None
|
|
34
|
+
videos: Optional[Sequence[Video]] = None
|
|
35
|
+
audios: Optional[Sequence[Audio]] = None
|
|
36
|
+
files: Optional[Sequence[File]] = None
|
|
37
|
+
|
|
38
|
+
def input_content_string(self) -> str:
|
|
39
|
+
import json
|
|
40
|
+
|
|
41
|
+
if isinstance(self.input_content, (str)):
|
|
42
|
+
return self.input_content
|
|
43
|
+
elif isinstance(self.input_content, BaseModel):
|
|
44
|
+
return self.input_content.model_dump_json(exclude_none=True)
|
|
45
|
+
elif isinstance(self.input_content, Message):
|
|
46
|
+
return json.dumps(self.input_content.to_dict())
|
|
47
|
+
elif isinstance(self.input_content, list) and self.input_content and isinstance(self.input_content[0], Message):
|
|
48
|
+
return json.dumps([m.to_dict() for m in self.input_content])
|
|
49
|
+
else:
|
|
50
|
+
return str(self.input_content)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
53
|
+
"""Convert to dictionary representation"""
|
|
54
|
+
result: Dict[str, Any] = {}
|
|
55
|
+
|
|
56
|
+
if self.input_content is not None:
|
|
57
|
+
if isinstance(self.input_content, (str)):
|
|
58
|
+
result["input_content"] = self.input_content
|
|
59
|
+
elif isinstance(self.input_content, BaseModel):
|
|
60
|
+
result["input_content"] = self.input_content.model_dump(exclude_none=True)
|
|
61
|
+
elif isinstance(self.input_content, Message):
|
|
62
|
+
result["input_content"] = self.input_content.to_dict()
|
|
63
|
+
elif (
|
|
64
|
+
isinstance(self.input_content, list)
|
|
65
|
+
and self.input_content
|
|
66
|
+
and isinstance(self.input_content[0], Message)
|
|
67
|
+
):
|
|
68
|
+
result["input_content"] = [m.to_dict() for m in self.input_content]
|
|
69
|
+
else:
|
|
70
|
+
result["input_content"] = self.input_content
|
|
71
|
+
|
|
72
|
+
if self.images:
|
|
73
|
+
result["images"] = [img.to_dict() for img in self.images]
|
|
74
|
+
if self.videos:
|
|
75
|
+
result["videos"] = [vid.to_dict() for vid in self.videos]
|
|
76
|
+
if self.audios:
|
|
77
|
+
result["audios"] = [aud.to_dict() for aud in self.audios]
|
|
78
|
+
if self.files:
|
|
79
|
+
result["files"] = [file.to_dict() for file in self.files]
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RunInput":
|
|
85
|
+
"""Create RunInput from dictionary"""
|
|
86
|
+
images = None
|
|
87
|
+
if data.get("images"):
|
|
88
|
+
images = [Image.model_validate(img_data) for img_data in data["images"]]
|
|
89
|
+
|
|
90
|
+
videos = None
|
|
91
|
+
if data.get("videos"):
|
|
92
|
+
videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
|
|
93
|
+
|
|
94
|
+
audios = None
|
|
95
|
+
if data.get("audios"):
|
|
96
|
+
audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
|
|
97
|
+
|
|
98
|
+
files = None
|
|
99
|
+
if data.get("files"):
|
|
100
|
+
files = [File.model_validate(file_data) for file_data in data["files"]]
|
|
101
|
+
|
|
102
|
+
return cls(
|
|
103
|
+
input_content=data.get("input_content", ""), images=images, videos=videos, audios=audios, files=files
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
17
107
|
class RunEvent(str, Enum):
|
|
18
108
|
"""Events that can be sent by the run() functions"""
|
|
19
109
|
|
|
@@ -27,6 +117,9 @@ class RunEvent(str, Enum):
|
|
|
27
117
|
run_paused = "RunPaused"
|
|
28
118
|
run_continued = "RunContinued"
|
|
29
119
|
|
|
120
|
+
pre_hook_started = "PreHookStarted"
|
|
121
|
+
pre_hook_completed = "PreHookCompleted"
|
|
122
|
+
|
|
30
123
|
tool_call_started = "ToolCallStarted"
|
|
31
124
|
tool_call_completed = "ToolCallCompleted"
|
|
32
125
|
|
|
@@ -53,6 +146,7 @@ class BaseAgentRunEvent(BaseRunOutputEvent):
|
|
|
53
146
|
agent_id: str = ""
|
|
54
147
|
agent_name: str = ""
|
|
55
148
|
run_id: Optional[str] = None
|
|
149
|
+
parent_run_id: Optional[str] = None
|
|
56
150
|
session_id: Optional[str] = None
|
|
57
151
|
|
|
58
152
|
# Step context for workflow execution
|
|
@@ -153,6 +247,11 @@ class RunErrorEvent(BaseAgentRunEvent):
|
|
|
153
247
|
event: str = RunEvent.run_error.value
|
|
154
248
|
content: Optional[str] = None
|
|
155
249
|
|
|
250
|
+
# From exceptions
|
|
251
|
+
error_type: Optional[str] = None
|
|
252
|
+
error_id: Optional[str] = None
|
|
253
|
+
additional_data: Optional[Dict[str, Any]] = None
|
|
254
|
+
|
|
156
255
|
|
|
157
256
|
@dataclass
|
|
158
257
|
class RunCancelledEvent(BaseAgentRunEvent):
|
|
@@ -164,6 +263,20 @@ class RunCancelledEvent(BaseAgentRunEvent):
|
|
|
164
263
|
return True
|
|
165
264
|
|
|
166
265
|
|
|
266
|
+
@dataclass
|
|
267
|
+
class PreHookStartedEvent(BaseAgentRunEvent):
|
|
268
|
+
event: str = RunEvent.pre_hook_started.value
|
|
269
|
+
pre_hook_name: Optional[str] = None
|
|
270
|
+
run_input: Optional[RunInput] = None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
274
|
+
class PreHookCompletedEvent(BaseAgentRunEvent):
|
|
275
|
+
event: str = RunEvent.pre_hook_completed.value
|
|
276
|
+
pre_hook_name: Optional[str] = None
|
|
277
|
+
run_input: Optional[RunInput] = None
|
|
278
|
+
|
|
279
|
+
|
|
167
280
|
@dataclass
|
|
168
281
|
class MemoryUpdateStartedEvent(BaseAgentRunEvent):
|
|
169
282
|
event: str = RunEvent.memory_update_started.value
|
|
@@ -244,6 +357,8 @@ RunOutputEvent = Union[
|
|
|
244
357
|
RunCancelledEvent,
|
|
245
358
|
RunPausedEvent,
|
|
246
359
|
RunContinuedEvent,
|
|
360
|
+
PreHookStartedEvent,
|
|
361
|
+
PreHookCompletedEvent,
|
|
247
362
|
ReasoningStartedEvent,
|
|
248
363
|
ReasoningStepEvent,
|
|
249
364
|
ReasoningCompletedEvent,
|
|
@@ -269,6 +384,8 @@ RUN_EVENT_TYPE_REGISTRY = {
|
|
|
269
384
|
RunEvent.run_cancelled.value: RunCancelledEvent,
|
|
270
385
|
RunEvent.run_paused.value: RunPausedEvent,
|
|
271
386
|
RunEvent.run_continued.value: RunContinuedEvent,
|
|
387
|
+
RunEvent.pre_hook_started.value: PreHookStartedEvent,
|
|
388
|
+
RunEvent.pre_hook_completed.value: PreHookCompletedEvent,
|
|
272
389
|
RunEvent.reasoning_started.value: ReasoningStartedEvent,
|
|
273
390
|
RunEvent.reasoning_step.value: ReasoningStepEvent,
|
|
274
391
|
RunEvent.reasoning_completed.value: ReasoningCompletedEvent,
|
|
@@ -292,80 +409,6 @@ def run_output_event_from_dict(data: dict) -> BaseRunOutputEvent:
|
|
|
292
409
|
return cls.from_dict(data) # type: ignore
|
|
293
410
|
|
|
294
411
|
|
|
295
|
-
@dataclass
|
|
296
|
-
class RunInput:
|
|
297
|
-
"""Container for the raw input data passed to Agent.run().
|
|
298
|
-
|
|
299
|
-
This captures the original input exactly as provided by the user,
|
|
300
|
-
separate from the processed messages that go to the model.
|
|
301
|
-
|
|
302
|
-
Attributes:
|
|
303
|
-
input_content: The literal input message/content passed to run()
|
|
304
|
-
images: Images directly passed to run()
|
|
305
|
-
videos: Videos directly passed to run()
|
|
306
|
-
audios: Audio files directly passed to run()
|
|
307
|
-
files: Files directly passed to run()
|
|
308
|
-
"""
|
|
309
|
-
|
|
310
|
-
input_content: Optional[Union[str, List, Dict, Message, BaseModel, List[Message]]] = None
|
|
311
|
-
images: Optional[Sequence[Image]] = None
|
|
312
|
-
videos: Optional[Sequence[Video]] = None
|
|
313
|
-
audios: Optional[Sequence[Audio]] = None
|
|
314
|
-
files: Optional[Sequence[File]] = None
|
|
315
|
-
|
|
316
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
317
|
-
"""Convert to dictionary representation"""
|
|
318
|
-
result: Dict[str, Any] = {}
|
|
319
|
-
|
|
320
|
-
if self.input_content is not None:
|
|
321
|
-
if isinstance(self.input_content, (str)):
|
|
322
|
-
result["input_content"] = self.input_content
|
|
323
|
-
elif isinstance(self.input_content, BaseModel):
|
|
324
|
-
result["input_content"] = self.input_content.model_dump(exclude_none=True)
|
|
325
|
-
elif isinstance(self.input_content, Message):
|
|
326
|
-
result["input_content"] = self.input_content.to_dict()
|
|
327
|
-
elif (
|
|
328
|
-
isinstance(self.input_content, list)
|
|
329
|
-
and self.input_content
|
|
330
|
-
and isinstance(self.input_content[0], Message)
|
|
331
|
-
):
|
|
332
|
-
result["input_content"] = [m.to_dict() for m in self.input_content]
|
|
333
|
-
else:
|
|
334
|
-
result["input_content"] = self.input_content
|
|
335
|
-
|
|
336
|
-
if self.images:
|
|
337
|
-
result["images"] = [img.to_dict() for img in self.images]
|
|
338
|
-
if self.videos:
|
|
339
|
-
result["videos"] = [vid.to_dict() for vid in self.videos]
|
|
340
|
-
if self.audios:
|
|
341
|
-
result["audios"] = [aud.to_dict() for aud in self.audios]
|
|
342
|
-
if self.files:
|
|
343
|
-
result["files"] = [file.to_dict() for file in self.files]
|
|
344
|
-
|
|
345
|
-
return result
|
|
346
|
-
|
|
347
|
-
@classmethod
|
|
348
|
-
def from_dict(cls, data: Dict[str, Any]) -> "RunInput":
|
|
349
|
-
"""Create RunInput from dictionary"""
|
|
350
|
-
images = None
|
|
351
|
-
if data.get("images"):
|
|
352
|
-
images = [Image.model_validate(img_data) for img_data in data["images"]]
|
|
353
|
-
|
|
354
|
-
videos = None
|
|
355
|
-
if data.get("videos"):
|
|
356
|
-
videos = [Video.model_validate(vid_data) for vid_data in data["videos"]]
|
|
357
|
-
|
|
358
|
-
audios = None
|
|
359
|
-
if data.get("audios"):
|
|
360
|
-
audios = [Audio.model_validate(aud_data) for aud_data in data["audios"]]
|
|
361
|
-
|
|
362
|
-
files = None
|
|
363
|
-
if data.get("files"):
|
|
364
|
-
files = [File.model_validate(file_data) for file_data in data["files"]]
|
|
365
|
-
|
|
366
|
-
return cls(input_content=data.get("input_content"), images=images, videos=videos, audios=audios, files=files)
|
|
367
|
-
|
|
368
|
-
|
|
369
412
|
@dataclass
|
|
370
413
|
class RunOutput:
|
|
371
414
|
"""Response returned by Agent.run() or Workflow.run() functions"""
|
|
@@ -378,6 +421,9 @@ class RunOutput:
|
|
|
378
421
|
workflow_id: Optional[str] = None
|
|
379
422
|
user_id: Optional[str] = None
|
|
380
423
|
|
|
424
|
+
# Input media and messages from user
|
|
425
|
+
input: Optional[RunInput] = None
|
|
426
|
+
|
|
381
427
|
content: Optional[Any] = None
|
|
382
428
|
content_type: str = "str"
|
|
383
429
|
|
|
@@ -401,9 +447,6 @@ class RunOutput:
|
|
|
401
447
|
files: Optional[List[File]] = None # Files attached to the response
|
|
402
448
|
response_audio: Optional[Audio] = None # Model audio response
|
|
403
449
|
|
|
404
|
-
# Input media and messages from user
|
|
405
|
-
input: Optional[RunInput] = None
|
|
406
|
-
|
|
407
450
|
citations: Optional[Citations] = None
|
|
408
451
|
references: Optional[List[MessageReferences]] = None
|
|
409
452
|
|