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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from os import getenv
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import jwt
|
|
7
|
+
from fastapi import Request, Response
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
10
|
+
|
|
11
|
+
from agno.utils.log import log_debug
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenSource(str, Enum):
|
|
15
|
+
"""Enum for JWT token source options."""
|
|
16
|
+
|
|
17
|
+
HEADER = "header"
|
|
18
|
+
COOKIE = "cookie"
|
|
19
|
+
BOTH = "both" # Try header first, then cookie
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JWTMiddleware(BaseHTTPMiddleware):
|
|
23
|
+
"""
|
|
24
|
+
JWT Middleware for validating tokens and storing JWT claims in request state.
|
|
25
|
+
|
|
26
|
+
This middleware:
|
|
27
|
+
1. Extracts JWT token from Authorization header, cookies, or both
|
|
28
|
+
2. Decodes and validates the token
|
|
29
|
+
3. Stores JWT claims in request.state for easy access in endpoints
|
|
30
|
+
|
|
31
|
+
Token Sources:
|
|
32
|
+
- "header": Extract from Authorization header (default)
|
|
33
|
+
- "cookie": Extract from HTTP cookie
|
|
34
|
+
- "both": Try header first, then cookie as fallback
|
|
35
|
+
|
|
36
|
+
Claims are stored as:
|
|
37
|
+
- request.state.user_id: User ID from configured claim
|
|
38
|
+
- request.state.session_id: Session ID from configured claim
|
|
39
|
+
- request.state.dependencies: Dictionary of dependency claims
|
|
40
|
+
- request.state.session_state: Dictionary of session state claims
|
|
41
|
+
- request.state.authenticated: Boolean authentication status
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
app,
|
|
48
|
+
secret_key: Optional[str] = None,
|
|
49
|
+
algorithm: str = "HS256",
|
|
50
|
+
token_source: TokenSource = TokenSource.HEADER,
|
|
51
|
+
token_header_key: str = "Authorization",
|
|
52
|
+
cookie_name: str = "access_token",
|
|
53
|
+
validate: bool = True,
|
|
54
|
+
excluded_route_paths: Optional[List[str]] = None,
|
|
55
|
+
scopes_claim: Optional[str] = None,
|
|
56
|
+
user_id_claim: str = "sub",
|
|
57
|
+
session_id_claim: str = "session_id",
|
|
58
|
+
dependencies_claims: Optional[List[str]] = None,
|
|
59
|
+
session_state_claims: Optional[List[str]] = None,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize the JWT middleware.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
app: The FastAPI app instance
|
|
66
|
+
secret_key: The secret key to use for JWT validation (optional, will use JWT_SECRET_KEY environment variable if not provided)
|
|
67
|
+
algorithm: The algorithm to use for JWT validation
|
|
68
|
+
token_header_key: The key to use for the Authorization header (only used when token_source is header)
|
|
69
|
+
token_source: Where to extract the JWT token from (header, cookie, or both)
|
|
70
|
+
cookie_name: The name of the cookie containing the JWT token (only used when token_source is cookie/both)
|
|
71
|
+
validate: Whether to validate the JWT token
|
|
72
|
+
excluded_route_paths: A list of route paths to exclude from JWT validation
|
|
73
|
+
scopes_claim: The claim to use for scopes extraction
|
|
74
|
+
user_id_claim: The claim to use for user ID extraction
|
|
75
|
+
session_id_claim: The claim to use for session ID extraction
|
|
76
|
+
dependencies_claims: A list of claims to extract from the JWT token for dependencies
|
|
77
|
+
session_state_claims: A list of claims to extract from the JWT token for session state
|
|
78
|
+
"""
|
|
79
|
+
super().__init__(app)
|
|
80
|
+
self.secret_key = secret_key or getenv("JWT_SECRET_KEY")
|
|
81
|
+
if not self.secret_key:
|
|
82
|
+
raise ValueError("Secret key is required")
|
|
83
|
+
self.algorithm = algorithm
|
|
84
|
+
self.token_header_key = token_header_key
|
|
85
|
+
self.token_source = token_source
|
|
86
|
+
self.cookie_name = cookie_name
|
|
87
|
+
self.validate = validate
|
|
88
|
+
self.excluded_route_paths = excluded_route_paths
|
|
89
|
+
self.scopes_claim = scopes_claim
|
|
90
|
+
self.user_id_claim = user_id_claim
|
|
91
|
+
self.session_id_claim = session_id_claim
|
|
92
|
+
self.dependencies_claims = dependencies_claims or []
|
|
93
|
+
self.session_state_claims = session_state_claims or []
|
|
94
|
+
|
|
95
|
+
def _extract_token_from_header(self, request: Request) -> Optional[str]:
|
|
96
|
+
"""Extract JWT token from Authorization header."""
|
|
97
|
+
authorization = request.headers.get(self.token_header_key, "")
|
|
98
|
+
if not authorization:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Remove the "Bearer " prefix (if present)
|
|
103
|
+
_, token = authorization.split(" ", 1)
|
|
104
|
+
return token
|
|
105
|
+
except ValueError:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _extract_token_from_cookie(self, request: Request) -> Optional[str]:
|
|
109
|
+
"""Extract JWT token from cookie."""
|
|
110
|
+
return request.cookies.get(self.cookie_name)
|
|
111
|
+
|
|
112
|
+
def _extract_token(self, request: Request) -> Optional[str]:
|
|
113
|
+
"""Extract JWT token based on configured token source."""
|
|
114
|
+
if self.token_source == TokenSource.HEADER:
|
|
115
|
+
return self._extract_token_from_header(request)
|
|
116
|
+
elif self.token_source == TokenSource.COOKIE:
|
|
117
|
+
return self._extract_token_from_cookie(request)
|
|
118
|
+
elif self.token_source == TokenSource.BOTH:
|
|
119
|
+
# Try header first, then cookie
|
|
120
|
+
token = self._extract_token_from_header(request)
|
|
121
|
+
if token is None:
|
|
122
|
+
token = self._extract_token_from_cookie(request)
|
|
123
|
+
return token
|
|
124
|
+
else:
|
|
125
|
+
log_debug(f"Unknown token source: {self.token_source}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def _get_missing_token_error_message(self) -> str:
|
|
129
|
+
"""Get appropriate error message for missing token based on token source."""
|
|
130
|
+
if self.token_source == TokenSource.HEADER:
|
|
131
|
+
return "Authorization header missing"
|
|
132
|
+
elif self.token_source == TokenSource.COOKIE:
|
|
133
|
+
return f"JWT cookie '{self.cookie_name}' missing"
|
|
134
|
+
elif self.token_source == TokenSource.BOTH:
|
|
135
|
+
return f"JWT token missing from both Authorization header and '{self.cookie_name}' cookie"
|
|
136
|
+
else:
|
|
137
|
+
return "JWT token missing"
|
|
138
|
+
|
|
139
|
+
def _is_route_excluded(self, path: str) -> bool:
|
|
140
|
+
"""Check if a route path matches any of the excluded patterns."""
|
|
141
|
+
if not self.excluded_route_paths:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
for excluded_path in self.excluded_route_paths:
|
|
145
|
+
# Support both exact matches and wildcard patterns
|
|
146
|
+
if fnmatch.fnmatch(path, excluded_path):
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
152
|
+
if self._is_route_excluded(request.url.path):
|
|
153
|
+
return await call_next(request)
|
|
154
|
+
|
|
155
|
+
# Extract JWT token from configured source (header, cookie, or both)
|
|
156
|
+
token = self._extract_token(request)
|
|
157
|
+
|
|
158
|
+
if not token:
|
|
159
|
+
if self.validate:
|
|
160
|
+
error_msg = self._get_missing_token_error_message()
|
|
161
|
+
return JSONResponse(status_code=401, content={"detail": error_msg})
|
|
162
|
+
return await call_next(request)
|
|
163
|
+
|
|
164
|
+
# Decode JWT token
|
|
165
|
+
try:
|
|
166
|
+
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) # type: ignore
|
|
167
|
+
|
|
168
|
+
# Extract scopes claims
|
|
169
|
+
scopes = []
|
|
170
|
+
if self.scopes_claim in payload:
|
|
171
|
+
extracted_scopes = payload[self.scopes_claim]
|
|
172
|
+
if isinstance(extracted_scopes, str):
|
|
173
|
+
scopes = extracted_scopes.split(" ")
|
|
174
|
+
else:
|
|
175
|
+
scopes = extracted_scopes
|
|
176
|
+
if scopes:
|
|
177
|
+
request.state.scopes = scopes
|
|
178
|
+
|
|
179
|
+
# Extract user information
|
|
180
|
+
if self.user_id_claim in payload:
|
|
181
|
+
user_id = payload[self.user_id_claim]
|
|
182
|
+
request.state.user_id = user_id
|
|
183
|
+
if self.session_id_claim in payload:
|
|
184
|
+
session_id = payload[self.session_id_claim]
|
|
185
|
+
request.state.session_id = session_id
|
|
186
|
+
else:
|
|
187
|
+
session_id = None
|
|
188
|
+
|
|
189
|
+
# Extract dependency claims
|
|
190
|
+
dependencies = {}
|
|
191
|
+
for claim in self.dependencies_claims:
|
|
192
|
+
if claim in payload:
|
|
193
|
+
dependencies[claim] = payload[claim]
|
|
194
|
+
|
|
195
|
+
if dependencies:
|
|
196
|
+
request.state.dependencies = dependencies
|
|
197
|
+
|
|
198
|
+
# Extract session state claims
|
|
199
|
+
session_state = {}
|
|
200
|
+
for claim in self.session_state_claims:
|
|
201
|
+
if claim in payload:
|
|
202
|
+
session_state[claim] = payload[claim]
|
|
203
|
+
|
|
204
|
+
if session_state:
|
|
205
|
+
request.state.session_state = session_state
|
|
206
|
+
|
|
207
|
+
request.state.token = token
|
|
208
|
+
request.state.authenticated = True
|
|
209
|
+
|
|
210
|
+
log_debug(f"JWT decoded successfully for user: {user_id}")
|
|
211
|
+
if dependencies:
|
|
212
|
+
log_debug(f"Extracted dependencies: {dependencies}")
|
|
213
|
+
if session_state:
|
|
214
|
+
log_debug(f"Extracted session state: {session_state}")
|
|
215
|
+
|
|
216
|
+
except jwt.ExpiredSignatureError:
|
|
217
|
+
if self.validate:
|
|
218
|
+
return JSONResponse(status_code=401, content={"detail": "Token has expired"})
|
|
219
|
+
request.state.authenticated = False
|
|
220
|
+
request.state.token = token
|
|
221
|
+
|
|
222
|
+
except jwt.InvalidTokenError as e:
|
|
223
|
+
if self.validate:
|
|
224
|
+
return JSONResponse(status_code=401, content={"detail": f"Invalid token: {str(e)}"})
|
|
225
|
+
request.state.authenticated = False
|
|
226
|
+
request.state.token = token
|
|
227
|
+
except Exception as e:
|
|
228
|
+
if self.validate:
|
|
229
|
+
return JSONResponse(status_code=401, content={"detail": f"Error decoding token: {str(e)}"})
|
|
230
|
+
request.state.authenticated = False
|
|
231
|
+
request.state.token = token
|
|
232
|
+
|
|
233
|
+
return await call_next(request)
|
agno/os/router.py
CHANGED
|
@@ -16,6 +16,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
16
16
|
from pydantic import BaseModel
|
|
17
17
|
|
|
18
18
|
from agno.agent.agent import Agent
|
|
19
|
+
from agno.exceptions import InputCheckError, OutputCheckError
|
|
19
20
|
from agno.media import Audio, Image, Video
|
|
20
21
|
from agno.media import File as FileMedia
|
|
21
22
|
from agno.os.auth import get_authentication_dependency, validate_websocket_token
|
|
@@ -74,7 +75,6 @@ async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict
|
|
|
74
75
|
kwargs = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
75
76
|
|
|
76
77
|
# Handle JSON parameters. They are passed as strings and need to be deserialized.
|
|
77
|
-
|
|
78
78
|
if session_state := kwargs.get("session_state"):
|
|
79
79
|
try:
|
|
80
80
|
session_state_dict = json.loads(session_state) # type: ignore
|
|
@@ -82,6 +82,7 @@ async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict
|
|
|
82
82
|
except json.JSONDecodeError:
|
|
83
83
|
kwargs.pop("session_state")
|
|
84
84
|
log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
|
|
85
|
+
|
|
85
86
|
if dependencies := kwargs.get("dependencies"):
|
|
86
87
|
try:
|
|
87
88
|
dependencies_dict = json.loads(dependencies) # type: ignore
|
|
@@ -245,7 +246,14 @@ async def agent_response_streamer(
|
|
|
245
246
|
)
|
|
246
247
|
async for run_response_chunk in run_response:
|
|
247
248
|
yield format_sse_event(run_response_chunk) # type: ignore
|
|
248
|
-
|
|
249
|
+
except (InputCheckError, OutputCheckError) as e:
|
|
250
|
+
error_response = RunErrorEvent(
|
|
251
|
+
content=str(e),
|
|
252
|
+
error_type=e.type,
|
|
253
|
+
error_id=e.error_id,
|
|
254
|
+
additional_data=e.additional_data,
|
|
255
|
+
)
|
|
256
|
+
yield format_sse_event(error_response)
|
|
249
257
|
except Exception as e:
|
|
250
258
|
import traceback
|
|
251
259
|
|
|
@@ -274,6 +282,14 @@ async def agent_continue_response_streamer(
|
|
|
274
282
|
)
|
|
275
283
|
async for run_response_chunk in continue_response:
|
|
276
284
|
yield format_sse_event(run_response_chunk) # type: ignore
|
|
285
|
+
except (InputCheckError, OutputCheckError) as e:
|
|
286
|
+
error_response = RunErrorEvent(
|
|
287
|
+
content=str(e),
|
|
288
|
+
error_type=e.type,
|
|
289
|
+
error_id=e.error_id,
|
|
290
|
+
additional_data=e.additional_data,
|
|
291
|
+
)
|
|
292
|
+
yield format_sse_event(error_response)
|
|
277
293
|
|
|
278
294
|
except Exception as e:
|
|
279
295
|
import traceback
|
|
@@ -281,6 +297,8 @@ async def agent_continue_response_streamer(
|
|
|
281
297
|
traceback.print_exc(limit=3)
|
|
282
298
|
error_response = RunErrorEvent(
|
|
283
299
|
content=str(e),
|
|
300
|
+
error_type=e.type if hasattr(e, "type") else None,
|
|
301
|
+
error_id=e.error_id if hasattr(e, "error_id") else None,
|
|
284
302
|
)
|
|
285
303
|
yield format_sse_event(error_response)
|
|
286
304
|
return
|
|
@@ -313,6 +331,14 @@ async def team_response_streamer(
|
|
|
313
331
|
)
|
|
314
332
|
async for run_response_chunk in run_response:
|
|
315
333
|
yield format_sse_event(run_response_chunk) # type: ignore
|
|
334
|
+
except (InputCheckError, OutputCheckError) as e:
|
|
335
|
+
error_response = TeamRunErrorEvent(
|
|
336
|
+
content=str(e),
|
|
337
|
+
error_type=e.type,
|
|
338
|
+
error_id=e.error_id,
|
|
339
|
+
additional_data=e.additional_data,
|
|
340
|
+
)
|
|
341
|
+
yield format_sse_event(error_response)
|
|
316
342
|
|
|
317
343
|
except Exception as e:
|
|
318
344
|
import traceback
|
|
@@ -320,6 +346,8 @@ async def team_response_streamer(
|
|
|
320
346
|
traceback.print_exc()
|
|
321
347
|
error_response = TeamRunErrorEvent(
|
|
322
348
|
content=str(e),
|
|
349
|
+
error_type=e.type if hasattr(e, "type") else None,
|
|
350
|
+
error_id=e.error_id if hasattr(e, "error_id") else None,
|
|
323
351
|
)
|
|
324
352
|
yield format_sse_event(error_response)
|
|
325
353
|
return
|
|
@@ -366,9 +394,28 @@ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os:
|
|
|
366
394
|
|
|
367
395
|
await websocket_manager.register_workflow_websocket(workflow_run_output.run_id, websocket) # type: ignore
|
|
368
396
|
|
|
397
|
+
except (InputCheckError, OutputCheckError) as e:
|
|
398
|
+
await websocket.send_text(
|
|
399
|
+
json.dumps(
|
|
400
|
+
{
|
|
401
|
+
"event": "error",
|
|
402
|
+
"error": str(e),
|
|
403
|
+
"error_type": e.type,
|
|
404
|
+
"error_id": e.error_id,
|
|
405
|
+
"additional_data": e.additional_data,
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
)
|
|
369
409
|
except Exception as e:
|
|
370
410
|
logger.error(f"Error executing workflow via WebSocket: {e}")
|
|
371
|
-
|
|
411
|
+
error_payload = {
|
|
412
|
+
"event": "error",
|
|
413
|
+
"error": str(e),
|
|
414
|
+
"error_type": e.type if hasattr(e, "type") else None,
|
|
415
|
+
"error_id": e.error_id if hasattr(e, "error_id") else None,
|
|
416
|
+
}
|
|
417
|
+
error_payload = {k: v for k, v in error_payload.items() if v is not None}
|
|
418
|
+
await websocket.send_text(json.dumps(error_payload))
|
|
372
419
|
|
|
373
420
|
|
|
374
421
|
async def workflow_response_streamer(
|
|
@@ -391,12 +438,23 @@ async def workflow_response_streamer(
|
|
|
391
438
|
async for run_response_chunk in run_response:
|
|
392
439
|
yield format_sse_event(run_response_chunk) # type: ignore
|
|
393
440
|
|
|
441
|
+
except (InputCheckError, OutputCheckError) as e:
|
|
442
|
+
error_response = WorkflowErrorEvent(
|
|
443
|
+
error=str(e),
|
|
444
|
+
error_type=e.type,
|
|
445
|
+
error_id=e.error_id,
|
|
446
|
+
additional_data=e.additional_data,
|
|
447
|
+
)
|
|
448
|
+
yield format_sse_event(error_response)
|
|
449
|
+
|
|
394
450
|
except Exception as e:
|
|
395
451
|
import traceback
|
|
396
452
|
|
|
397
453
|
traceback.print_exc()
|
|
398
454
|
error_response = WorkflowErrorEvent(
|
|
399
455
|
error=str(e),
|
|
456
|
+
error_type=e.type if hasattr(e, "type") else None,
|
|
457
|
+
error_id=e.error_id if hasattr(e, "error_id") else None,
|
|
400
458
|
)
|
|
401
459
|
yield format_sse_event(error_response)
|
|
402
460
|
return
|
|
@@ -464,7 +522,7 @@ def get_websocket_router(
|
|
|
464
522
|
await websocket.send_text(json.dumps({"event": "error", "error": f"Unknown action: {action}"}))
|
|
465
523
|
|
|
466
524
|
except Exception as e:
|
|
467
|
-
if "1012" not in str(e):
|
|
525
|
+
if "1012" not in str(e) and "1001" not in str(e):
|
|
468
526
|
logger.error(f"WebSocket error: {e}")
|
|
469
527
|
finally:
|
|
470
528
|
# Clean up the websocket connection
|
|
@@ -520,7 +578,7 @@ def get_base_router(
|
|
|
520
578
|
"content": {
|
|
521
579
|
"application/json": {
|
|
522
580
|
"example": {
|
|
523
|
-
"
|
|
581
|
+
"id": "demo",
|
|
524
582
|
"description": "Example AgentOS configuration",
|
|
525
583
|
"available_models": [],
|
|
526
584
|
"databases": ["9c884dc4-9066-448c-9074-ef49ec7eb73c"],
|
|
@@ -582,7 +640,7 @@ def get_base_router(
|
|
|
582
640
|
)
|
|
583
641
|
async def config() -> ConfigResponse:
|
|
584
642
|
return ConfigResponse(
|
|
585
|
-
os_id=os.
|
|
643
|
+
os_id=os.id or "Unnamed OS",
|
|
586
644
|
description=os.description,
|
|
587
645
|
available_models=os.config.available_models if os.config else [],
|
|
588
646
|
databases=[db.id for db in os.dbs.values()],
|
|
@@ -669,7 +727,7 @@ def get_base_router(
|
|
|
669
727
|
"content": {
|
|
670
728
|
"text/event-stream": {
|
|
671
729
|
"examples": {
|
|
672
|
-
"
|
|
730
|
+
"event_stream": {
|
|
673
731
|
"summary": "Example event stream response",
|
|
674
732
|
"value": 'event: RunStarted\ndata: {"content": "Hello!", "run_id": "123..."}\n\n',
|
|
675
733
|
}
|
|
@@ -692,6 +750,25 @@ def get_base_router(
|
|
|
692
750
|
):
|
|
693
751
|
kwargs = await _get_request_kwargs(request, create_agent_run)
|
|
694
752
|
|
|
753
|
+
if hasattr(request.state, "user_id"):
|
|
754
|
+
if user_id:
|
|
755
|
+
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
756
|
+
user_id = request.state.user_id
|
|
757
|
+
if hasattr(request.state, "session_id"):
|
|
758
|
+
if session_id:
|
|
759
|
+
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
760
|
+
session_id = request.state.session_id
|
|
761
|
+
if hasattr(request.state, "session_state"):
|
|
762
|
+
session_state = request.state.session_state
|
|
763
|
+
if "session_state" in kwargs:
|
|
764
|
+
log_warning("Session state parameter passed in both request state and kwargs, using request state")
|
|
765
|
+
kwargs["session_state"] = session_state
|
|
766
|
+
if hasattr(request.state, "dependencies"):
|
|
767
|
+
dependencies = request.state.dependencies
|
|
768
|
+
if "dependencies" in kwargs:
|
|
769
|
+
log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
|
|
770
|
+
kwargs["dependencies"] = dependencies
|
|
771
|
+
|
|
695
772
|
agent = get_agent_by_id(agent_id, os.agents)
|
|
696
773
|
if agent is None:
|
|
697
774
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
@@ -774,21 +851,25 @@ def get_base_router(
|
|
|
774
851
|
media_type="text/event-stream",
|
|
775
852
|
)
|
|
776
853
|
else:
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
854
|
+
try:
|
|
855
|
+
run_response = cast(
|
|
856
|
+
RunOutput,
|
|
857
|
+
await agent.arun(
|
|
858
|
+
input=message,
|
|
859
|
+
session_id=session_id,
|
|
860
|
+
user_id=user_id,
|
|
861
|
+
images=base64_images if base64_images else None,
|
|
862
|
+
audio=base64_audios if base64_audios else None,
|
|
863
|
+
videos=base64_videos if base64_videos else None,
|
|
864
|
+
files=input_files if input_files else None,
|
|
865
|
+
stream=False,
|
|
866
|
+
**kwargs,
|
|
867
|
+
),
|
|
868
|
+
)
|
|
869
|
+
return run_response.to_dict()
|
|
870
|
+
|
|
871
|
+
except InputCheckError as e:
|
|
872
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
792
873
|
|
|
793
874
|
@router.post(
|
|
794
875
|
"/agents/{agent_id}/runs/{run_id}/cancel",
|
|
@@ -849,11 +930,17 @@ def get_base_router(
|
|
|
849
930
|
async def continue_agent_run(
|
|
850
931
|
agent_id: str,
|
|
851
932
|
run_id: str,
|
|
933
|
+
request: Request,
|
|
852
934
|
tools: str = Form(...), # JSON string of tools
|
|
853
935
|
session_id: Optional[str] = Form(None),
|
|
854
936
|
user_id: Optional[str] = Form(None),
|
|
855
937
|
stream: bool = Form(True),
|
|
856
938
|
):
|
|
939
|
+
if hasattr(request.state, "user_id"):
|
|
940
|
+
user_id = request.state.user_id
|
|
941
|
+
if hasattr(request.state, "session_id"):
|
|
942
|
+
session_id = request.state.session_id
|
|
943
|
+
|
|
857
944
|
# Parse the JSON string manually
|
|
858
945
|
try:
|
|
859
946
|
tools_data = json.loads(tools) if tools else None
|
|
@@ -891,17 +978,21 @@ def get_base_router(
|
|
|
891
978
|
media_type="text/event-stream",
|
|
892
979
|
)
|
|
893
980
|
else:
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
981
|
+
try:
|
|
982
|
+
run_response_obj = cast(
|
|
983
|
+
RunOutput,
|
|
984
|
+
await agent.acontinue_run(
|
|
985
|
+
run_id=run_id, # run_id from path
|
|
986
|
+
updated_tools=updated_tools,
|
|
987
|
+
session_id=session_id,
|
|
988
|
+
user_id=user_id,
|
|
989
|
+
stream=False,
|
|
990
|
+
),
|
|
991
|
+
)
|
|
992
|
+
return run_response_obj.to_dict()
|
|
993
|
+
|
|
994
|
+
except InputCheckError as e:
|
|
995
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
905
996
|
|
|
906
997
|
@router.get(
|
|
907
998
|
"/agents",
|
|
@@ -1041,6 +1132,25 @@ def get_base_router(
|
|
|
1041
1132
|
):
|
|
1042
1133
|
kwargs = await _get_request_kwargs(request, create_team_run)
|
|
1043
1134
|
|
|
1135
|
+
if hasattr(request.state, "user_id"):
|
|
1136
|
+
if user_id:
|
|
1137
|
+
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
1138
|
+
user_id = request.state.user_id
|
|
1139
|
+
if hasattr(request.state, "session_id"):
|
|
1140
|
+
if session_id:
|
|
1141
|
+
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
1142
|
+
session_id = request.state.session_id
|
|
1143
|
+
if hasattr(request.state, "session_state"):
|
|
1144
|
+
session_state = request.state.session_state
|
|
1145
|
+
if "session_state" in kwargs:
|
|
1146
|
+
log_warning("Session state parameter passed in both request state and kwargs, using request state")
|
|
1147
|
+
kwargs["session_state"] = session_state
|
|
1148
|
+
if hasattr(request.state, "dependencies"):
|
|
1149
|
+
dependencies = request.state.dependencies
|
|
1150
|
+
if "dependencies" in kwargs:
|
|
1151
|
+
log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
|
|
1152
|
+
kwargs["dependencies"] = dependencies
|
|
1153
|
+
|
|
1044
1154
|
logger.debug(f"Creating team run: {message=} {session_id=} {monitor=} {user_id=} {team_id=} {files=} {kwargs=}")
|
|
1045
1155
|
|
|
1046
1156
|
team = get_team_by_id(team_id, os.teams)
|
|
@@ -1122,18 +1232,22 @@ def get_base_router(
|
|
|
1122
1232
|
media_type="text/event-stream",
|
|
1123
1233
|
)
|
|
1124
1234
|
else:
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1235
|
+
try:
|
|
1236
|
+
run_response = await team.arun(
|
|
1237
|
+
input=message,
|
|
1238
|
+
session_id=session_id,
|
|
1239
|
+
user_id=user_id,
|
|
1240
|
+
images=base64_images if base64_images else None,
|
|
1241
|
+
audio=base64_audios if base64_audios else None,
|
|
1242
|
+
videos=base64_videos if base64_videos else None,
|
|
1243
|
+
files=document_files if document_files else None,
|
|
1244
|
+
stream=False,
|
|
1245
|
+
**kwargs,
|
|
1246
|
+
)
|
|
1247
|
+
return run_response.to_dict()
|
|
1248
|
+
|
|
1249
|
+
except InputCheckError as e:
|
|
1250
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1137
1251
|
|
|
1138
1252
|
@router.post(
|
|
1139
1253
|
"/teams/{team_id}/runs/{run_id}/cancel",
|
|
@@ -1464,6 +1578,25 @@ def get_base_router(
|
|
|
1464
1578
|
):
|
|
1465
1579
|
kwargs = await _get_request_kwargs(request, create_workflow_run)
|
|
1466
1580
|
|
|
1581
|
+
if hasattr(request.state, "user_id"):
|
|
1582
|
+
if user_id:
|
|
1583
|
+
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
1584
|
+
user_id = request.state.user_id
|
|
1585
|
+
if hasattr(request.state, "session_id"):
|
|
1586
|
+
if session_id:
|
|
1587
|
+
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
1588
|
+
session_id = request.state.session_id
|
|
1589
|
+
if hasattr(request.state, "session_state"):
|
|
1590
|
+
session_state = request.state.session_state
|
|
1591
|
+
if "session_state" in kwargs:
|
|
1592
|
+
log_warning("Session state parameter passed in both request state and kwargs, using request state")
|
|
1593
|
+
kwargs["session_state"] = session_state
|
|
1594
|
+
if hasattr(request.state, "dependencies"):
|
|
1595
|
+
dependencies = request.state.dependencies
|
|
1596
|
+
if "dependencies" in kwargs:
|
|
1597
|
+
log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
|
|
1598
|
+
kwargs["dependencies"] = dependencies
|
|
1599
|
+
|
|
1467
1600
|
# Retrieve the workflow by ID
|
|
1468
1601
|
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
1469
1602
|
if workflow is None:
|
|
@@ -1497,6 +1630,9 @@ def get_base_router(
|
|
|
1497
1630
|
**kwargs,
|
|
1498
1631
|
)
|
|
1499
1632
|
return run_response.to_dict()
|
|
1633
|
+
|
|
1634
|
+
except InputCheckError as e:
|
|
1635
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1500
1636
|
except Exception as e:
|
|
1501
1637
|
# Handle unexpected runtime errors
|
|
1502
1638
|
raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
|
agno/os/routers/home.py
CHANGED
|
@@ -30,7 +30,7 @@ def get_home_router(os: "AgentOS") -> APIRouter:
|
|
|
30
30
|
"value": {
|
|
31
31
|
"name": "AgentOS API",
|
|
32
32
|
"description": "AI Agent Operating System API",
|
|
33
|
-
"
|
|
33
|
+
"id": "demo-os",
|
|
34
34
|
"version": "1.0.0",
|
|
35
35
|
},
|
|
36
36
|
}
|
|
@@ -45,7 +45,7 @@ def get_home_router(os: "AgentOS") -> APIRouter:
|
|
|
45
45
|
return {
|
|
46
46
|
"name": "AgentOS API",
|
|
47
47
|
"description": os.description or "AI Agent Operating System API",
|
|
48
|
-
"
|
|
48
|
+
"id": os.id or "agno-agentos",
|
|
49
49
|
"version": os.version or "1.0.0",
|
|
50
50
|
}
|
|
51
51
|
|