fast-agent-mcp 0.2.57__py3-none-any.whl → 0.3.0__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.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/__init__.py +127 -0
- fast_agent/agents/__init__.py +36 -0
- {mcp_agent/core → fast_agent/agents}/agent_types.py +2 -1
- fast_agent/agents/llm_agent.py +217 -0
- fast_agent/agents/llm_decorator.py +486 -0
- mcp_agent/agents/base_agent.py → fast_agent/agents/mcp_agent.py +377 -385
- fast_agent/agents/tool_agent.py +168 -0
- {mcp_agent → fast_agent}/agents/workflow/chain_agent.py +43 -33
- {mcp_agent → fast_agent}/agents/workflow/evaluator_optimizer.py +31 -35
- {mcp_agent → fast_agent}/agents/workflow/iterative_planner.py +56 -47
- {mcp_agent → fast_agent}/agents/workflow/orchestrator_models.py +4 -4
- {mcp_agent → fast_agent}/agents/workflow/parallel_agent.py +34 -41
- {mcp_agent → fast_agent}/agents/workflow/router_agent.py +54 -39
- {mcp_agent → fast_agent}/cli/__main__.py +5 -3
- {mcp_agent → fast_agent}/cli/commands/check_config.py +95 -66
- {mcp_agent → fast_agent}/cli/commands/go.py +20 -11
- {mcp_agent → fast_agent}/cli/commands/quickstart.py +4 -4
- {mcp_agent → fast_agent}/cli/commands/server_helpers.py +1 -1
- {mcp_agent → fast_agent}/cli/commands/setup.py +64 -134
- {mcp_agent → fast_agent}/cli/commands/url_parser.py +9 -8
- {mcp_agent → fast_agent}/cli/main.py +36 -16
- {mcp_agent → fast_agent}/cli/terminal.py +2 -2
- {mcp_agent → fast_agent}/config.py +13 -2
- fast_agent/constants.py +8 -0
- {mcp_agent → fast_agent}/context.py +24 -19
- {mcp_agent → fast_agent}/context_dependent.py +9 -5
- fast_agent/core/__init__.py +17 -0
- {mcp_agent → fast_agent}/core/agent_app.py +39 -36
- fast_agent/core/core_app.py +135 -0
- {mcp_agent → fast_agent}/core/direct_decorators.py +12 -26
- {mcp_agent → fast_agent}/core/direct_factory.py +95 -73
- {mcp_agent → fast_agent/core}/executor/executor.py +4 -5
- {mcp_agent → fast_agent}/core/fastagent.py +32 -32
- fast_agent/core/logging/__init__.py +5 -0
- {mcp_agent → fast_agent/core}/logging/events.py +3 -3
- {mcp_agent → fast_agent/core}/logging/json_serializer.py +1 -1
- {mcp_agent → fast_agent/core}/logging/listeners.py +85 -7
- {mcp_agent → fast_agent/core}/logging/logger.py +7 -7
- {mcp_agent → fast_agent/core}/logging/transport.py +10 -11
- fast_agent/core/prompt.py +9 -0
- {mcp_agent → fast_agent}/core/validation.py +4 -4
- fast_agent/event_progress.py +61 -0
- fast_agent/history/history_exporter.py +44 -0
- {mcp_agent → fast_agent}/human_input/__init__.py +9 -12
- {mcp_agent → fast_agent}/human_input/elicitation_handler.py +26 -8
- {mcp_agent → fast_agent}/human_input/elicitation_state.py +7 -7
- {mcp_agent → fast_agent}/human_input/simple_form.py +6 -4
- {mcp_agent → fast_agent}/human_input/types.py +1 -18
- fast_agent/interfaces.py +228 -0
- fast_agent/llm/__init__.py +9 -0
- mcp_agent/llm/augmented_llm.py → fast_agent/llm/fastagent_llm.py +128 -218
- fast_agent/llm/internal/passthrough.py +137 -0
- mcp_agent/llm/augmented_llm_playback.py → fast_agent/llm/internal/playback.py +29 -25
- mcp_agent/llm/augmented_llm_silent.py → fast_agent/llm/internal/silent.py +10 -17
- fast_agent/llm/internal/slow.py +38 -0
- {mcp_agent → fast_agent}/llm/memory.py +40 -30
- {mcp_agent → fast_agent}/llm/model_database.py +35 -2
- {mcp_agent → fast_agent}/llm/model_factory.py +103 -77
- fast_agent/llm/model_info.py +126 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/anthropic_utils.py +7 -7
- fast_agent/llm/provider/anthropic/llm_anthropic.py +603 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/multipart_converter_anthropic.py +79 -86
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2192 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/google}/google_converter.py +66 -14
- fast_agent/llm/provider/google/llm_google_native.py +431 -0
- mcp_agent/llm/providers/augmented_llm_aliyun.py → fast_agent/llm/provider/openai/llm_aliyun.py +6 -7
- mcp_agent/llm/providers/augmented_llm_azure.py → fast_agent/llm/provider/openai/llm_azure.py +4 -4
- mcp_agent/llm/providers/augmented_llm_deepseek.py → fast_agent/llm/provider/openai/llm_deepseek.py +10 -11
- mcp_agent/llm/providers/augmented_llm_generic.py → fast_agent/llm/provider/openai/llm_generic.py +4 -4
- mcp_agent/llm/providers/augmented_llm_google_oai.py → fast_agent/llm/provider/openai/llm_google_oai.py +4 -4
- mcp_agent/llm/providers/augmented_llm_groq.py → fast_agent/llm/provider/openai/llm_groq.py +14 -16
- mcp_agent/llm/providers/augmented_llm_openai.py → fast_agent/llm/provider/openai/llm_openai.py +133 -206
- mcp_agent/llm/providers/augmented_llm_openrouter.py → fast_agent/llm/provider/openai/llm_openrouter.py +6 -6
- mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py → fast_agent/llm/provider/openai/llm_tensorzero_openai.py +17 -16
- mcp_agent/llm/providers/augmented_llm_xai.py → fast_agent/llm/provider/openai/llm_xai.py +6 -6
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/multipart_converter_openai.py +125 -63
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_multipart.py +12 -12
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_utils.py +18 -16
- {mcp_agent → fast_agent}/llm/provider_key_manager.py +2 -2
- {mcp_agent → fast_agent}/llm/provider_types.py +2 -0
- {mcp_agent → fast_agent}/llm/sampling_converter.py +15 -12
- {mcp_agent → fast_agent}/llm/usage_tracking.py +23 -5
- fast_agent/mcp/__init__.py +43 -0
- {mcp_agent → fast_agent}/mcp/elicitation_factory.py +3 -3
- {mcp_agent → fast_agent}/mcp/elicitation_handlers.py +19 -10
- {mcp_agent → fast_agent}/mcp/gen_client.py +3 -3
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +183 -0
- {mcp_agent → fast_agent}/mcp/helpers/server_config_helpers.py +8 -8
- {mcp_agent → fast_agent}/mcp/hf_auth.py +25 -23
- fast_agent/mcp/interfaces.py +93 -0
- {mcp_agent → fast_agent}/mcp/logger_textio.py +4 -4
- {mcp_agent → fast_agent}/mcp/mcp_agent_client_session.py +49 -44
- {mcp_agent → fast_agent}/mcp/mcp_aggregator.py +66 -115
- {mcp_agent → fast_agent}/mcp/mcp_connection_manager.py +16 -23
- {mcp_agent/core → fast_agent/mcp}/mcp_content.py +23 -15
- {mcp_agent → fast_agent}/mcp/mime_utils.py +39 -0
- fast_agent/mcp/prompt.py +159 -0
- mcp_agent/mcp/prompt_message_multipart.py → fast_agent/mcp/prompt_message_extended.py +27 -20
- {mcp_agent → fast_agent}/mcp/prompt_render.py +21 -19
- {mcp_agent → fast_agent}/mcp/prompt_serialization.py +46 -46
- fast_agent/mcp/prompts/__main__.py +7 -0
- {mcp_agent → fast_agent}/mcp/prompts/prompt_helpers.py +31 -30
- {mcp_agent → fast_agent}/mcp/prompts/prompt_load.py +8 -8
- {mcp_agent → fast_agent}/mcp/prompts/prompt_server.py +11 -19
- {mcp_agent → fast_agent}/mcp/prompts/prompt_template.py +18 -18
- {mcp_agent → fast_agent}/mcp/resource_utils.py +1 -1
- {mcp_agent → fast_agent}/mcp/sampling.py +31 -26
- {mcp_agent/mcp_server → fast_agent/mcp/server}/__init__.py +1 -1
- {mcp_agent/mcp_server → fast_agent/mcp/server}/agent_server.py +5 -6
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +90 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis-campaign.py +5 -4
- {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +25 -3
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
- {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
- fast_agent/resources/setup/.gitignore +24 -0
- fast_agent/resources/setup/agent.py +18 -0
- fast_agent/resources/setup/fastagent.config.yaml +44 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/types/__init__.py +32 -0
- fast_agent/types/llm_stop_reason.py +77 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console_display.py +1005 -0
- {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +56 -39
- mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
- {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
- {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
- fast_agent/ui/mcp_ui_utils.py +224 -0
- {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
- {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
- {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/METADATA +7 -7
- fast_agent_mcp-0.3.0.dist-info/RECORD +202 -0
- fast_agent_mcp-0.3.0.dist-info/entry_points.txt +5 -0
- fast_agent_mcp-0.2.57.dist-info/RECORD +0 -192
- fast_agent_mcp-0.2.57.dist-info/entry_points.txt +0 -6
- mcp_agent/__init__.py +0 -114
- mcp_agent/agents/agent.py +0 -92
- mcp_agent/agents/workflow/__init__.py +0 -1
- mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
- mcp_agent/app.py +0 -175
- mcp_agent/core/__init__.py +0 -26
- mcp_agent/core/prompt.py +0 -191
- mcp_agent/event_progress.py +0 -134
- mcp_agent/human_input/handler.py +0 -81
- mcp_agent/llm/__init__.py +0 -2
- mcp_agent/llm/augmented_llm_passthrough.py +0 -232
- mcp_agent/llm/augmented_llm_slow.py +0 -53
- mcp_agent/llm/providers/__init__.py +0 -8
- mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -717
- mcp_agent/llm/providers/augmented_llm_bedrock.py +0 -1788
- mcp_agent/llm/providers/augmented_llm_google_native.py +0 -495
- mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
- mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
- mcp_agent/llm/sampling_format_converter.py +0 -37
- mcp_agent/logging/__init__.py +0 -0
- mcp_agent/mcp/__init__.py +0 -50
- mcp_agent/mcp/helpers/__init__.py +0 -25
- mcp_agent/mcp/helpers/content_helpers.py +0 -187
- mcp_agent/mcp/interfaces.py +0 -266
- mcp_agent/mcp/prompts/__init__.py +0 -0
- mcp_agent/mcp/prompts/__main__.py +0 -10
- mcp_agent/mcp_server_registry.py +0 -343
- mcp_agent/tools/tool_definition.py +0 -14
- mcp_agent/ui/console_display.py +0 -790
- mcp_agent/ui/console_display_legacy.py +0 -401
- {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
- {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
- {mcp_agent → fast_agent}/cli/constants.py +0 -0
- {mcp_agent → fast_agent}/core/error_handling.py +0 -0
- {mcp_agent → fast_agent}/core/exceptions.py +0 -0
- {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
- {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
- {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
- {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
- {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
- {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
- {mcp_agent → fast_agent}/mcp/common.py +0 -0
- {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
- {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
- {mcp_agent → fast_agent}/py.typed +0 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
- {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
- {mcp_agent → fast_agent/ui}/console.py +0 -0
- {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, List, Tuple, Type, Union, cast
|
|
3
|
+
|
|
4
|
+
from anthropic import AsyncAnthropic, AuthenticationError
|
|
5
|
+
from anthropic.lib.streaming import AsyncMessageStream
|
|
6
|
+
from anthropic.types import (
|
|
7
|
+
Message,
|
|
8
|
+
MessageParam,
|
|
9
|
+
TextBlock,
|
|
10
|
+
TextBlockParam,
|
|
11
|
+
ToolParam,
|
|
12
|
+
ToolUseBlock,
|
|
13
|
+
ToolUseBlockParam,
|
|
14
|
+
Usage,
|
|
15
|
+
)
|
|
16
|
+
from mcp import Tool
|
|
17
|
+
from mcp.types import (
|
|
18
|
+
CallToolRequest,
|
|
19
|
+
CallToolRequestParams,
|
|
20
|
+
CallToolResult,
|
|
21
|
+
ContentBlock,
|
|
22
|
+
TextContent,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from fast_agent.core.exceptions import ProviderKeyError
|
|
26
|
+
from fast_agent.core.logging.logger import get_logger
|
|
27
|
+
from fast_agent.core.prompt import Prompt
|
|
28
|
+
from fast_agent.event_progress import ProgressAction
|
|
29
|
+
from fast_agent.interfaces import ModelT
|
|
30
|
+
from fast_agent.llm.fastagent_llm import (
|
|
31
|
+
FastAgentLLM,
|
|
32
|
+
RequestParams,
|
|
33
|
+
)
|
|
34
|
+
from fast_agent.llm.provider.anthropic.multipart_converter_anthropic import (
|
|
35
|
+
AnthropicConverter,
|
|
36
|
+
)
|
|
37
|
+
from fast_agent.llm.provider_types import Provider
|
|
38
|
+
from fast_agent.llm.usage_tracking import TurnUsage
|
|
39
|
+
from fast_agent.types import PromptMessageExtended
|
|
40
|
+
from fast_agent.types.llm_stop_reason import LlmStopReason
|
|
41
|
+
|
|
42
|
+
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-0"
|
|
43
|
+
STRUCTURED_OUTPUT_TOOL_NAME = "return_structured_output"
|
|
44
|
+
|
|
45
|
+
# Type alias for system field - can be string or list of text blocks with cache control
|
|
46
|
+
SystemParam = Union[str, List[TextBlockParam]]
|
|
47
|
+
|
|
48
|
+
logger = get_logger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AnthropicLLM(FastAgentLLM[MessageParam, Message]):
|
|
52
|
+
# Anthropic-specific parameter exclusions
|
|
53
|
+
ANTHROPIC_EXCLUDE_FIELDS = {
|
|
54
|
+
FastAgentLLM.PARAM_MESSAGES,
|
|
55
|
+
FastAgentLLM.PARAM_MODEL,
|
|
56
|
+
FastAgentLLM.PARAM_SYSTEM_PROMPT,
|
|
57
|
+
FastAgentLLM.PARAM_STOP_SEQUENCES,
|
|
58
|
+
FastAgentLLM.PARAM_MAX_TOKENS,
|
|
59
|
+
FastAgentLLM.PARAM_METADATA,
|
|
60
|
+
FastAgentLLM.PARAM_USE_HISTORY,
|
|
61
|
+
FastAgentLLM.PARAM_MAX_ITERATIONS,
|
|
62
|
+
FastAgentLLM.PARAM_PARALLEL_TOOL_CALLS,
|
|
63
|
+
FastAgentLLM.PARAM_TEMPLATE_VARS,
|
|
64
|
+
FastAgentLLM.PARAM_MCP_METADATA,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
68
|
+
# Initialize logger - keep it simple without name reference
|
|
69
|
+
super().__init__(*args, provider=Provider.ANTHROPIC, **kwargs)
|
|
70
|
+
|
|
71
|
+
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
|
72
|
+
"""Initialize Anthropic-specific default parameters"""
|
|
73
|
+
# Get base defaults from parent (includes ModelDatabase lookup)
|
|
74
|
+
base_params = super()._initialize_default_params(kwargs)
|
|
75
|
+
|
|
76
|
+
# Override with Anthropic-specific settings
|
|
77
|
+
chosen_model = kwargs.get("model", DEFAULT_ANTHROPIC_MODEL)
|
|
78
|
+
base_params.model = chosen_model
|
|
79
|
+
|
|
80
|
+
return base_params
|
|
81
|
+
|
|
82
|
+
def _base_url(self) -> str | None:
|
|
83
|
+
assert self.context.config
|
|
84
|
+
return self.context.config.anthropic.base_url if self.context.config.anthropic else None
|
|
85
|
+
|
|
86
|
+
def _get_cache_mode(self) -> str:
|
|
87
|
+
"""Get the cache mode configuration."""
|
|
88
|
+
cache_mode = "auto" # Default to auto
|
|
89
|
+
if self.context.config and self.context.config.anthropic:
|
|
90
|
+
cache_mode = self.context.config.anthropic.cache_mode
|
|
91
|
+
return cache_mode
|
|
92
|
+
|
|
93
|
+
async def _prepare_tools(
|
|
94
|
+
self, structured_model: Type[ModelT] | None = None, tools: List[Tool] | None = None
|
|
95
|
+
) -> List[ToolParam]:
|
|
96
|
+
"""Prepare tools based on whether we're in structured output mode."""
|
|
97
|
+
if structured_model:
|
|
98
|
+
return [
|
|
99
|
+
ToolParam(
|
|
100
|
+
name=STRUCTURED_OUTPUT_TOOL_NAME,
|
|
101
|
+
description="Return the response in the required JSON format",
|
|
102
|
+
input_schema=structured_model.model_json_schema(),
|
|
103
|
+
)
|
|
104
|
+
]
|
|
105
|
+
else:
|
|
106
|
+
# Regular mode - use tools from aggregator
|
|
107
|
+
return [
|
|
108
|
+
ToolParam(
|
|
109
|
+
name=tool.name,
|
|
110
|
+
description=tool.description or "",
|
|
111
|
+
input_schema=tool.inputSchema,
|
|
112
|
+
)
|
|
113
|
+
for tool in tools or []
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def _apply_system_cache(self, base_args: dict, cache_mode: str) -> None:
|
|
117
|
+
"""Apply cache control to system prompt if cache mode allows it."""
|
|
118
|
+
system_content: SystemParam | None = base_args.get("system")
|
|
119
|
+
|
|
120
|
+
if cache_mode != "off" and system_content:
|
|
121
|
+
# Convert string to list format with cache control
|
|
122
|
+
if isinstance(system_content, str):
|
|
123
|
+
base_args["system"] = [
|
|
124
|
+
TextBlockParam(
|
|
125
|
+
type="text", text=system_content, cache_control={"type": "ephemeral"}
|
|
126
|
+
)
|
|
127
|
+
]
|
|
128
|
+
logger.debug(
|
|
129
|
+
"Applied cache_control to system prompt (caches tools+system in one block)"
|
|
130
|
+
)
|
|
131
|
+
# If it's already a list (shouldn't happen in current flow but type-safe)
|
|
132
|
+
elif isinstance(system_content, list):
|
|
133
|
+
logger.debug("System prompt already in list format")
|
|
134
|
+
else:
|
|
135
|
+
logger.debug(f"Unexpected system prompt type: {type(system_content)}")
|
|
136
|
+
|
|
137
|
+
async def _apply_conversation_cache(self, messages: List[MessageParam], cache_mode: str) -> int:
|
|
138
|
+
"""Apply conversation caching if in auto mode. Returns number of cache blocks applied."""
|
|
139
|
+
applied_count = 0
|
|
140
|
+
if cache_mode == "auto" and self.history.should_apply_conversation_cache():
|
|
141
|
+
cache_updates = self.history.get_conversation_cache_updates()
|
|
142
|
+
|
|
143
|
+
# Remove cache control from old positions
|
|
144
|
+
if cache_updates["remove"]:
|
|
145
|
+
self.history.remove_cache_control_from_messages(messages, cache_updates["remove"])
|
|
146
|
+
logger.debug(
|
|
147
|
+
f"Removed conversation cache_control from positions {cache_updates['remove']}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Add cache control to new positions
|
|
151
|
+
if cache_updates["add"]:
|
|
152
|
+
applied_count = self.history.add_cache_control_to_messages(
|
|
153
|
+
messages, cache_updates["add"]
|
|
154
|
+
)
|
|
155
|
+
if applied_count > 0:
|
|
156
|
+
self.history.apply_conversation_cache_updates(cache_updates)
|
|
157
|
+
logger.debug(
|
|
158
|
+
f"Applied conversation cache_control to positions {cache_updates['add']} ({applied_count} blocks)"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"Failed to apply conversation cache_control to positions {cache_updates['add']}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return applied_count
|
|
166
|
+
|
|
167
|
+
def _is_structured_output_request(self, tool_uses: List[Any]) -> bool:
|
|
168
|
+
"""
|
|
169
|
+
Check if the tool uses contain a structured output request.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
tool_uses: List of tool use blocks from the response
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if any tool is the structured output tool
|
|
176
|
+
"""
|
|
177
|
+
return any(tool.name == STRUCTURED_OUTPUT_TOOL_NAME for tool in tool_uses)
|
|
178
|
+
|
|
179
|
+
def _build_tool_calls_dict(self, tool_uses: List[ToolUseBlock]) -> dict[str, CallToolRequest]:
|
|
180
|
+
"""
|
|
181
|
+
Convert Anthropic tool use blocks into our CallToolRequest.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
tool_uses: List of tool use blocks from Anthropic response
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dictionary mapping tool_use_id to CallToolRequest objects
|
|
188
|
+
"""
|
|
189
|
+
tool_calls = {}
|
|
190
|
+
for tool_use in tool_uses:
|
|
191
|
+
tool_call = CallToolRequest(
|
|
192
|
+
method="tools/call",
|
|
193
|
+
params=CallToolRequestParams(
|
|
194
|
+
name=tool_use.name,
|
|
195
|
+
arguments=cast("dict[str, Any] | None", tool_use.input),
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
tool_calls[tool_use.id] = tool_call
|
|
199
|
+
return tool_calls
|
|
200
|
+
|
|
201
|
+
async def _handle_structured_output_response(
|
|
202
|
+
self,
|
|
203
|
+
tool_use_block: ToolUseBlock,
|
|
204
|
+
structured_model: Type[ModelT],
|
|
205
|
+
messages: List[MessageParam],
|
|
206
|
+
) -> Tuple[LlmStopReason, List[ContentBlock]]:
|
|
207
|
+
"""
|
|
208
|
+
Handle a structured output tool response from Anthropic.
|
|
209
|
+
|
|
210
|
+
This handles the special case where Anthropic's model was forced to use
|
|
211
|
+
a 'return_structured_output' tool via tool_choice. The tool input contains
|
|
212
|
+
the JSON data we want, so we extract it and format it for display.
|
|
213
|
+
|
|
214
|
+
Even though we don't call an external tool, we must create a CallToolResult
|
|
215
|
+
to satisfy Anthropic's API requirement that every tool_use has a corresponding
|
|
216
|
+
tool_result in the next message.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
tool_use_block: The tool use block containing structured output
|
|
220
|
+
structured_model: The model class for structured output
|
|
221
|
+
messages: The message list to append tool results to
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Tuple of (stop_reason, response_content_blocks)
|
|
225
|
+
"""
|
|
226
|
+
tool_args = tool_use_block.input
|
|
227
|
+
tool_use_id = tool_use_block.id
|
|
228
|
+
|
|
229
|
+
# Create the content for responses
|
|
230
|
+
structured_content = TextContent(type="text", text=json.dumps(tool_args))
|
|
231
|
+
|
|
232
|
+
tool_result = CallToolResult(isError=False, content=[structured_content])
|
|
233
|
+
messages.append(
|
|
234
|
+
AnthropicConverter.create_tool_results_message([(tool_use_id, tool_result)])
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
logger.debug("Structured output received, treating as END_TURN")
|
|
238
|
+
|
|
239
|
+
return LlmStopReason.END_TURN, [structured_content]
|
|
240
|
+
|
|
241
|
+
async def _process_stream(self, stream: AsyncMessageStream, model: str) -> Message:
|
|
242
|
+
"""Process the streaming response and display real-time token usage."""
|
|
243
|
+
# Track estimated output tokens by counting text chunks
|
|
244
|
+
estimated_tokens = 0
|
|
245
|
+
|
|
246
|
+
# Process the raw event stream to get token counts
|
|
247
|
+
async for event in stream:
|
|
248
|
+
# Count tokens in real-time from content_block_delta events
|
|
249
|
+
if (
|
|
250
|
+
event.type == "content_block_delta"
|
|
251
|
+
and hasattr(event, "delta")
|
|
252
|
+
and event.delta.type == "text_delta"
|
|
253
|
+
):
|
|
254
|
+
# Use base class method for token estimation and progress emission
|
|
255
|
+
estimated_tokens = self._update_streaming_progress(
|
|
256
|
+
event.delta.text, model, estimated_tokens
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Also check for final message_delta events with actual usage info
|
|
260
|
+
elif (
|
|
261
|
+
event.type == "message_delta"
|
|
262
|
+
and hasattr(event, "usage")
|
|
263
|
+
and event.usage.output_tokens
|
|
264
|
+
):
|
|
265
|
+
actual_tokens = event.usage.output_tokens
|
|
266
|
+
# Emit final progress with actual token count
|
|
267
|
+
token_str = str(actual_tokens).rjust(5)
|
|
268
|
+
data = {
|
|
269
|
+
"progress_action": ProgressAction.STREAMING,
|
|
270
|
+
"model": model,
|
|
271
|
+
"agent_name": self.name,
|
|
272
|
+
"chat_turn": self.chat_turn(),
|
|
273
|
+
"details": token_str.strip(),
|
|
274
|
+
}
|
|
275
|
+
logger.info("Streaming progress", data=data)
|
|
276
|
+
|
|
277
|
+
# Get the final message with complete usage data
|
|
278
|
+
message = await stream.get_final_message()
|
|
279
|
+
|
|
280
|
+
# Log final usage information
|
|
281
|
+
if hasattr(message, "usage") and message.usage:
|
|
282
|
+
logger.info(
|
|
283
|
+
f"Streaming complete - Model: {model}, Input tokens: {message.usage.input_tokens}, Output tokens: {message.usage.output_tokens}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return message
|
|
287
|
+
|
|
288
|
+
async def _anthropic_completion(
|
|
289
|
+
self,
|
|
290
|
+
message_param,
|
|
291
|
+
request_params: RequestParams | None = None,
|
|
292
|
+
structured_model: Type[ModelT] | None = None,
|
|
293
|
+
tools: List[Tool] | None = None,
|
|
294
|
+
) -> PromptMessageExtended:
|
|
295
|
+
"""
|
|
296
|
+
Process a query using an LLM and available tools.
|
|
297
|
+
Override this method to use a different LLM.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
api_key = self._api_key()
|
|
301
|
+
base_url = self._base_url()
|
|
302
|
+
if base_url and base_url.endswith("/v1"):
|
|
303
|
+
base_url = base_url.rstrip("/v1")
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
anthropic = AsyncAnthropic(api_key=api_key, base_url=base_url)
|
|
307
|
+
messages: List[MessageParam] = []
|
|
308
|
+
params = self.get_request_params(request_params)
|
|
309
|
+
except AuthenticationError as e:
|
|
310
|
+
raise ProviderKeyError(
|
|
311
|
+
"Invalid Anthropic API key",
|
|
312
|
+
"The configured Anthropic API key was rejected.\nPlease check that your API key is valid and not expired.",
|
|
313
|
+
) from e
|
|
314
|
+
|
|
315
|
+
# Always include prompt messages, but only include conversation history
|
|
316
|
+
messages.extend(self.history.get(include_completion_history=params.use_history))
|
|
317
|
+
messages.append(message_param) # message_param is the current user turn
|
|
318
|
+
|
|
319
|
+
# Get cache mode configuration
|
|
320
|
+
cache_mode = self._get_cache_mode()
|
|
321
|
+
logger.debug(f"Anthropic cache_mode: {cache_mode}")
|
|
322
|
+
|
|
323
|
+
available_tools = await self._prepare_tools(structured_model, tools)
|
|
324
|
+
|
|
325
|
+
response_content_blocks: List[ContentBlock] = []
|
|
326
|
+
tool_calls: dict[str, CallToolRequest] | None = None
|
|
327
|
+
model = self.default_request_params.model or DEFAULT_ANTHROPIC_MODEL
|
|
328
|
+
|
|
329
|
+
# Create base arguments dictionary
|
|
330
|
+
base_args = {
|
|
331
|
+
"model": model,
|
|
332
|
+
"messages": messages,
|
|
333
|
+
"stop_sequences": params.stopSequences,
|
|
334
|
+
"tools": available_tools,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if self.instruction or params.systemPrompt:
|
|
338
|
+
base_args["system"] = self.instruction or params.systemPrompt
|
|
339
|
+
|
|
340
|
+
if structured_model:
|
|
341
|
+
base_args["tool_choice"] = {"type": "tool", "name": STRUCTURED_OUTPUT_TOOL_NAME}
|
|
342
|
+
|
|
343
|
+
if params.maxTokens is not None:
|
|
344
|
+
base_args["max_tokens"] = params.maxTokens
|
|
345
|
+
|
|
346
|
+
self._log_chat_progress(self.chat_turn(), model=model)
|
|
347
|
+
# Use the base class method to prepare all arguments with Anthropic-specific exclusions
|
|
348
|
+
# Do this BEFORE applying cache control so metadata doesn't override cached fields
|
|
349
|
+
arguments = self.prepare_provider_arguments(
|
|
350
|
+
base_args, params, self.ANTHROPIC_EXCLUDE_FIELDS
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Apply cache control to system prompt AFTER merging arguments
|
|
354
|
+
self._apply_system_cache(arguments, cache_mode)
|
|
355
|
+
|
|
356
|
+
# Apply conversation caching
|
|
357
|
+
applied_count = await self._apply_conversation_cache(messages, cache_mode)
|
|
358
|
+
|
|
359
|
+
# Verify we don't exceed Anthropic's 4 cache block limit
|
|
360
|
+
if applied_count > 0:
|
|
361
|
+
total_cache_blocks = applied_count
|
|
362
|
+
if cache_mode != "off" and arguments["system"]:
|
|
363
|
+
total_cache_blocks += 1 # tools+system cache block
|
|
364
|
+
if total_cache_blocks > 4:
|
|
365
|
+
logger.warning(
|
|
366
|
+
f"Total cache blocks ({total_cache_blocks}) exceeds Anthropic limit of 4"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
logger.debug(f"{arguments}")
|
|
370
|
+
# Use streaming API with helper
|
|
371
|
+
async with anthropic.messages.stream(**arguments) as stream:
|
|
372
|
+
# Process the stream
|
|
373
|
+
response = await self._process_stream(stream, model)
|
|
374
|
+
|
|
375
|
+
# Track usage if response is valid and has usage data
|
|
376
|
+
if (
|
|
377
|
+
hasattr(response, "usage")
|
|
378
|
+
and response.usage
|
|
379
|
+
and not isinstance(response, BaseException)
|
|
380
|
+
):
|
|
381
|
+
try:
|
|
382
|
+
turn_usage = TurnUsage.from_anthropic(
|
|
383
|
+
response.usage, model or DEFAULT_ANTHROPIC_MODEL
|
|
384
|
+
)
|
|
385
|
+
self._finalize_turn_usage(turn_usage)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.warning(f"Failed to track usage: {e}")
|
|
388
|
+
|
|
389
|
+
if isinstance(response, AuthenticationError):
|
|
390
|
+
raise ProviderKeyError(
|
|
391
|
+
"Invalid Anthropic API key",
|
|
392
|
+
"The configured Anthropic API key was rejected.\nPlease check that your API key is valid and not expired.",
|
|
393
|
+
) from response
|
|
394
|
+
elif isinstance(response, BaseException):
|
|
395
|
+
error_details = str(response)
|
|
396
|
+
logger.error(f"Error: {error_details}", data=BaseException)
|
|
397
|
+
|
|
398
|
+
# Try to extract more useful information for API errors
|
|
399
|
+
if hasattr(response, "status_code") and hasattr(response, "response"):
|
|
400
|
+
try:
|
|
401
|
+
error_json = response.response.json()
|
|
402
|
+
error_details = f"Error code: {response.status_code} - {error_json}"
|
|
403
|
+
except: # noqa: E722
|
|
404
|
+
error_details = f"Error code: {response.status_code} - {str(response)}"
|
|
405
|
+
|
|
406
|
+
# Convert other errors to text response
|
|
407
|
+
error_message = f"Error during generation: {error_details}"
|
|
408
|
+
response = Message(
|
|
409
|
+
id="error",
|
|
410
|
+
model="error",
|
|
411
|
+
role="assistant",
|
|
412
|
+
type="message",
|
|
413
|
+
content=[TextBlock(type="text", text=error_message)],
|
|
414
|
+
stop_reason="end_turn",
|
|
415
|
+
usage=Usage(input_tokens=0, output_tokens=0),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
logger.debug(
|
|
419
|
+
f"{model} response:",
|
|
420
|
+
data=response,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
response_as_message = self.convert_message_to_message_param(response)
|
|
424
|
+
messages.append(response_as_message)
|
|
425
|
+
if response.content and response.content[0].type == "text":
|
|
426
|
+
response_content_blocks.append(TextContent(type="text", text=response.content[0].text))
|
|
427
|
+
|
|
428
|
+
stop_reason: LlmStopReason = LlmStopReason.END_TURN
|
|
429
|
+
|
|
430
|
+
match response.stop_reason:
|
|
431
|
+
case "stop_sequence":
|
|
432
|
+
stop_reason = LlmStopReason.STOP_SEQUENCE
|
|
433
|
+
case "max_tokens":
|
|
434
|
+
stop_reason = LlmStopReason.MAX_TOKENS
|
|
435
|
+
case "refusal":
|
|
436
|
+
stop_reason = LlmStopReason.SAFETY
|
|
437
|
+
case "pause":
|
|
438
|
+
stop_reason = LlmStopReason.PAUSE
|
|
439
|
+
case "tool_use":
|
|
440
|
+
stop_reason = LlmStopReason.TOOL_USE
|
|
441
|
+
tool_uses: list[ToolUseBlock] = [
|
|
442
|
+
c for c in response.content if c.type == "tool_use"
|
|
443
|
+
]
|
|
444
|
+
if structured_model and self._is_structured_output_request(tool_uses):
|
|
445
|
+
stop_reason, structured_blocks = await self._handle_structured_output_response(
|
|
446
|
+
tool_uses[0], structured_model, messages
|
|
447
|
+
)
|
|
448
|
+
response_content_blocks.extend(structured_blocks)
|
|
449
|
+
else:
|
|
450
|
+
tool_calls = self._build_tool_calls_dict(tool_uses)
|
|
451
|
+
|
|
452
|
+
# Only save the new conversation messages to history if use_history is true
|
|
453
|
+
# Keep the prompt messages separate
|
|
454
|
+
if params.use_history:
|
|
455
|
+
# Get current prompt messages
|
|
456
|
+
prompt_messages = self.history.get(include_completion_history=False)
|
|
457
|
+
new_messages = messages[len(prompt_messages) :]
|
|
458
|
+
self.history.set(new_messages)
|
|
459
|
+
|
|
460
|
+
self._log_chat_finished(model=model)
|
|
461
|
+
|
|
462
|
+
return Prompt.assistant(
|
|
463
|
+
*response_content_blocks, stop_reason=stop_reason, tool_calls=tool_calls
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
async def _apply_prompt_provider_specific(
|
|
467
|
+
self,
|
|
468
|
+
multipart_messages: List["PromptMessageExtended"],
|
|
469
|
+
request_params: RequestParams | None = None,
|
|
470
|
+
tools: List[Tool] | None = None,
|
|
471
|
+
is_template: bool = False,
|
|
472
|
+
) -> PromptMessageExtended:
|
|
473
|
+
# Check the last message role
|
|
474
|
+
last_message = multipart_messages[-1]
|
|
475
|
+
|
|
476
|
+
# Add all previous messages to history (or all messages if last is from assistant)
|
|
477
|
+
messages_to_add = (
|
|
478
|
+
multipart_messages[:-1] if last_message.role == "user" else multipart_messages
|
|
479
|
+
)
|
|
480
|
+
converted = []
|
|
481
|
+
|
|
482
|
+
# Get cache mode configuration
|
|
483
|
+
cache_mode = self._get_cache_mode()
|
|
484
|
+
|
|
485
|
+
for msg in messages_to_add:
|
|
486
|
+
anthropic_msg = AnthropicConverter.convert_to_anthropic(msg)
|
|
487
|
+
|
|
488
|
+
# Apply caching to template messages if cache_mode is "prompt" or "auto"
|
|
489
|
+
if is_template and cache_mode in ["prompt", "auto"] and anthropic_msg.get("content"):
|
|
490
|
+
content_list = anthropic_msg["content"]
|
|
491
|
+
if isinstance(content_list, list) and content_list:
|
|
492
|
+
# Apply cache control to the last content block
|
|
493
|
+
last_block = content_list[-1]
|
|
494
|
+
if isinstance(last_block, dict):
|
|
495
|
+
last_block["cache_control"] = {"type": "ephemeral"}
|
|
496
|
+
logger.debug(
|
|
497
|
+
f"Applied cache_control to template message with role {anthropic_msg.get('role')}"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
converted.append(anthropic_msg)
|
|
501
|
+
|
|
502
|
+
self.history.extend(converted, is_prompt=is_template)
|
|
503
|
+
|
|
504
|
+
if last_message.role == "user":
|
|
505
|
+
logger.debug("Last message in prompt is from user, generating assistant response")
|
|
506
|
+
message_param = AnthropicConverter.convert_to_anthropic(last_message)
|
|
507
|
+
return await self._anthropic_completion(message_param, request_params, tools=tools)
|
|
508
|
+
else:
|
|
509
|
+
# For assistant messages: Return the last message content as text
|
|
510
|
+
logger.debug("Last message in prompt is from assistant, returning it directly")
|
|
511
|
+
return last_message
|
|
512
|
+
|
|
513
|
+
async def _apply_prompt_provider_specific_structured(
|
|
514
|
+
self,
|
|
515
|
+
multipart_messages: List[PromptMessageExtended],
|
|
516
|
+
model: Type[ModelT],
|
|
517
|
+
request_params: RequestParams | None = None,
|
|
518
|
+
) -> Tuple[ModelT | None, PromptMessageExtended]: # noqa: F821
|
|
519
|
+
request_params = self.get_request_params(request_params)
|
|
520
|
+
|
|
521
|
+
# Check the last message role
|
|
522
|
+
last_message = multipart_messages[-1]
|
|
523
|
+
|
|
524
|
+
# Add all previous messages to history (or all messages if last is from assistant)
|
|
525
|
+
messages_to_add = (
|
|
526
|
+
multipart_messages[:-1] if last_message.role == "user" else multipart_messages
|
|
527
|
+
)
|
|
528
|
+
converted = []
|
|
529
|
+
|
|
530
|
+
for msg in messages_to_add:
|
|
531
|
+
anthropic_msg = AnthropicConverter.convert_to_anthropic(msg)
|
|
532
|
+
converted.append(anthropic_msg)
|
|
533
|
+
|
|
534
|
+
self.history.extend(converted, is_prompt=False)
|
|
535
|
+
|
|
536
|
+
if last_message.role == "user":
|
|
537
|
+
logger.debug("Last message in prompt is from user, generating structured response")
|
|
538
|
+
message_param = AnthropicConverter.convert_to_anthropic(last_message)
|
|
539
|
+
|
|
540
|
+
# Call _anthropic_completion with the structured model
|
|
541
|
+
result: PromptMessageExtended = await self._anthropic_completion(
|
|
542
|
+
message_param, request_params, structured_model=model
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
for content in result.content:
|
|
546
|
+
if content.type == "text":
|
|
547
|
+
try:
|
|
548
|
+
data = json.loads(content.text)
|
|
549
|
+
parsed_model = model(**data)
|
|
550
|
+
return parsed_model, result
|
|
551
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
552
|
+
logger.error(f"Failed to parse structured output: {e}")
|
|
553
|
+
return None, result
|
|
554
|
+
|
|
555
|
+
# If no valid response found
|
|
556
|
+
return None, Prompt.assistant()
|
|
557
|
+
else:
|
|
558
|
+
# For assistant messages: Return the last message content
|
|
559
|
+
logger.debug("Last message in prompt is from assistant, returning it directly")
|
|
560
|
+
return None, last_message
|
|
561
|
+
|
|
562
|
+
@classmethod
|
|
563
|
+
def convert_message_to_message_param(cls, message: Message, **kwargs) -> MessageParam:
|
|
564
|
+
"""Convert a response object to an input parameter object to allow LLM calls to be chained."""
|
|
565
|
+
content = []
|
|
566
|
+
|
|
567
|
+
for content_block in message.content:
|
|
568
|
+
if content_block.type == "text":
|
|
569
|
+
content.append(TextBlock(type="text", text=content_block.text))
|
|
570
|
+
elif content_block.type == "tool_use":
|
|
571
|
+
content.append(
|
|
572
|
+
ToolUseBlockParam(
|
|
573
|
+
type="tool_use",
|
|
574
|
+
name=content_block.name,
|
|
575
|
+
input=content_block.input,
|
|
576
|
+
id=content_block.id,
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
return MessageParam(role="assistant", content=content, **kwargs)
|
|
581
|
+
|
|
582
|
+
def _show_usage(self, raw_usage: Usage, turn_usage: TurnUsage) -> None:
|
|
583
|
+
"""This is a debug routine, leaving in for convenience"""
|
|
584
|
+
# Print raw usage for debugging
|
|
585
|
+
print(f"\n=== USAGE DEBUG ({turn_usage.model}) ===")
|
|
586
|
+
print(f"Raw usage: {raw_usage}")
|
|
587
|
+
print(
|
|
588
|
+
f"Turn usage: input={turn_usage.input_tokens}, output={turn_usage.output_tokens}, current_context={turn_usage.current_context_tokens}"
|
|
589
|
+
)
|
|
590
|
+
print(
|
|
591
|
+
f"Cache: read={turn_usage.cache_usage.cache_read_tokens}, write={turn_usage.cache_usage.cache_write_tokens}"
|
|
592
|
+
)
|
|
593
|
+
print(f"Effective input: {turn_usage.effective_input_tokens}")
|
|
594
|
+
print(
|
|
595
|
+
f"Accumulator: total_turns={self.usage_accumulator.turn_count}, cumulative_billing={self.usage_accumulator.cumulative_billing_tokens}, current_context={self.usage_accumulator.current_context_tokens}"
|
|
596
|
+
)
|
|
597
|
+
if self.usage_accumulator.context_usage_percentage:
|
|
598
|
+
print(
|
|
599
|
+
f"Context usage: {self.usage_accumulator.context_usage_percentage:.1f}% of {self.usage_accumulator.context_window_size}"
|
|
600
|
+
)
|
|
601
|
+
if self.usage_accumulator.cache_hit_rate:
|
|
602
|
+
print(f"Cache hit rate: {self.usage_accumulator.cache_hit_rate:.1f}%")
|
|
603
|
+
print("===========================\n")
|