agno 2.2.5__py3-none-any.whl → 2.2.7__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 +500 -423
- agno/api/os.py +1 -1
- agno/culture/manager.py +12 -8
- agno/guardrails/prompt_injection.py +1 -0
- agno/knowledge/chunking/agentic.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +37 -5
- agno/memory/manager.py +9 -4
- agno/models/anthropic/claude.py +1 -2
- agno/models/azure/ai_foundry.py +31 -14
- agno/models/azure/openai_chat.py +12 -4
- agno/models/base.py +106 -65
- agno/models/cerebras/cerebras.py +11 -6
- agno/models/groq/groq.py +7 -4
- agno/models/meta/llama.py +12 -6
- agno/models/meta/llama_openai.py +5 -1
- agno/models/openai/chat.py +26 -17
- agno/models/openai/responses.py +11 -63
- agno/models/requesty/requesty.py +5 -2
- agno/models/utils.py +254 -8
- agno/models/vertexai/claude.py +9 -13
- agno/os/app.py +13 -12
- agno/os/routers/evals/evals.py +8 -8
- agno/os/routers/evals/utils.py +1 -0
- agno/os/schema.py +56 -38
- agno/os/utils.py +27 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +5 -0
- agno/run/base.py +18 -1
- agno/run/team.py +13 -9
- agno/run/workflow.py +39 -0
- agno/session/summary.py +8 -2
- agno/session/workflow.py +4 -3
- agno/team/team.py +302 -369
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -25
- agno/tools/function.py +98 -17
- agno/tools/mcp/mcp.py +8 -1
- agno/tools/notion.py +204 -0
- agno/utils/agent.py +78 -0
- agno/utils/events.py +2 -0
- agno/utils/hooks.py +1 -1
- agno/utils/models/claude.py +25 -8
- agno/utils/print_response/workflow.py +115 -16
- agno/vectordb/__init__.py +2 -1
- agno/vectordb/milvus/milvus.py +5 -0
- agno/vectordb/redis/__init__.py +5 -0
- agno/vectordb/redis/redisdb.py +687 -0
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/step.py +13 -2
- agno/workflow/workflow.py +969 -72
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/METADATA +10 -3
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/RECORD +57 -52
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/WHEEL +0 -0
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/top_level.txt +0 -0
agno/api/os.py
CHANGED
agno/culture/manager.py
CHANGED
|
@@ -8,6 +8,7 @@ from agno.db.base import AsyncBaseDb, BaseDb
|
|
|
8
8
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
9
9
|
from agno.models.base import Model
|
|
10
10
|
from agno.models.message import Message
|
|
11
|
+
from agno.models.utils import get_model
|
|
11
12
|
from agno.tools.function import Function
|
|
12
13
|
from agno.utils.log import (
|
|
13
14
|
log_debug,
|
|
@@ -55,7 +56,7 @@ class CultureManager:
|
|
|
55
56
|
|
|
56
57
|
def __init__(
|
|
57
58
|
self,
|
|
58
|
-
model: Optional[Model] = None,
|
|
59
|
+
model: Optional[Union[Model, str]] = None,
|
|
59
60
|
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
60
61
|
system_message: Optional[str] = None,
|
|
61
62
|
culture_capture_instructions: Optional[str] = None,
|
|
@@ -66,9 +67,7 @@ class CultureManager:
|
|
|
66
67
|
clear_knowledge: bool = True,
|
|
67
68
|
debug_mode: bool = False,
|
|
68
69
|
):
|
|
69
|
-
self.model = model
|
|
70
|
-
if self.model is not None and isinstance(self.model, str):
|
|
71
|
-
raise ValueError("Model must be a Model object, not a string")
|
|
70
|
+
self.model = get_model(model)
|
|
72
71
|
self.db = db
|
|
73
72
|
self.system_message = system_message
|
|
74
73
|
self.culture_capture_instructions = culture_capture_instructions
|
|
@@ -135,9 +134,10 @@ class CultureManager:
|
|
|
135
134
|
if not self.db:
|
|
136
135
|
return None
|
|
137
136
|
|
|
138
|
-
self.db
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
138
|
+
return await self.db.get_all_cultural_knowledge(name=name)
|
|
139
|
+
else:
|
|
140
|
+
return self.db.get_all_cultural_knowledge(name=name)
|
|
141
141
|
|
|
142
142
|
def add_cultural_knowledge(
|
|
143
143
|
self,
|
|
@@ -231,7 +231,11 @@ class CultureManager:
|
|
|
231
231
|
if not messages or not isinstance(messages, list):
|
|
232
232
|
raise ValueError("Invalid messages list")
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
235
|
+
knowledge = await self.aget_all_knowledge()
|
|
236
|
+
else:
|
|
237
|
+
knowledge = self.get_all_knowledge()
|
|
238
|
+
|
|
235
239
|
if knowledge is None:
|
|
236
240
|
knowledge = []
|
|
237
241
|
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
from typing import List, Optional
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
2
|
|
|
3
3
|
from agno.knowledge.chunking.strategy import ChunkingStrategy
|
|
4
4
|
from agno.knowledge.document.base import Document
|
|
5
5
|
from agno.models.base import Model
|
|
6
6
|
from agno.models.defaults import DEFAULT_OPENAI_MODEL_ID
|
|
7
7
|
from agno.models.message import Message
|
|
8
|
+
from agno.models.utils import get_model
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class AgenticChunking(ChunkingStrategy):
|
|
11
12
|
"""Chunking strategy that uses an LLM to determine natural breakpoints in the text"""
|
|
12
13
|
|
|
13
|
-
def __init__(self, model: Optional[Model] = None, max_chunk_size: int = 5000):
|
|
14
|
+
def __init__(self, model: Optional[Union[Model, str]] = None, max_chunk_size: int = 5000):
|
|
15
|
+
# Convert model string to Model instance
|
|
16
|
+
model = get_model(model)
|
|
14
17
|
if model is None:
|
|
15
18
|
try:
|
|
16
19
|
from agno.models.openai import OpenAIChat
|
|
17
20
|
except Exception:
|
|
18
21
|
raise ValueError("`openai` isn't installed. Please install it with `pip install openai`")
|
|
19
22
|
model = OpenAIChat(DEFAULT_OPENAI_MODEL_ID)
|
|
23
|
+
|
|
20
24
|
self.max_chunk_size = max_chunk_size
|
|
21
25
|
self.model = model
|
|
22
26
|
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from os import getenv
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from agno.knowledge.embedder.base import Embedder
|
|
7
|
+
from agno.utils.log import logger
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from vllm import LLM # type: ignore
|
|
11
|
+
from vllm.outputs import EmbeddingRequestOutput # type: ignore
|
|
12
|
+
except ImportError:
|
|
13
|
+
raise ImportError("`vllm` not installed. Please install using `pip install vllm`.")
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from openai import AsyncOpenAI
|
|
17
|
+
from openai import OpenAI as OpenAIClient
|
|
18
|
+
from openai.types.create_embedding_response import CreateEmbeddingResponse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class VLLMEmbedder(Embedder):
|
|
23
|
+
"""
|
|
24
|
+
VLLM Embedder supporting both local and remote deployment modes.
|
|
25
|
+
|
|
26
|
+
Local Mode (default):
|
|
27
|
+
- Loads model locally and runs inference on your GPU/CPU
|
|
28
|
+
- No API key required
|
|
29
|
+
- Example: VLLMEmbedder(id="intfloat/e5-mistral-7b-instruct")
|
|
30
|
+
|
|
31
|
+
Remote Mode:
|
|
32
|
+
- Connects to a remote vLLM server via OpenAI-compatible API
|
|
33
|
+
- Uses OpenAI SDK to communicate with vLLM's OpenAI-compatible endpoint
|
|
34
|
+
- Requires base_url and optionally api_key
|
|
35
|
+
- Example: VLLMEmbedder(base_url="http://localhost:8000/v1", api_key="your-key")
|
|
36
|
+
- Ref: https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
id: str = "sentence-transformers/all-MiniLM-L6-v2"
|
|
40
|
+
dimensions: int = 4096
|
|
41
|
+
# Local mode parameters
|
|
42
|
+
enforce_eager: bool = True
|
|
43
|
+
vllm_kwargs: Optional[Dict[str, Any]] = None
|
|
44
|
+
vllm_client: Optional[LLM] = None
|
|
45
|
+
# Remote mode parameters
|
|
46
|
+
api_key: Optional[str] = getenv("VLLM_API_KEY")
|
|
47
|
+
base_url: Optional[str] = None
|
|
48
|
+
request_params: Optional[Dict[str, Any]] = None
|
|
49
|
+
client_params: Optional[Dict[str, Any]] = None
|
|
50
|
+
remote_client: Optional["OpenAIClient"] = None # OpenAI-compatible client for vLLM server
|
|
51
|
+
async_remote_client: Optional["AsyncOpenAI"] = None # Async OpenAI-compatible client for vLLM server
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_remote(self) -> bool:
|
|
55
|
+
"""Determine if we should use remote mode."""
|
|
56
|
+
return self.base_url is not None
|
|
57
|
+
|
|
58
|
+
def _get_vllm_client(self) -> LLM:
|
|
59
|
+
"""Get local VLLM client."""
|
|
60
|
+
if self.vllm_client:
|
|
61
|
+
return self.vllm_client
|
|
62
|
+
|
|
63
|
+
_vllm_params: Dict[str, Any] = {
|
|
64
|
+
"model": self.id,
|
|
65
|
+
"task": "embed",
|
|
66
|
+
"enforce_eager": self.enforce_eager,
|
|
67
|
+
}
|
|
68
|
+
if self.vllm_kwargs:
|
|
69
|
+
_vllm_params.update(self.vllm_kwargs)
|
|
70
|
+
self.vllm_client = LLM(**_vllm_params)
|
|
71
|
+
return self.vllm_client
|
|
72
|
+
|
|
73
|
+
def _get_remote_client(self) -> "OpenAIClient":
|
|
74
|
+
"""Get OpenAI-compatible client for remote vLLM server."""
|
|
75
|
+
if self.remote_client:
|
|
76
|
+
return self.remote_client
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
from openai import OpenAI as OpenAIClient
|
|
80
|
+
except ImportError:
|
|
81
|
+
raise ImportError("`openai` package required for remote vLLM mode. ")
|
|
82
|
+
|
|
83
|
+
_client_params: Dict[str, Any] = {
|
|
84
|
+
"api_key": self.api_key or "EMPTY", # VLLM can run without API key
|
|
85
|
+
"base_url": self.base_url,
|
|
86
|
+
}
|
|
87
|
+
if self.client_params:
|
|
88
|
+
_client_params.update(self.client_params)
|
|
89
|
+
self.remote_client = OpenAIClient(**_client_params)
|
|
90
|
+
return self.remote_client
|
|
91
|
+
|
|
92
|
+
def _get_async_remote_client(self) -> "AsyncOpenAI":
|
|
93
|
+
"""Get async OpenAI-compatible client for remote vLLM server."""
|
|
94
|
+
if self.async_remote_client:
|
|
95
|
+
return self.async_remote_client
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
from openai import AsyncOpenAI
|
|
99
|
+
except ImportError:
|
|
100
|
+
raise ImportError("`openai` package required for remote vLLM mode. ")
|
|
101
|
+
|
|
102
|
+
_client_params: Dict[str, Any] = {
|
|
103
|
+
"api_key": self.api_key or "EMPTY",
|
|
104
|
+
"base_url": self.base_url,
|
|
105
|
+
}
|
|
106
|
+
if self.client_params:
|
|
107
|
+
_client_params.update(self.client_params)
|
|
108
|
+
self.async_remote_client = AsyncOpenAI(**_client_params)
|
|
109
|
+
return self.async_remote_client
|
|
110
|
+
|
|
111
|
+
def _create_embedding_local(self, text: str) -> Optional[EmbeddingRequestOutput]:
|
|
112
|
+
"""Create embedding using local VLLM."""
|
|
113
|
+
try:
|
|
114
|
+
outputs = self._get_vllm_client().embed([text])
|
|
115
|
+
return outputs[0] if outputs else None
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"Error creating local embedding: {e}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def _create_embedding_remote(self, text: str) -> "CreateEmbeddingResponse":
|
|
121
|
+
"""Create embedding using remote vLLM server."""
|
|
122
|
+
_request_params: Dict[str, Any] = {
|
|
123
|
+
"input": text,
|
|
124
|
+
"model": self.id,
|
|
125
|
+
}
|
|
126
|
+
if self.request_params:
|
|
127
|
+
_request_params.update(self.request_params)
|
|
128
|
+
return self._get_remote_client().embeddings.create(**_request_params)
|
|
129
|
+
|
|
130
|
+
def get_embedding(self, text: str) -> List[float]:
|
|
131
|
+
try:
|
|
132
|
+
if self.is_remote:
|
|
133
|
+
# Remote mode: OpenAI-compatible API
|
|
134
|
+
response: "CreateEmbeddingResponse" = self._create_embedding_remote(text=text)
|
|
135
|
+
return response.data[0].embedding
|
|
136
|
+
else:
|
|
137
|
+
# Local mode: Direct VLLM
|
|
138
|
+
output = self._create_embedding_local(text=text)
|
|
139
|
+
if output and hasattr(output, "outputs") and hasattr(output.outputs, "embedding"):
|
|
140
|
+
embedding = output.outputs.embedding
|
|
141
|
+
if len(embedding) != self.dimensions:
|
|
142
|
+
logger.warning(f"Expected embedding dimension {self.dimensions}, but got {len(embedding)}")
|
|
143
|
+
return embedding
|
|
144
|
+
return []
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"Error extracting embedding: {e}")
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
def get_embedding_and_usage(self, text: str) -> Tuple[List[float], Optional[Dict]]:
|
|
150
|
+
if self.is_remote:
|
|
151
|
+
try:
|
|
152
|
+
response: "CreateEmbeddingResponse" = self._create_embedding_remote(text=text)
|
|
153
|
+
embedding = response.data[0].embedding
|
|
154
|
+
usage = response.usage
|
|
155
|
+
if usage:
|
|
156
|
+
return embedding, usage.model_dump()
|
|
157
|
+
return embedding, None
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.warning(f"Error in remote embedding: {e}")
|
|
160
|
+
return [], None
|
|
161
|
+
else:
|
|
162
|
+
embedding = self.get_embedding(text=text)
|
|
163
|
+
# Local VLLM doesn't provide usage information
|
|
164
|
+
return embedding, None
|
|
165
|
+
|
|
166
|
+
async def async_get_embedding(self, text: str) -> List[float]:
|
|
167
|
+
"""Async version of get_embedding using thread executor for local mode."""
|
|
168
|
+
if self.is_remote:
|
|
169
|
+
# Remote mode: async client for vLLM server
|
|
170
|
+
try:
|
|
171
|
+
req: Dict[str, Any] = {
|
|
172
|
+
"input": text,
|
|
173
|
+
"model": self.id,
|
|
174
|
+
}
|
|
175
|
+
if self.request_params:
|
|
176
|
+
req.update(self.request_params)
|
|
177
|
+
response: "CreateEmbeddingResponse" = await self._get_async_remote_client().embeddings.create(**req)
|
|
178
|
+
return response.data[0].embedding
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Error in async remote embedding: {e}")
|
|
181
|
+
return []
|
|
182
|
+
else:
|
|
183
|
+
# Local mode: use thread executor for CPU-bound operations
|
|
184
|
+
loop = asyncio.get_event_loop()
|
|
185
|
+
return await loop.run_in_executor(None, self.get_embedding, text)
|
|
186
|
+
|
|
187
|
+
async def async_get_embedding_and_usage(self, text: str) -> Tuple[List[float], Optional[Dict]]:
|
|
188
|
+
"""Async version of get_embedding_and_usage using thread executor for local mode."""
|
|
189
|
+
if self.is_remote:
|
|
190
|
+
try:
|
|
191
|
+
req: Dict[str, Any] = {
|
|
192
|
+
"input": text,
|
|
193
|
+
"model": self.id,
|
|
194
|
+
}
|
|
195
|
+
if self.request_params:
|
|
196
|
+
req.update(self.request_params)
|
|
197
|
+
response: "CreateEmbeddingResponse" = await self._get_async_remote_client().embeddings.create(**req)
|
|
198
|
+
embedding = response.data[0].embedding
|
|
199
|
+
usage = response.usage
|
|
200
|
+
return embedding, usage.model_dump() if usage else None
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.warning(f"Error in async remote embedding: {e}")
|
|
203
|
+
return [], None
|
|
204
|
+
else:
|
|
205
|
+
# Local mode: use thread executor for CPU-bound operations
|
|
206
|
+
try:
|
|
207
|
+
loop = asyncio.get_event_loop()
|
|
208
|
+
return await loop.run_in_executor(None, self.get_embedding_and_usage, text)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.warning(f"Error in async local embedding: {e}")
|
|
211
|
+
return [], None
|
|
212
|
+
|
|
213
|
+
async def async_get_embeddings_batch_and_usage(
|
|
214
|
+
self, texts: List[str]
|
|
215
|
+
) -> Tuple[List[List[float]], List[Optional[Dict]]]:
|
|
216
|
+
"""
|
|
217
|
+
Get embeddings and usage for multiple texts in batches (async version).
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
texts: List of text strings to embed
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Tuple of (List of embedding vectors, List of usage dictionaries)
|
|
224
|
+
"""
|
|
225
|
+
all_embeddings = []
|
|
226
|
+
all_usage = []
|
|
227
|
+
logger.info(f"Getting embeddings for {len(texts)} texts in batches of {self.batch_size} (async)")
|
|
228
|
+
|
|
229
|
+
for i in range(0, len(texts), self.batch_size):
|
|
230
|
+
batch_texts = texts[i : i + self.batch_size]
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if self.is_remote:
|
|
234
|
+
# Remote mode: use batch API
|
|
235
|
+
req: Dict[str, Any] = {
|
|
236
|
+
"input": batch_texts,
|
|
237
|
+
"model": self.id,
|
|
238
|
+
}
|
|
239
|
+
if self.request_params:
|
|
240
|
+
req.update(self.request_params)
|
|
241
|
+
response: "CreateEmbeddingResponse" = await self._get_async_remote_client().embeddings.create(**req)
|
|
242
|
+
batch_embeddings = [data.embedding for data in response.data]
|
|
243
|
+
all_embeddings.extend(batch_embeddings)
|
|
244
|
+
|
|
245
|
+
# For each embedding in the batch, add the same usage information
|
|
246
|
+
usage_dict = response.usage.model_dump() if response.usage else None
|
|
247
|
+
all_usage.extend([usage_dict] * len(batch_embeddings))
|
|
248
|
+
else:
|
|
249
|
+
# Local mode: process individually using thread executor
|
|
250
|
+
for text in batch_texts:
|
|
251
|
+
embedding, usage = await self.async_get_embedding_and_usage(text)
|
|
252
|
+
all_embeddings.append(embedding)
|
|
253
|
+
all_usage.append(usage)
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning(f"Error in async batch embedding: {e}")
|
|
257
|
+
# Fallback: add empty results for failed batch
|
|
258
|
+
for _ in batch_texts:
|
|
259
|
+
all_embeddings.append([])
|
|
260
|
+
all_usage.append(None)
|
|
261
|
+
|
|
262
|
+
return all_embeddings, all_usage
|
agno/knowledge/knowledge.py
CHANGED
|
@@ -4,7 +4,6 @@ import io
|
|
|
4
4
|
import time
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from functools import cached_property
|
|
8
7
|
from io import BytesIO
|
|
9
8
|
from os.path import basename
|
|
10
9
|
from pathlib import Path
|
|
@@ -187,10 +186,14 @@ class Knowledge:
|
|
|
187
186
|
paths: Optional[List[str]] = None,
|
|
188
187
|
urls: Optional[List[str]] = None,
|
|
189
188
|
metadata: Optional[Dict[str, str]] = None,
|
|
189
|
+
topics: Optional[List[str]] = None,
|
|
190
|
+
text_contents: Optional[List[str]] = None,
|
|
191
|
+
reader: Optional[Reader] = None,
|
|
190
192
|
include: Optional[List[str]] = None,
|
|
191
193
|
exclude: Optional[List[str]] = None,
|
|
192
194
|
upsert: bool = True,
|
|
193
195
|
skip_if_exists: bool = False,
|
|
196
|
+
remote_content: Optional[RemoteContent] = None,
|
|
194
197
|
) -> None: ...
|
|
195
198
|
|
|
196
199
|
def add_contents(self, *args, **kwargs) -> None:
|
|
@@ -208,10 +211,14 @@ class Knowledge:
|
|
|
208
211
|
paths: Optional list of file paths to load content from
|
|
209
212
|
urls: Optional list of URLs to load content from
|
|
210
213
|
metadata: Optional metadata dictionary to apply to all content
|
|
214
|
+
topics: Optional list of topics to add
|
|
215
|
+
text_contents: Optional list of text content strings to add
|
|
216
|
+
reader: Optional reader to use for processing content
|
|
211
217
|
include: Optional list of file patterns to include
|
|
212
218
|
exclude: Optional list of file patterns to exclude
|
|
213
219
|
upsert: Whether to update existing content if it already exists
|
|
214
220
|
skip_if_exists: Whether to skip adding content if it already exists
|
|
221
|
+
remote_content: Optional remote content (S3, GCS, etc.) to add
|
|
215
222
|
"""
|
|
216
223
|
asyncio.run(self.add_contents_async(*args, **kwargs))
|
|
217
224
|
|
|
@@ -1449,14 +1456,16 @@ class Knowledge:
|
|
|
1449
1456
|
def get_valid_filters(self) -> Set[str]:
|
|
1450
1457
|
if self.valid_metadata_filters is None:
|
|
1451
1458
|
self.valid_metadata_filters = set()
|
|
1452
|
-
self.valid_metadata_filters.update(self._get_filters_from_db)
|
|
1459
|
+
self.valid_metadata_filters.update(self._get_filters_from_db())
|
|
1453
1460
|
return self.valid_metadata_filters
|
|
1454
1461
|
|
|
1455
|
-
def
|
|
1462
|
+
async def aget_valid_filters(self) -> Set[str]:
|
|
1456
1463
|
if self.valid_metadata_filters is None:
|
|
1457
1464
|
self.valid_metadata_filters = set()
|
|
1458
|
-
self.valid_metadata_filters.update(self.
|
|
1465
|
+
self.valid_metadata_filters.update(await self._aget_filters_from_db())
|
|
1466
|
+
return self.valid_metadata_filters
|
|
1459
1467
|
|
|
1468
|
+
def _validate_filters(self, filters: Optional[Dict[str, Any]]) -> Tuple[Dict[str, Any], List[str]]:
|
|
1460
1469
|
if not filters:
|
|
1461
1470
|
return {}, []
|
|
1462
1471
|
|
|
@@ -1480,6 +1489,20 @@ class Knowledge:
|
|
|
1480
1489
|
|
|
1481
1490
|
return valid_filters, invalid_keys
|
|
1482
1491
|
|
|
1492
|
+
def validate_filters(self, filters: Optional[Dict[str, Any]]) -> Tuple[Dict[str, Any], List[str]]:
|
|
1493
|
+
if self.valid_metadata_filters is None:
|
|
1494
|
+
self.valid_metadata_filters = set()
|
|
1495
|
+
self.valid_metadata_filters.update(self._get_filters_from_db())
|
|
1496
|
+
|
|
1497
|
+
return self._validate_filters(filters)
|
|
1498
|
+
|
|
1499
|
+
async def async_validate_filters(self, filters: Optional[Dict[str, Any]]) -> Tuple[Dict[str, Any], List[str]]:
|
|
1500
|
+
if self.valid_metadata_filters is None:
|
|
1501
|
+
self.valid_metadata_filters = set()
|
|
1502
|
+
self.valid_metadata_filters.update(await self._aget_filters_from_db())
|
|
1503
|
+
|
|
1504
|
+
return self._validate_filters(filters)
|
|
1505
|
+
|
|
1483
1506
|
def add_filters(self, metadata: Dict[str, Any]) -> None:
|
|
1484
1507
|
if self.valid_metadata_filters is None:
|
|
1485
1508
|
self.valid_metadata_filters = set()
|
|
@@ -1488,7 +1511,6 @@ class Knowledge:
|
|
|
1488
1511
|
for key in metadata.keys():
|
|
1489
1512
|
self.valid_metadata_filters.add(key)
|
|
1490
1513
|
|
|
1491
|
-
@cached_property
|
|
1492
1514
|
def _get_filters_from_db(self) -> Set[str]:
|
|
1493
1515
|
if self.contents_db is None:
|
|
1494
1516
|
return set()
|
|
@@ -1499,6 +1521,16 @@ class Knowledge:
|
|
|
1499
1521
|
valid_filters.update(content.metadata.keys())
|
|
1500
1522
|
return valid_filters
|
|
1501
1523
|
|
|
1524
|
+
async def _aget_filters_from_db(self) -> Set[str]:
|
|
1525
|
+
if self.contents_db is None:
|
|
1526
|
+
return set()
|
|
1527
|
+
contents, _ = await self.aget_content()
|
|
1528
|
+
valid_filters: Set[str] = set()
|
|
1529
|
+
for content in contents:
|
|
1530
|
+
if content.metadata:
|
|
1531
|
+
valid_filters.update(content.metadata.keys())
|
|
1532
|
+
return valid_filters
|
|
1533
|
+
|
|
1502
1534
|
def remove_vector_by_id(self, id: str) -> bool:
|
|
1503
1535
|
from agno.vectordb import VectorDb
|
|
1504
1536
|
|
agno/memory/manager.py
CHANGED
|
@@ -11,6 +11,7 @@ from agno.db.base import AsyncBaseDb, BaseDb
|
|
|
11
11
|
from agno.db.schemas import UserMemory
|
|
12
12
|
from agno.models.base import Model
|
|
13
13
|
from agno.models.message import Message
|
|
14
|
+
from agno.models.utils import get_model
|
|
14
15
|
from agno.tools.function import Function
|
|
15
16
|
from agno.utils.log import (
|
|
16
17
|
log_debug,
|
|
@@ -66,7 +67,7 @@ class MemoryManager:
|
|
|
66
67
|
|
|
67
68
|
def __init__(
|
|
68
69
|
self,
|
|
69
|
-
model: Optional[Model] = None,
|
|
70
|
+
model: Optional[Union[Model, str]] = None,
|
|
70
71
|
system_message: Optional[str] = None,
|
|
71
72
|
memory_capture_instructions: Optional[str] = None,
|
|
72
73
|
additional_instructions: Optional[str] = None,
|
|
@@ -77,9 +78,7 @@ class MemoryManager:
|
|
|
77
78
|
clear_memories: bool = False,
|
|
78
79
|
debug_mode: bool = False,
|
|
79
80
|
):
|
|
80
|
-
self.model = model
|
|
81
|
-
if self.model is not None and isinstance(self.model, str):
|
|
82
|
-
raise ValueError("Model must be a Model object, not a string")
|
|
81
|
+
self.model = model # type: ignore[assignment]
|
|
83
82
|
self.system_message = system_message
|
|
84
83
|
self.memory_capture_instructions = memory_capture_instructions
|
|
85
84
|
self.additional_instructions = additional_instructions
|
|
@@ -90,6 +89,12 @@ class MemoryManager:
|
|
|
90
89
|
self.clear_memories = clear_memories
|
|
91
90
|
self.debug_mode = debug_mode
|
|
92
91
|
|
|
92
|
+
self._get_models()
|
|
93
|
+
|
|
94
|
+
def _get_models(self) -> None:
|
|
95
|
+
if self.model is not None:
|
|
96
|
+
self.model = get_model(self.model)
|
|
97
|
+
|
|
93
98
|
def get_model(self) -> Model:
|
|
94
99
|
if self.model is None:
|
|
95
100
|
try:
|
agno/models/anthropic/claude.py
CHANGED
|
@@ -98,7 +98,6 @@ class Claude(Model):
|
|
|
98
98
|
timeout: Optional[float] = None
|
|
99
99
|
client_params: Optional[Dict[str, Any]] = None
|
|
100
100
|
|
|
101
|
-
# Anthropic clients
|
|
102
101
|
client: Optional[AnthropicClient] = None
|
|
103
102
|
async_client: Optional[AsyncAnthropicClient] = None
|
|
104
103
|
|
|
@@ -145,7 +144,7 @@ class Claude(Model):
|
|
|
145
144
|
"""
|
|
146
145
|
Returns an instance of the async Anthropic client.
|
|
147
146
|
"""
|
|
148
|
-
if self.async_client:
|
|
147
|
+
if self.async_client and not self.async_client.is_closed():
|
|
149
148
|
return self.async_client
|
|
150
149
|
|
|
151
150
|
_client_params = self._get_client_params()
|
agno/models/azure/ai_foundry.py
CHANGED
|
@@ -160,7 +160,9 @@ class AzureAIFoundry(Model):
|
|
|
160
160
|
Returns:
|
|
161
161
|
ChatCompletionsClient: An instance of the Azure AI client.
|
|
162
162
|
"""
|
|
163
|
-
if
|
|
163
|
+
# Check if client exists and is not closed
|
|
164
|
+
# Azure's client doesn't have is_closed(), so we check if _client exists
|
|
165
|
+
if self.client and hasattr(self.client, "_client"):
|
|
164
166
|
return self.client
|
|
165
167
|
|
|
166
168
|
client_params = self._get_client_params()
|
|
@@ -174,11 +176,28 @@ class AzureAIFoundry(Model):
|
|
|
174
176
|
Returns:
|
|
175
177
|
AsyncChatCompletionsClient: An instance of the asynchronous Azure AI client.
|
|
176
178
|
"""
|
|
179
|
+
# Check if client exists and is not closed
|
|
180
|
+
# Azure's async client doesn't have is_closed(), so we check if _client exists
|
|
181
|
+
if self.async_client and hasattr(self.async_client, "_client"):
|
|
182
|
+
return self.async_client
|
|
183
|
+
|
|
177
184
|
client_params = self._get_client_params()
|
|
178
185
|
|
|
179
186
|
self.async_client = AsyncChatCompletionsClient(**client_params)
|
|
180
187
|
return self.async_client
|
|
181
188
|
|
|
189
|
+
def close(self) -> None:
|
|
190
|
+
"""Close the synchronous client and clean up resources."""
|
|
191
|
+
if self.client:
|
|
192
|
+
self.client.close()
|
|
193
|
+
self.client = None
|
|
194
|
+
|
|
195
|
+
async def aclose(self) -> None:
|
|
196
|
+
"""Close the asynchronous client and clean up resources."""
|
|
197
|
+
if self.async_client:
|
|
198
|
+
await self.async_client.close()
|
|
199
|
+
self.async_client = None
|
|
200
|
+
|
|
182
201
|
def invoke(
|
|
183
202
|
self,
|
|
184
203
|
messages: List[Message],
|
|
@@ -236,11 +255,10 @@ class AzureAIFoundry(Model):
|
|
|
236
255
|
run_response.metrics.set_time_to_first_token()
|
|
237
256
|
|
|
238
257
|
assistant_message.metrics.start_timer()
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
258
|
+
provider_response = await self.get_async_client().complete(
|
|
259
|
+
messages=[format_message(m) for m in messages],
|
|
260
|
+
**self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
|
|
261
|
+
)
|
|
244
262
|
assistant_message.metrics.stop_timer()
|
|
245
263
|
|
|
246
264
|
model_response = self._parse_provider_response(provider_response, response_format=response_format) # type: ignore
|
|
@@ -316,14 +334,13 @@ class AzureAIFoundry(Model):
|
|
|
316
334
|
|
|
317
335
|
assistant_message.metrics.start_timer()
|
|
318
336
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
yield self._parse_provider_response_delta(chunk)
|
|
337
|
+
async_stream = await self.get_async_client().complete(
|
|
338
|
+
messages=[format_message(m) for m in messages],
|
|
339
|
+
stream=True,
|
|
340
|
+
**self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
|
|
341
|
+
)
|
|
342
|
+
async for chunk in async_stream: # type: ignore
|
|
343
|
+
yield self._parse_provider_response_delta(chunk)
|
|
327
344
|
|
|
328
345
|
assistant_message.metrics.stop_timer()
|
|
329
346
|
|
agno/models/azure/openai_chat.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
7
|
from agno.models.openai.like import OpenAILike
|
|
8
|
+
from agno.utils.log import log_debug
|
|
8
9
|
|
|
9
10
|
try:
|
|
10
11
|
from openai import AsyncAzureOpenAI as AsyncAzureOpenAIClient
|
|
@@ -70,7 +71,6 @@ class AzureOpenAI(OpenAILike):
|
|
|
70
71
|
"base_url": self.base_url,
|
|
71
72
|
"azure_ad_token": self.azure_ad_token,
|
|
72
73
|
"azure_ad_token_provider": self.azure_ad_token_provider,
|
|
73
|
-
"http_client": self.http_client,
|
|
74
74
|
}
|
|
75
75
|
if self.default_headers is not None:
|
|
76
76
|
_client_params["default_headers"] = self.default_headers
|
|
@@ -95,7 +95,13 @@ class AzureOpenAI(OpenAILike):
|
|
|
95
95
|
|
|
96
96
|
_client_params: Dict[str, Any] = self._get_client_params()
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
if self.http_client:
|
|
99
|
+
if isinstance(self.http_client, httpx.Client):
|
|
100
|
+
_client_params["http_client"] = self.http_client
|
|
101
|
+
else:
|
|
102
|
+
log_debug("http_client is not an instance of httpx.Client.")
|
|
103
|
+
|
|
104
|
+
# Create client
|
|
99
105
|
self.client = AzureOpenAIClient(**_client_params)
|
|
100
106
|
return self.client
|
|
101
107
|
|
|
@@ -106,14 +112,16 @@ class AzureOpenAI(OpenAILike):
|
|
|
106
112
|
Returns:
|
|
107
113
|
AsyncAzureOpenAIClient: An instance of the asynchronous OpenAI client.
|
|
108
114
|
"""
|
|
109
|
-
if self.async_client:
|
|
115
|
+
if self.async_client and not self.async_client.is_closed():
|
|
110
116
|
return self.async_client
|
|
111
117
|
|
|
112
118
|
_client_params: Dict[str, Any] = self._get_client_params()
|
|
113
119
|
|
|
114
|
-
if self.http_client:
|
|
120
|
+
if self.http_client and isinstance(self.http_client, httpx.AsyncClient):
|
|
115
121
|
_client_params["http_client"] = self.http_client
|
|
116
122
|
else:
|
|
123
|
+
if self.http_client:
|
|
124
|
+
log_debug("The current http_client is not async. A default httpx.AsyncClient will be used instead.")
|
|
117
125
|
# Create a new async HTTP client with custom limits
|
|
118
126
|
_client_params["http_client"] = httpx.AsyncClient(
|
|
119
127
|
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
|