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/models/anthropic/claude.py
CHANGED
|
@@ -4,7 +4,8 @@ from dataclasses import asdict, dataclass
|
|
|
4
4
|
from os import getenv
|
|
5
5
|
from typing import Any, Dict, List, Optional, Type, Union
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
8
9
|
|
|
9
10
|
from agno.exceptions import ModelProviderError, ModelRateLimitError
|
|
10
11
|
from agno.models.base import Model
|
|
@@ -12,6 +13,7 @@ from agno.models.message import Citations, DocumentCitation, Message, UrlCitatio
|
|
|
12
13
|
from agno.models.metrics import Metrics
|
|
13
14
|
from agno.models.response import ModelResponse
|
|
14
15
|
from agno.run.agent import RunOutput
|
|
16
|
+
from agno.utils.http import get_default_async_client, get_default_sync_client
|
|
15
17
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
16
18
|
from agno.utils.models.claude import MCPServerConfiguration, format_messages, format_tools_for_model
|
|
17
19
|
|
|
@@ -25,6 +27,11 @@ try:
|
|
|
25
27
|
from anthropic import (
|
|
26
28
|
AsyncAnthropic as AsyncAnthropicClient,
|
|
27
29
|
)
|
|
30
|
+
from anthropic.lib.streaming._beta_types import (
|
|
31
|
+
BetaRawContentBlockStartEvent,
|
|
32
|
+
ParsedBetaContentBlockStopEvent,
|
|
33
|
+
ParsedBetaMessageStopEvent,
|
|
34
|
+
)
|
|
28
35
|
from anthropic.types import (
|
|
29
36
|
CitationPageLocation,
|
|
30
37
|
CitationsWebSearchResultLocation,
|
|
@@ -39,6 +46,7 @@ try:
|
|
|
39
46
|
from anthropic.types import (
|
|
40
47
|
Message as AnthropicMessage,
|
|
41
48
|
)
|
|
49
|
+
|
|
42
50
|
except ImportError as e:
|
|
43
51
|
raise ImportError("`anthropic` not installed. Please install it with `pip install anthropic`") from e
|
|
44
52
|
|
|
@@ -72,6 +80,30 @@ class Claude(Model):
|
|
|
72
80
|
"claude-3-5-haiku-latest",
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
# Models that DO NOT support native structured outputs
|
|
84
|
+
# All future models are assumed to support structured outputs
|
|
85
|
+
NON_STRUCTURED_OUTPUT_MODELS = {
|
|
86
|
+
# Claude 3.x family (all versions)
|
|
87
|
+
"claude-3-opus-20240229",
|
|
88
|
+
"claude-3-sonnet-20240229",
|
|
89
|
+
"claude-3-haiku-20240307",
|
|
90
|
+
"claude-3-opus",
|
|
91
|
+
"claude-3-sonnet",
|
|
92
|
+
"claude-3-haiku",
|
|
93
|
+
# Claude 3.5 family (all versions except Sonnet 4.5)
|
|
94
|
+
"claude-3-5-sonnet-20240620",
|
|
95
|
+
"claude-3-5-sonnet-20241022",
|
|
96
|
+
"claude-3-5-sonnet",
|
|
97
|
+
"claude-3-5-haiku-20241022",
|
|
98
|
+
"claude-3-5-haiku-latest",
|
|
99
|
+
"claude-3-5-haiku",
|
|
100
|
+
# Claude Sonnet 4.x family (versions before 4.5)
|
|
101
|
+
"claude-sonnet-4-20250514",
|
|
102
|
+
"claude-sonnet-4",
|
|
103
|
+
# Claude Opus 4.x family (versions before 4.1)
|
|
104
|
+
# (Add any Opus 4.x models released before 4.1 if they exist)
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
id: str = "claude-sonnet-4-5-20250929"
|
|
76
108
|
name: str = "Claude"
|
|
77
109
|
provider: str = "Anthropic"
|
|
@@ -99,6 +131,7 @@ class Claude(Model):
|
|
|
99
131
|
api_key: Optional[str] = None
|
|
100
132
|
default_headers: Optional[Dict[str, Any]] = None
|
|
101
133
|
timeout: Optional[float] = None
|
|
134
|
+
http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
|
|
102
135
|
client_params: Optional[Dict[str, Any]] = None
|
|
103
136
|
|
|
104
137
|
client: Optional[AnthropicClient] = None
|
|
@@ -109,6 +142,9 @@ class Claude(Model):
|
|
|
109
142
|
# Validate thinking support immediately at model creation
|
|
110
143
|
if self.thinking:
|
|
111
144
|
self._validate_thinking_support()
|
|
145
|
+
# Set structured outputs capability flag for supported models
|
|
146
|
+
if self._supports_structured_outputs():
|
|
147
|
+
self.supports_native_structured_outputs = True
|
|
112
148
|
# Set up skills configuration if skills are enabled
|
|
113
149
|
if self.skills:
|
|
114
150
|
self._setup_skills_configuration()
|
|
@@ -132,13 +168,72 @@ class Claude(Model):
|
|
|
132
168
|
client_params["default_headers"] = self.default_headers
|
|
133
169
|
return client_params
|
|
134
170
|
|
|
135
|
-
def
|
|
171
|
+
def _supports_structured_outputs(self) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Check if the current model supports native structured outputs.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
bool: True if model supports structured outputs
|
|
177
|
+
"""
|
|
178
|
+
# If model is in blacklist, it doesn't support structured outputs
|
|
179
|
+
if self.id in self.NON_STRUCTURED_OUTPUT_MODELS:
|
|
180
|
+
log_warning(
|
|
181
|
+
f"Model '{self.id}' does not support structured outputs. "
|
|
182
|
+
"Structured output features will not be available for this model."
|
|
183
|
+
)
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# Check for legacy model patterns that don't support structured outputs
|
|
187
|
+
if self.id.startswith("claude-3-"):
|
|
188
|
+
return False
|
|
189
|
+
if self.id.startswith("claude-sonnet-4-") and not self.id.startswith("claude-sonnet-4-5"):
|
|
190
|
+
return False
|
|
191
|
+
if self.id.startswith("claude-opus-4-") and not self.id.startswith("claude-opus-4-1"):
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def _using_structured_outputs(
|
|
197
|
+
self,
|
|
198
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
199
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
200
|
+
) -> bool:
|
|
201
|
+
"""
|
|
202
|
+
Check if structured outputs are being used in this request.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
response_format: Response format parameter
|
|
206
|
+
tools: Tools list to check for strict mode
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
bool: True if structured outputs are in use
|
|
210
|
+
"""
|
|
211
|
+
# Check for output_format usage
|
|
212
|
+
if response_format is not None and self._supports_structured_outputs():
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
# Check for strict tools
|
|
216
|
+
if tools:
|
|
217
|
+
for tool in tools:
|
|
218
|
+
if tool.get("type") == "function":
|
|
219
|
+
func_def = tool.get("function", {})
|
|
220
|
+
if func_def.get("strict") is True:
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
def _has_beta_features(
|
|
226
|
+
self,
|
|
227
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
228
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
229
|
+
) -> bool:
|
|
136
230
|
"""Check if the model has any Anthropic beta features enabled."""
|
|
137
231
|
return (
|
|
138
232
|
self.mcp_servers is not None
|
|
139
233
|
or self.context_management is not None
|
|
140
234
|
or self.skills is not None
|
|
141
235
|
or self.betas is not None
|
|
236
|
+
or self._using_structured_outputs(response_format, tools)
|
|
142
237
|
)
|
|
143
238
|
|
|
144
239
|
def get_client(self) -> AnthropicClient:
|
|
@@ -149,6 +244,16 @@ class Claude(Model):
|
|
|
149
244
|
return self.client
|
|
150
245
|
|
|
151
246
|
_client_params = self._get_client_params()
|
|
247
|
+
if self.http_client:
|
|
248
|
+
if isinstance(self.http_client, httpx.Client):
|
|
249
|
+
_client_params["http_client"] = self.http_client
|
|
250
|
+
else:
|
|
251
|
+
log_warning("http_client is not an instance of httpx.Client. Using default global httpx.Client.")
|
|
252
|
+
# Use global sync client when user http_client is invalid
|
|
253
|
+
_client_params["http_client"] = get_default_sync_client()
|
|
254
|
+
else:
|
|
255
|
+
# Use global sync client when no custom http_client is provided
|
|
256
|
+
_client_params["http_client"] = get_default_sync_client()
|
|
152
257
|
self.client = AnthropicClient(**_client_params)
|
|
153
258
|
return self.client
|
|
154
259
|
|
|
@@ -160,6 +265,18 @@ class Claude(Model):
|
|
|
160
265
|
return self.async_client
|
|
161
266
|
|
|
162
267
|
_client_params = self._get_client_params()
|
|
268
|
+
if self.http_client:
|
|
269
|
+
if isinstance(self.http_client, httpx.AsyncClient):
|
|
270
|
+
_client_params["http_client"] = self.http_client
|
|
271
|
+
else:
|
|
272
|
+
log_warning(
|
|
273
|
+
"http_client is not an instance of httpx.AsyncClient. Using default global httpx.AsyncClient."
|
|
274
|
+
)
|
|
275
|
+
# Use global async client when user http_client is invalid
|
|
276
|
+
_client_params["http_client"] = get_default_async_client()
|
|
277
|
+
else:
|
|
278
|
+
# Use global async client when no custom http_client is provided
|
|
279
|
+
_client_params["http_client"] = get_default_async_client()
|
|
163
280
|
self.async_client = AsyncAnthropicClient(**_client_params)
|
|
164
281
|
return self.async_client
|
|
165
282
|
|
|
@@ -199,7 +316,70 @@ class Claude(Model):
|
|
|
199
316
|
if beta not in self.betas:
|
|
200
317
|
self.betas.append(beta)
|
|
201
318
|
|
|
202
|
-
def
|
|
319
|
+
def _ensure_additional_properties_false(self, schema: Dict[str, Any]) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Recursively ensure all object types have additionalProperties: false.
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(schema, dict):
|
|
324
|
+
if schema.get("type") == "object":
|
|
325
|
+
schema["additionalProperties"] = False
|
|
326
|
+
|
|
327
|
+
# Recursively process nested schemas
|
|
328
|
+
for key, value in schema.items():
|
|
329
|
+
if key in ["properties", "items", "allOf", "anyOf", "oneOf"]:
|
|
330
|
+
if isinstance(value, dict):
|
|
331
|
+
self._ensure_additional_properties_false(value)
|
|
332
|
+
elif isinstance(value, list):
|
|
333
|
+
for item in value:
|
|
334
|
+
if isinstance(item, dict):
|
|
335
|
+
self._ensure_additional_properties_false(item)
|
|
336
|
+
|
|
337
|
+
def _build_output_format(self, response_format: Optional[Union[Dict, Type[BaseModel]]]) -> Optional[Dict[str, Any]]:
|
|
338
|
+
"""
|
|
339
|
+
Build Anthropic output_format parameter from response_format.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
response_format: Pydantic model or dict format
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dict with output_format structure or None
|
|
346
|
+
"""
|
|
347
|
+
if response_format is None:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
if not self._supports_structured_outputs():
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
# Handle Pydantic BaseModel
|
|
354
|
+
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
|
355
|
+
try:
|
|
356
|
+
# Try to use Anthropic SDK's transform_schema helper if available
|
|
357
|
+
from anthropic import transform_schema
|
|
358
|
+
|
|
359
|
+
schema = transform_schema(response_format.model_json_schema())
|
|
360
|
+
except (ImportError, AttributeError):
|
|
361
|
+
# Fallback to direct schema conversion
|
|
362
|
+
schema = response_format.model_json_schema()
|
|
363
|
+
# Ensure additionalProperties is False
|
|
364
|
+
if isinstance(schema, dict):
|
|
365
|
+
if "additionalProperties" not in schema:
|
|
366
|
+
schema["additionalProperties"] = False
|
|
367
|
+
# Recursively ensure all object types have additionalProperties: false
|
|
368
|
+
self._ensure_additional_properties_false(schema)
|
|
369
|
+
|
|
370
|
+
return {"type": "json_schema", "schema": schema}
|
|
371
|
+
|
|
372
|
+
# Handle dict format (already in correct structure)
|
|
373
|
+
elif isinstance(response_format, dict):
|
|
374
|
+
return response_format
|
|
375
|
+
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
def get_request_params(
|
|
379
|
+
self,
|
|
380
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
381
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
382
|
+
) -> Dict[str, Any]:
|
|
203
383
|
"""
|
|
204
384
|
Generate keyword arguments for API requests.
|
|
205
385
|
"""
|
|
@@ -220,8 +400,20 @@ class Claude(Model):
|
|
|
220
400
|
_request_params["top_p"] = self.top_p
|
|
221
401
|
if self.top_k:
|
|
222
402
|
_request_params["top_k"] = self.top_k
|
|
223
|
-
|
|
224
|
-
|
|
403
|
+
|
|
404
|
+
# Build betas list - include existing betas and add new one if needed
|
|
405
|
+
betas_list = list(self.betas) if self.betas else []
|
|
406
|
+
|
|
407
|
+
# Add structured outputs beta header if using structured outputs
|
|
408
|
+
if self._using_structured_outputs(response_format, tools):
|
|
409
|
+
beta_header = "structured-outputs-2025-11-13"
|
|
410
|
+
if beta_header not in betas_list:
|
|
411
|
+
betas_list.append(beta_header)
|
|
412
|
+
|
|
413
|
+
# Include betas if any are present
|
|
414
|
+
if betas_list:
|
|
415
|
+
_request_params["betas"] = betas_list
|
|
416
|
+
|
|
225
417
|
if self.context_management:
|
|
226
418
|
_request_params["context_management"] = self.context_management
|
|
227
419
|
if self.mcp_servers:
|
|
@@ -229,26 +421,51 @@ class Claude(Model):
|
|
|
229
421
|
{k: v for k, v in asdict(server).items() if v is not None} for server in self.mcp_servers
|
|
230
422
|
]
|
|
231
423
|
if self.skills:
|
|
232
|
-
_request_params["betas"] = self.betas
|
|
233
424
|
_request_params["container"] = {"skills": self.skills}
|
|
234
425
|
if self.request_params:
|
|
235
426
|
_request_params.update(self.request_params)
|
|
236
427
|
|
|
237
428
|
return _request_params
|
|
238
429
|
|
|
430
|
+
def _validate_structured_outputs_usage(
|
|
431
|
+
self,
|
|
432
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
433
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""
|
|
436
|
+
Validate that structured outputs are only used with supported models.
|
|
437
|
+
|
|
438
|
+
Raises:
|
|
439
|
+
ValueError: If structured outputs are used with unsupported model
|
|
440
|
+
"""
|
|
441
|
+
if not self._using_structured_outputs(response_format, tools):
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
if not self._supports_structured_outputs():
|
|
445
|
+
raise ValueError(f"Model '{self.id}' does not support structured outputs.\n\n")
|
|
446
|
+
|
|
239
447
|
def _prepare_request_kwargs(
|
|
240
|
-
self,
|
|
448
|
+
self,
|
|
449
|
+
system_message: str,
|
|
450
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
451
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
241
452
|
) -> Dict[str, Any]:
|
|
242
453
|
"""
|
|
243
454
|
Prepare the request keyword arguments for the API call.
|
|
244
455
|
|
|
245
456
|
Args:
|
|
246
457
|
system_message (str): The concatenated system messages.
|
|
458
|
+
tools: Optional list of tools
|
|
459
|
+
response_format: Optional response format (Pydantic model or dict)
|
|
247
460
|
|
|
248
461
|
Returns:
|
|
249
462
|
Dict[str, Any]: The request keyword arguments.
|
|
250
463
|
"""
|
|
251
|
-
|
|
464
|
+
# Validate structured outputs usage
|
|
465
|
+
self._validate_structured_outputs_usage(response_format, tools)
|
|
466
|
+
|
|
467
|
+
# Pass response_format and tools to get_request_params for beta header handling
|
|
468
|
+
request_kwargs = self.get_request_params(response_format=response_format, tools=tools).copy()
|
|
252
469
|
if system_message:
|
|
253
470
|
if self.cache_system_prompt:
|
|
254
471
|
cache_control = (
|
|
@@ -269,9 +486,15 @@ class Claude(Model):
|
|
|
269
486
|
else:
|
|
270
487
|
tools = [code_execution_tool]
|
|
271
488
|
|
|
489
|
+
# Format tools (this will handle strict mode)
|
|
272
490
|
if tools:
|
|
273
491
|
request_kwargs["tools"] = format_tools_for_model(tools)
|
|
274
492
|
|
|
493
|
+
# Build output_format if response_format is provided
|
|
494
|
+
output_format = self._build_output_format(response_format)
|
|
495
|
+
if output_format:
|
|
496
|
+
request_kwargs["output_format"] = output_format
|
|
497
|
+
|
|
275
498
|
if request_kwargs:
|
|
276
499
|
log_debug(f"Calling {self.provider} with request parameters: {request_kwargs}", log_level=2)
|
|
277
500
|
return request_kwargs
|
|
@@ -293,9 +516,9 @@ class Claude(Model):
|
|
|
293
516
|
run_response.metrics.set_time_to_first_token()
|
|
294
517
|
|
|
295
518
|
chat_messages, system_message = format_messages(messages)
|
|
296
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
519
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
297
520
|
|
|
298
|
-
if self._has_beta_features():
|
|
521
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
299
522
|
assistant_message.metrics.start_timer()
|
|
300
523
|
provider_response = self.get_client().beta.messages.create(
|
|
301
524
|
model=self.id,
|
|
@@ -356,14 +579,14 @@ class Claude(Model):
|
|
|
356
579
|
APIStatusError: For other API-related errors
|
|
357
580
|
"""
|
|
358
581
|
chat_messages, system_message = format_messages(messages)
|
|
359
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
582
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
360
583
|
|
|
361
584
|
try:
|
|
362
585
|
if run_response and run_response.metrics:
|
|
363
586
|
run_response.metrics.set_time_to_first_token()
|
|
364
587
|
|
|
365
588
|
# Beta features
|
|
366
|
-
if self._has_beta_features():
|
|
589
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
367
590
|
assistant_message.metrics.start_timer()
|
|
368
591
|
with self.get_client().beta.messages.stream(
|
|
369
592
|
model=self.id,
|
|
@@ -371,7 +594,7 @@ class Claude(Model):
|
|
|
371
594
|
**request_kwargs,
|
|
372
595
|
) as stream:
|
|
373
596
|
for chunk in stream:
|
|
374
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
597
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
375
598
|
else:
|
|
376
599
|
assistant_message.metrics.start_timer()
|
|
377
600
|
with self.get_client().messages.stream(
|
|
@@ -380,7 +603,7 @@ class Claude(Model):
|
|
|
380
603
|
**request_kwargs,
|
|
381
604
|
) as stream:
|
|
382
605
|
for chunk in stream: # type: ignore
|
|
383
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
606
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
384
607
|
|
|
385
608
|
assistant_message.metrics.stop_timer()
|
|
386
609
|
|
|
@@ -416,10 +639,10 @@ class Claude(Model):
|
|
|
416
639
|
run_response.metrics.set_time_to_first_token()
|
|
417
640
|
|
|
418
641
|
chat_messages, system_message = format_messages(messages)
|
|
419
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
642
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
420
643
|
|
|
421
644
|
# Beta features
|
|
422
|
-
if self._has_beta_features():
|
|
645
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
423
646
|
assistant_message.metrics.start_timer()
|
|
424
647
|
provider_response = await self.get_async_client().beta.messages.create(
|
|
425
648
|
model=self.id,
|
|
@@ -481,9 +704,9 @@ class Claude(Model):
|
|
|
481
704
|
run_response.metrics.set_time_to_first_token()
|
|
482
705
|
|
|
483
706
|
chat_messages, system_message = format_messages(messages)
|
|
484
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
707
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
485
708
|
|
|
486
|
-
if self._has_beta_features():
|
|
709
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
487
710
|
assistant_message.metrics.start_timer()
|
|
488
711
|
async with self.get_async_client().beta.messages.stream(
|
|
489
712
|
model=self.id,
|
|
@@ -491,7 +714,7 @@ class Claude(Model):
|
|
|
491
714
|
**request_kwargs,
|
|
492
715
|
) as stream:
|
|
493
716
|
async for chunk in stream:
|
|
494
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
717
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
495
718
|
else:
|
|
496
719
|
assistant_message.metrics.start_timer()
|
|
497
720
|
async with self.get_async_client().messages.stream(
|
|
@@ -500,7 +723,7 @@ class Claude(Model):
|
|
|
500
723
|
**request_kwargs,
|
|
501
724
|
) as stream:
|
|
502
725
|
async for chunk in stream: # type: ignore
|
|
503
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
726
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
504
727
|
|
|
505
728
|
assistant_message.metrics.stop_timer()
|
|
506
729
|
|
|
@@ -525,12 +748,18 @@ class Claude(Model):
|
|
|
525
748
|
return tool_call_prompt
|
|
526
749
|
return None
|
|
527
750
|
|
|
528
|
-
def _parse_provider_response(
|
|
751
|
+
def _parse_provider_response(
|
|
752
|
+
self,
|
|
753
|
+
response: Union[AnthropicMessage, BetaMessage],
|
|
754
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
755
|
+
**kwargs,
|
|
756
|
+
) -> ModelResponse:
|
|
529
757
|
"""
|
|
530
758
|
Parse the Claude response into a ModelResponse.
|
|
531
759
|
|
|
532
760
|
Args:
|
|
533
761
|
response: Raw response from Anthropic
|
|
762
|
+
response_format: Optional response format for structured output parsing
|
|
534
763
|
|
|
535
764
|
Returns:
|
|
536
765
|
ModelResponse: Parsed response data
|
|
@@ -543,10 +772,32 @@ class Claude(Model):
|
|
|
543
772
|
if response.content:
|
|
544
773
|
for block in response.content:
|
|
545
774
|
if block.type == "text":
|
|
775
|
+
text_content = block.text
|
|
776
|
+
|
|
546
777
|
if model_response.content is None:
|
|
547
|
-
model_response.content =
|
|
778
|
+
model_response.content = text_content
|
|
548
779
|
else:
|
|
549
|
-
model_response.content +=
|
|
780
|
+
model_response.content += text_content
|
|
781
|
+
|
|
782
|
+
# Handle structured outputs (JSON outputs)
|
|
783
|
+
if (
|
|
784
|
+
response_format is not None
|
|
785
|
+
and isinstance(response_format, type)
|
|
786
|
+
and issubclass(response_format, BaseModel)
|
|
787
|
+
):
|
|
788
|
+
if text_content:
|
|
789
|
+
try:
|
|
790
|
+
# Parse JSON from text content
|
|
791
|
+
parsed_data = json.loads(text_content)
|
|
792
|
+
# Validate against Pydantic model
|
|
793
|
+
model_response.parsed = response_format.model_validate(parsed_data)
|
|
794
|
+
log_debug(f"Successfully parsed structured output: {model_response.parsed}")
|
|
795
|
+
except json.JSONDecodeError as e:
|
|
796
|
+
log_warning(f"Failed to parse JSON from structured output: {e}")
|
|
797
|
+
except ValidationError as e:
|
|
798
|
+
log_warning(f"Failed to validate structured output against schema: {e}")
|
|
799
|
+
except Exception as e:
|
|
800
|
+
log_warning(f"Unexpected error parsing structured output: {e}")
|
|
550
801
|
|
|
551
802
|
# Capture citations from the response
|
|
552
803
|
if block.citations is not None:
|
|
@@ -634,24 +885,29 @@ class Claude(Model):
|
|
|
634
885
|
ContentBlockStopEvent,
|
|
635
886
|
MessageStopEvent,
|
|
636
887
|
BetaRawContentBlockDeltaEvent,
|
|
888
|
+
BetaRawContentBlockStartEvent,
|
|
889
|
+
ParsedBetaContentBlockStopEvent,
|
|
890
|
+
ParsedBetaMessageStopEvent,
|
|
637
891
|
],
|
|
892
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
638
893
|
) -> ModelResponse:
|
|
639
894
|
"""
|
|
640
895
|
Parse the Claude streaming response into ModelProviderResponse objects.
|
|
641
896
|
|
|
642
897
|
Args:
|
|
643
898
|
response: Raw response chunk from Anthropic
|
|
899
|
+
response_format: Optional response format for structured output parsing
|
|
644
900
|
|
|
645
901
|
Returns:
|
|
646
902
|
ModelResponse: Iterator of parsed response data
|
|
647
903
|
"""
|
|
648
904
|
model_response = ModelResponse()
|
|
649
905
|
|
|
650
|
-
if isinstance(response, ContentBlockStartEvent):
|
|
906
|
+
if isinstance(response, (ContentBlockStartEvent, BetaRawContentBlockStartEvent)):
|
|
651
907
|
if response.content_block.type == "redacted_reasoning_content":
|
|
652
908
|
model_response.redacted_reasoning_content = response.content_block.data
|
|
653
909
|
|
|
654
|
-
if isinstance(response, ContentBlockDeltaEvent):
|
|
910
|
+
if isinstance(response, (ContentBlockDeltaEvent, BetaRawContentBlockDeltaEvent)):
|
|
655
911
|
# Handle text content
|
|
656
912
|
if response.delta.type == "text_delta":
|
|
657
913
|
model_response.content = response.delta.text
|
|
@@ -663,7 +919,7 @@ class Claude(Model):
|
|
|
663
919
|
"signature": response.delta.signature,
|
|
664
920
|
}
|
|
665
921
|
|
|
666
|
-
elif isinstance(response, ContentBlockStopEvent):
|
|
922
|
+
elif isinstance(response, (ContentBlockStopEvent, ParsedBetaContentBlockStopEvent)):
|
|
667
923
|
if response.content_block.type == "tool_use": # type: ignore
|
|
668
924
|
tool_use = response.content_block # type: ignore
|
|
669
925
|
tool_name = tool_use.name
|
|
@@ -683,11 +939,24 @@ class Claude(Model):
|
|
|
683
939
|
}
|
|
684
940
|
]
|
|
685
941
|
|
|
686
|
-
# Capture citations from the final response
|
|
687
|
-
elif isinstance(response, MessageStopEvent):
|
|
942
|
+
# Capture citations from the final response and handle structured outputs
|
|
943
|
+
elif isinstance(response, (MessageStopEvent, ParsedBetaMessageStopEvent)):
|
|
944
|
+
# In streaming mode, content has already been emitted via ContentBlockDeltaEvent chunks
|
|
945
|
+
# Setting content here would cause duplication since _populate_stream_data accumulates with +=
|
|
946
|
+
# Keep content empty to avoid duplication
|
|
688
947
|
model_response.content = ""
|
|
689
948
|
model_response.citations = Citations(raw=[], urls=[], documents=[])
|
|
949
|
+
|
|
950
|
+
# Accumulate text content for structured output parsing (but don't set model_response.content)
|
|
951
|
+
# The text was already streamed via ContentBlockDeltaEvent chunks
|
|
952
|
+
accumulated_text = ""
|
|
953
|
+
|
|
690
954
|
for block in response.message.content: # type: ignore
|
|
955
|
+
# Handle text blocks for structured output parsing
|
|
956
|
+
if block.type == "text":
|
|
957
|
+
accumulated_text += block.text
|
|
958
|
+
|
|
959
|
+
# Handle citations
|
|
691
960
|
citations = getattr(block, "citations", None)
|
|
692
961
|
if not citations:
|
|
693
962
|
continue
|
|
@@ -702,6 +971,28 @@ class Claude(Model):
|
|
|
702
971
|
DocumentCitation(document_title=citation.document_title, cited_text=citation.cited_text)
|
|
703
972
|
)
|
|
704
973
|
|
|
974
|
+
# Handle structured outputs (JSON outputs) from accumulated text
|
|
975
|
+
# Note: We parse from accumulated_text but don't set model_response.content to avoid duplication
|
|
976
|
+
# The content was already streamed via ContentBlockDeltaEvent chunks
|
|
977
|
+
if (
|
|
978
|
+
response_format is not None
|
|
979
|
+
and isinstance(response_format, type)
|
|
980
|
+
and issubclass(response_format, BaseModel)
|
|
981
|
+
):
|
|
982
|
+
if accumulated_text:
|
|
983
|
+
try:
|
|
984
|
+
# Parse JSON from accumulated text content
|
|
985
|
+
parsed_data = json.loads(accumulated_text)
|
|
986
|
+
# Validate against Pydantic model
|
|
987
|
+
model_response.parsed = response_format.model_validate(parsed_data)
|
|
988
|
+
log_debug(f"Successfully parsed structured output from stream: {model_response.parsed}")
|
|
989
|
+
except json.JSONDecodeError as e:
|
|
990
|
+
log_warning(f"Failed to parse JSON from structured output in stream: {e}")
|
|
991
|
+
except ValidationError as e:
|
|
992
|
+
log_warning(f"Failed to validate structured output against schema in stream: {e}")
|
|
993
|
+
except Exception as e:
|
|
994
|
+
log_warning(f"Unexpected error parsing structured output in stream: {e}")
|
|
995
|
+
|
|
705
996
|
# Capture context management information if present
|
|
706
997
|
if self.context_management is not None and hasattr(response.message, "context_management"): # type: ignore
|
|
707
998
|
context_mgmt = response.message.context_management # type: ignore
|
agno/models/aws/claude.py
CHANGED
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Type, Union
|
|
4
4
|
|
|
5
|
+
import httpx
|
|
5
6
|
from pydantic import BaseModel
|
|
6
7
|
|
|
7
8
|
from agno.exceptions import ModelProviderError, ModelRateLimitError
|
|
@@ -9,6 +10,7 @@ from agno.models.anthropic import Claude as AnthropicClaude
|
|
|
9
10
|
from agno.models.message import Message
|
|
10
11
|
from agno.models.response import ModelResponse
|
|
11
12
|
from agno.run.agent import RunOutput
|
|
13
|
+
from agno.utils.http import get_default_async_client, get_default_sync_client
|
|
12
14
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
13
15
|
from agno.utils.models.claude import format_messages
|
|
14
16
|
|
|
@@ -99,9 +101,23 @@ class Claude(AnthropicClaude):
|
|
|
99
101
|
"aws_region": self.aws_region,
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
if self.timeout is not None:
|
|
105
|
+
client_params["timeout"] = self.timeout
|
|
106
|
+
|
|
102
107
|
if self.client_params:
|
|
103
108
|
client_params.update(self.client_params)
|
|
104
109
|
|
|
110
|
+
if self.http_client:
|
|
111
|
+
if isinstance(self.http_client, httpx.Client):
|
|
112
|
+
client_params["http_client"] = self.http_client
|
|
113
|
+
else:
|
|
114
|
+
log_warning("http_client is not an instance of httpx.Client. Using default global httpx.Client.")
|
|
115
|
+
# Use global sync client when user http_client is invalid
|
|
116
|
+
client_params["http_client"] = get_default_sync_client()
|
|
117
|
+
else:
|
|
118
|
+
# Use global sync client when no custom http_client is provided
|
|
119
|
+
client_params["http_client"] = get_default_sync_client()
|
|
120
|
+
|
|
105
121
|
self.client = AnthropicBedrock(
|
|
106
122
|
**client_params, # type: ignore
|
|
107
123
|
)
|
|
@@ -132,9 +148,25 @@ class Claude(AnthropicClaude):
|
|
|
132
148
|
"aws_region": self.aws_region,
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
if self.timeout is not None:
|
|
152
|
+
client_params["timeout"] = self.timeout
|
|
153
|
+
|
|
135
154
|
if self.client_params:
|
|
136
155
|
client_params.update(self.client_params)
|
|
137
156
|
|
|
157
|
+
if self.http_client:
|
|
158
|
+
if isinstance(self.http_client, httpx.AsyncClient):
|
|
159
|
+
client_params["http_client"] = self.http_client
|
|
160
|
+
else:
|
|
161
|
+
log_warning(
|
|
162
|
+
"http_client is not an instance of httpx.AsyncClient. Using default global httpx.AsyncClient."
|
|
163
|
+
)
|
|
164
|
+
# Use global async client when user http_client is invalid
|
|
165
|
+
client_params["http_client"] = get_default_async_client()
|
|
166
|
+
else:
|
|
167
|
+
# Use global async client when no custom http_client is provided
|
|
168
|
+
client_params["http_client"] = get_default_async_client()
|
|
169
|
+
|
|
138
170
|
self.async_client = AsyncAnthropicBedrock(
|
|
139
171
|
**client_params, # type: ignore
|
|
140
172
|
)
|