agno 2.2.13__py3-none-any.whl → 2.3.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 (92) hide show
  1. agno/agent/agent.py +197 -110
  2. agno/api/api.py +2 -0
  3. agno/db/base.py +26 -0
  4. agno/db/dynamo/dynamo.py +8 -0
  5. agno/db/dynamo/schemas.py +1 -0
  6. agno/db/firestore/firestore.py +8 -0
  7. agno/db/firestore/schemas.py +1 -0
  8. agno/db/gcs_json/gcs_json_db.py +8 -0
  9. agno/db/in_memory/in_memory_db.py +8 -1
  10. agno/db/json/json_db.py +8 -0
  11. agno/db/migrations/manager.py +199 -0
  12. agno/db/migrations/versions/__init__.py +0 -0
  13. agno/db/migrations/versions/v2_3_0.py +938 -0
  14. agno/db/mongo/async_mongo.py +16 -6
  15. agno/db/mongo/mongo.py +11 -0
  16. agno/db/mongo/schemas.py +3 -0
  17. agno/db/mongo/utils.py +17 -0
  18. agno/db/mysql/mysql.py +76 -3
  19. agno/db/mysql/schemas.py +20 -10
  20. agno/db/postgres/async_postgres.py +99 -25
  21. agno/db/postgres/postgres.py +75 -6
  22. agno/db/postgres/schemas.py +30 -20
  23. agno/db/redis/redis.py +15 -2
  24. agno/db/redis/schemas.py +4 -0
  25. agno/db/schemas/memory.py +13 -0
  26. agno/db/singlestore/schemas.py +11 -0
  27. agno/db/singlestore/singlestore.py +79 -5
  28. agno/db/sqlite/async_sqlite.py +97 -19
  29. agno/db/sqlite/schemas.py +10 -0
  30. agno/db/sqlite/sqlite.py +79 -2
  31. agno/db/surrealdb/surrealdb.py +8 -0
  32. agno/knowledge/chunking/semantic.py +7 -2
  33. agno/knowledge/embedder/nebius.py +1 -1
  34. agno/knowledge/knowledge.py +57 -86
  35. agno/knowledge/reader/csv_reader.py +7 -9
  36. agno/knowledge/reader/docx_reader.py +5 -5
  37. agno/knowledge/reader/field_labeled_csv_reader.py +16 -18
  38. agno/knowledge/reader/json_reader.py +5 -4
  39. agno/knowledge/reader/markdown_reader.py +8 -8
  40. agno/knowledge/reader/pdf_reader.py +11 -11
  41. agno/knowledge/reader/pptx_reader.py +5 -5
  42. agno/knowledge/reader/s3_reader.py +3 -3
  43. agno/knowledge/reader/text_reader.py +8 -8
  44. agno/knowledge/reader/web_search_reader.py +1 -48
  45. agno/knowledge/reader/website_reader.py +10 -10
  46. agno/models/anthropic/claude.py +319 -28
  47. agno/models/aws/claude.py +32 -0
  48. agno/models/azure/openai_chat.py +19 -10
  49. agno/models/base.py +612 -545
  50. agno/models/cerebras/cerebras.py +8 -11
  51. agno/models/cohere/chat.py +27 -1
  52. agno/models/google/gemini.py +39 -7
  53. agno/models/groq/groq.py +25 -11
  54. agno/models/meta/llama.py +20 -9
  55. agno/models/meta/llama_openai.py +3 -19
  56. agno/models/nebius/nebius.py +4 -4
  57. agno/models/openai/chat.py +30 -14
  58. agno/models/openai/responses.py +10 -13
  59. agno/models/response.py +1 -0
  60. agno/models/vertexai/claude.py +26 -0
  61. agno/os/app.py +8 -19
  62. agno/os/router.py +54 -0
  63. agno/os/routers/knowledge/knowledge.py +2 -2
  64. agno/os/schema.py +2 -2
  65. agno/session/agent.py +57 -92
  66. agno/session/summary.py +1 -1
  67. agno/session/team.py +62 -112
  68. agno/session/workflow.py +353 -57
  69. agno/team/team.py +227 -125
  70. agno/tools/models/nebius.py +5 -5
  71. agno/tools/models_labs.py +20 -10
  72. agno/tools/nano_banana.py +151 -0
  73. agno/tools/yfinance.py +12 -11
  74. agno/utils/http.py +111 -0
  75. agno/utils/media.py +11 -0
  76. agno/utils/models/claude.py +8 -0
  77. agno/utils/print_response/agent.py +33 -12
  78. agno/utils/print_response/team.py +22 -12
  79. agno/vectordb/couchbase/couchbase.py +6 -2
  80. agno/workflow/condition.py +13 -0
  81. agno/workflow/loop.py +13 -0
  82. agno/workflow/parallel.py +13 -0
  83. agno/workflow/router.py +13 -0
  84. agno/workflow/step.py +120 -20
  85. agno/workflow/steps.py +13 -0
  86. agno/workflow/workflow.py +76 -63
  87. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/METADATA +6 -2
  88. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/RECORD +91 -88
  89. agno/tools/googlesearch.py +0 -98
  90. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/WHEEL +0 -0
  91. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/licenses/LICENSE +0 -0
  92. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/top_level.txt +0 -0
