agno 2.1.2__py3-none-any.whl → 2.3.13__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 +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/os/auth.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import List, Set
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, Request
|
|
2
4
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
3
5
|
|
|
6
|
+
from agno.os.scopes import get_accessible_resource_ids
|
|
4
7
|
from agno.os.settings import AgnoAPISettings
|
|
5
8
|
|
|
6
9
|
# Create a global HTTPBearer instance
|
|
@@ -18,7 +21,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
18
21
|
A dependency function that can be used with FastAPI's Depends()
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
|
-
def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
|
|
24
|
+
async def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
|
|
22
25
|
# If no security key is set, skip authentication entirely
|
|
23
26
|
if not settings or not settings.os_security_key:
|
|
24
27
|
return True
|
|
@@ -40,7 +43,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
40
43
|
|
|
41
44
|
def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
|
|
42
45
|
"""
|
|
43
|
-
Validate a bearer token for WebSocket authentication.
|
|
46
|
+
Validate a bearer token for WebSocket authentication (legacy os_security_key method).
|
|
44
47
|
|
|
45
48
|
Args:
|
|
46
49
|
token: The bearer token to validate
|
|
@@ -55,3 +58,187 @@ def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
|
|
|
55
58
|
|
|
56
59
|
# Verify the token matches the configured security key
|
|
57
60
|
return token == settings.os_security_key
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_accessible_resources(request: Request, resource_type: str) -> Set[str]:
|
|
64
|
+
"""
|
|
65
|
+
Get the set of resource IDs the user has access to based on their scopes.
|
|
66
|
+
|
|
67
|
+
This function is used to filter lists of resources (agents, teams, workflows)
|
|
68
|
+
based on the user's scopes from their JWT token.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
request: The FastAPI request object (contains request.state.scopes)
|
|
72
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Set of resource IDs the user can access. Returns {"*"} for wildcard access.
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
accessible_ids = get_accessible_resources(request, "agents")
|
|
79
|
+
if "*" not in accessible_ids:
|
|
80
|
+
agents = [a for a in agents if a.id in accessible_ids]
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
>>> # User with specific agent access
|
|
84
|
+
>>> # Token scopes: ["agent-os:my-os:agents:my-agent:read"]
|
|
85
|
+
>>> get_accessible_resources(request, "agents")
|
|
86
|
+
{'my-agent'}
|
|
87
|
+
|
|
88
|
+
>>> # User with wildcard access
|
|
89
|
+
>>> # Token scopes: ["agent-os:my-os:agents:*:read"] or ["admin"]
|
|
90
|
+
>>> get_accessible_resources(request, "agents")
|
|
91
|
+
{'*'}
|
|
92
|
+
|
|
93
|
+
>>> # User with agent-os level access (global resource scope)
|
|
94
|
+
>>> # Token scopes: ["agent-os:my-os:agents:read"]
|
|
95
|
+
>>> get_accessible_resources(request, "agents")
|
|
96
|
+
{'*'}
|
|
97
|
+
"""
|
|
98
|
+
# Check if accessible_resource_ids is already cached in request state (set by JWT middleware)
|
|
99
|
+
# This happens when user doesn't have global scope but has specific resource scopes
|
|
100
|
+
cached_ids = getattr(request.state, "accessible_resource_ids", None)
|
|
101
|
+
if cached_ids is not None:
|
|
102
|
+
return cached_ids
|
|
103
|
+
|
|
104
|
+
# Get user's scopes from request state (set by JWT middleware)
|
|
105
|
+
user_scopes = getattr(request.state, "scopes", [])
|
|
106
|
+
|
|
107
|
+
# Get accessible resource IDs
|
|
108
|
+
accessible_ids = get_accessible_resource_ids(user_scopes=user_scopes, resource_type=resource_type)
|
|
109
|
+
|
|
110
|
+
return accessible_ids
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def filter_resources_by_access(request: Request, resources: List, resource_type: str) -> List:
|
|
114
|
+
"""
|
|
115
|
+
Filter a list of resources based on user's access permissions.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
request: The FastAPI request object
|
|
119
|
+
resources: List of resource objects (agents, teams, or workflows) with 'id' attribute
|
|
120
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Filtered list of resources the user has access to
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
agents = filter_resources_by_access(request, all_agents, "agents")
|
|
127
|
+
teams = filter_resources_by_access(request, all_teams, "teams")
|
|
128
|
+
workflows = filter_resources_by_access(request, all_workflows, "workflows")
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> # User with specific access
|
|
132
|
+
>>> agents = [Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
|
|
133
|
+
>>> # Token scopes: ["agent-os:my-os:agents:agent-1:read", "agent-os:my-os:agents:agent-2:read"]
|
|
134
|
+
>>> filter_resources_by_access(request, agents, "agents")
|
|
135
|
+
[Agent(id="agent-1"), Agent(id="agent-2")]
|
|
136
|
+
|
|
137
|
+
>>> # User with wildcard access
|
|
138
|
+
>>> # Token scopes: ["admin"]
|
|
139
|
+
>>> filter_resources_by_access(request, agents, "agents")
|
|
140
|
+
[Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
|
|
141
|
+
"""
|
|
142
|
+
accessible_ids = get_accessible_resources(request, resource_type)
|
|
143
|
+
|
|
144
|
+
# Wildcard access - return all resources
|
|
145
|
+
if "*" in accessible_ids:
|
|
146
|
+
return resources
|
|
147
|
+
|
|
148
|
+
# Filter to only accessible resources
|
|
149
|
+
return [r for r in resources if r.id in accessible_ids]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_resource_access(request: Request, resource_id: str, resource_type: str, action: str = "read") -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Check if user has access to a specific resource.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
request: The FastAPI request object
|
|
158
|
+
resource_id: ID of the resource to check
|
|
159
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
160
|
+
action: Action to check ("read", "run", etc.)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if user has access, False otherwise
|
|
164
|
+
|
|
165
|
+
Usage:
|
|
166
|
+
if not check_resource_access(request, agent_id, "agents", "run"):
|
|
167
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
>>> # Token scopes: ["agent-os:my-os:agents:my-agent:read", "agent-os:my-os:agents:my-agent:run"]
|
|
171
|
+
>>> check_resource_access(request, "my-agent", "agents", "run")
|
|
172
|
+
True
|
|
173
|
+
|
|
174
|
+
>>> check_resource_access(request, "other-agent", "agents", "run")
|
|
175
|
+
False
|
|
176
|
+
"""
|
|
177
|
+
accessible_ids = get_accessible_resources(request, resource_type)
|
|
178
|
+
|
|
179
|
+
# Wildcard access grants all permissions
|
|
180
|
+
if "*" in accessible_ids:
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
# Check if user has access to this specific resource
|
|
184
|
+
return resource_id in accessible_ids
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def require_resource_access(resource_type: str, action: str, resource_id_param: str):
|
|
188
|
+
"""
|
|
189
|
+
Create a dependency that checks if the user has access to a specific resource.
|
|
190
|
+
|
|
191
|
+
This dependency factory creates a FastAPI dependency that automatically checks
|
|
192
|
+
authorization when authorization is enabled. It extracts the resource ID from
|
|
193
|
+
the path parameters and verifies the user has the required access.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
197
|
+
action: Action to check ("read", "run")
|
|
198
|
+
resource_id_param: Name of the path parameter containing the resource ID
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
A dependency function for use with FastAPI's Depends()
|
|
202
|
+
|
|
203
|
+
Usage:
|
|
204
|
+
@router.post("/agents/{agent_id}/runs")
|
|
205
|
+
async def create_agent_run(
|
|
206
|
+
agent_id: str,
|
|
207
|
+
request: Request,
|
|
208
|
+
_: None = Depends(require_resource_access("agents", "run", "agent_id")),
|
|
209
|
+
):
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
@router.get("/agents/{agent_id}")
|
|
213
|
+
async def get_agent(
|
|
214
|
+
agent_id: str,
|
|
215
|
+
request: Request,
|
|
216
|
+
_: None = Depends(require_resource_access("agents", "read", "agent_id")),
|
|
217
|
+
):
|
|
218
|
+
...
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
>>> # Creates dependency for checking agent run access
|
|
222
|
+
>>> dep = require_resource_access("agents", "run", "agent_id")
|
|
223
|
+
|
|
224
|
+
>>> # Creates dependency for checking team read access
|
|
225
|
+
>>> dep = require_resource_access("teams", "read", "team_id")
|
|
226
|
+
"""
|
|
227
|
+
# Map resource_type to singular form for error messages
|
|
228
|
+
resource_singular = {
|
|
229
|
+
"agents": "agent",
|
|
230
|
+
"teams": "team",
|
|
231
|
+
"workflows": "workflow",
|
|
232
|
+
}.get(resource_type, resource_type.rstrip("s"))
|
|
233
|
+
|
|
234
|
+
async def dependency(request: Request):
|
|
235
|
+
# Only check authorization if it's enabled
|
|
236
|
+
if not getattr(request.state, "authorization_enabled", False):
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Get the resource_id from path parameters
|
|
240
|
+
resource_id = request.path_params.get(resource_id_param)
|
|
241
|
+
if resource_id and not check_resource_access(request, resource_id, resource_type, action):
|
|
242
|
+
raise HTTPException(status_code=403, detail=f"Access denied to {action} this {resource_singular}")
|
|
243
|
+
|
|
244
|
+
return dependency
|
agno/os/config.py
CHANGED
|
@@ -5,6 +5,15 @@ from typing import Generic, List, Optional, TypeVar
|
|
|
5
5
|
from pydantic import BaseModel, field_validator
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class AuthorizationConfig(BaseModel):
|
|
9
|
+
"""Configuration for the JWT middleware"""
|
|
10
|
+
|
|
11
|
+
verification_keys: Optional[List[str]] = None
|
|
12
|
+
jwks_file: Optional[str] = None
|
|
13
|
+
algorithm: Optional[str] = None
|
|
14
|
+
verify_audience: Optional[bool] = None
|
|
15
|
+
|
|
16
|
+
|
|
8
17
|
class EvalsDomainConfig(BaseModel):
|
|
9
18
|
"""Configuration for the Evals domain of the AgentOS"""
|
|
10
19
|
|
|
@@ -36,6 +45,12 @@ class MemoryDomainConfig(BaseModel):
|
|
|
36
45
|
display_name: Optional[str] = None
|
|
37
46
|
|
|
38
47
|
|
|
48
|
+
class TracesDomainConfig(BaseModel):
|
|
49
|
+
"""Configuration for the Traces domain of the AgentOS"""
|
|
50
|
+
|
|
51
|
+
display_name: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
|
|
39
54
|
DomainConfigType = TypeVar("DomainConfigType")
|
|
40
55
|
|
|
41
56
|
|
|
@@ -44,6 +59,7 @@ class DatabaseConfig(BaseModel, Generic[DomainConfigType]):
|
|
|
44
59
|
|
|
45
60
|
db_id: str
|
|
46
61
|
domain_config: Optional[DomainConfigType] = None
|
|
62
|
+
tables: Optional[List[str]] = None
|
|
47
63
|
|
|
48
64
|
|
|
49
65
|
class EvalsConfig(EvalsDomainConfig):
|
|
@@ -76,6 +92,12 @@ class MetricsConfig(MetricsDomainConfig):
|
|
|
76
92
|
dbs: Optional[List[DatabaseConfig[MetricsDomainConfig]]] = None
|
|
77
93
|
|
|
78
94
|
|
|
95
|
+
class TracesConfig(TracesDomainConfig):
|
|
96
|
+
"""Configuration for the Traces domain of the AgentOS"""
|
|
97
|
+
|
|
98
|
+
dbs: Optional[List[DatabaseConfig[TracesDomainConfig]]] = None
|
|
99
|
+
|
|
100
|
+
|
|
79
101
|
class ChatConfig(BaseModel):
|
|
80
102
|
"""Configuration for the Chat page of the AgentOS"""
|
|
81
103
|
|
|
@@ -101,3 +123,4 @@ class AgentOSConfig(BaseModel):
|
|
|
101
123
|
memory: Optional[MemoryConfig] = None
|
|
102
124
|
session: Optional[SessionConfig] = None
|
|
103
125
|
metrics: Optional[MetricsConfig] = None
|
|
126
|
+
traces: Optional[TracesConfig] = None
|
agno/os/interfaces/a2a/router.py
CHANGED
|
@@ -11,7 +11,7 @@ from typing_extensions import List
|
|
|
11
11
|
try:
|
|
12
12
|
from a2a.types import SendMessageSuccessResponse, Task, TaskState, TaskStatus
|
|
13
13
|
except ImportError as e:
|
|
14
|
-
raise ImportError("`a2a` not installed. Please install it with `pip install -U a2a`") from e
|
|
14
|
+
raise ImportError("`a2a` not installed. Please install it with `pip install -U a2a-sdk`") from e
|
|
15
15
|
|
|
16
16
|
from agno.agent import Agent
|
|
17
17
|
from agno.os.interfaces.a2a.utils import (
|
|
@@ -19,8 +19,7 @@ from agno.os.interfaces.a2a.utils import (
|
|
|
19
19
|
map_run_output_to_a2a_task,
|
|
20
20
|
stream_a2a_response_with_error_handling,
|
|
21
21
|
)
|
|
22
|
-
from agno.os.
|
|
23
|
-
from agno.os.utils import get_agent_by_id, get_team_by_id, get_workflow_by_id
|
|
22
|
+
from agno.os.utils import get_agent_by_id, get_request_kwargs, get_team_by_id, get_workflow_by_id
|
|
24
23
|
from agno.team import Team
|
|
25
24
|
from agno.workflow import Workflow
|
|
26
25
|
|
|
@@ -36,9 +35,8 @@ def attach_routes(
|
|
|
36
35
|
|
|
37
36
|
@router.post(
|
|
38
37
|
"/message/send",
|
|
39
|
-
tags=["A2A"],
|
|
40
38
|
operation_id="send_message",
|
|
41
|
-
|
|
39
|
+
name="send_message",
|
|
42
40
|
description="Send a message to an Agno Agent, Team, or Workflow. "
|
|
43
41
|
"The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
|
|
44
42
|
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
|
|
@@ -76,7 +74,7 @@ def attach_routes(
|
|
|
76
74
|
)
|
|
77
75
|
async def a2a_send_message(request: Request):
|
|
78
76
|
request_body = await request.json()
|
|
79
|
-
kwargs = await
|
|
77
|
+
kwargs = await get_request_kwargs(request, a2a_send_message)
|
|
80
78
|
|
|
81
79
|
# 1. Get the Agent, Team, or Workflow to run
|
|
82
80
|
agent_id = request_body.get("params", {}).get("message", {}).get("agentId") or request.headers.get("X-Agent-ID")
|
|
@@ -159,9 +157,8 @@ def attach_routes(
|
|
|
159
157
|
|
|
160
158
|
@router.post(
|
|
161
159
|
"/message/stream",
|
|
162
|
-
tags=["A2A"],
|
|
163
160
|
operation_id="stream_message",
|
|
164
|
-
|
|
161
|
+
name="stream_message",
|
|
165
162
|
description="Stream a message to an Agno Agent, Team, or Workflow."
|
|
166
163
|
"The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
|
|
167
164
|
"Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
|
|
@@ -183,7 +180,7 @@ def attach_routes(
|
|
|
183
180
|
)
|
|
184
181
|
async def a2a_stream_message(request: Request):
|
|
185
182
|
request_body = await request.json()
|
|
186
|
-
kwargs = await
|
|
183
|
+
kwargs = await get_request_kwargs(request, a2a_stream_message)
|
|
187
184
|
|
|
188
185
|
# 1. Get the Agent, Team, or Workflow to run
|
|
189
186
|
agent_id = request_body.get("params", {}).get("message", {}).get("agentId")
|
|
@@ -223,7 +220,7 @@ def attach_routes(
|
|
|
223
220
|
session_id=context_id,
|
|
224
221
|
user_id=user_id,
|
|
225
222
|
stream=True,
|
|
226
|
-
|
|
223
|
+
stream_events=True,
|
|
227
224
|
**kwargs,
|
|
228
225
|
)
|
|
229
226
|
else:
|
|
@@ -236,7 +233,7 @@ def attach_routes(
|
|
|
236
233
|
session_id=context_id,
|
|
237
234
|
user_id=user_id,
|
|
238
235
|
stream=True,
|
|
239
|
-
|
|
236
|
+
stream_events=True,
|
|
240
237
|
**kwargs,
|
|
241
238
|
)
|
|
242
239
|
|
agno/os/interfaces/a2a/utils.py
CHANGED
|
@@ -110,7 +110,7 @@ async def map_a2a_request_to_run_input(request_body: dict, stream: bool = True)
|
|
|
110
110
|
|
|
111
111
|
Returns:
|
|
112
112
|
RunInput: The Agno RunInput
|
|
113
|
-
stream:
|
|
113
|
+
stream: Whether we are in stream mode
|
|
114
114
|
"""
|
|
115
115
|
|
|
116
116
|
# 1. Validate the request
|
|
@@ -19,6 +19,7 @@ from agno.agent.agent import Agent
|
|
|
19
19
|
from agno.os.interfaces.agui.utils import (
|
|
20
20
|
async_stream_agno_response_as_agui_events,
|
|
21
21
|
convert_agui_messages_to_agno_messages,
|
|
22
|
+
validate_agui_state,
|
|
22
23
|
)
|
|
23
24
|
from agno.team.team import Team
|
|
24
25
|
|
|
@@ -32,6 +33,7 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
|
|
|
32
33
|
try:
|
|
33
34
|
# Preparing the input for the Agent and emitting the run started event
|
|
34
35
|
messages = convert_agui_messages_to_agno_messages(run_input.messages or [])
|
|
36
|
+
|
|
35
37
|
yield RunStartedEvent(type=EventType.RUN_STARTED, thread_id=run_input.thread_id, run_id=run_id)
|
|
36
38
|
|
|
37
39
|
# Look for user_id in run_input.forwarded_props
|
|
@@ -39,13 +41,18 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
|
|
|
39
41
|
if run_input.forwarded_props and isinstance(run_input.forwarded_props, dict):
|
|
40
42
|
user_id = run_input.forwarded_props.get("user_id")
|
|
41
43
|
|
|
44
|
+
# Validating the session state is of the expected type (dict)
|
|
45
|
+
session_state = validate_agui_state(run_input.state, run_input.thread_id)
|
|
46
|
+
|
|
42
47
|
# Request streaming response from agent
|
|
43
48
|
response_stream = agent.arun(
|
|
44
49
|
input=messages,
|
|
45
50
|
session_id=run_input.thread_id,
|
|
46
51
|
stream=True,
|
|
47
|
-
|
|
52
|
+
stream_events=True,
|
|
48
53
|
user_id=user_id,
|
|
54
|
+
session_state=session_state,
|
|
55
|
+
run_id=run_id,
|
|
49
56
|
)
|
|
50
57
|
|
|
51
58
|
# Stream the response content in AG-UI format
|
|
@@ -75,13 +82,18 @@ async def run_team(team: Team, input: RunAgentInput) -> AsyncIterator[BaseEvent]
|
|
|
75
82
|
if input.forwarded_props and isinstance(input.forwarded_props, dict):
|
|
76
83
|
user_id = input.forwarded_props.get("user_id")
|
|
77
84
|
|
|
85
|
+
# Validating the session state is of the expected type (dict)
|
|
86
|
+
session_state = validate_agui_state(input.state, input.thread_id)
|
|
87
|
+
|
|
78
88
|
# Request streaming response from team
|
|
79
89
|
response_stream = team.arun(
|
|
80
90
|
input=messages,
|
|
81
91
|
session_id=input.thread_id,
|
|
82
92
|
stream=True,
|
|
83
|
-
|
|
93
|
+
stream_steps=True,
|
|
84
94
|
user_id=user_id,
|
|
95
|
+
session_state=session_state,
|
|
96
|
+
run_id=run_id,
|
|
85
97
|
)
|
|
86
98
|
|
|
87
99
|
# Stream the response content in AG-UI format
|
|
@@ -101,7 +113,10 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
101
113
|
|
|
102
114
|
encoder = EventEncoder()
|
|
103
115
|
|
|
104
|
-
@router.post(
|
|
116
|
+
@router.post(
|
|
117
|
+
"/agui",
|
|
118
|
+
name="run_agent",
|
|
119
|
+
)
|
|
105
120
|
async def run_agent_agui(run_input: RunAgentInput):
|
|
106
121
|
async def event_generator():
|
|
107
122
|
if agent:
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import uuid
|
|
5
5
|
from collections.abc import Iterator
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import AsyncIterator, List, Set, Tuple, Union
|
|
6
|
+
from dataclasses import asdict, dataclass, is_dataclass
|
|
7
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Set, Tuple, Union
|
|
8
8
|
|
|
9
9
|
from ag_ui.core import (
|
|
10
10
|
BaseEvent,
|
|
11
|
+
CustomEvent,
|
|
11
12
|
EventType,
|
|
12
13
|
RunFinishedEvent,
|
|
13
14
|
StepFinishedEvent,
|
|
@@ -21,14 +22,48 @@ from ag_ui.core import (
|
|
|
21
22
|
ToolCallStartEvent,
|
|
22
23
|
)
|
|
23
24
|
from ag_ui.core.types import Message as AGUIMessage
|
|
25
|
+
from pydantic import BaseModel
|
|
24
26
|
|
|
25
27
|
from agno.models.message import Message
|
|
26
28
|
from agno.run.agent import RunContentEvent, RunEvent, RunOutputEvent, RunPausedEvent
|
|
27
29
|
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
28
30
|
from agno.run.team import TeamRunEvent, TeamRunOutputEvent
|
|
31
|
+
from agno.utils.log import log_debug, log_warning
|
|
29
32
|
from agno.utils.message import get_text_from_message
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
def validate_agui_state(state: Any, thread_id: str) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""Validate the given AGUI state is of the expected type (dict)."""
|
|
37
|
+
if state is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if isinstance(state, dict):
|
|
41
|
+
return state
|
|
42
|
+
|
|
43
|
+
if isinstance(state, BaseModel):
|
|
44
|
+
try:
|
|
45
|
+
return state.model_dump()
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
if is_dataclass(state):
|
|
50
|
+
try:
|
|
51
|
+
return asdict(state) # type: ignore
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
if hasattr(state, "to_dict") and callable(getattr(state, "to_dict")):
|
|
56
|
+
try:
|
|
57
|
+
result = state.to_dict() # type: ignore
|
|
58
|
+
if isinstance(result, dict):
|
|
59
|
+
return result
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
log_warning(f"AGUI state must be a dict, got {type(state).__name__}. State will be ignored. Thread: {thread_id}")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
32
67
|
@dataclass
|
|
33
68
|
class EventBuffer:
|
|
34
69
|
"""Buffer to manage event ordering constraints, relevant when mapping Agno responses to AG-UI events."""
|
|
@@ -81,23 +116,43 @@ class EventBuffer:
|
|
|
81
116
|
|
|
82
117
|
def convert_agui_messages_to_agno_messages(messages: List[AGUIMessage]) -> List[Message]:
|
|
83
118
|
"""Convert AG-UI messages to Agno messages."""
|
|
84
|
-
|
|
119
|
+
# First pass: collect all tool_call_ids that have results
|
|
120
|
+
tool_call_ids_with_results: Set[str] = set()
|
|
121
|
+
for msg in messages:
|
|
122
|
+
if msg.role == "tool" and msg.tool_call_id:
|
|
123
|
+
tool_call_ids_with_results.add(msg.tool_call_id)
|
|
124
|
+
|
|
125
|
+
# Second pass: convert messages
|
|
126
|
+
result: List[Message] = []
|
|
127
|
+
seen_tool_call_ids: Set[str] = set()
|
|
128
|
+
|
|
85
129
|
for msg in messages:
|
|
86
130
|
if msg.role == "tool":
|
|
131
|
+
# Deduplicate tool results - keep only first occurrence
|
|
132
|
+
if msg.tool_call_id in seen_tool_call_ids:
|
|
133
|
+
log_debug(f"Skipping duplicate AGUI tool result: {msg.tool_call_id}")
|
|
134
|
+
continue
|
|
135
|
+
seen_tool_call_ids.add(msg.tool_call_id)
|
|
87
136
|
result.append(Message(role="tool", tool_call_id=msg.tool_call_id, content=msg.content))
|
|
137
|
+
|
|
88
138
|
elif msg.role == "assistant":
|
|
89
139
|
tool_calls = None
|
|
90
140
|
if msg.tool_calls:
|
|
91
|
-
tool_calls
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
)
|
|
141
|
+
# Filter tool_calls to only those with results in this message sequence
|
|
142
|
+
filtered_calls = [call for call in msg.tool_calls if call.id in tool_call_ids_with_results]
|
|
143
|
+
if filtered_calls:
|
|
144
|
+
tool_calls = [call.model_dump() for call in filtered_calls]
|
|
145
|
+
result.append(Message(role="assistant", content=msg.content, tool_calls=tool_calls))
|
|
146
|
+
|
|
99
147
|
elif msg.role == "user":
|
|
100
148
|
result.append(Message(role="user", content=msg.content))
|
|
149
|
+
|
|
150
|
+
elif msg.role == "system":
|
|
151
|
+
pass # Skip - agent builds its own system message from configuration
|
|
152
|
+
|
|
153
|
+
else:
|
|
154
|
+
log_warning(f"Unknown AGUI message role: {msg.role}")
|
|
155
|
+
|
|
101
156
|
return result
|
|
102
157
|
|
|
103
158
|
|
|
@@ -215,7 +270,25 @@ def _create_events_from_chunk(
|
|
|
215
270
|
parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
|
|
216
271
|
|
|
217
272
|
if not parent_message_id:
|
|
218
|
-
|
|
273
|
+
# Create parent message for tool calls without preceding assistant message
|
|
274
|
+
parent_message_id = str(uuid.uuid4())
|
|
275
|
+
|
|
276
|
+
# Emit a text message to serve as the parent
|
|
277
|
+
text_start = TextMessageStartEvent(
|
|
278
|
+
type=EventType.TEXT_MESSAGE_START,
|
|
279
|
+
message_id=parent_message_id,
|
|
280
|
+
role="assistant",
|
|
281
|
+
)
|
|
282
|
+
events_to_emit.append(text_start)
|
|
283
|
+
|
|
284
|
+
text_end = TextMessageEndEvent(
|
|
285
|
+
type=EventType.TEXT_MESSAGE_END,
|
|
286
|
+
message_id=parent_message_id,
|
|
287
|
+
)
|
|
288
|
+
events_to_emit.append(text_end)
|
|
289
|
+
|
|
290
|
+
# Set this as the pending parent for subsequent tool calls in this batch
|
|
291
|
+
event_buffer.set_pending_tool_calls_parent_id(parent_message_id)
|
|
219
292
|
|
|
220
293
|
start_event = ToolCallStartEvent(
|
|
221
294
|
type=EventType.TOOL_CALL_START,
|
|
@@ -261,6 +334,23 @@ def _create_events_from_chunk(
|
|
|
261
334
|
step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
|
|
262
335
|
events_to_emit.append(step_finished_event)
|
|
263
336
|
|
|
337
|
+
# Handle custom events
|
|
338
|
+
elif chunk.event == RunEvent.custom_event:
|
|
339
|
+
# Use the name of the event class if available, otherwise default to the CustomEvent
|
|
340
|
+
try:
|
|
341
|
+
custom_event_name = chunk.__class__.__name__
|
|
342
|
+
except Exception:
|
|
343
|
+
custom_event_name = chunk.event
|
|
344
|
+
|
|
345
|
+
# Use the complete Agno event as value if parsing it works, else the event content field
|
|
346
|
+
try:
|
|
347
|
+
custom_event_value = chunk.to_dict()
|
|
348
|
+
except Exception:
|
|
349
|
+
custom_event_value = chunk.content # type: ignore
|
|
350
|
+
|
|
351
|
+
custom_event = CustomEvent(name=custom_event_name, value=custom_event_value)
|
|
352
|
+
events_to_emit.append(custom_event)
|
|
353
|
+
|
|
264
354
|
return events_to_emit, message_started, message_id
|
|
265
355
|
|
|
266
356
|
|
|
@@ -289,37 +379,60 @@ def _create_completion_events(
|
|
|
289
379
|
end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
|
|
290
380
|
events_to_emit.append(end_message_event)
|
|
291
381
|
|
|
292
|
-
#
|
|
293
|
-
if isinstance(chunk, RunPausedEvent)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
start_event = ToolCallStartEvent(
|
|
304
|
-
type=EventType.TOOL_CALL_START,
|
|
305
|
-
tool_call_id=tool.tool_call_id,
|
|
306
|
-
tool_call_name=tool.tool_name,
|
|
307
|
-
parent_message_id=parent_message_id,
|
|
382
|
+
# Emit external execution tools
|
|
383
|
+
if isinstance(chunk, RunPausedEvent):
|
|
384
|
+
external_tools = chunk.tools_awaiting_external_execution
|
|
385
|
+
if external_tools:
|
|
386
|
+
# First, emit an assistant message for external tool calls
|
|
387
|
+
assistant_message_id = str(uuid.uuid4())
|
|
388
|
+
assistant_start_event = TextMessageStartEvent(
|
|
389
|
+
type=EventType.TEXT_MESSAGE_START,
|
|
390
|
+
message_id=assistant_message_id,
|
|
391
|
+
role="assistant",
|
|
308
392
|
)
|
|
309
|
-
events_to_emit.append(
|
|
393
|
+
events_to_emit.append(assistant_start_event)
|
|
394
|
+
|
|
395
|
+
# Add any text content if present for the assistant message
|
|
396
|
+
if chunk.content:
|
|
397
|
+
content_event = TextMessageContentEvent(
|
|
398
|
+
type=EventType.TEXT_MESSAGE_CONTENT,
|
|
399
|
+
message_id=assistant_message_id,
|
|
400
|
+
delta=str(chunk.content),
|
|
401
|
+
)
|
|
402
|
+
events_to_emit.append(content_event)
|
|
310
403
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
404
|
+
# End the assistant message
|
|
405
|
+
assistant_end_event = TextMessageEndEvent(
|
|
406
|
+
type=EventType.TEXT_MESSAGE_END,
|
|
407
|
+
message_id=assistant_message_id,
|
|
315
408
|
)
|
|
316
|
-
events_to_emit.append(
|
|
409
|
+
events_to_emit.append(assistant_end_event)
|
|
410
|
+
|
|
411
|
+
# Emit tool call events for external execution
|
|
412
|
+
for tool in external_tools:
|
|
413
|
+
if tool.tool_call_id is None or tool.tool_name is None:
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
start_event = ToolCallStartEvent(
|
|
417
|
+
type=EventType.TOOL_CALL_START,
|
|
418
|
+
tool_call_id=tool.tool_call_id,
|
|
419
|
+
tool_call_name=tool.tool_name,
|
|
420
|
+
parent_message_id=assistant_message_id, # Use the assistant message as parent
|
|
421
|
+
)
|
|
422
|
+
events_to_emit.append(start_event)
|
|
317
423
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
424
|
+
args_event = ToolCallArgsEvent(
|
|
425
|
+
type=EventType.TOOL_CALL_ARGS,
|
|
426
|
+
tool_call_id=tool.tool_call_id,
|
|
427
|
+
delta=json.dumps(tool.tool_args),
|
|
428
|
+
)
|
|
429
|
+
events_to_emit.append(args_event)
|
|
430
|
+
|
|
431
|
+
end_event = ToolCallEndEvent(
|
|
432
|
+
type=EventType.TOOL_CALL_END,
|
|
433
|
+
tool_call_id=tool.tool_call_id,
|
|
434
|
+
)
|
|
435
|
+
events_to_emit.append(end_event)
|
|
323
436
|
|
|
324
437
|
run_finished_event = RunFinishedEvent(type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id)
|
|
325
438
|
events_to_emit.append(run_finished_event)
|