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
@@ -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
- from pydantic import BaseModel
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 _has_beta_features(self) -> bool:
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 get_request_params(self) -> Dict[str, Any]:
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
- if self.betas:
224
- _request_params["betas"] = self.betas
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, system_message: str, tools: Optional[List[Dict[str, Any]]] = None
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
- request_kwargs = self.get_request_params().copy()
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(self, response: Union[AnthropicMessage, BetaMessage], **kwargs) -> ModelResponse:
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 = block.text
778
+ model_response.content = text_content
548
779
  else:
549
- model_response.content += block.text
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
  )