@@ -12,12 +12,12 @@ from agno.utils.log import log_error, log_warning
12
12
 
13
13
 
14
14
  class NebiusTools(Toolkit):
15
- """Tools for interacting with Nebius AI Studio's text-to-image API"""
15
+ """Tools for interacting with Nebius Token Factory's text-to-image API"""
16
16
 
17
17
  def __init__(
18
18
  self,
19
19
  api_key: Optional[str] = None,
20
- base_url: str = "https://api.studio.nebius.com/v1",
20
+ base_url: str = "https://api.tokenfactory.nebius.com/v1",
21
21
  image_model: str = "black-forest-labs/flux-schnell",
22
22
  image_quality: Optional[str] = "standard",
23
23
  image_size: Optional[str] = "1024x1024",
@@ -26,11 +26,11 @@ class NebiusTools(Toolkit):
26
26
  all: bool = False,
27
27
  **kwargs,
28
28
  ):
29
- """Initialize Nebius AI Studio text-to-image tools.
29
+ """Initialize Nebius Token Factory text-to-image tools.
30
30
 
31
31
  Args:
32
32
  api_key: Nebius API key. If not provided, will look for NEBIUS_API_KEY environment variable.
33
- base_url: The base URL for the Nebius AI Studio API. This should be configured according to Nebius's documentation.
33
+ base_url: The base URL for the Nebius Token Factory API. This should be configured according to Nebius's documentation.
34
34
  image_model: The model to use for generation. Options include:
35
35
  - "black-forest-labs/flux-schnell" (fastest)
36
36
  - "black-forest-labs/flux-dev" (balanced)
@@ -69,7 +69,7 @@ class NebiusTools(Toolkit):
69
69
  agent: Agent,
70
70
  prompt: str,
71
71
  ) -> ToolResult:
72
- """Generate images based on a text prompt using Nebius AI Studio.
72
+ """Generate images based on a text prompt using Nebius Token Factory.
73
73
 
74
74
  Args:
75
75
  agent: The agent instance for adding images
agno/tools/models_labs.py CHANGED
@@ -4,10 +4,8 @@ from os import getenv
4
4
  from typing import Any, Dict, List, Optional, Union
5
5
  from uuid import uuid4
6
6
 
7
- from agno.agent import Agent
8
7
  from agno.media import Audio, Image, Video
9
8
  from agno.models.response import FileType
10
- from agno.team import Team
11
9
  from agno.tools import Toolkit
12
10
  from agno.tools.function import ToolResult
13
11
  from agno.utils.log import log_debug, log_info, logger
@@ -22,12 +20,14 @@ MODELS_LAB_URLS = {
22
20
  "MP4": "https://modelslab.com/api/v6/video/text2video",
23
21
  "MP3": "https://modelslab.com/api/v6/voice/music_gen",
24
22
  "GIF": "https://modelslab.com/api/v6/video/text2video",
23
+ "WAV": "https://modelslab.com/api/v6/voice/sfx",
25
24
  }
26
25
 
27
26
  MODELS_LAB_FETCH_URLS = {
28
27
  "MP4": "https://modelslab.com/api/v6/video/fetch",
29
28
  "MP3": "https://modelslab.com/api/v6/voice/fetch",
30
29
  "GIF": "https://modelslab.com/api/v6/video/fetch",
30
+ "WAV": "https://modelslab.com/api/v6/voice/fetch",
31
31
  }
