agno 2.3.21__py3-none-any.whl → 2.3.22__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 +26 -1
- agno/agent/remote.py +233 -72
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/v2_3_0.py +92 -53
- agno/db/postgres/async_postgres.py +162 -40
- agno/db/postgres/postgres.py +181 -31
- agno/db/postgres/utils.py +6 -2
- agno/knowledge/chunking/document.py +3 -2
- agno/knowledge/chunking/markdown.py +8 -3
- agno/knowledge/chunking/recursive.py +2 -2
- agno/models/openai/chat.py +1 -1
- agno/models/openai/responses.py +14 -7
- agno/os/middleware/jwt.py +66 -27
- agno/os/routers/agents/router.py +2 -2
- agno/os/routers/knowledge/knowledge.py +3 -3
- agno/os/routers/teams/router.py +2 -2
- agno/os/routers/workflows/router.py +2 -2
- agno/reasoning/deepseek.py +11 -1
- agno/reasoning/gemini.py +6 -2
- agno/reasoning/groq.py +8 -3
- agno/reasoning/openai.py +2 -0
- agno/remote/base.py +105 -8
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +370 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/team/remote.py +219 -59
- agno/team/team.py +22 -2
- agno/tools/mcp/mcp.py +299 -17
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/utils/mcp.py +49 -8
- agno/utils/string.py +43 -1
- agno/workflow/condition.py +4 -2
- agno/workflow/loop.py +20 -1
- agno/workflow/remote.py +172 -32
- agno/workflow/router.py +4 -1
- agno/workflow/steps.py +4 -0
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/METADATA +13 -14
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/RECORD +52 -38
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/WHEEL +0 -0
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.21.dist-info → agno-2.3.22.dist-info}/top_level.txt +0 -0
agno/os/middleware/jwt.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import re
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from os import getenv
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
8
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
9
9
|
|
|
10
10
|
import jwt
|
|
11
11
|
from fastapi import Request, Response
|
|
@@ -168,7 +168,9 @@ class JWTValidator:
|
|
|
168
168
|
except Exception as e:
|
|
169
169
|
log_warning(f"Failed to parse JWKS key: {e}")
|
|
170
170
|
|
|
171
|
-
def validate_token(
|
|
171
|
+
def validate_token(
|
|
172
|
+
self, token: str, expected_audience: Optional[Union[str, Iterable[str]]] = None
|
|
173
|
+
) -> Dict[str, Any]:
|
|
172
174
|
"""
|
|
173
175
|
Validate JWT token and extract claims.
|
|
174
176
|
|
|
@@ -191,10 +193,9 @@ class JWTValidator:
|
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
# Configure audience verification
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
decode_options["verify_aud"] = False
|
|
196
|
+
# We'll decode without audience verification and if we need to verify the audience,
|
|
197
|
+
# we'll manually verify the audience to provide better error messages
|
|
198
|
+
decode_options["verify_aud"] = False
|
|
198
199
|
|
|
199
200
|
# If validation is disabled, decode without signature verification
|
|
200
201
|
if not self.validate:
|
|
@@ -206,6 +207,7 @@ class JWTValidator:
|
|
|
206
207
|
decode_kwargs["options"] = decode_options
|
|
207
208
|
|
|
208
209
|
last_exception: Optional[Exception] = None
|
|
210
|
+
payload: Optional[Dict[str, Any]] = None
|
|
209
211
|
|
|
210
212
|
# Try JWKS keys first if configured
|
|
211
213
|
if self.jwks_keys:
|
|
@@ -222,9 +224,7 @@ class JWTValidator:
|
|
|
222
224
|
jwk = self.jwks_keys["_default"]
|
|
223
225
|
|
|
224
226
|
if jwk:
|
|
225
|
-
|
|
226
|
-
except jwt.InvalidAudienceError:
|
|
227
|
-
raise
|
|
227
|
+
payload = jwt.decode(token, jwk.key, **decode_kwargs)
|
|
228
228
|
except jwt.ExpiredSignatureError:
|
|
229
229
|
raise
|
|
230
230
|
except jwt.InvalidTokenError as e:
|
|
@@ -233,20 +233,54 @@ class JWTValidator:
|
|
|
233
233
|
last_exception = e
|
|
234
234
|
|
|
235
235
|
# Try each static verification key until one succeeds
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
236
|
+
if payload is None:
|
|
237
|
+
for key in self.verification_keys:
|
|
238
|
+
try:
|
|
239
|
+
payload = jwt.decode(token, key, **decode_kwargs)
|
|
240
|
+
break
|
|
241
|
+
except jwt.ExpiredSignatureError:
|
|
242
|
+
raise
|
|
243
|
+
except jwt.InvalidTokenError as e:
|
|
244
|
+
last_exception = e
|
|
245
|
+
continue
|
|
246
246
|
|
|
247
|
-
if
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
if payload is None:
|
|
248
|
+
if last_exception:
|
|
249
|
+
raise last_exception
|
|
250
|
+
raise jwt.InvalidTokenError("No verification keys configured")
|
|
251
|
+
|
|
252
|
+
# Manually verify audience if expected_audience was provided
|
|
253
|
+
if expected_audience:
|
|
254
|
+
token_audience = payload.get(self.audience_claim)
|
|
255
|
+
if token_audience is None:
|
|
256
|
+
raise jwt.InvalidTokenError(
|
|
257
|
+
f'Token is missing the "{self.audience_claim}" claim. '
|
|
258
|
+
f"Audience verification requires this claim to be present in the token."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Normalize expected_audience to a list
|
|
262
|
+
if isinstance(expected_audience, str):
|
|
263
|
+
expected_audiences = [expected_audience]
|
|
264
|
+
elif isinstance(expected_audience, Iterable):
|
|
265
|
+
expected_audiences = list(expected_audience)
|
|
266
|
+
else:
|
|
267
|
+
expected_audiences = []
|
|
268
|
+
|
|
269
|
+
# Normalize token_audience to a list
|
|
270
|
+
if isinstance(token_audience, str):
|
|
271
|
+
token_audiences = [token_audience]
|
|
272
|
+
elif isinstance(token_audience, list):
|
|
273
|
+
token_audiences = token_audience
|
|
274
|
+
else:
|
|
275
|
+
token_audiences = [token_audience] if token_audience else []
|
|
276
|
+
|
|
277
|
+
# Check if any token audience matches any expected audience
|
|
278
|
+
if not any(aud in expected_audiences for aud in token_audiences):
|
|
279
|
+
raise jwt.InvalidAudienceError(
|
|
280
|
+
f"Invalid audience. Expected one of: {expected_audiences}, got: {token_audiences}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return payload
|
|
250
284
|
|
|
251
285
|
def extract_claims(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
252
286
|
"""
|
|
@@ -364,6 +398,7 @@ class JWTMiddleware(BaseHTTPMiddleware):
|
|
|
364
398
|
user_id_claim: str = "sub",
|
|
365
399
|
session_id_claim: str = "session_id",
|
|
366
400
|
audience_claim: str = "aud",
|
|
401
|
+
audience: Optional[Union[str, Iterable[str]]] = None,
|
|
367
402
|
verify_audience: bool = False,
|
|
368
403
|
dependencies_claims: Optional[List[str]] = None,
|
|
369
404
|
session_state_claims: Optional[List[str]] = None,
|
|
@@ -400,7 +435,8 @@ class JWTMiddleware(BaseHTTPMiddleware):
|
|
|
400
435
|
user_id_claim: JWT claim name for user ID (default: "sub")
|
|
401
436
|
session_id_claim: JWT claim name for session ID (default: "session_id")
|
|
402
437
|
audience_claim: JWT claim name for audience/OS ID (default: "aud")
|
|
403
|
-
|
|
438
|
+
audience: Optional expected audience claim to validate against the token's audience claim (default: AgentOS ID)
|
|
439
|
+
verify_audience: Whether to verify the token's audience claim matches the expected audience claim (default: False)
|
|
404
440
|
dependencies_claims: A list of claims to extract from the JWT token for dependencies
|
|
405
441
|
session_state_claims: A list of claims to extract from the JWT token for session state
|
|
406
442
|
scope_mappings: Optional dictionary mapping route patterns to required scopes.
|
|
@@ -453,6 +489,8 @@ class JWTMiddleware(BaseHTTPMiddleware):
|
|
|
453
489
|
self.dependencies_claims: List[str] = dependencies_claims or []
|
|
454
490
|
self.session_state_claims: List[str] = session_state_claims or []
|
|
455
491
|
|
|
492
|
+
self.audience = audience
|
|
493
|
+
|
|
456
494
|
# RBAC configuration (opt-in via scope_mappings)
|
|
457
495
|
self.authorization = authorization
|
|
458
496
|
|
|
@@ -648,7 +686,9 @@ class JWTMiddleware(BaseHTTPMiddleware):
|
|
|
648
686
|
|
|
649
687
|
try:
|
|
650
688
|
# Validate token and extract claims (with audience verification if configured)
|
|
651
|
-
expected_audience =
|
|
689
|
+
expected_audience = None
|
|
690
|
+
if self.verify_audience:
|
|
691
|
+
expected_audience = self.audience or agent_os_id
|
|
652
692
|
payload: Dict[str, Any] = self.validator.validate_token(token, expected_audience) # type: ignore
|
|
653
693
|
|
|
654
694
|
# Extract standard claims and store in request.state
|
|
@@ -755,11 +795,10 @@ class JWTMiddleware(BaseHTTPMiddleware):
|
|
|
755
795
|
request.state.authenticated = True
|
|
756
796
|
|
|
757
797
|
except jwt.InvalidAudienceError:
|
|
758
|
-
log_warning(f"Invalid audience - expected: {
|
|
798
|
+
log_warning(f"Invalid token audience - expected: {expected_audience}")
|
|
759
799
|
return self._create_error_response(
|
|
760
|
-
401, "Invalid audience - token not valid for this AgentOS instance", origin, cors_allowed_origins
|
|
800
|
+
401, "Invalid token audience - token not valid for this AgentOS instance", origin, cors_allowed_origins
|
|
761
801
|
)
|
|
762
|
-
|
|
763
802
|
except jwt.ExpiredSignatureError as e:
|
|
764
803
|
if self.validate:
|
|
765
804
|
log_warning(f"Token has expired: {str(e)}")
|
agno/os/routers/agents/router.py
CHANGED
|
@@ -220,11 +220,11 @@ def get_agent_router(
|
|
|
220
220
|
kwargs = await get_request_kwargs(request, create_agent_run)
|
|
221
221
|
|
|
222
222
|
if hasattr(request.state, "user_id") and request.state.user_id is not None:
|
|
223
|
-
if user_id:
|
|
223
|
+
if user_id and user_id != request.state.user_id:
|
|
224
224
|
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
225
225
|
user_id = request.state.user_id
|
|
226
226
|
if hasattr(request.state, "session_id") and request.state.session_id is not None:
|
|
227
|
-
if session_id:
|
|
227
|
+
if session_id and session_id != request.state.session_id:
|
|
228
228
|
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
229
229
|
session_id = request.state.session_id
|
|
230
230
|
if hasattr(request.state, "session_state") and request.state.session_state is not None:
|
|
@@ -297,7 +297,7 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
|
|
|
297
297
|
else:
|
|
298
298
|
raise HTTPException(status_code=400, detail=f"Invalid reader_id: {update_data.reader_id}")
|
|
299
299
|
|
|
300
|
-
updated_content_dict = knowledge.
|
|
300
|
+
updated_content_dict = await knowledge.apatch_content(content)
|
|
301
301
|
if not updated_content_dict:
|
|
302
302
|
raise HTTPException(status_code=404, detail=f"Content not found: {content_id}")
|
|
303
303
|
|
|
@@ -1029,13 +1029,13 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
|
|
|
1029
1029
|
search_types=search_types,
|
|
1030
1030
|
)
|
|
1031
1031
|
)
|
|
1032
|
-
|
|
1032
|
+
filters = await knowledge.async_get_valid_filters()
|
|
1033
1033
|
return ConfigResponseSchema(
|
|
1034
1034
|
readers=reader_schemas,
|
|
1035
1035
|
vector_dbs=vector_dbs,
|
|
1036
1036
|
readersForType=types_of_readers,
|
|
1037
1037
|
chunkers=chunkers_dict,
|
|
1038
|
-
filters=
|
|
1038
|
+
filters=filters,
|
|
1039
1039
|
)
|
|
1040
1040
|
|
|
1041
1041
|
return router
|
agno/os/routers/teams/router.py
CHANGED
|
@@ -169,11 +169,11 @@ def get_team_router(
|
|
|
169
169
|
kwargs = await get_request_kwargs(request, create_team_run)
|
|
170
170
|
|
|
171
171
|
if hasattr(request.state, "user_id") and request.state.user_id is not None:
|
|
172
|
-
if user_id:
|
|
172
|
+
if user_id and user_id != request.state.user_id:
|
|
173
173
|
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
174
174
|
user_id = request.state.user_id
|
|
175
175
|
if hasattr(request.state, "session_id") and request.state.session_id is not None:
|
|
176
|
-
if session_id:
|
|
176
|
+
if session_id and session_id != request.state.session_id:
|
|
177
177
|
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
178
178
|
session_id = request.state.session_id
|
|
179
179
|
if hasattr(request.state, "session_state") and request.state.session_state is not None:
|
|
@@ -626,11 +626,11 @@ def get_workflow_router(
|
|
|
626
626
|
kwargs = await get_request_kwargs(request, create_workflow_run)
|
|
627
627
|
|
|
628
628
|
if hasattr(request.state, "user_id") and request.state.user_id is not None:
|
|
629
|
-
if user_id:
|
|
629
|
+
if user_id and user_id != request.state.user_id:
|
|
630
630
|
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
631
631
|
user_id = request.state.user_id
|
|
632
632
|
if hasattr(request.state, "session_id") and request.state.session_id is not None:
|
|
633
|
-
if session_id:
|
|
633
|
+
if session_id and session_id != request.state.session_id:
|
|
634
634
|
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
635
635
|
session_id = request.state.session_id
|
|
636
636
|
if hasattr(request.state, "session_state") and request.state.session_state is not None:
|
agno/reasoning/deepseek.py
CHANGED
|
@@ -8,7 +8,17 @@ from agno.utils.log import logger
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def is_deepseek_reasoning_model(reasoning_model: Model) -> bool:
|
|
11
|
-
|
|
11
|
+
"""Check if the model is a DeepSeek reasoning model.
|
|
12
|
+
|
|
13
|
+
Matches:
|
|
14
|
+
- deepseek-reasoner
|
|
15
|
+
- deepseek-r1 and variants (deepseek-r1-distill-*, etc.)
|
|
16
|
+
"""
|
|
17
|
+
if reasoning_model.__class__.__name__ != "DeepSeek":
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
model_id = reasoning_model.id.lower()
|
|
21
|
+
return "reasoner" in model_id or "r1" in model_id
|
|
12
22
|
|
|
13
23
|
|
|
14
24
|
def get_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
agno/reasoning/gemini.py
CHANGED
|
@@ -13,9 +13,13 @@ def is_gemini_reasoning_model(reasoning_model: Model) -> bool:
|
|
|
13
13
|
if not is_gemini_class:
|
|
14
14
|
return False
|
|
15
15
|
|
|
16
|
-
# Check if it's a Gemini
|
|
16
|
+
# Check if it's a Gemini model with thinking support
|
|
17
|
+
# - Gemini 2.5+ models support thinking
|
|
18
|
+
# - Gemini 3+ models support thinking (including DeepThink variants)
|
|
17
19
|
model_id = reasoning_model.id.lower()
|
|
18
|
-
has_thinking_support =
|
|
20
|
+
has_thinking_support = (
|
|
21
|
+
"2.5" in model_id or "3.0" in model_id or "3.5" in model_id or "deepthink" in model_id or "gemini-3" in model_id
|
|
22
|
+
)
|
|
19
23
|
|
|
20
24
|
# Also check if thinking parameters are set
|
|
21
25
|
# Note: thinking_budget=0 explicitly disables thinking mode per Google's API docs
|
agno/reasoning/groq.py
CHANGED
|
@@ -8,7 +8,12 @@ from agno.utils.log import logger
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def is_groq_reasoning_model(reasoning_model: Model) -> bool:
|
|
11
|
-
return reasoning_model.__class__.__name__ == "Groq" and
|
|
11
|
+
return reasoning_model.__class__.__name__ == "Groq" and (
|
|
12
|
+
"deepseek" in reasoning_model.id.lower()
|
|
13
|
+
or "openai/gpt-oss-20b" in reasoning_model.id.lower()
|
|
14
|
+
or "openai/gpt-oss-120b" in reasoning_model.id.lower()
|
|
15
|
+
or "qwen/qwen3-32b" in reasoning_model.id.lower()
|
|
16
|
+
)
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
def get_groq_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
@@ -95,7 +100,7 @@ def get_groq_reasoning_stream(
|
|
|
95
100
|
reasoning_content: str = ""
|
|
96
101
|
|
|
97
102
|
try:
|
|
98
|
-
for event in reasoning_agent.run(input=messages, stream=True,
|
|
103
|
+
for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
|
|
99
104
|
if hasattr(event, "event"):
|
|
100
105
|
if event.event == RunEvent.run_content:
|
|
101
106
|
# Check for reasoning_content attribute first (native reasoning)
|
|
@@ -146,7 +151,7 @@ async def aget_groq_reasoning_stream(
|
|
|
146
151
|
reasoning_content: str = ""
|
|
147
152
|
|
|
148
153
|
try:
|
|
149
|
-
async for event in reasoning_agent.arun(input=messages, stream=True,
|
|
154
|
+
async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
|
|
150
155
|
if hasattr(event, "event"):
|
|
151
156
|
if event.event == RunEvent.run_content:
|
|
152
157
|
# Check for reasoning_content attribute first (native reasoning)
|
agno/reasoning/openai.py
CHANGED
|
@@ -21,6 +21,8 @@ def is_openai_reasoning_model(reasoning_model: Model) -> bool:
|
|
|
21
21
|
or ("o1" in reasoning_model.id)
|
|
22
22
|
or ("4.1" in reasoning_model.id)
|
|
23
23
|
or ("4.5" in reasoning_model.id)
|
|
24
|
+
or ("5.1" in reasoning_model.id)
|
|
25
|
+
or ("5.2" in reasoning_model.id)
|
|
24
26
|
)
|
|
25
27
|
) or (isinstance(reasoning_model, OpenAILike) and "deepseek-r1" in reasoning_model.id.lower())
|
|
26
28
|
|
agno/remote/base.py
CHANGED
|
@@ -2,7 +2,7 @@ import time
|
|
|
2
2
|
from abc import abstractmethod
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import date
|
|
5
|
-
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Optional, Sequence, Tuple, Union
|
|
5
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Literal, Optional, Sequence, Tuple, Union
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
@@ -18,6 +18,8 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from fastapi import UploadFile
|
|
19
19
|
|
|
20
20
|
from agno.client import AgentOSClient
|
|
21
|
+
from agno.client.a2a import A2AClient
|
|
22
|
+
from agno.client.a2a.schemas import AgentCard
|
|
21
23
|
from agno.os.routers.evals.schemas import EvalSchema
|
|
22
24
|
from agno.os.routers.knowledge.schemas import (
|
|
23
25
|
ConfigResponseSchema,
|
|
@@ -64,7 +66,7 @@ class RemoteDb:
|
|
|
64
66
|
"""Create a RemoteDb instance from an AgentResponse/TeamResponse/WorkflowResponse and ConfigResponse.
|
|
65
67
|
|
|
66
68
|
Args:
|
|
67
|
-
|
|
69
|
+
db_id (str): The id of the remote database
|
|
68
70
|
client: The AgentOSClient for remote operations.
|
|
69
71
|
config: The ConfigResponse containing database table information.
|
|
70
72
|
|
|
@@ -344,31 +346,52 @@ class RemoteKnowledge:
|
|
|
344
346
|
class BaseRemote:
|
|
345
347
|
# Private cache for OS config with TTL: (config, timestamp)
|
|
346
348
|
_cached_config: Optional[Tuple["ConfigResponse", float]] = field(default=None, init=False, repr=False)
|
|
349
|
+
# Private cache for agent card with TTL: (agent_card, timestamp)
|
|
350
|
+
_cached_agent_card: Optional[Tuple[Optional["AgentCard"], float]] = field(default=None, init=False, repr=False)
|
|
347
351
|
|
|
348
352
|
def __init__(
|
|
349
353
|
self,
|
|
350
354
|
base_url: str,
|
|
351
355
|
timeout: float = 60.0,
|
|
356
|
+
protocol: Literal["agentos", "a2a"] = "agentos",
|
|
357
|
+
a2a_protocol: Literal["json-rpc", "rest"] = "rest",
|
|
352
358
|
config_ttl: float = 300.0,
|
|
353
359
|
):
|
|
354
360
|
"""Initialize BaseRemote for remote execution.
|
|
355
361
|
|
|
362
|
+
Supports two protocols:
|
|
363
|
+
- "agentos": Agno's proprietary AgentOS REST API (default)
|
|
364
|
+
- "a2a": A2A (Agent-to-Agent) protocol for cross-framework communication
|
|
365
|
+
|
|
356
366
|
For local execution, provide agent/team/workflow instances.
|
|
357
367
|
For remote execution, provide base_url.
|
|
358
368
|
|
|
359
369
|
Args:
|
|
360
370
|
base_url: Base URL for remote instance (e.g., "http://localhost:7777")
|
|
361
371
|
timeout: Request timeout in seconds (default: 60)
|
|
372
|
+
protocol: Communication protocol - "agentos" (default) or "a2a"
|
|
373
|
+
a2a_protocol: For A2A protocol only - Whether to use JSON-RPC or REST protocol.
|
|
362
374
|
config_ttl: Time-to-live for cached config in seconds (default: 300)
|
|
363
375
|
"""
|
|
364
376
|
self.base_url = base_url.rstrip("/")
|
|
365
377
|
self.timeout: float = timeout
|
|
378
|
+
self.protocol = protocol
|
|
379
|
+
self.a2a_protocol = a2a_protocol
|
|
366
380
|
self.config_ttl: float = config_ttl
|
|
367
381
|
self._cached_config = None
|
|
382
|
+
self._cached_agent_card = None
|
|
383
|
+
|
|
384
|
+
self.agentos_client = None
|
|
385
|
+
self.a2a_client = None
|
|
368
386
|
|
|
369
|
-
|
|
387
|
+
if protocol == "agentos":
|
|
388
|
+
self.agentos_client = self.get_os_client()
|
|
389
|
+
elif protocol == "a2a":
|
|
390
|
+
self.a2a_client = self.get_a2a_client()
|
|
391
|
+
else:
|
|
392
|
+
raise ValueError(f"Invalid protocol: {protocol}")
|
|
370
393
|
|
|
371
|
-
def
|
|
394
|
+
def get_os_client(self) -> "AgentOSClient":
|
|
372
395
|
"""Get an AgentOSClient for fetching remote configuration.
|
|
373
396
|
|
|
374
397
|
This is used internally by AgentOS to fetch configuration from remote
|
|
@@ -384,11 +407,31 @@ class BaseRemote:
|
|
|
384
407
|
timeout=self.timeout,
|
|
385
408
|
)
|
|
386
409
|
|
|
410
|
+
def get_a2a_client(self) -> "A2AClient":
|
|
411
|
+
"""Get an A2AClient for A2A protocol communication.
|
|
412
|
+
|
|
413
|
+
Returns cached client if available, otherwise creates a new one.
|
|
414
|
+
This method provides lazy initialization of the A2A client.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
A2AClient: Client configured for A2A protocol communication
|
|
418
|
+
"""
|
|
419
|
+
from agno.client.a2a import A2AClient
|
|
420
|
+
|
|
421
|
+
return A2AClient(
|
|
422
|
+
base_url=self.base_url,
|
|
423
|
+
timeout=int(self.timeout),
|
|
424
|
+
protocol=self.a2a_protocol,
|
|
425
|
+
)
|
|
426
|
+
|
|
387
427
|
@property
|
|
388
|
-
def _config(self) -> "ConfigResponse":
|
|
428
|
+
def _config(self) -> Optional["ConfigResponse"]:
|
|
389
429
|
"""Get the OS config from remote, cached with TTL."""
|
|
390
430
|
from agno.os.schema import ConfigResponse
|
|
391
431
|
|
|
432
|
+
if self.protocol == "a2a":
|
|
433
|
+
return None
|
|
434
|
+
|
|
392
435
|
current_time = time.time()
|
|
393
436
|
|
|
394
437
|
# Check if cache is valid
|
|
@@ -398,15 +441,15 @@ class BaseRemote:
|
|
|
398
441
|
return config
|
|
399
442
|
|
|
400
443
|
# Fetch fresh config
|
|
401
|
-
config: ConfigResponse = self.
|
|
444
|
+
config: ConfigResponse = self.agentos_client.get_config() # type: ignore
|
|
402
445
|
self._cached_config = (config, current_time)
|
|
403
446
|
return config
|
|
404
447
|
|
|
405
|
-
def refresh_os_config(self) -> "ConfigResponse":
|
|
448
|
+
async def refresh_os_config(self) -> "ConfigResponse":
|
|
406
449
|
"""Force refresh the cached OS config."""
|
|
407
450
|
from agno.os.schema import ConfigResponse
|
|
408
451
|
|
|
409
|
-
config: ConfigResponse = self.
|
|
452
|
+
config: ConfigResponse = await self.agentos_client.aget_config() # type: ignore
|
|
410
453
|
self._cached_config = (config, time.time())
|
|
411
454
|
return config
|
|
412
455
|
|
|
@@ -437,6 +480,60 @@ class BaseRemote:
|
|
|
437
480
|
return {"Authorization": f"Bearer {auth_token}"}
|
|
438
481
|
return None
|
|
439
482
|
|
|
483
|
+
def get_agent_card(self) -> Optional["AgentCard"]:
|
|
484
|
+
"""Get agent card for A2A protocol agents, cached with TTL.
|
|
485
|
+
|
|
486
|
+
Fetches the agent card from the standard /.well-known/agent.json endpoint
|
|
487
|
+
to populate agent metadata (name, description, etc.) for A2A agents.
|
|
488
|
+
|
|
489
|
+
Returns None for non-A2A protocols or if the server doesn't support agent cards.
|
|
490
|
+
"""
|
|
491
|
+
if self.protocol != "a2a":
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
current_time = time.time()
|
|
495
|
+
|
|
496
|
+
# Check if cache is valid
|
|
497
|
+
if self._cached_agent_card is not None:
|
|
498
|
+
agent_card, cached_at = self._cached_agent_card
|
|
499
|
+
if current_time - cached_at < self.config_ttl:
|
|
500
|
+
return agent_card
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
agent_card = self.a2a_client.get_agent_card() # type: ignore
|
|
504
|
+
self._cached_agent_card = (agent_card, current_time)
|
|
505
|
+
return agent_card
|
|
506
|
+
except Exception:
|
|
507
|
+
self._cached_agent_card = (None, current_time)
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
async def aget_agent_card(self) -> Optional["AgentCard"]:
|
|
511
|
+
"""Get agent card for A2A protocol agents, cached with TTL.
|
|
512
|
+
|
|
513
|
+
Fetches the agent card from the standard /.well-known/agent.json endpoint
|
|
514
|
+
to populate agent metadata (name, description, etc.) for A2A agents.
|
|
515
|
+
|
|
516
|
+
Returns None for non-A2A protocols or if the server doesn't support agent cards.
|
|
517
|
+
"""
|
|
518
|
+
if self.protocol != "a2a":
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
current_time = time.time()
|
|
522
|
+
|
|
523
|
+
# Check if cache is valid
|
|
524
|
+
if self._cached_agent_card is not None:
|
|
525
|
+
agent_card, cached_at = self._cached_agent_card
|
|
526
|
+
if current_time - cached_at < self.config_ttl:
|
|
527
|
+
return agent_card
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
agent_card = await self.a2a_client.aget_agent_card() # type: ignore
|
|
531
|
+
self._cached_agent_card = (agent_card, current_time)
|
|
532
|
+
return agent_card
|
|
533
|
+
except Exception:
|
|
534
|
+
self._cached_agent_card = (None, current_time)
|
|
535
|
+
return None
|
|
536
|
+
|
|
440
537
|
@abstractmethod
|
|
441
538
|
def arun( # type: ignore
|
|
442
539
|
self,
|
agno/skills/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from agno.skills.agent_skills import Skills
|
|
2
|
+
from agno.skills.errors import SkillError, SkillParseError, SkillValidationError
|
|
3
|
+
from agno.skills.loaders import LocalSkills, SkillLoader
|
|
4
|
+
from agno.skills.skill import Skill
|
|
5
|
+
from agno.skills.validator import validate_metadata, validate_skill_directory
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Skills",
|
|
9
|
+
"LocalSkills",
|
|
10
|
+
"SkillLoader",
|
|
11
|
+
"Skill",
|
|
12
|
+
"SkillError",
|
|
13
|
+
"SkillParseError",
|
|
14
|
+
"SkillValidationError",
|
|
15
|
+
"validate_metadata",
|
|
16
|
+
"validate_skill_directory",
|
|
17
|
+
]
|