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.
Files changed (57) hide show
  1. agno/agent/agent.py +500 -423
  2. agno/api/os.py +1 -1
  3. agno/culture/manager.py +12 -8
  4. agno/guardrails/prompt_injection.py +1 -0
  5. agno/knowledge/chunking/agentic.py +6 -2
  6. agno/knowledge/embedder/vllm.py +262 -0
  7. agno/knowledge/knowledge.py +37 -5
  8. agno/memory/manager.py +9 -4
  9. agno/models/anthropic/claude.py +1 -2
  10. agno/models/azure/ai_foundry.py +31 -14
  11. agno/models/azure/openai_chat.py +12 -4
  12. agno/models/base.py +106 -65
  13. agno/models/cerebras/cerebras.py +11 -6
  14. agno/models/groq/groq.py +7 -4
  15. agno/models/meta/llama.py +12 -6
  16. agno/models/meta/llama_openai.py +5 -1
  17. agno/models/openai/chat.py +26 -17
  18. agno/models/openai/responses.py +11 -63
  19. agno/models/requesty/requesty.py +5 -2
  20. agno/models/utils.py +254 -8
  21. agno/models/vertexai/claude.py +9 -13
  22. agno/os/app.py +13 -12
  23. agno/os/routers/evals/evals.py +8 -8
  24. agno/os/routers/evals/utils.py +1 -0
  25. agno/os/schema.py +56 -38
  26. agno/os/utils.py +27 -0
  27. agno/run/__init__.py +6 -0
  28. agno/run/agent.py +5 -0
  29. agno/run/base.py +18 -1
  30. agno/run/team.py +13 -9
  31. agno/run/workflow.py +39 -0
  32. agno/session/summary.py +8 -2
  33. agno/session/workflow.py +4 -3
  34. agno/team/team.py +302 -369
  35. agno/tools/exa.py +21 -16
  36. agno/tools/file.py +153 -25
  37. agno/tools/function.py +98 -17
  38. agno/tools/mcp/mcp.py +8 -1
  39. agno/tools/notion.py +204 -0
  40. agno/utils/agent.py +78 -0
  41. agno/utils/events.py +2 -0
  42. agno/utils/hooks.py +1 -1
  43. agno/utils/models/claude.py +25 -8
  44. agno/utils/print_response/workflow.py +115 -16
  45. agno/vectordb/__init__.py +2 -1
  46. agno/vectordb/milvus/milvus.py +5 -0
  47. agno/vectordb/redis/__init__.py +5 -0
  48. agno/vectordb/redis/redisdb.py +687 -0
  49. agno/workflow/__init__.py +2 -0
  50. agno/workflow/agent.py +299 -0
  51. agno/workflow/step.py +13 -2
  52. agno/workflow/workflow.py +969 -72
  53. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/METADATA +10 -3
  54. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/RECORD +57 -52
  55. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/WHEEL +0 -0
  56. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/licenses/LICENSE +0 -0
  57. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/top_level.txt +0 -0
agno/api/os.py CHANGED
@@ -14,4 +14,4 @@ def log_os_telemetry(launch: OSLaunch) -> None:
14
14
  )
15
15
  response.raise_for_status()
16
16
  except Exception as e:
17
- log_debug(f"Could not create OS launch: {e}")
17
+ log_debug(f"Could not register OS launch for telemetry: {e}")
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 = cast(AsyncBaseDb, self.db)
139
-
140
- return await self.db.get_all_cultural_knowledge(name=name)
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
- knowledge = self.get_all_knowledge()
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
 
@@ -32,6 +32,7 @@ class PromptInjectionGuardrail(BaseGuardrail):
32
32
  "ignore safeguards",
33
33
  "admin override",
34
34
  "root access",
35
+ "forget everything",
35
36
  ]
36
37
 
37
38
  def check(self, run_input: Union[RunInput, TeamRunInput]) -> None:
@@ -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
@@ -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 validate_filters(self, filters: Optional[Dict[str, Any]]) -> Tuple[Dict[str, Any], List[str]]:
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._get_filters_from_db)
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:
@@ -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()
@@ -160,7 +160,9 @@ class AzureAIFoundry(Model):
160
160
  Returns:
161
161
  ChatCompletionsClient: An instance of the Azure AI client.
162
162
  """
163
- if self.client:
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
- async with self.get_async_client() as client:
240
- provider_response = await client.complete(
241
- messages=[format_message(m) for m in messages],
242
- **self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
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
- async with self.get_async_client() as client:
320
- async_stream = await client.complete(
321
- messages=[format_message(m) for m in messages],
322
- stream=True,
323
- **self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
324
- )
325
- async for chunk in async_stream: # type: ignore
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
 
@@ -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
- # -*- Create client
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)