agno 2.0.11__py3-none-any.whl → 2.1.1__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.
Files changed (93) hide show
  1. agno/agent/agent.py +607 -176
  2. agno/db/in_memory/in_memory_db.py +42 -29
  3. agno/db/mongo/mongo.py +65 -66
  4. agno/db/postgres/postgres.py +6 -4
  5. agno/db/utils.py +50 -22
  6. agno/exceptions.py +62 -1
  7. agno/guardrails/__init__.py +6 -0
  8. agno/guardrails/base.py +19 -0
  9. agno/guardrails/openai.py +144 -0
  10. agno/guardrails/pii.py +94 -0
  11. agno/guardrails/prompt_injection.py +51 -0
  12. agno/knowledge/embedder/aws_bedrock.py +9 -4
  13. agno/knowledge/embedder/azure_openai.py +54 -0
  14. agno/knowledge/embedder/base.py +2 -0
  15. agno/knowledge/embedder/cohere.py +184 -5
  16. agno/knowledge/embedder/google.py +79 -1
  17. agno/knowledge/embedder/huggingface.py +9 -4
  18. agno/knowledge/embedder/jina.py +63 -0
  19. agno/knowledge/embedder/mistral.py +78 -11
  20. agno/knowledge/embedder/ollama.py +5 -0
  21. agno/knowledge/embedder/openai.py +18 -54
  22. agno/knowledge/embedder/voyageai.py +69 -16
  23. agno/knowledge/knowledge.py +11 -4
  24. agno/knowledge/reader/pdf_reader.py +4 -3
  25. agno/knowledge/reader/website_reader.py +3 -2
  26. agno/models/base.py +125 -32
  27. agno/models/cerebras/cerebras.py +1 -0
  28. agno/models/cerebras/cerebras_openai.py +1 -0
  29. agno/models/dashscope/dashscope.py +1 -0
  30. agno/models/google/gemini.py +27 -5
  31. agno/models/openai/chat.py +13 -4
  32. agno/models/openai/responses.py +1 -1
  33. agno/models/perplexity/perplexity.py +2 -3
  34. agno/models/requesty/__init__.py +5 -0
  35. agno/models/requesty/requesty.py +49 -0
  36. agno/models/vllm/vllm.py +1 -0
  37. agno/models/xai/xai.py +1 -0
  38. agno/os/app.py +98 -126
  39. agno/os/interfaces/__init__.py +1 -0
  40. agno/os/interfaces/agui/agui.py +21 -5
  41. agno/os/interfaces/base.py +4 -2
  42. agno/os/interfaces/slack/slack.py +13 -8
  43. agno/os/interfaces/whatsapp/router.py +2 -0
  44. agno/os/interfaces/whatsapp/whatsapp.py +12 -5
  45. agno/os/mcp.py +2 -2
  46. agno/os/middleware/__init__.py +7 -0
  47. agno/os/middleware/jwt.py +233 -0
  48. agno/os/router.py +182 -46
  49. agno/os/routers/home.py +2 -2
  50. agno/os/routers/memory/memory.py +23 -1
  51. agno/os/routers/memory/schemas.py +1 -1
  52. agno/os/routers/session/session.py +20 -3
  53. agno/os/utils.py +74 -8
  54. agno/run/agent.py +120 -77
  55. agno/run/base.py +2 -13
  56. agno/run/team.py +115 -72
  57. agno/run/workflow.py +5 -15
  58. agno/session/summary.py +9 -10
  59. agno/session/team.py +2 -1
  60. agno/team/team.py +721 -169
  61. agno/tools/firecrawl.py +4 -4
  62. agno/tools/function.py +42 -2
  63. agno/tools/knowledge.py +3 -3
  64. agno/tools/searxng.py +2 -2
  65. agno/tools/serper.py +2 -2
  66. agno/tools/spider.py +2 -2
  67. agno/tools/workflow.py +4 -5
  68. agno/utils/events.py +66 -1
  69. agno/utils/hooks.py +57 -0
  70. agno/utils/media.py +11 -9
  71. agno/utils/print_response/agent.py +43 -5
  72. agno/utils/print_response/team.py +48 -12
  73. agno/utils/serialize.py +32 -0
  74. agno/vectordb/cassandra/cassandra.py +44 -4
  75. agno/vectordb/chroma/chromadb.py +79 -8
  76. agno/vectordb/clickhouse/clickhousedb.py +43 -6
  77. agno/vectordb/couchbase/couchbase.py +76 -5
  78. agno/vectordb/lancedb/lance_db.py +38 -3
  79. agno/vectordb/milvus/milvus.py +76 -4
  80. agno/vectordb/mongodb/mongodb.py +76 -4
  81. agno/vectordb/pgvector/pgvector.py +50 -6
  82. agno/vectordb/pineconedb/pineconedb.py +39 -2
  83. agno/vectordb/qdrant/qdrant.py +76 -26
  84. agno/vectordb/singlestore/singlestore.py +77 -4
  85. agno/vectordb/upstashdb/upstashdb.py +42 -2
  86. agno/vectordb/weaviate/weaviate.py +39 -3
  87. agno/workflow/types.py +5 -6
  88. agno/workflow/workflow.py +58 -2
  89. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/METADATA +4 -3
  90. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/RECORD +93 -82
  91. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/WHEEL +0 -0
  92. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/licenses/LICENSE +0 -0
  93. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  """Main class for the AG-UI app, used to expose an Agno Agent or Team in an AG-UI compatible format."""
