fast-agent-mcp 0.2.58__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (234) hide show
  1. fast_agent/__init__.py +127 -0
  2. fast_agent/agents/__init__.py +36 -0
  3. {mcp_agent/core → fast_agent/agents}/agent_types.py +2 -1
  4. fast_agent/agents/llm_agent.py +217 -0
  5. fast_agent/agents/llm_decorator.py +486 -0
  6. mcp_agent/agents/base_agent.py → fast_agent/agents/mcp_agent.py +377 -385
  7. fast_agent/agents/tool_agent.py +168 -0
  8. {mcp_agent → fast_agent}/agents/workflow/chain_agent.py +43 -33
  9. {mcp_agent → fast_agent}/agents/workflow/evaluator_optimizer.py +31 -35
  10. {mcp_agent → fast_agent}/agents/workflow/iterative_planner.py +56 -47
  11. {mcp_agent → fast_agent}/agents/workflow/orchestrator_models.py +4 -4
  12. {mcp_agent → fast_agent}/agents/workflow/parallel_agent.py +34 -41
  13. {mcp_agent → fast_agent}/agents/workflow/router_agent.py +54 -39
  14. {mcp_agent → fast_agent}/cli/__main__.py +5 -3
  15. {mcp_agent → fast_agent}/cli/commands/check_config.py +95 -66
  16. {mcp_agent → fast_agent}/cli/commands/go.py +20 -11
  17. {mcp_agent → fast_agent}/cli/commands/quickstart.py +4 -4
  18. {mcp_agent → fast_agent}/cli/commands/server_helpers.py +1 -1
  19. {mcp_agent → fast_agent}/cli/commands/setup.py +75 -134
  20. {mcp_agent → fast_agent}/cli/commands/url_parser.py +9 -8
  21. {mcp_agent → fast_agent}/cli/main.py +36 -16
  22. {mcp_agent → fast_agent}/cli/terminal.py +2 -2
  23. {mcp_agent → fast_agent}/config.py +10 -2
  24. fast_agent/constants.py +8 -0
  25. {mcp_agent → fast_agent}/context.py +24 -19
  26. {mcp_agent → fast_agent}/context_dependent.py +9 -5
  27. fast_agent/core/__init__.py +52 -0
  28. {mcp_agent → fast_agent}/core/agent_app.py +39 -36
  29. fast_agent/core/core_app.py +135 -0
  30. {mcp_agent → fast_agent}/core/direct_decorators.py +12 -26
  31. {mcp_agent → fast_agent}/core/direct_factory.py +95 -73
  32. {mcp_agent → fast_agent/core}/executor/executor.py +4 -5
  33. {mcp_agent → fast_agent}/core/fastagent.py +32 -32
  34. fast_agent/core/logging/__init__.py +5 -0
  35. {mcp_agent → fast_agent/core}/logging/events.py +3 -3
  36. {mcp_agent → fast_agent/core}/logging/json_serializer.py +1 -1
  37. {mcp_agent → fast_agent/core}/logging/listeners.py +85 -7
  38. {mcp_agent → fast_agent/core}/logging/logger.py +7 -7
  39. {mcp_agent → fast_agent/core}/logging/transport.py +10 -11
  40. fast_agent/core/prompt.py +9 -0
  41. {mcp_agent → fast_agent}/core/validation.py +4 -4
  42. fast_agent/event_progress.py +61 -0
  43. fast_agent/history/history_exporter.py +44 -0
  44. {mcp_agent → fast_agent}/human_input/__init__.py +9 -12
  45. {mcp_agent → fast_agent}/human_input/elicitation_handler.py +26 -8
  46. {mcp_agent → fast_agent}/human_input/elicitation_state.py +7 -7
  47. {mcp_agent → fast_agent}/human_input/simple_form.py +6 -4
  48. {mcp_agent → fast_agent}/human_input/types.py +1 -18
  49. fast_agent/interfaces.py +228 -0
  50. fast_agent/llm/__init__.py +9 -0
  51. mcp_agent/llm/augmented_llm.py → fast_agent/llm/fastagent_llm.py +127 -218
  52. fast_agent/llm/internal/passthrough.py +137 -0
  53. mcp_agent/llm/augmented_llm_playback.py → fast_agent/llm/internal/playback.py +29 -25
  54. mcp_agent/llm/augmented_llm_silent.py → fast_agent/llm/internal/silent.py +10 -17
  55. fast_agent/llm/internal/slow.py +38 -0
  56. {mcp_agent → fast_agent}/llm/memory.py +40 -30
  57. {mcp_agent → fast_agent}/llm/model_database.py +35 -2
  58. {mcp_agent → fast_agent}/llm/model_factory.py +103 -77
  59. fast_agent/llm/model_info.py +126 -0
  60. {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/anthropic_utils.py +7 -7
  61. fast_agent/llm/provider/anthropic/llm_anthropic.py +603 -0
  62. {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/multipart_converter_anthropic.py +79 -86
  63. {mcp_agent/llm/providers → fast_agent/llm/provider/bedrock}/bedrock_utils.py +3 -1
  64. mcp_agent/llm/providers/augmented_llm_bedrock.py → fast_agent/llm/provider/bedrock/llm_bedrock.py +833 -717
  65. {mcp_agent/llm/providers → fast_agent/llm/provider/google}/google_converter.py +66 -14
  66. fast_agent/llm/provider/google/llm_google_native.py +431 -0
  67. mcp_agent/llm/providers/augmented_llm_aliyun.py → fast_agent/llm/provider/openai/llm_aliyun.py +6 -7
  68. mcp_agent/llm/providers/augmented_llm_azure.py → fast_agent/llm/provider/openai/llm_azure.py +4 -4
  69. mcp_agent/llm/providers/augmented_llm_deepseek.py → fast_agent/llm/provider/openai/llm_deepseek.py +10 -11
  70. mcp_agent/llm/providers/augmented_llm_generic.py → fast_agent/llm/provider/openai/llm_generic.py +4 -4
  71. mcp_agent/llm/providers/augmented_llm_google_oai.py → fast_agent/llm/provider/openai/llm_google_oai.py +4 -4
  72. mcp_agent/llm/providers/augmented_llm_groq.py → fast_agent/llm/provider/openai/llm_groq.py +14 -16
  73. mcp_agent/llm/providers/augmented_llm_openai.py → fast_agent/llm/provider/openai/llm_openai.py +133 -207
  74. mcp_agent/llm/providers/augmented_llm_openrouter.py → fast_agent/llm/provider/openai/llm_openrouter.py +6 -6
  75. mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py → fast_agent/llm/provider/openai/llm_tensorzero_openai.py +17 -16
  76. mcp_agent/llm/providers/augmented_llm_xai.py → fast_agent/llm/provider/openai/llm_xai.py +6 -6
  77. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/multipart_converter_openai.py +125 -63
  78. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_multipart.py +12 -12
  79. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_utils.py +18 -16
  80. {mcp_agent → fast_agent}/llm/provider_key_manager.py +2 -2
  81. {mcp_agent → fast_agent}/llm/provider_types.py +2 -0
  82. {mcp_agent → fast_agent}/llm/sampling_converter.py +15 -12
  83. {mcp_agent → fast_agent}/llm/usage_tracking.py +23 -5
  84. fast_agent/mcp/__init__.py +54 -0
  85. {mcp_agent → fast_agent}/mcp/elicitation_factory.py +3 -3
  86. {mcp_agent → fast_agent}/mcp/elicitation_handlers.py +19 -10
  87. {mcp_agent → fast_agent}/mcp/gen_client.py +3 -3
  88. fast_agent/mcp/helpers/__init__.py +36 -0
  89. fast_agent/mcp/helpers/content_helpers.py +183 -0
  90. {mcp_agent → fast_agent}/mcp/helpers/server_config_helpers.py +8 -8
  91. {mcp_agent → fast_agent}/mcp/hf_auth.py +25 -23
  92. fast_agent/mcp/interfaces.py +93 -0
  93. {mcp_agent → fast_agent}/mcp/logger_textio.py +4 -4
  94. {mcp_agent → fast_agent}/mcp/mcp_agent_client_session.py +49 -44
  95. {mcp_agent → fast_agent}/mcp/mcp_aggregator.py +66 -115
  96. {mcp_agent → fast_agent}/mcp/mcp_connection_manager.py +16 -23
  97. {mcp_agent/core → fast_agent/mcp}/mcp_content.py +23 -15
  98. {mcp_agent → fast_agent}/mcp/mime_utils.py +39 -0
  99. fast_agent/mcp/prompt.py +159 -0
  100. mcp_agent/mcp/prompt_message_multipart.py → fast_agent/mcp/prompt_message_extended.py +27 -20
  101. {mcp_agent → fast_agent}/mcp/prompt_render.py +21 -19
  102. {mcp_agent → fast_agent}/mcp/prompt_serialization.py +46 -46
  103. fast_agent/mcp/prompts/__main__.py +7 -0
  104. {mcp_agent → fast_agent}/mcp/prompts/prompt_helpers.py +31 -30
  105. {mcp_agent → fast_agent}/mcp/prompts/prompt_load.py +8 -8
  106. {mcp_agent → fast_agent}/mcp/prompts/prompt_server.py +11 -19
  107. {mcp_agent → fast_agent}/mcp/prompts/prompt_template.py +18 -18
  108. {mcp_agent → fast_agent}/mcp/resource_utils.py +1 -1
  109. {mcp_agent → fast_agent}/mcp/sampling.py +31 -26
  110. {mcp_agent/mcp_server → fast_agent/mcp/server}/__init__.py +1 -1
  111. {mcp_agent/mcp_server → fast_agent/mcp/server}/agent_server.py +5 -6
  112. fast_agent/mcp/ui_agent.py +48 -0
  113. fast_agent/mcp/ui_mixin.py +209 -0
  114. fast_agent/mcp_server_registry.py +90 -0
  115. {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis-campaign.py +5 -4
  116. {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis.py +1 -1
  117. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
  118. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
  119. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
  120. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
  121. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
  122. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  123. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
  124. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
  125. {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
  126. {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
  127. {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
  128. {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
  129. {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
  130. {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
  131. {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
  132. {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
  133. {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
  134. {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
  135. fast_agent/resources/setup/.gitignore +24 -0
  136. fast_agent/resources/setup/agent.py +18 -0
  137. fast_agent/resources/setup/fastagent.config.yaml +44 -0
  138. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  139. fast_agent/resources/setup/pyproject.toml.tmpl +17 -0
  140. fast_agent/tools/elicitation.py +369 -0
  141. fast_agent/types/__init__.py +32 -0
  142. fast_agent/types/llm_stop_reason.py +77 -0
  143. fast_agent/ui/__init__.py +38 -0
  144. fast_agent/ui/console_display.py +1005 -0
  145. {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +17 -12
  146. mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
  147. {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
  148. {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
  149. fast_agent/ui/mcp_ui_utils.py +224 -0
  150. {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
  151. {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
  152. {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
  153. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.1.dist-info}/METADATA +7 -7
  154. fast_agent_mcp-0.3.1.dist-info/RECORD +203 -0
  155. fast_agent_mcp-0.3.1.dist-info/entry_points.txt +5 -0
  156. fast_agent_mcp-0.2.58.dist-info/RECORD +0 -193
  157. fast_agent_mcp-0.2.58.dist-info/entry_points.txt +0 -6
  158. mcp_agent/__init__.py +0 -114
  159. mcp_agent/agents/agent.py +0 -92
  160. mcp_agent/agents/workflow/__init__.py +0 -1
  161. mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
  162. mcp_agent/app.py +0 -175
  163. mcp_agent/core/__init__.py +0 -26
  164. mcp_agent/core/prompt.py +0 -191
  165. mcp_agent/event_progress.py +0 -134
  166. mcp_agent/human_input/handler.py +0 -81
  167. mcp_agent/llm/__init__.py +0 -2
  168. mcp_agent/llm/augmented_llm_passthrough.py +0 -232
  169. mcp_agent/llm/augmented_llm_slow.py +0 -53
  170. mcp_agent/llm/providers/__init__.py +0 -8
  171. mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -718
  172. mcp_agent/llm/providers/augmented_llm_google_native.py +0 -496
  173. mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
  174. mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
  175. mcp_agent/llm/sampling_format_converter.py +0 -37
  176. mcp_agent/logging/__init__.py +0 -0
  177. mcp_agent/mcp/__init__.py +0 -50
  178. mcp_agent/mcp/helpers/__init__.py +0 -25
  179. mcp_agent/mcp/helpers/content_helpers.py +0 -187
  180. mcp_agent/mcp/interfaces.py +0 -266
  181. mcp_agent/mcp/prompts/__init__.py +0 -0
  182. mcp_agent/mcp/prompts/__main__.py +0 -10
  183. mcp_agent/mcp_server_registry.py +0 -343
  184. mcp_agent/tools/tool_definition.py +0 -14
  185. mcp_agent/ui/console_display.py +0 -790
  186. mcp_agent/ui/console_display_legacy.py +0 -401
  187. {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
  188. {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
  189. {mcp_agent → fast_agent}/cli/constants.py +0 -0
  190. {mcp_agent → fast_agent}/core/error_handling.py +0 -0
  191. {mcp_agent → fast_agent}/core/exceptions.py +0 -0
  192. {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
  193. {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
  194. {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
  195. {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
  196. {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
  197. {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
  198. {mcp_agent → fast_agent}/mcp/common.py +0 -0
  199. {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
  200. {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
  201. {mcp_agent → fast_agent}/py.typed +0 -0
  202. {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
  203. {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
  204. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
  205. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +0 -0
  206. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
  207. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
  208. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
  209. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
  210. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
  211. {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
  212. {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
  213. {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
  214. {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
  215. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  216. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
  217. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  218. {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
  219. {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
  220. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
  221. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
  222. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
  223. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
  224. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
  225. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
  226. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
  227. {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
  228. {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
  229. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
  230. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
  231. {mcp_agent → fast_agent/ui}/console.py +0 -0
  232. {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
  233. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.1.dist-info}/WHEEL +0 -0
  234. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.1.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")