32
32
 
33
33
 
@@ -78,6 +78,13 @@ class ModelsLabTools(Toolkit):
78
78
  "output_type": self.file_type.value,
79
79
  }
80
80
  base_payload |= video_template # Use |= instead of update()
81
+ elif self.file_type == FileType.WAV:
82
+ sfx_template = {
83
+ "duration": 10,
84
+ "output_format": "wav",
85
+ "temp": False,
86
+ }
87
+ base_payload |= sfx_template # Use |= instead of update()
81
88
  else:
82
89
  audio_template = {
83
90
  "base64": False,
@@ -101,7 +108,7 @@ class ModelsLabTools(Toolkit):
101
108
  elif self.file_type == FileType.GIF:
102
109
  image_artifact = Image(id=str(media_id), url=media_url)
103
110
  artifacts["images"].append(image_artifact)
104
- elif self.file_type == FileType.MP3:
111
+ elif self.file_type in [FileType.MP3, FileType.WAV]:
105
112
  audio_artifact = Audio(id=str(media_id), url=media_url)
106
113
  artifacts["audios"].append(audio_artifact)
107
114
 
@@ -131,7 +138,7 @@ class ModelsLabTools(Toolkit):
131
138
 
132
139
  return False
133
140
 
134
- def generate_media(self, agent: Union[Agent, Team], prompt: str) -> ToolResult:
141
+ def generate_media(self, prompt: str) -> ToolResult:
135
142
  """Generate media (video, image, or audio) given a prompt."""
136
143
  if not self.api_key:
137
144
  return ToolResult(content="Please set the MODELS_LAB_API_KEY")
@@ -157,7 +164,6 @@ class ModelsLabTools(Toolkit):
157
164
  return ToolResult(content=f"Error: {result['error']}")
158
165
 
159
166
  eta = result.get("eta")
160
- url_links = result.get("future_links")
161
167
  media_id = str(uuid4())
162
168
 
163
169
  # Collect all media artifacts
@@ -165,17 +171,21 @@ class ModelsLabTools(Toolkit):
165
171
  all_videos = []
166
172
  all_audios = []
167
173
 
174
+ if self.file_type == FileType.WAV:
175
+ url_links = result.get("output", [])
176
+ else:
177
+ url_links = result.get("future_links")
168
178
  for media_url in url_links:
169
179
  artifacts = self._create_media_artifacts(media_id, media_url, str(eta))
170
180
  all_images.extend(artifacts["images"])
171
181
  all_videos.extend(artifacts["videos"])
172
182
  all_audios.extend(artifacts["audios"])
173
183
 
174
- if self.wait_for_completion and isinstance(eta, int):
175
- if self._wait_for_media(media_id, eta):
176
- log_info("Media generation completed successfully")
177
- else:
178
- logger.warning("Media generation timed out")
184
+ if self.wait_for_completion and isinstance(eta, int):
185
+ if self._wait_for_media(media_id, eta):
186
+ log_info("Media generation completed successfully")
187
+ else:
188
+ logger.warning("Media generation timed out")
179
189
 
180
190
  # Return ToolResult with appropriate media artifacts
181
191
  return ToolResult(
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from io import BytesIO
5
+ from typing import Any, List, Optional
6
+ from uuid import uuid4
7
+
8
+ from agno.media import Image
9
+ from agno.tools import Toolkit
10
+ from agno.tools.function import ToolResult
11
+ from agno.utils.log import log_debug, logger
12
+
13
+ try:
14
+ from google import genai
15
+ from google.genai import types
16
+ from PIL import Image as PILImage
17
+
18
+ except ImportError as exc:
19
+ missing = []
20
+ try:
21
+ from google.genai import types
22
+ except ImportError:
23
+ missing.append("google-genai")
24
+
25
+ try:
26
+ from PIL import Image as PILImage
27
+ except ImportError:
28
+ missing.append("Pillow")
29
+
30
+ raise ImportError(
31
+ f"Missing required package(s): {', '.join(missing)}. Install using: pip install {' '.join(missing)}"
32
+ ) from exc
33
+
34
+
35
+ # Note: Expand this list as new models become supported by the Google Content Generation API.
36
+ ALLOWED_MODELS = ["gemini-2.5-flash-image"]
37
+ ALLOWED_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]
38
+
39
+
40
+ class NanoBananaTools(Toolkit):
41
+ def __init__(
42
+ self,
43
+ model: str = "gemini-2.5-flash-image",
44
+ aspect_ratio: str = "1:1",
45
+ api_key: Optional[str] = None,
46
+ enable_create_image: bool = True,
47
+ **kwargs,
48
+ ):
49
+ self.model = model
50
+ self.aspect_ratio = aspect_ratio
51
+ self.api_key = api_key or os.getenv("GOOGLE_API_KEY")
52
+
53
+ # Validate model
54
+ if model not in ALLOWED_MODELS:
55
+ raise ValueError(f"Invalid model '{model}'. Supported: {', '.join(ALLOWED_MODELS)}")
56
+
57
+ if self.aspect_ratio not in ALLOWED_RATIOS:
58
+ raise ValueError(f"Invalid aspect_ratio '{self.aspect_ratio}'. Supported: {', '.join(ALLOWED_RATIOS)}")
59
+
60
+ if not self.api_key:
61
+ raise ValueError("GOOGLE_API_KEY not set. Export it: `export GOOGLE_API_KEY=<your-key>`")
62
+
63
+ tools: List[Any] = []
64
+ if enable_create_image:
65
+ tools.append(self.create_image)
66
+
67
+ super().__init__(name="nano_banana", tools=tools, **kwargs)
68
+
69
+ def create_image(self, prompt: str) -> ToolResult:
70
+ """Generate an image from a text prompt."""
71
+ try:
72
+ client = genai.Client(api_key=self.api_key)
73
+ log_debug(f"NanoBanana generating image with prompt: {prompt}")
74
+
75
+ cfg = types.GenerateContentConfig(
76
+ response_modalities=["IMAGE"],
77
+ image_config=types.ImageConfig(aspect_ratio=self.aspect_ratio),
78
+ )
79
+
80
+ response = client.models.generate_content(
81
+ model=self.model,
82
+ contents=[prompt],
83
+ config=cfg,
84
+ )
85
+
86
+ generated_images: List[Image] = []
87
+ response_str = ""
88
+
89
+ if not hasattr(response, "candidates") or not response.candidates:
90
+ logger.warning("No candidates in response")
91
+ return ToolResult(content="No images were generated in the response")
92
+
93
+ # Process each candidate
94
+ for candidate in response.candidates:
95
+ if not hasattr(candidate, "content") or not candidate.content or not candidate.content.parts:
96
+ continue
97
+
98
+ for part in candidate.content.parts:
99
+ if hasattr(part, "text") and part.text:
100
+ response_str += part.text + "\n"
101
+
102
+ if hasattr(part, "inline_data") and part.inline_data:
103
+ try:
104
+ # Extract image data from the blob
105
+ image_data = part.inline_data.data
106
+ mime_type = getattr(part.inline_data, "mime_type", "image/png")
107
+
108
+ if image_data:
109
+ pil_img = PILImage.open(BytesIO(image_data))
110
+
111
+ # Save to buffer with proper format
112
+ buffer = BytesIO()
113
+ image_format = "PNG" if "png" in mime_type.lower() else "JPEG"
114
+ pil_img.save(buffer, format=image_format)
115
+ buffer.seek(0)
116
+
117
+ agno_img = Image(
118
+ id=str(uuid4()),
119
+ content=buffer.getvalue(),
120
+ original_prompt=prompt,
121
+ )
122
+ generated_images.append(agno_img)
123
+
124
+ log_debug(f"Successfully processed image with ID: {agno_img.id}")
125
+ response_str += f"Image generated successfully (ID: {agno_img.id}).\n"
126
+
127
+ except Exception as img_exc:
128
+ logger.error(f"Failed to process image data: {img_exc}")
129
+ response_str += f"Failed to process image: {img_exc}\n"
130
+
131
+ if hasattr(response, "usage_metadata") and response.usage_metadata:
132
+ log_debug(
133
+ f"Token usage - Prompt: {response.usage_metadata.prompt_token_count}, "
134
+ f"Response: {response.usage_metadata.candidates_token_count}, "
135
+ f"Total: {response.usage_metadata.total_token_count}"
136
+ )
137
+
138
+ if generated_images:
139
+ return ToolResult(
140
+ content=response_str.strip() or "Image(s) generated successfully",
141
+ images=generated_images,
142
+ )
143
+ else:
144
+ return ToolResult(
145
+ content=response_str.strip() or "No images were generated",
146
+ images=None,
147
+ )
148
+
149
+ except Exception as exc:
150
+ logger.error(f"NanoBanana image generation failed: {exc}")
151
+ return ToolResult(content=f"Error generating image: {str(exc)}")
agno/tools/yfinance.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, List
2
+ from typing import Any, List, Optional
3
3
 
4
4
  from agno.tools import Toolkit
5
5
  from agno.utils.log import log_debug
@@ -18,6 +18,7 @@ class YFinanceTools(Toolkit):
18
18
 
19
19
  def __init__(
20
20
  self,
21
+ session: Optional[Any] = None,
21
22
  **kwargs,
22
23
  ):
23
24
  tools: List[Any] = [
@@ -31,7 +32,7 @@ class YFinanceTools(Toolkit):
31
32
  self.get_technical_indicators,
32
33
  self.get_historical_stock_prices,
33
34
  ]
34
-
35
+ self.session = session
35
36
  super().__init__(name="yfinance_tools", tools=tools, **kwargs)
36
37
 
37
38
  def get_current_stock_price(self, symbol: str) -> str:
@@ -46,7 +47,7 @@ class YFinanceTools(Toolkit):
46
47
  """
47
48
  try:
48
49
  log_debug(f"Fetching current price for {symbol}")
49
- stock = yf.Ticker(symbol)
50
+ stock = yf.Ticker(symbol, session=self.session)
50
51
  # Use "regularMarketPrice" for regular market hours, or "currentPrice" for pre/post market
51
52
  current_price = stock.info.get("regularMarketPrice", stock.info.get("currentPrice"))
52
53
  return f"{current_price:.4f}" if current_price else f"Could not fetch current price for {symbol}"
@@ -63,7 +64,7 @@ class YFinanceTools(Toolkit):
63
64
  str: JSON containing company profile and overview.
64
65
  """
65
66
  try:
66
- company_info_full = yf.Ticker(symbol).info
67
+ company_info_full = yf.Ticker(symbol, session=self.session).info
67
68
  if company_info_full is None:
68
69
  return f"Could not fetch company info for {symbol}"
69
70
 
@@ -120,7 +121,7 @@ class YFinanceTools(Toolkit):
120
121
  """
121
122
  try:
122
123
  log_debug(f"Fetching historical prices for {symbol}")
123
- stock = yf.Ticker(symbol)
124
+ stock = yf.Ticker(symbol, session=self.session)
124
125
  historical_price = stock.history(period=period, interval=interval)
125
126
  return historical_price.to_json(orient="index")
126
127
  except Exception as e:
@@ -150,7 +151,7 @@ class YFinanceTools(Toolkit):
150
151
  """
151
152
  try:
152
153
  log_debug(f"Fetching fundamentals for {symbol}")
153
- stock = yf.Ticker(symbol)
154
+ stock = yf.Ticker(symbol, session=self.session)
154
155
  info = stock.info
155
156
  fundamentals = {
156
157
  "symbol": symbol,
@@ -181,7 +182,7 @@ class YFinanceTools(Toolkit):
181
182
  """
182
183
  try:
183
184
  log_debug(f"Fetching income statements for {symbol}")
184
- stock = yf.Ticker(symbol)
185
+ stock = yf.Ticker(symbol, session=self.session)
185
186
  financials = stock.financials
186
187
  return financials.to_json(orient="index")
187
188
  except Exception as e:
@@ -198,7 +199,7 @@ class YFinanceTools(Toolkit):
198
199
  """
199
200
  try:
200
201
  log_debug(f"Fetching key financial ratios for {symbol}")
201
- stock = yf.Ticker(symbol)
202
+ stock = yf.Ticker(symbol, session=self.session)
202
203
  key_ratios = stock.info
203
204
  return json.dumps(key_ratios, indent=2)
204
205
  except Exception as e:
@@ -215,7 +216,7 @@ class YFinanceTools(Toolkit):
215
216
  """
216
217
  try:
217
218
  log_debug(f"Fetching analyst recommendations for {symbol}")
218
- stock = yf.Ticker(symbol)
219
+ stock = yf.Ticker(symbol, session=self.session)
219
220
  recommendations = stock.recommendations
220
221
  return recommendations.to_json(orient="index")
221
222
  except Exception as e:
@@ -233,7 +234,7 @@ class YFinanceTools(Toolkit):
233
234
  """
234
235
  try:
235
236
  log_debug(f"Fetching company news for {symbol}")
236
- news = yf.Ticker(symbol).news
237
+ news = yf.Ticker(symbol, session=self.session).news
237
238
  return json.dumps(news[:num_stories], indent=2)
238
239
  except Exception as e:
239
240
  return f"Error fetching company news for {symbol}: {e}"
@@ -251,7 +252,7 @@ class YFinanceTools(Toolkit):
251
252
  """
252
253
  try:
253
254
  log_debug(f"Fetching technical indicators for {symbol}")
254
- indicators = yf.Ticker(symbol).history(period=period)
255
+ indicators = yf.Ticker(symbol, session=self.session).history(period=period)
255
256
  return indicators.to_json(orient="index")
256
257
  except Exception as e:
257
258
  return f"Error fetching technical indicators for {symbol}: {e}"
agno/utils/http.py CHANGED
@@ -10,6 +10,117 @@ logger = logging.getLogger(__name__)
10
10
  DEFAULT_MAX_RETRIES = 3
11
11
  DEFAULT_BACKOFF_FACTOR = 2 # Exponential backoff: 1, 2, 4, 8...
12
12
 
13
+ # Global httpx clients for resource efficiency
14
+ # These are shared across all models to reuse connection pools and avoid resource leaks.
15
+ # Consumers can override these at application startup using set_default_sync_client()
16
+ # and set_default_async_client() to customize limits, timeouts, proxies, etc.
17
+ _global_sync_client: Optional[httpx.Client] = None
18
+ _global_async_client: Optional[httpx.AsyncClient] = None
19
+
20
+
21
+ def get_default_sync_client() -> httpx.Client:
22
+ """Get or create the global synchronous httpx client.
23
+
24
+ Returns:
25
+ A singleton httpx.Client instance with default limits.
26
+ """
27
+ global _global_sync_client
28
+ if _global_sync_client is None or _global_sync_client.is_closed:
29
+ _global_sync_client = httpx.Client(
30
+ limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200), http2=True, follow_redirects=True
31
+ )
32
+ return _global_sync_client
33
+
34
+
35
+ def get_default_async_client() -> httpx.AsyncClient:
36
+ """Get or create the global asynchronous httpx client.
37
+
38
+ Returns:
39
+ A singleton httpx.AsyncClient instance with default limits.
40
+ """
41
+ global _global_async_client
42
+ if _global_async_client is None or _global_async_client.is_closed:
43
+ _global_async_client = httpx.AsyncClient(
44
+ limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200), http2=True, follow_redirects=True
45
+ )
46
+ return _global_async_client
47
+
48
+
49
+ def close_sync_client() -> None:
50
+ """Closes the global sync httpx client.
51
+
52
+ Should be called during application shutdown.
53
+ """
54
+ global _global_sync_client
55
+ if _global_sync_client is not None and not _global_sync_client.is_closed:
56
+ _global_sync_client.close()
57
+
58
+
59
+ async def aclose_default_clients() -> None:
60
+ """Asynchronously close the global httpx clients.
61
+
62
+ Should be called during application shutdown in async contexts.
63
+ """
64
+ global _global_sync_client, _global_async_client
65
+ if _global_sync_client is not None and not _global_sync_client.is_closed:
66
+ _global_sync_client.close()
67
+ if _global_async_client is not None and not _global_async_client.is_closed:
68
+ await _global_async_client.aclose()
69
+
70
+
71
+ def set_default_sync_client(client: httpx.Client) -> None:
72
+ """Set the global synchronous httpx client.
73
+
74
+ IMPORTANT: Call before creating any model instances. Models cache clients on first use.
75
+
76
+ Allows consumers to override the default httpx client with custom configuration
77
+ (e.g., custom limits, timeouts, proxies, SSL verification, etc.).
78
+ This is useful at application startup to customize how all models connect.
79
+
80
+ Example:
81
+ >>> import httpx
82
+ >>> from agno.utils.http import set_default_sync_client
83
+ >>> custom_client = httpx.Client(
84
+ ... limits=httpx.Limits(max_connections=500),
85
+ ... timeout=httpx.Timeout(30.0),
86
+ ... verify=False # for dev environments
87
+ ... )
88
+ >>> set_default_sync_client(custom_client)
89
+ >>> # All models will now use this custom client
90
+
91
+ Args:
92
+ client: An httpx.Client instance to use as the global sync client.
93
+ """
94
+ global _global_sync_client
95
+ _global_sync_client = client
96
+
97
+
98
+ def set_default_async_client(client: httpx.AsyncClient) -> None:
99
+ """Set the global asynchronous httpx client.
100
+
101
+ IMPORTANT: Call before creating any model instances. Models cache clients on first use.
102
+
103
+ Allows consumers to override the default async httpx client with custom configuration
104
+ (e.g., custom limits, timeouts, proxies, SSL verification, etc.).
105
+ This is useful at application startup to customize how all models connect.
106
+
107
+ Example:
108
+ >>> import httpx
109
+ >>> from agno.utils.http import set_default_async_client
110
+ >>> custom_client = httpx.AsyncClient(
111
+ ... limits=httpx.Limits(max_connections=500),
112
+ ... timeout=httpx.Timeout(30.0),
113
+ ... verify=False # for dev environments
114
+ ... )
115
+ >>> set_default_async_client(custom_client)
116
+ >>> # All models will now use this custom client
117
+
118
+ Args:
119
+ client: An httpx.AsyncClient instance to use as the global async client.
120
+ """
121
+ global _global_async_client
122
+ _global_async_client = client
123
+
13
124
 
14
125
  def fetch_with_retry(
15
126
  url: str,
agno/utils/media.py CHANGED
@@ -56,6 +56,17 @@ def download_image(url: str, output_path: str) -> bool:
56
56
  return False
57
57
 
58
58
 
59
+ def download_audio(url: str, output_path: str) -> str:
60
+ """Download audio from URL"""
61
+ response = httpx.get(url)
62
+ response.raise_for_status()
63
+
64
+ with open(output_path, "wb") as f:
65
+ for chunk in response.iter_bytes(chunk_size=8192):
66
+ f.write(chunk)
67
+ return output_path
68
+
69
+
59
70
  def download_video(url: str, output_path: str) -> str:
60
71
  """Download video from URL"""
61
72
  response = httpx.get(url)
@@ -320,6 +320,7 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
320
320
  def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]:
321
321
  """
322
322
  Transforms function definitions into a format accepted by the Anthropic API.
323
+ Now supports strict mode for structured outputs.
323
324
  """
324
325
  if not tools:
325
326
  return None
@@ -352,7 +353,14 @@ def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Opti
352
353
  "type": parameters.get("type", "object"),
353
354
  "properties": input_properties,
354
355
  "required": required_params,
356
+ "additionalProperties": False,
355
357
  },
356
358
  }
359
+
360
+ # Add strict mode if specified (check both function dict and tool_def top level)
361
+ strict_mode = func_def.get("strict") or tool_def.get("strict")
362
+ if strict_mode is True:
363
+ tool["strict"] = True
364
+
357
365
  parsed_tools.append(tool)
358
366
  return parsed_tools
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import warnings
2
3
  from collections.abc import Set
3
4
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast, get_args
4
5
 
@@ -34,8 +35,6 @@ def print_response_stream(
34
35
  images: Optional[Sequence[Image]] = None,
35
36
  videos: Optional[Sequence[Video]] = None,
36
37
  files: Optional[Sequence[File]] = None,
37
- stream_events: bool = False,
38
- stream_intermediate_steps: bool = False,
39
38
  knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
40
39
  debug_mode: Optional[bool] = None,
41
40
  markdown: bool = False,
@@ -82,9 +81,6 @@ def print_response_stream(
82
81
 
83
82
  input_content = get_text_from_message(input)
84
83
 
85
- # Consider both stream_events and stream_intermediate_steps (deprecated)
86
- stream_events = stream_events or stream_intermediate_steps
87
-
88
84
  for response_event in agent.run(
89
85
  input=input,
90
86
  session_id=session_id,
@@ -95,7 +91,6 @@ def print_response_stream(
95
91
  videos=videos,
96
92
  files=files,
97
93
  stream=True,
98
- stream_events=stream_events,
99
94
  knowledge_filters=knowledge_filters,
100
95
  debug_mode=debug_mode,
101
96
  add_history_to_context=add_history_to_context,
@@ -226,8 +221,6 @@ async def aprint_response_stream(
226
221
  images: Optional[Sequence[Image]] = None,
227
222
  videos: Optional[Sequence[Video]] = None,
228
223
  files: Optional[Sequence[File]] = None,
229
- stream_events: bool = False,
230
- stream_intermediate_steps: bool = False,
231
224
  knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
232
225
  debug_mode: Optional[bool] = None,
233
226
  markdown: bool = False,
@@ -272,9 +265,6 @@ async def aprint_response_stream(
272
265
  if render:
273
266
  live_log.update(Group(*panels))
274
267
 
275
- # Considering both stream_events and stream_intermediate_steps (deprecated)
276
- stream_events = stream_events or stream_intermediate_steps
277
-
278
268
  result = agent.arun(
279
269
  input=input,
280
270
  session_id=session_id,
@@ -285,7 +275,6 @@ async def aprint_response_stream(
285
275
  videos=videos,
286
276
  files=files,
287
277
  stream=True,
288
- stream_events=stream_events,
289
278
  knowledge_filters=knowledge_filters,
290
279
  debug_mode=debug_mode,
291
280
  add_history_to_context=add_history_to_context,
@@ -507,6 +496,8 @@ def print_response(
507
496
  videos: Optional[Sequence[Video]] = None,
508
497
  files: Optional[Sequence[File]] = None,
509
498
  knowledge_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
499
+ stream_events: Optional[bool] = None,
500
+ stream_intermediate_steps: Optional[bool] = None,
510
501
  debug_mode: Optional[bool] = None,
511
502
  markdown: bool = False,
512
503
  show_message: bool = True,
@@ -521,6 +512,19 @@ def print_response(
521
512
  metadata: Optional[Dict[str, Any]] = None,
522
513
  **kwargs: Any,
523
514
  ):
515
+ if stream_events is not None:
516
+ warnings.warn(
517
+ "The 'stream_events' parameter is deprecated and will be removed in future versions. Event streaming is always enabled using the print_response function.",
518
+ DeprecationWarning,
519
+ stacklevel=2,
520
+ )
521
+ if stream_intermediate_steps is not None:
522
+ warnings.warn(
523
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Event streaming is always enabled using the print_response function.",
524
+ DeprecationWarning,
525
+ stacklevel=2,
526
+ )
527
+
524
528
  with Live(console=console) as live_log:
525
529
  status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
526
530
  live_log.update(status)
@@ -551,6 +555,7 @@ def print_response(
551
555
  videos=videos,
552
556
  files=files,
553
557
  stream=False,
558
+ stream_events=True,
554
559
  knowledge_filters=knowledge_filters,
555
560
  debug_mode=debug_mode,
556
561
  add_history_to_context=add_history_to_context,
@@ -628,6 +633,8 @@ async def aprint_response(
628
633
  show_message: bool = True,
629
634
  show_reasoning: bool = True,
630
635
  show_full_reasoning: bool = False,
636
+ stream_events: Optional[bool] = None,
637
+ stream_intermediate_steps: Optional[bool] = None,
631
638
  tags_to_include_in_markdown: Set[str] = {"think", "thinking"},
632
639
  console: Optional[Any] = None,
633
640
  add_history_to_context: Optional[bool] = None,
@@ -637,6 +644,19 @@ async def aprint_response(
637
644
  metadata: Optional[Dict[str, Any]] = None,
638
645
  **kwargs: Any,
639
646
  ):
647
+ if stream_events is not None:
648
+ warnings.warn(
649
+ "The 'stream_events' parameter is deprecated and will be removed in future versions. Event streaming is always enabled using the aprint_response function.",
650
+ DeprecationWarning,
651
+ stacklevel=2,
652
+ )
653
+ if stream_intermediate_steps is not None:
654
+ warnings.warn(
655
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Event streaming is always enabled using the aprint_response function.",
656
+ DeprecationWarning,
657
+ stacklevel=2,
658
+ )
659
+
640
660
  with Live(console=console) as live_log:
641
661
  status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
642
662
  live_log.update(status)
@@ -667,6 +687,7 @@ async def aprint_response(
667
687
  videos=videos,
668
688
  files=files,
669
689
  stream=False,
690
+ stream_events=True,
670
691
  knowledge_filters=knowledge_filters,
671
692
  debug_mode=debug_mode,
672
693
  add_history_to_context=add_history_to_context,