2
2
 
3
- from typing import Optional
3
+ from typing import List, Optional
4
4
 
5
5
  from fastapi.routing import APIRouter
6
6
 
@@ -15,16 +15,32 @@ class AGUI(BaseInterface):
15
15
 
16
16
  router: APIRouter
17
17
 
18
- def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None):
18
+ def __init__(
19
+ self,
20
+ agent: Optional[Agent] = None,
21
+ team: Optional[Team] = None,
22
+ prefix: str = "",
23
+ tags: Optional[List[str]] = None,
24
+ ):
25
+ """
26
+ Initialize the AGUI interface.
27
+
28
+ Args:
29
+ agent: The agent to expose via AG-UI
30
+ team: The team to expose via AG-UI
31
+ prefix: Custom prefix for the router (e.g., "/agui/v1", "/chat/public")
32
+ tags: Custom tags for the router (e.g., ["AGUI", "Chat"], defaults to ["AGUI"])
33
+ """
19
34
  self.agent = agent
20
35
  self.team = team
36
+ self.prefix = prefix
37
+ self.tags = tags or ["AGUI"]
21
38
 
22
39
  if not (self.agent or self.team):
23
40
  raise ValueError("AGUI requires an agent or a team")
24
41
 
25
- def get_router(self, **kwargs) -> APIRouter:
26
- # Cannot be overridden
27
- self.router = APIRouter(tags=["AGUI"])
42
+ def get_router(self) -> APIRouter:
43
+ self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
28
44
 
29
45
  self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
30
46
 
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Optional
2
+ from typing import List, Optional
3
3
 
4
4
  from fastapi import APIRouter
5
5
 
@@ -11,11 +11,13 @@ from agno.workflow.workflow import Workflow
11
11
  class BaseInterface(ABC):
12
12
  type: str
13
13
  version: str = "1.0"
14
- router_prefix: str = ""
15
14
  agent: Optional[Agent] = None
16
15
  team: Optional[Team] = None
17
16
  workflow: Optional[Workflow] = None
18
17
 
18
+ prefix: str
19
+ tags: List[str]
20
+
19
21
  router: APIRouter
20
22
 
21
23
  @abstractmethod
@@ -1,5 +1,4 @@
1
- import logging
2
- from typing import Optional
1
+ from typing import List, Optional
3
2
 
4
3
  from fastapi.routing import APIRouter
5
4
 
@@ -9,25 +8,31 @@ from agno.os.interfaces.slack.router import attach_routes
9
8
  from agno.team.team import Team
10
9
  from agno.workflow.workflow import Workflow
11
10
 
12
- logger = logging.getLogger(__name__)
13
-
14
11
 
15
12
  class Slack(BaseInterface):
16
13
  type = "slack"
17
14
 
18
15
  router: APIRouter
19
16
 
