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.
- agno/agent/agent.py +197 -110
- agno/api/api.py +2 -0
- agno/db/base.py +26 -0
- agno/db/dynamo/dynamo.py +8 -0
- agno/db/dynamo/schemas.py +1 -0
- agno/db/firestore/firestore.py +8 -0
- agno/db/firestore/schemas.py +1 -0
- agno/db/gcs_json/gcs_json_db.py +8 -0
- agno/db/in_memory/in_memory_db.py +8 -1
- agno/db/json/json_db.py +8 -0
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/async_mongo.py +16 -6
- agno/db/mongo/mongo.py +11 -0
- agno/db/mongo/schemas.py +3 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/mysql.py +76 -3
- agno/db/mysql/schemas.py +20 -10
- agno/db/postgres/async_postgres.py +99 -25
- agno/db/postgres/postgres.py +75 -6
- agno/db/postgres/schemas.py +30 -20
- agno/db/redis/redis.py +15 -2
- agno/db/redis/schemas.py +4 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +11 -0
- agno/db/singlestore/singlestore.py +79 -5
- agno/db/sqlite/async_sqlite.py +97 -19
- agno/db/sqlite/schemas.py +10 -0
- agno/db/sqlite/sqlite.py +79 -2
- agno/db/surrealdb/surrealdb.py +8 -0
- agno/knowledge/chunking/semantic.py +7 -2
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/knowledge.py +57 -86
- agno/knowledge/reader/csv_reader.py +7 -9
- agno/knowledge/reader/docx_reader.py +5 -5
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -18
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +11 -11
- agno/knowledge/reader/pptx_reader.py +5 -5
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +8 -8
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/models/anthropic/claude.py +319 -28
- agno/models/aws/claude.py +32 -0
- agno/models/azure/openai_chat.py +19 -10
- agno/models/base.py +612 -545
- agno/models/cerebras/cerebras.py +8 -11
- agno/models/cohere/chat.py +27 -1
- agno/models/google/gemini.py +39 -7
- agno/models/groq/groq.py +25 -11
- agno/models/meta/llama.py +20 -9
- agno/models/meta/llama_openai.py +3 -19
- agno/models/nebius/nebius.py +4 -4
- agno/models/openai/chat.py +30 -14
- agno/models/openai/responses.py +10 -13
- agno/models/response.py +1 -0
- agno/models/vertexai/claude.py +26 -0
- agno/os/app.py +8 -19
- agno/os/router.py +54 -0
- agno/os/routers/knowledge/knowledge.py +2 -2
- agno/os/schema.py +2 -2
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -112
- agno/session/workflow.py +353 -57
- agno/team/team.py +227 -125
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/http.py +111 -0
- agno/utils/media.py +11 -0
- agno/utils/models/claude.py +8 -0
- agno/utils/print_response/agent.py +33 -12
- agno/utils/print_response/team.py +22 -12
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/workflow/condition.py +13 -0
- agno/workflow/loop.py +13 -0
- agno/workflow/parallel.py +13 -0
- agno/workflow/router.py +13 -0
- agno/workflow/step.py +120 -20
- agno/workflow/steps.py +13 -0
- agno/workflow/workflow.py +76 -63
- {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/METADATA +6 -2
- {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/RECORD +91 -88
- agno/tools/googlesearch.py +0 -98
- {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/WHEEL +0 -0
- {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/top_level.txt +0 -0
agno/tools/models/nebius.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
agno/utils/models/claude.py
CHANGED
|
@@ -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,
|