agno 2.1.2__py3-none-any.whl → 2.3.13__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 +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.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,8 +13,11 @@ 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.tools.function import Function
|
|
17
|
+
from agno.utils.http import get_default_async_client, get_default_sync_client
|
|
15
18
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
16
19
|
from agno.utils.models.claude import MCPServerConfiguration, format_messages, format_tools_for_model
|
|
20
|
+
from agno.utils.tokens import count_schema_tokens
|
|
17
21
|
|
|
18
22
|
try:
|
|
19
23
|
from anthropic import Anthropic as AnthropicClient
|
|
@@ -25,6 +29,11 @@ try:
|
|
|
25
29
|
from anthropic import (
|
|
26
30
|
AsyncAnthropic as AsyncAnthropicClient,
|
|
27
31
|
)
|
|
32
|
+
from anthropic.lib.streaming._beta_types import (
|
|
33
|
+
BetaRawContentBlockStartEvent,
|
|
34
|
+
ParsedBetaContentBlockStopEvent,
|
|
35
|
+
ParsedBetaMessageStopEvent,
|
|
36
|
+
)
|
|
28
37
|
from anthropic.types import (
|
|
29
38
|
CitationPageLocation,
|
|
30
39
|
CitationsWebSearchResultLocation,
|
|
@@ -39,12 +48,15 @@ try:
|
|
|
39
48
|
from anthropic.types import (
|
|
40
49
|
Message as AnthropicMessage,
|
|
41
50
|
)
|
|
51
|
+
|
|
42
52
|
except ImportError as e:
|
|
43
53
|
raise ImportError("`anthropic` not installed. Please install it with `pip install anthropic`") from e
|
|
44
54
|
|
|
45
55
|
# Import Beta types
|
|
46
56
|
try:
|
|
47
57
|
from anthropic.types.beta import BetaRawContentBlockDeltaEvent, BetaTextDelta
|
|
58
|
+
from anthropic.types.beta.beta_message import BetaMessage
|
|
59
|
+
from anthropic.types.beta.beta_usage import BetaUsage
|
|
48
60
|
except ImportError as e:
|
|
49
61
|
raise ImportError(
|
|
50
62
|
"`anthropic` not installed or missing beta components. Please install with `pip install anthropic`"
|
|
@@ -59,12 +71,47 @@ class Claude(Model):
|
|
|
59
71
|
For more information, see: https://docs.anthropic.com/en/api/messages
|
|
60
72
|
"""
|
|
61
73
|
|
|
74
|
+
# Models that DO NOT support extended thinking
|
|
75
|
+
# All future models are assumed to support thinking
|
|
76
|
+
# Based on official Anthropic documentation: https://docs.claude.com/en/docs/about-claude/models/overview
|
|
77
|
+
NON_THINKING_MODELS = {
|
|
78
|
+
# Claude Haiku 3 family (does not support thinking)
|
|
79
|
+
"claude-3-haiku-20240307",
|
|
80
|
+
# Claude Haiku 3.5 family (does not support thinking)
|
|
81
|
+
"claude-3-5-haiku-20241022",
|
|
82
|
+
"claude-3-5-haiku-latest",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Models that DO NOT support native structured outputs
|
|
86
|
+
# All future models are assumed to support structured outputs
|
|
87
|
+
NON_STRUCTURED_OUTPUT_MODELS = {
|
|
88
|
+
# Claude 3.x family (all versions)
|
|
89
|
+
"claude-3-opus-20240229",
|
|
90
|
+
"claude-3-sonnet-20240229",
|
|
91
|
+
"claude-3-haiku-20240307",
|
|
92
|
+
"claude-3-opus",
|
|
93
|
+
"claude-3-sonnet",
|
|
94
|
+
"claude-3-haiku",
|
|
95
|
+
# Claude 3.5 family (all versions except Sonnet 4.5)
|
|
96
|
+
"claude-3-5-sonnet-20240620",
|
|
97
|
+
"claude-3-5-sonnet-20241022",
|
|
98
|
+
"claude-3-5-sonnet",
|
|
99
|
+
"claude-3-5-haiku-20241022",
|
|
100
|
+
"claude-3-5-haiku-latest",
|
|
101
|
+
"claude-3-5-haiku",
|
|
102
|
+
# Claude Sonnet 4.x family (versions before 4.5)
|
|
103
|
+
"claude-sonnet-4-20250514",
|
|
104
|
+
"claude-sonnet-4",
|
|
105
|
+
# Claude Opus 4.x family (versions before 4.1)
|
|
106
|
+
# (Add any Opus 4.x models released before 4.1 if they exist)
|
|
107
|
+
}
|
|
108
|
+
|
|
62
109
|
id: str = "claude-sonnet-4-5-20250929"
|
|
63
110
|
name: str = "Claude"
|
|
64
111
|
provider: str = "Anthropic"
|
|
65
112
|
|
|
66
113
|
# Request parameters
|
|
67
|
-
max_tokens: Optional[int] =
|
|
114
|
+
max_tokens: Optional[int] = 8192
|
|
68
115
|
thinking: Optional[Dict[str, Any]] = None
|
|
69
116
|
temperature: Optional[float] = None
|
|
70
117
|
stop_sequences: Optional[List[str]] = None
|
|
@@ -73,27 +120,51 @@ class Claude(Model):
|
|
|
73
120
|
cache_system_prompt: Optional[bool] = False
|
|
74
121
|
extended_cache_time: Optional[bool] = False
|
|
75
122
|
request_params: Optional[Dict[str, Any]] = None
|
|
123
|
+
|
|
124
|
+
# Anthropic beta and experimental features
|
|
125
|
+
betas: Optional[List[str]] = None # Enables specific experimental or newly released features.
|
|
126
|
+
context_management: Optional[Dict[str, Any]] = None
|
|
76
127
|
mcp_servers: Optional[List[MCPServerConfiguration]] = None
|
|
128
|
+
skills: Optional[List[Dict[str, str]]] = (
|
|
129
|
+
None # e.g., [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]
|
|
130
|
+
)
|
|
77
131
|
|
|
78
132
|
# Client parameters
|
|
79
133
|
api_key: Optional[str] = None
|
|
134
|
+
auth_token: Optional[str] = None
|
|
80
135
|
default_headers: Optional[Dict[str, Any]] = None
|
|
81
136
|
timeout: Optional[float] = None
|
|
137
|
+
http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
|
|
82
138
|
client_params: Optional[Dict[str, Any]] = None
|
|
83
139
|
|
|
84
|
-
# Anthropic clients
|
|
85
140
|
client: Optional[AnthropicClient] = None
|
|
86
141
|
async_client: Optional[AsyncAnthropicClient] = None
|
|
87
142
|
|
|
143
|
+
def __post_init__(self):
|
|
144
|
+
"""Validate model configuration after initialization"""
|
|
145
|
+
# Validate thinking support immediately at model creation
|
|
146
|
+
if self.thinking:
|
|
147
|
+
self._validate_thinking_support()
|
|
148
|
+
# Set structured outputs capability flag for supported models
|
|
149
|
+
if self._supports_structured_outputs():
|
|
150
|
+
self.supports_native_structured_outputs = True
|
|
151
|
+
# Set up skills configuration if skills are enabled
|
|
152
|
+
if self.skills:
|
|
153
|
+
self._setup_skills_configuration()
|
|
154
|
+
|
|
88
155
|
def _get_client_params(self) -> Dict[str, Any]:
|
|
89
156
|
client_params: Dict[str, Any] = {}
|
|
90
157
|
|
|
91
158
|
self.api_key = self.api_key or getenv("ANTHROPIC_API_KEY")
|
|
92
|
-
|
|
93
|
-
|
|
159
|
+
self.auth_token = self.auth_token or getenv("ANTHROPIC_AUTH_TOKEN")
|
|
160
|
+
if not (self.api_key or self.auth_token):
|
|
161
|
+
log_error(
|
|
162
|
+
"ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN not set. Please set the ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable."
|
|
163
|
+
)
|
|
94
164
|
|
|
95
165
|
# Add API key to client parameters
|
|
96
166
|
client_params["api_key"] = self.api_key
|
|
167
|
+
client_params["auth_token"] = self.auth_token
|
|
97
168
|
if self.timeout is not None:
|
|
98
169
|
client_params["timeout"] = self.timeout
|
|
99
170
|
|
|
@@ -104,6 +175,188 @@ class Claude(Model):
|
|
|
104
175
|
client_params["default_headers"] = self.default_headers
|
|
105
176
|
return client_params
|
|
106
177
|
|
|
178
|
+
def _supports_structured_outputs(self) -> bool:
|
|
179
|
+
"""
|
|
180
|
+
Check if the current model supports native structured outputs.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
bool: True if model supports structured outputs
|
|
184
|
+
"""
|
|
185
|
+
# If model is in blacklist, it doesn't support structured outputs
|
|
186
|
+
if self.id in self.NON_STRUCTURED_OUTPUT_MODELS:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
# Check for legacy model patterns which don't support structured outputs
|
|
190
|
+
if self.id.startswith("claude-3-"):
|
|
191
|
+
return False
|
|
192
|
+
if self.id.startswith("claude-sonnet-4-") and not self.id.startswith("claude-sonnet-4-5"):
|
|
193
|
+
return False
|
|
194
|
+
if self.id.startswith("claude-opus-4-") and not self.id.startswith("claude-opus-4-1"):
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
def _using_structured_outputs(
|
|
200
|
+
self,
|
|
201
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
202
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
203
|
+
) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Check if structured outputs are being used in this request.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
response_format: Response format parameter
|
|
209
|
+
tools: Tools list to check for strict mode
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
bool: True if structured outputs are in use
|
|
213
|
+
"""
|
|
214
|
+
# Check for output_format usage
|
|
215
|
+
if response_format is not None:
|
|
216
|
+
if self._supports_structured_outputs():
|
|
217
|
+
return True
|
|
218
|
+
else:
|
|
219
|
+
log_warning(
|
|
220
|
+
f"Model '{self.id}' does not support structured outputs. "
|
|
221
|
+
"Structured output features will not be available for this model."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Check for strict tools
|
|
225
|
+
if tools:
|
|
226
|
+
for tool in tools:
|
|
227
|
+
if tool.get("type") == "function":
|
|
228
|
+
func_def = tool.get("function", {})
|
|
229
|
+
if func_def.get("strict") is True:
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def _validate_thinking_support(self) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Validate that the current model supports extended thinking.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If thinking is enabled but the model doesn't support it
|
|
240
|
+
"""
|
|
241
|
+
if self.thinking and self.id in self.NON_THINKING_MODELS:
|
|
242
|
+
non_thinking_models = "\n - ".join(sorted(self.NON_THINKING_MODELS))
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Model '{self.id}' does not support extended thinking.\n\n"
|
|
245
|
+
f"The following models do NOT support thinking:\n - {non_thinking_models}\n\n"
|
|
246
|
+
f"All other Claude models support extended thinking by default.\n"
|
|
247
|
+
f"For more information, see: https://docs.anthropic.com/en/docs/about-claude/models/overview"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _setup_skills_configuration(self) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Set up configuration for Claude Agent Skills.
|
|
253
|
+
Automatically configures betas array with required values.
|
|
254
|
+
|
|
255
|
+
Skills enable document creation capabilities (PowerPoint, Excel, Word, PDF).
|
|
256
|
+
For more information, see: https://docs.claude.com/en/docs/agents-and-tools/agent-skills/quickstart
|
|
257
|
+
"""
|
|
258
|
+
# Required betas for skills
|
|
259
|
+
required_betas = ["code-execution-2025-08-25", "skills-2025-10-02"]
|
|
260
|
+
|
|
261
|
+
# Initialize or merge betas
|
|
262
|
+
if self.betas is None:
|
|
263
|
+
self.betas = required_betas
|
|
264
|
+
else:
|
|
265
|
+
# Add required betas if not present
|
|
266
|
+
for beta in required_betas:
|
|
267
|
+
if beta not in self.betas:
|
|
268
|
+
self.betas.append(beta)
|
|
269
|
+
|
|
270
|
+
def _ensure_additional_properties_false(self, schema: Dict[str, Any]) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Recursively ensure all object types have additionalProperties: false.
|
|
273
|
+
"""
|
|
274
|
+
if isinstance(schema, dict):
|
|
275
|
+
if schema.get("type") == "object":
|
|
276
|
+
schema["additionalProperties"] = False
|
|
277
|
+
|
|
278
|
+
# Recursively process nested schemas
|
|
279
|
+
for key, value in schema.items():
|
|
280
|
+
if key in ["properties", "items", "allOf", "anyOf", "oneOf"]:
|
|
281
|
+
if isinstance(value, dict):
|
|
282
|
+
self._ensure_additional_properties_false(value)
|
|
283
|
+
elif isinstance(value, list):
|
|
284
|
+
for item in value:
|
|
285
|
+
if isinstance(item, dict):
|
|
286
|
+
self._ensure_additional_properties_false(item)
|
|
287
|
+
|
|
288
|
+
def _build_output_format(self, response_format: Optional[Union[Dict, Type[BaseModel]]]) -> Optional[Dict[str, Any]]:
|
|
289
|
+
"""
|
|
290
|
+
Build Anthropic output_format parameter from response_format.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
response_format: Pydantic model or dict format
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Dict with output_format structure or None
|
|
297
|
+
"""
|
|
298
|
+
if response_format is None:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
if not self._supports_structured_outputs():
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
# Handle Pydantic BaseModel
|
|
305
|
+
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
|
306
|
+
try:
|
|
307
|
+
# Try to use Anthropic SDK's transform_schema helper if available
|
|
308
|
+
from anthropic import transform_schema
|
|
309
|
+
|
|
310
|
+
schema = transform_schema(response_format.model_json_schema())
|
|
311
|
+
except (ImportError, AttributeError):
|
|
312
|
+
# Fallback to direct schema conversion
|
|
313
|
+
schema = response_format.model_json_schema()
|
|
314
|
+
# Ensure additionalProperties is False
|
|
315
|
+
if isinstance(schema, dict):
|
|
316
|
+
if "additionalProperties" not in schema:
|
|
317
|
+
schema["additionalProperties"] = False
|
|
318
|
+
# Recursively ensure all object types have additionalProperties: false
|
|
319
|
+
self._ensure_additional_properties_false(schema)
|
|
320
|
+
|
|
321
|
+
return {"type": "json_schema", "schema": schema}
|
|
322
|
+
|
|
323
|
+
# Handle dict format (already in correct structure)
|
|
324
|
+
elif isinstance(response_format, dict):
|
|
325
|
+
return response_format
|
|
326
|
+
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
def _validate_structured_outputs_usage(
|
|
330
|
+
self,
|
|
331
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
332
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Validate that structured outputs are only used with supported models.
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
ValueError: If structured outputs are used with unsupported model
|
|
339
|
+
"""
|
|
340
|
+
if not self._using_structured_outputs(response_format, tools):
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
if not self._supports_structured_outputs():
|
|
344
|
+
raise ValueError(f"Model '{self.id}' does not support structured outputs.\n\n")
|
|
345
|
+
|
|
346
|
+
def _has_beta_features(
|
|
347
|
+
self,
|
|
348
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
349
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
350
|
+
) -> bool:
|
|
351
|
+
"""Check if the model has any Anthropic beta features enabled."""
|
|
352
|
+
return (
|
|
353
|
+
self.mcp_servers is not None
|
|
354
|
+
or self.context_management is not None
|
|
355
|
+
or self.skills is not None
|
|
356
|
+
or self.betas is not None
|
|
357
|
+
or self._using_structured_outputs(response_format, tools)
|
|
358
|
+
)
|
|
359
|
+
|
|
107
360
|
def get_client(self) -> AnthropicClient:
|
|
108
361
|
"""
|
|
109
362
|
Returns an instance of the Anthropic client.
|
|
@@ -112,6 +365,16 @@ class Claude(Model):
|
|
|
112
365
|
return self.client
|
|
113
366
|
|
|
114
367
|
_client_params = self._get_client_params()
|
|
368
|
+
if self.http_client:
|
|
369
|
+
if isinstance(self.http_client, httpx.Client):
|
|
370
|
+
_client_params["http_client"] = self.http_client
|
|
371
|
+
else:
|
|
372
|
+
log_warning("http_client is not an instance of httpx.Client. Using default global httpx.Client.")
|
|
373
|
+
# Use global sync client when user http_client is invalid
|
|
374
|
+
_client_params["http_client"] = get_default_sync_client()
|
|
375
|
+
else:
|
|
376
|
+
# Use global sync client when no custom http_client is provided
|
|
377
|
+
_client_params["http_client"] = get_default_sync_client()
|
|
115
378
|
self.client = AnthropicClient(**_client_params)
|
|
116
379
|
return self.client
|
|
117
380
|
|
|
@@ -119,17 +382,79 @@ class Claude(Model):
|
|
|
119
382
|
"""
|
|
120
383
|
Returns an instance of the async Anthropic client.
|
|
121
384
|
"""
|
|
122
|
-
if self.async_client:
|
|
385
|
+
if self.async_client and not self.async_client.is_closed():
|
|
123
386
|
return self.async_client
|
|
124
387
|
|
|
125
388
|
_client_params = self._get_client_params()
|
|
389
|
+
if self.http_client:
|
|
390
|
+
if isinstance(self.http_client, httpx.AsyncClient):
|
|
391
|
+
_client_params["http_client"] = self.http_client
|
|
392
|
+
else:
|
|
393
|
+
log_warning(
|
|
394
|
+
"http_client is not an instance of httpx.AsyncClient. Using default global httpx.AsyncClient."
|
|
395
|
+
)
|
|
396
|
+
# Use global async client when user http_client is invalid
|
|
397
|
+
_client_params["http_client"] = get_default_async_client()
|
|
398
|
+
else:
|
|
399
|
+
# Use global async client when no custom http_client is provided
|
|
400
|
+
_client_params["http_client"] = get_default_async_client()
|
|
126
401
|
self.async_client = AsyncAnthropicClient(**_client_params)
|
|
127
402
|
return self.async_client
|
|
128
403
|
|
|
129
|
-
def
|
|
404
|
+
def count_tokens(
|
|
405
|
+
self,
|
|
406
|
+
messages: List[Message],
|
|
407
|
+
tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
|
|
408
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
409
|
+
) -> int:
|
|
410
|
+
anthropic_messages, system_prompt = format_messages(messages, compress_tool_results=True)
|
|
411
|
+
anthropic_tools = None
|
|
412
|
+
if tools:
|
|
413
|
+
formatted_tools = self._format_tools(tools)
|
|
414
|
+
anthropic_tools = format_tools_for_model(formatted_tools)
|
|
415
|
+
|
|
416
|
+
kwargs: Dict[str, Any] = {"messages": anthropic_messages, "model": self.id}
|
|
417
|
+
if system_prompt:
|
|
418
|
+
kwargs["system"] = system_prompt
|
|
419
|
+
if anthropic_tools:
|
|
420
|
+
kwargs["tools"] = anthropic_tools
|
|
421
|
+
|
|
422
|
+
response = self.get_client().messages.count_tokens(**kwargs)
|
|
423
|
+
return response.input_tokens + count_schema_tokens(response_format, self.id)
|
|
424
|
+
|
|
425
|
+
async def acount_tokens(
|
|
426
|
+
self,
|
|
427
|
+
messages: List[Message],
|
|
428
|
+
tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
|
|
429
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
430
|
+
) -> int:
|
|
431
|
+
anthropic_messages, system_prompt = format_messages(messages, compress_tool_results=True)
|
|
432
|
+
anthropic_tools = None
|
|
433
|
+
if tools:
|
|
434
|
+
formatted_tools = self._format_tools(tools)
|
|
435
|
+
anthropic_tools = format_tools_for_model(formatted_tools)
|
|
436
|
+
|
|
437
|
+
kwargs: Dict[str, Any] = {"messages": anthropic_messages, "model": self.id}
|
|
438
|
+
if system_prompt:
|
|
439
|
+
kwargs["system"] = system_prompt
|
|
440
|
+
if anthropic_tools:
|
|
441
|
+
kwargs["tools"] = anthropic_tools
|
|
442
|
+
|
|
443
|
+
response = await self.get_async_client().messages.count_tokens(**kwargs)
|
|
444
|
+
return response.input_tokens + count_schema_tokens(response_format, self.id)
|
|
445
|
+
|
|
446
|
+
def get_request_params(
|
|
447
|
+
self,
|
|
448
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
449
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
450
|
+
) -> Dict[str, Any]:
|
|
130
451
|
"""
|
|
131
452
|
Generate keyword arguments for API requests.
|
|
132
453
|
"""
|
|
454
|
+
# Validate thinking support if thinking is enabled
|
|
455
|
+
if self.thinking:
|
|
456
|
+
self._validate_thinking_support()
|
|
457
|
+
|
|
133
458
|
_request_params: Dict[str, Any] = {}
|
|
134
459
|
if self.max_tokens:
|
|
135
460
|
_request_params["max_tokens"] = self.max_tokens
|
|
@@ -143,28 +468,55 @@ class Claude(Model):
|
|
|
143
468
|
_request_params["top_p"] = self.top_p
|
|
144
469
|
if self.top_k:
|
|
145
470
|
_request_params["top_k"] = self.top_k
|
|
471
|
+
|
|
472
|
+
# Build betas list - include existing betas and add new one if needed
|
|
473
|
+
betas_list = list(self.betas) if self.betas else []
|
|
474
|
+
|
|
475
|
+
# Add structured outputs beta header if using structured outputs
|
|
476
|
+
if self._using_structured_outputs(response_format, tools):
|
|
477
|
+
beta_header = "structured-outputs-2025-11-13"
|
|
478
|
+
if beta_header not in betas_list:
|
|
479
|
+
betas_list.append(beta_header)
|
|
480
|
+
|
|
481
|
+
# Include betas if any are present
|
|
482
|
+
if betas_list:
|
|
483
|
+
_request_params["betas"] = betas_list
|
|
484
|
+
|
|
485
|
+
if self.context_management:
|
|
486
|
+
_request_params["context_management"] = self.context_management
|
|
146
487
|
if self.mcp_servers:
|
|
147
488
|
_request_params["mcp_servers"] = [
|
|
148
489
|
{k: v for k, v in asdict(server).items() if v is not None} for server in self.mcp_servers
|
|
149
490
|
]
|
|
491
|
+
if self.skills:
|
|
492
|
+
_request_params["container"] = {"skills": self.skills}
|
|
150
493
|
if self.request_params:
|
|
151
494
|
_request_params.update(self.request_params)
|
|
152
495
|
|
|
153
496
|
return _request_params
|
|
154
497
|
|
|
155
498
|
def _prepare_request_kwargs(
|
|
156
|
-
self,
|
|
499
|
+
self,
|
|
500
|
+
system_message: str,
|
|
501
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
502
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
157
503
|
) -> Dict[str, Any]:
|
|
158
504
|
"""
|
|
159
505
|
Prepare the request keyword arguments for the API call.
|
|
160
506
|
|
|
161
507
|
Args:
|
|
162
508
|
system_message (str): The concatenated system messages.
|
|
509
|
+
tools: Optional list of tools
|
|
510
|
+
response_format: Optional response format (Pydantic model or dict)
|
|
163
511
|
|
|
164
512
|
Returns:
|
|
165
513
|
Dict[str, Any]: The request keyword arguments.
|
|
166
514
|
"""
|
|
167
|
-
|
|
515
|
+
# Validate structured outputs usage
|
|
516
|
+
self._validate_structured_outputs_usage(response_format, tools)
|
|
517
|
+
|
|
518
|
+
# Pass response_format and tools to get_request_params for beta header handling
|
|
519
|
+
request_kwargs = self.get_request_params(response_format=response_format, tools=tools).copy()
|
|
168
520
|
if system_message:
|
|
169
521
|
if self.cache_system_prompt:
|
|
170
522
|
cache_control = (
|
|
@@ -176,9 +528,24 @@ class Claude(Model):
|
|
|
176
528
|
else:
|
|
177
529
|
request_kwargs["system"] = [{"text": system_message, "type": "text"}]
|
|
178
530
|
|
|
531
|
+
# Add code execution tool if skills are enabled
|
|
532
|
+
if self.skills:
|
|
533
|
+
code_execution_tool = {"type": "code_execution_20250825", "name": "code_execution"}
|
|
534
|
+
if tools:
|
|
535
|
+
# Add code_execution to existing tools, code execution is needed for generating and processing files
|
|
536
|
+
tools = tools + [code_execution_tool]
|
|
537
|
+
else:
|
|
538
|
+
tools = [code_execution_tool]
|
|
539
|
+
|
|
540
|
+
# Format tools (this will handle strict mode)
|
|
179
541
|
if tools:
|
|
180
542
|
request_kwargs["tools"] = format_tools_for_model(tools)
|
|
181
543
|
|
|
544
|
+
# Build output_format if response_format is provided
|
|
545
|
+
output_format = self._build_output_format(response_format)
|
|
546
|
+
if output_format:
|
|
547
|
+
request_kwargs["output_format"] = output_format
|
|
548
|
+
|
|
182
549
|
if request_kwargs:
|
|
183
550
|
log_debug(f"Calling {self.provider} with request parameters: {request_kwargs}", log_level=2)
|
|
184
551
|
return request_kwargs
|
|
@@ -191,6 +558,7 @@ class Claude(Model):
|
|
|
191
558
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
192
559
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
193
560
|
run_response: Optional[RunOutput] = None,
|
|
561
|
+
compress_tool_results: bool = False,
|
|
194
562
|
) -> ModelResponse:
|
|
195
563
|
"""
|
|
196
564
|
Send a request to the Anthropic API to generate a response.
|
|
@@ -199,15 +567,15 @@ class Claude(Model):
|
|
|
199
567
|
if run_response and run_response.metrics:
|
|
200
568
|
run_response.metrics.set_time_to_first_token()
|
|
201
569
|
|
|
202
|
-
chat_messages, system_message = format_messages(messages)
|
|
203
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
570
|
+
chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
|
|
571
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
204
572
|
|
|
205
|
-
if self.
|
|
573
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
206
574
|
assistant_message.metrics.start_timer()
|
|
207
575
|
provider_response = self.get_client().beta.messages.create(
|
|
208
576
|
model=self.id,
|
|
209
577
|
messages=chat_messages, # type: ignore
|
|
210
|
-
**
|
|
578
|
+
**request_kwargs,
|
|
211
579
|
)
|
|
212
580
|
else:
|
|
213
581
|
assistant_message.metrics.start_timer()
|
|
@@ -247,6 +615,7 @@ class Claude(Model):
|
|
|
247
615
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
248
616
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
249
617
|
run_response: Optional[RunOutput] = None,
|
|
618
|
+
compress_tool_results: bool = False,
|
|
250
619
|
) -> Any:
|
|
251
620
|
"""
|
|
252
621
|
Stream a response from the Anthropic API.
|
|
@@ -262,14 +631,15 @@ class Claude(Model):
|
|
|
262
631
|
RateLimitError: If the API rate limit is exceeded
|
|
263
632
|
APIStatusError: For other API-related errors
|
|
264
633
|
"""
|
|
265
|
-
chat_messages, system_message = format_messages(messages)
|
|
266
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
634
|
+
chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
|
|
635
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
267
636
|
|
|
268
637
|
try:
|
|
269
638
|
if run_response and run_response.metrics:
|
|
270
639
|
run_response.metrics.set_time_to_first_token()
|
|
271
640
|
|
|
272
|
-
|
|
641
|
+
# Beta features
|
|
642
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
273
643
|
assistant_message.metrics.start_timer()
|
|
274
644
|
with self.get_client().beta.messages.stream(
|
|
275
645
|
model=self.id,
|
|
@@ -277,7 +647,7 @@ class Claude(Model):
|
|
|
277
647
|
**request_kwargs,
|
|
278
648
|
) as stream:
|
|
279
649
|
for chunk in stream:
|
|
280
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
650
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
281
651
|
else:
|
|
282
652
|
assistant_message.metrics.start_timer()
|
|
283
653
|
with self.get_client().messages.stream(
|
|
@@ -286,7 +656,7 @@ class Claude(Model):
|
|
|
286
656
|
**request_kwargs,
|
|
287
657
|
) as stream:
|
|
288
658
|
for chunk in stream: # type: ignore
|
|
289
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
659
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
290
660
|
|
|
291
661
|
assistant_message.metrics.stop_timer()
|
|
292
662
|
|
|
@@ -313,6 +683,7 @@ class Claude(Model):
|
|
|
313
683
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
314
684
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
315
685
|
run_response: Optional[RunOutput] = None,
|
|
686
|
+
compress_tool_results: bool = False,
|
|
316
687
|
) -> ModelResponse:
|
|
317
688
|
"""
|
|
318
689
|
Send an asynchronous request to the Anthropic API to generate a response.
|
|
@@ -321,15 +692,16 @@ class Claude(Model):
|
|
|
321
692
|
if run_response and run_response.metrics:
|
|
322
693
|
run_response.metrics.set_time_to_first_token()
|
|
323
694
|
|
|
324
|
-
chat_messages, system_message = format_messages(messages)
|
|
325
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
695
|
+
chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
|
|
696
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
326
697
|
|
|
327
|
-
|
|
698
|
+
# Beta features
|
|
699
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
328
700
|
assistant_message.metrics.start_timer()
|
|
329
701
|
provider_response = await self.get_async_client().beta.messages.create(
|
|
330
702
|
model=self.id,
|
|
331
703
|
messages=chat_messages, # type: ignore
|
|
332
|
-
**
|
|
704
|
+
**request_kwargs,
|
|
333
705
|
)
|
|
334
706
|
else:
|
|
335
707
|
assistant_message.metrics.start_timer()
|
|
@@ -369,6 +741,7 @@ class Claude(Model):
|
|
|
369
741
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
370
742
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
371
743
|
run_response: Optional[RunOutput] = None,
|
|
744
|
+
compress_tool_results: bool = False,
|
|
372
745
|
) -> AsyncIterator[ModelResponse]:
|
|
373
746
|
"""
|
|
374
747
|
Stream an asynchronous response from the Anthropic API.
|
|
@@ -385,10 +758,10 @@ class Claude(Model):
|
|
|
385
758
|
if run_response and run_response.metrics:
|
|
386
759
|
run_response.metrics.set_time_to_first_token()
|
|
387
760
|
|
|
388
|
-
chat_messages, system_message = format_messages(messages)
|
|
389
|
-
request_kwargs = self._prepare_request_kwargs(system_message, tools)
|
|
761
|
+
chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
|
|
762
|
+
request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
|
|
390
763
|
|
|
391
|
-
if self.
|
|
764
|
+
if self._has_beta_features(response_format=response_format, tools=tools):
|
|
392
765
|
assistant_message.metrics.start_timer()
|
|
393
766
|
async with self.get_async_client().beta.messages.stream(
|
|
394
767
|
model=self.id,
|
|
@@ -396,7 +769,7 @@ class Claude(Model):
|
|
|
396
769
|
**request_kwargs,
|
|
397
770
|
) as stream:
|
|
398
771
|
async for chunk in stream:
|
|
399
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
772
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
400
773
|
else:
|
|
401
774
|
assistant_message.metrics.start_timer()
|
|
402
775
|
async with self.get_async_client().messages.stream(
|
|
@@ -405,7 +778,7 @@ class Claude(Model):
|
|
|
405
778
|
**request_kwargs,
|
|
406
779
|
) as stream:
|
|
407
780
|
async for chunk in stream: # type: ignore
|
|
408
|
-
yield self._parse_provider_response_delta(chunk) # type: ignore
|
|
781
|
+
yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
|
|
409
782
|
|
|
410
783
|
assistant_message.metrics.stop_timer()
|
|
411
784
|
|
|
@@ -430,12 +803,18 @@ class Claude(Model):
|
|
|
430
803
|
return tool_call_prompt
|
|
431
804
|
return None
|
|
432
805
|
|
|
433
|
-
def _parse_provider_response(
|
|
806
|
+
def _parse_provider_response(
|
|
807
|
+
self,
|
|
808
|
+
response: Union[AnthropicMessage, BetaMessage],
|
|
809
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
810
|
+
**kwargs,
|
|
811
|
+
) -> ModelResponse:
|
|
434
812
|
"""
|
|
435
813
|
Parse the Claude response into a ModelResponse.
|
|
436
814
|
|
|
437
815
|
Args:
|
|
438
816
|
response: Raw response from Anthropic
|
|
817
|
+
response_format: Optional response format for structured output parsing
|
|
439
818
|
|
|
440
819
|
Returns:
|
|
441
820
|
ModelResponse: Parsed response data
|
|
@@ -448,10 +827,32 @@ class Claude(Model):
|
|
|
448
827
|
if response.content:
|
|
449
828
|
for block in response.content:
|
|
450
829
|
if block.type == "text":
|
|
830
|
+
text_content = block.text
|
|
831
|
+
|
|
451
832
|
if model_response.content is None:
|
|
452
|
-
model_response.content =
|
|
833
|
+
model_response.content = text_content
|
|
453
834
|
else:
|
|
454
|
-
model_response.content +=
|
|
835
|
+
model_response.content += text_content
|
|
836
|
+
|
|
837
|
+
# Handle structured outputs (JSON outputs)
|
|
838
|
+
if (
|
|
839
|
+
response_format is not None
|
|
840
|
+
and isinstance(response_format, type)
|
|
841
|
+
and issubclass(response_format, BaseModel)
|
|
842
|
+
):
|
|
843
|
+
if text_content:
|
|
844
|
+
try:
|
|
845
|
+
# Parse JSON from text content
|
|
846
|
+
parsed_data = json.loads(text_content)
|
|
847
|
+
# Validate against Pydantic model
|
|
848
|
+
model_response.parsed = response_format.model_validate(parsed_data)
|
|
849
|
+
log_debug(f"Successfully parsed structured output: {model_response.parsed}")
|
|
850
|
+
except json.JSONDecodeError as e:
|
|
851
|
+
log_warning(f"Failed to parse JSON from structured output: {e}")
|
|
852
|
+
except ValidationError as e:
|
|
853
|
+
log_warning(f"Failed to validate structured output against schema: {e}")
|
|
854
|
+
except Exception as e:
|
|
855
|
+
log_warning(f"Unexpected error parsing structured output: {e}")
|
|
455
856
|
|
|
456
857
|
# Capture citations from the response
|
|
457
858
|
if block.citations is not None:
|
|
@@ -505,6 +906,30 @@ class Claude(Model):
|
|
|
505
906
|
if response.usage is not None:
|
|
506
907
|
model_response.response_usage = self._get_metrics(response.usage)
|
|
507
908
|
|
|
909
|
+
# Capture context management information if present
|
|
910
|
+
if self.context_management is not None and hasattr(response, "context_management"):
|
|
911
|
+
if response.context_management is not None: # type: ignore
|
|
912
|
+
model_response.provider_data = model_response.provider_data or {}
|
|
913
|
+
if hasattr(response.context_management, "model_dump"):
|
|
914
|
+
model_response.provider_data["context_management"] = response.context_management.model_dump() # type: ignore
|
|
915
|
+
else:
|
|
916
|
+
model_response.provider_data["context_management"] = response.context_management # type: ignore
|
|
917
|
+
# Extract file IDs if skills are enabled
|
|
918
|
+
if self.skills and response.content:
|
|
919
|
+
file_ids: List[str] = []
|
|
920
|
+
for block in response.content:
|
|
921
|
+
if block.type == "bash_code_execution_tool_result":
|
|
922
|
+
if hasattr(block, "content") and hasattr(block.content, "content"):
|
|
923
|
+
if isinstance(block.content.content, list):
|
|
924
|
+
for output_block in block.content.content:
|
|
925
|
+
if hasattr(output_block, "file_id"):
|
|
926
|
+
file_ids.append(output_block.file_id)
|
|
927
|
+
|
|
928
|
+
if file_ids:
|
|
929
|
+
if model_response.provider_data is None:
|
|
930
|
+
model_response.provider_data = {}
|
|
931
|
+
model_response.provider_data["file_ids"] = file_ids
|
|
932
|
+
|
|
508
933
|
return model_response
|
|
509
934
|
|
|
510
935
|
def _parse_provider_response_delta(
|
|
@@ -515,24 +940,29 @@ class Claude(Model):
|
|
|
515
940
|
ContentBlockStopEvent,
|
|
516
941
|
MessageStopEvent,
|
|
517
942
|
BetaRawContentBlockDeltaEvent,
|
|
943
|
+
BetaRawContentBlockStartEvent,
|
|
944
|
+
ParsedBetaContentBlockStopEvent,
|
|
945
|
+
ParsedBetaMessageStopEvent,
|
|
518
946
|
],
|
|
947
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
519
948
|
) -> ModelResponse:
|
|
520
949
|
"""
|
|
521
950
|
Parse the Claude streaming response into ModelProviderResponse objects.
|
|
522
951
|
|
|
523
952
|
Args:
|
|
524
953
|
response: Raw response chunk from Anthropic
|
|
954
|
+
response_format: Optional response format for structured output parsing
|
|
525
955
|
|
|
526
956
|
Returns:
|
|
527
957
|
ModelResponse: Iterator of parsed response data
|
|
528
958
|
"""
|
|
529
959
|
model_response = ModelResponse()
|
|
530
960
|
|
|
531
|
-
if isinstance(response, ContentBlockStartEvent):
|
|
961
|
+
if isinstance(response, (ContentBlockStartEvent, BetaRawContentBlockStartEvent)):
|
|
532
962
|
if response.content_block.type == "redacted_reasoning_content":
|
|
533
963
|
model_response.redacted_reasoning_content = response.content_block.data
|
|
534
964
|
|
|
535
|
-
if isinstance(response, ContentBlockDeltaEvent):
|
|
965
|
+
if isinstance(response, (ContentBlockDeltaEvent, BetaRawContentBlockDeltaEvent)):
|
|
536
966
|
# Handle text content
|
|
537
967
|
if response.delta.type == "text_delta":
|
|
538
968
|
model_response.content = response.delta.text
|
|
@@ -544,11 +974,11 @@ class Claude(Model):
|
|
|
544
974
|
"signature": response.delta.signature,
|
|
545
975
|
}
|
|
546
976
|
|
|
547
|
-
elif isinstance(response, ContentBlockStopEvent):
|
|
977
|
+
elif isinstance(response, (ContentBlockStopEvent, ParsedBetaContentBlockStopEvent)):
|
|
548
978
|
if response.content_block.type == "tool_use": # type: ignore
|
|
549
979
|
tool_use = response.content_block # type: ignore
|
|
550
|
-
tool_name = tool_use.name
|
|
551
|
-
tool_input = tool_use.input
|
|
980
|
+
tool_name = tool_use.name # type: ignore
|
|
981
|
+
tool_input = tool_use.input # type: ignore
|
|
552
982
|
|
|
553
983
|
function_def = {"name": tool_name}
|
|
554
984
|
if tool_input:
|
|
@@ -558,17 +988,30 @@ class Claude(Model):
|
|
|
558
988
|
|
|
559
989
|
model_response.tool_calls = [
|
|
560
990
|
{
|
|
561
|
-
"id": tool_use.id,
|
|
991
|
+
"id": tool_use.id, # type: ignore
|
|
562
992
|
"type": "function",
|
|
563
993
|
"function": function_def,
|
|
564
994
|
}
|
|
565
995
|
]
|
|
566
996
|
|
|
567
|
-
# Capture citations from the final response
|
|
568
|
-
elif isinstance(response, MessageStopEvent):
|
|
997
|
+
# Capture citations from the final response and handle structured outputs
|
|
998
|
+
elif isinstance(response, (MessageStopEvent, ParsedBetaMessageStopEvent)):
|
|
999
|
+
# In streaming mode, content has already been emitted via ContentBlockDeltaEvent chunks
|
|
1000
|
+
# Setting content here would cause duplication since _populate_stream_data accumulates with +=
|
|
1001
|
+
# Keep content empty to avoid duplication
|
|
569
1002
|
model_response.content = ""
|
|
570
1003
|
model_response.citations = Citations(raw=[], urls=[], documents=[])
|
|
1004
|
+
|
|
1005
|
+
# Accumulate text content for structured output parsing (but don't set model_response.content)
|
|
1006
|
+
# The text was already streamed via ContentBlockDeltaEvent chunks
|
|
1007
|
+
accumulated_text = ""
|
|
1008
|
+
|
|
571
1009
|
for block in response.message.content: # type: ignore
|
|
1010
|
+
# Handle text blocks for structured output parsing
|
|
1011
|
+
if block.type == "text":
|
|
1012
|
+
accumulated_text += block.text # type: ignore
|
|
1013
|
+
|
|
1014
|
+
# Handle citations
|
|
572
1015
|
citations = getattr(block, "citations", None)
|
|
573
1016
|
if not citations:
|
|
574
1017
|
continue
|
|
@@ -583,6 +1026,38 @@ class Claude(Model):
|
|
|
583
1026
|
DocumentCitation(document_title=citation.document_title, cited_text=citation.cited_text)
|
|
584
1027
|
)
|
|
585
1028
|
|
|
1029
|
+
# Handle structured outputs (JSON outputs) from accumulated text
|
|
1030
|
+
# Note: We parse from accumulated_text but don't set model_response.content to avoid duplication
|
|
1031
|
+
# The content was already streamed via ContentBlockDeltaEvent chunks
|
|
1032
|
+
if (
|
|
1033
|
+
response_format is not None
|
|
1034
|
+
and isinstance(response_format, type)
|
|
1035
|
+
and issubclass(response_format, BaseModel)
|
|
1036
|
+
):
|
|
1037
|
+
if accumulated_text:
|
|
1038
|
+
try:
|
|
1039
|
+
# Parse JSON from accumulated text content
|
|
1040
|
+
parsed_data = json.loads(accumulated_text)
|
|
1041
|
+
# Validate against Pydantic model
|
|
1042
|
+
model_response.parsed = response_format.model_validate(parsed_data)
|
|
1043
|
+
log_debug(f"Successfully parsed structured output from stream: {model_response.parsed}")
|
|
1044
|
+
except json.JSONDecodeError as e:
|
|
1045
|
+
log_warning(f"Failed to parse JSON from structured output in stream: {e}")
|
|
1046
|
+
except ValidationError as e:
|
|
1047
|
+
log_warning(f"Failed to validate structured output against schema in stream: {e}")
|
|
1048
|
+
except Exception as e:
|
|
1049
|
+
log_warning(f"Unexpected error parsing structured output in stream: {e}")
|
|
1050
|
+
|
|
1051
|
+
# Capture context management information if present
|
|
1052
|
+
if self.context_management is not None and hasattr(response.message, "context_management"): # type: ignore
|
|
1053
|
+
context_mgmt = response.message.context_management # type: ignore
|
|
1054
|
+
if context_mgmt is not None:
|
|
1055
|
+
model_response.provider_data = model_response.provider_data or {}
|
|
1056
|
+
if hasattr(context_mgmt, "model_dump"):
|
|
1057
|
+
model_response.provider_data["context_management"] = context_mgmt.model_dump()
|
|
1058
|
+
else:
|
|
1059
|
+
model_response.provider_data["context_management"] = context_mgmt
|
|
1060
|
+
|
|
586
1061
|
if hasattr(response, "message") and hasattr(response.message, "usage") and response.message.usage is not None: # type: ignore
|
|
587
1062
|
model_response.response_usage = self._get_metrics(response.message.usage) # type: ignore
|
|
588
1063
|
|
|
@@ -599,7 +1074,7 @@ class Claude(Model):
|
|
|
599
1074
|
|
|
600
1075
|
return model_response
|
|
601
1076
|
|
|
602
|
-
def _get_metrics(self, response_usage: Union[Usage, MessageDeltaUsage]) -> Metrics:
|
|
1077
|
+
def _get_metrics(self, response_usage: Union[Usage, MessageDeltaUsage, BetaUsage]) -> Metrics:
|
|
603
1078
|
"""
|
|
604
1079
|
Parse the given Anthropic-specific usage into an Agno Metrics object.
|
|
605
1080
|
|
|
@@ -619,7 +1094,7 @@ class Claude(Model):
|
|
|
619
1094
|
|
|
620
1095
|
# Anthropic-specific additional fields
|
|
621
1096
|
if response_usage.server_tool_use:
|
|
622
|
-
metrics.provider_metrics = {"server_tool_use": response_usage.server_tool_use}
|
|
1097
|
+
metrics.provider_metrics = {"server_tool_use": response_usage.server_tool_use.model_dump()}
|
|
623
1098
|
if isinstance(response_usage, Usage):
|
|
624
1099
|
if response_usage.service_tier:
|
|
625
1100
|
metrics.provider_metrics = metrics.provider_metrics or {}
|