20
- def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None, workflow: Optional[Workflow] = None):
17
+ def __init__(
18
+ self,
19
+ agent: Optional[Agent] = None,
20
+ team: Optional[Team] = None,
21
+ workflow: Optional[Workflow] = None,
22
+ prefix: str = "/slack",
23
+ tags: Optional[List[str]] = None,
24
+ ):
21
25
  self.agent = agent
22
26
  self.team = team
23
27
  self.workflow = workflow
28
+ self.prefix = prefix
29
+ self.tags = tags or ["Slack"]
24
30
 
25
31
  if not (self.agent or self.team or self.workflow):
26
32
  raise ValueError("Slack requires an agent, team or workflow")
27
33
 
28
- def get_router(self, **kwargs) -> APIRouter:
29
- # Cannot be overridden
30
- self.router = APIRouter(prefix="/slack", tags=["Slack"])
34
+ def get_router(self) -> APIRouter:
35
+ self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
31
36
 
32
37
  self.router = attach_routes(router=self.router, agent=self.agent, team=self.team, workflow=self.workflow)
33
38
 
@@ -123,6 +123,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
123
123
  response = await agent.arun(
124
124
  message_text,
125
125
  user_id=phone_number,
126
+ session_id=f"wa:{phone_number}",
126
127
  images=[Image(content=await get_media_async(message_image))] if message_image else None,
127
128
  files=[File(content=await get_media_async(message_doc))] if message_doc else None,
128
129
  videos=[Video(content=await get_media_async(message_video))] if message_video else None,
@@ -132,6 +133,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
132
133
  response = await team.arun( # type: ignore
133
134
  message_text,
134
135
  user_id=phone_number,
136
+ session_id=f"wa:{phone_number}",
135
137
  files=[File(content=await get_media_async(message_doc))] if message_doc else None,
136
138
  images=[Image(content=await get_media_async(message_image))] if message_image else None,
137
139
  videos=[Video(content=await get_media_async(message_video))] if message_video else None,
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import List, Optional
2
2
 
3
3
  from fastapi.routing import APIRouter
4
4
 
@@ -13,16 +13,23 @@ class Whatsapp(BaseInterface):
13
13
 
14
14
  router: APIRouter
15
15
 
16
- def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None):
16
+ def __init__(
17
+ self,
18
+ agent: Optional[Agent] = None,
19
+ team: Optional[Team] = None,
20
+ prefix: str = "/whatsapp",
21
+ tags: Optional[List[str]] = None,
22
+ ):
17
23
  self.agent = agent
18
24
  self.team = team
25
+ self.prefix = prefix
26
+ self.tags = tags or ["Whatsapp"]
19
27
 
20
28
  if not (self.agent or self.team):
21
29
  raise ValueError("Whatsapp requires an agent or a team")
22
30
 
23
- def get_router(self, **kwargs) -> APIRouter:
24
- # Cannot be overridden
25
- self.router = APIRouter(prefix="/whatsapp", tags=["Whatsapp"])
31
+ def get_router(self) -> APIRouter:
32
+ self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
26
33
 
27
34
  self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
28
35
 
agno/os/mcp.py CHANGED
@@ -54,7 +54,7 @@ def get_mcp_server(
54
54
  ) # type: ignore
55
55
  async def config() -> ConfigResponse:
56
56
  return ConfigResponse(
57
- os_id=os.os_id or "AgentOS",
57
+ os_id=os.id or "AgentOS",
58
58
  description=os.description,
59
59
  available_models=os.config.available_models if os.config else [],
60
60
  databases=[db.id for db in os.dbs.values()],
@@ -68,7 +68,7 @@ def get_mcp_server(
68
68
  teams=[TeamSummaryResponse.from_team(team) for team in os.teams] if os.teams else [],
69
69
  workflows=[WorkflowSummaryResponse.from_workflow(w) for w in os.workflows] if os.workflows else [],
70
70
  interfaces=[
71
- InterfaceResponse(type=interface.type, version=interface.version, route=interface.router_prefix)
71
+ InterfaceResponse(type=interface.type, version=interface.version, route=interface.prefix)
72
72
  for interface in os.interfaces
73
73
  ],
74
74
  )
@@ -0,0 +1,7 @@
1
+ from agno.os.middleware.jwt import (
2
+ JWTMiddleware,
3
+ )
4
+
5
+ __all__ = [
6
+ "JWTMiddleware",
7
+ ]
@@ -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)