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
@@ -1,718 +0,0 @@
1
- import json
2
- from typing import TYPE_CHECKING, Any, List, Tuple, Type
3
-
4
- from mcp.types import TextContent
5
-
6
- from mcp_agent.core.prompt import Prompt
7
- from mcp_agent.event_progress import ProgressAction
8
- from mcp_agent.llm.provider_types import Provider
9
- from mcp_agent.llm.providers.multipart_converter_anthropic import (
10
- AnthropicConverter,
11
- )
12
- from mcp_agent.llm.providers.sampling_converter_anthropic import (
13
- AnthropicSamplingConverter,
14
- )
15
- from mcp_agent.llm.usage_tracking import TurnUsage
16
- from mcp_agent.mcp.interfaces import ModelT
17
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
18
-
19
- if TYPE_CHECKING:
20
- from mcp import ListToolsResult
21
-
22
-
23
- from anthropic import AsyncAnthropic, AuthenticationError
24
- from anthropic.lib.streaming import AsyncMessageStream
25
- from anthropic.types import (
26
- Message,
27
- MessageParam,
28
- TextBlock,
29
- TextBlockParam,
30
- ToolParam,
31
- ToolUseBlockParam,
32
- Usage,
33
- )
34
- from mcp.types import (
35
- CallToolRequest,
36
- CallToolRequestParams,
37
- CallToolResult,
38
- ContentBlock,
39
- )
40
- from rich.text import Text
41
-
42
- from mcp_agent.core.exceptions import ProviderKeyError
43
- from mcp_agent.llm.augmented_llm import (
44
- AugmentedLLM,
45
- RequestParams,
46
- )
47
- from mcp_agent.logging.logger import get_logger
48
-
49
- DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-0"
50
-
51
-
52
- class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
53
- """
54
- The basic building block of agentic systems is an LLM enhanced with augmentations
55
- such as retrieval, tools, and memory provided from a collection of MCP servers.
56
- Our current models can actively use these capabilities—generating their own search queries,
57
- selecting appropriate tools, and determining what information to retain.
58
- """
59
-
60
- # Anthropic-specific parameter exclusions
61
- ANTHROPIC_EXCLUDE_FIELDS = {
62
- AugmentedLLM.PARAM_MESSAGES,
63
- AugmentedLLM.PARAM_MODEL,
64
- AugmentedLLM.PARAM_SYSTEM_PROMPT,
65
- AugmentedLLM.PARAM_STOP_SEQUENCES,
66
- AugmentedLLM.PARAM_MAX_TOKENS,
67
- AugmentedLLM.PARAM_METADATA,
68
- AugmentedLLM.PARAM_USE_HISTORY,
69
- AugmentedLLM.PARAM_MAX_ITERATIONS,
70
- AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
71
- AugmentedLLM.PARAM_TEMPLATE_VARS,
72
- AugmentedLLM.PARAM_MCP_METADATA,
73
- }
74
-
75
- def __init__(self, *args, **kwargs) -> None:
76
- # Initialize logger - keep it simple without name reference
77
- self.logger = get_logger(__name__)
78
-
79
- super().__init__(
80
- *args, provider=Provider.ANTHROPIC, type_converter=AnthropicSamplingConverter, **kwargs
81
- )
82
-
83
- def _initialize_default_params(self, kwargs: dict) -> RequestParams:
84
- """Initialize Anthropic-specific default parameters"""
85
- # Get base defaults from parent (includes ModelDatabase lookup)
86
- base_params = super()._initialize_default_params(kwargs)
87
-
88
- # Override with Anthropic-specific settings
89
- chosen_model = kwargs.get("model", DEFAULT_ANTHROPIC_MODEL)
90
- base_params.model = chosen_model
91
-
92
- return base_params
93
-
94
- def _base_url(self) -> str | None:
95
- assert self.context.config
96
- return self.context.config.anthropic.base_url if self.context.config.anthropic else None
97
-
98
- def _get_cache_mode(self) -> str:
99
- """Get the cache mode configuration."""
100
- cache_mode = "auto" # Default to auto
101
- if self.context.config and self.context.config.anthropic:
102
- cache_mode = self.context.config.anthropic.cache_mode
103
- return cache_mode
104
-
105
- async def _prepare_tools(self, structured_model: Type[ModelT] | None = None) -> List[ToolParam]:
106
- """Prepare tools based on whether we're in structured output mode."""
107
- if structured_model:
108
- # JSON mode - create a single tool for structured output
109
- return [
110
- ToolParam(
111
- name="return_structured_output",
112
- description="Return the response in the required JSON format",
113
- input_schema=structured_model.model_json_schema(),
114
- )
115
- ]
116
- else:
117
- # Regular mode - use tools from aggregator
118
- tool_list: ListToolsResult = await self.aggregator.list_tools()
119
- return [
120
- ToolParam(
121
- name=tool.name,
122
- description=tool.description or "",
123
- input_schema=tool.inputSchema,
124
- )
125
- for tool in tool_list.tools
126
- ]
127
-
128
- def _apply_system_cache(self, base_args: dict, cache_mode: str) -> None:
129
- """Apply cache control to system prompt if cache mode allows it."""
130
- if cache_mode != "off" and base_args["system"]:
131
- if isinstance(base_args["system"], str):
132
- base_args["system"] = [
133
- {
134
- "type": "text",
135
- "text": base_args["system"],
136
- "cache_control": {"type": "ephemeral"},
137
- }
138
- ]
139
- self.logger.debug(
140
- "Applied cache_control to system prompt (caches tools+system in one block)"
141
- )
142
- else:
143
- self.logger.debug(f"System prompt is not a string: {type(base_args['system'])}")
144
-
145
- async def _apply_conversation_cache(self, messages: List[MessageParam], cache_mode: str) -> int:
146
- """Apply conversation caching if in auto mode. Returns number of cache blocks applied."""
147
- applied_count = 0
148
- if cache_mode == "auto" and self.history.should_apply_conversation_cache():
149
- cache_updates = self.history.get_conversation_cache_updates()
150
-
151
- # Remove cache control from old positions
152
- if cache_updates["remove"]:
153
- self.history.remove_cache_control_from_messages(messages, cache_updates["remove"])
154
- self.logger.debug(
155
- f"Removed conversation cache_control from positions {cache_updates['remove']}"
156
- )
157
-
158
- # Add cache control to new positions
159
- if cache_updates["add"]:
160
- applied_count = self.history.add_cache_control_to_messages(
161
- messages, cache_updates["add"]
162
- )
163
- if applied_count > 0:
164
- self.history.apply_conversation_cache_updates(cache_updates)
165
- self.logger.debug(
166
- f"Applied conversation cache_control to positions {cache_updates['add']} ({applied_count} blocks)"
167
- )
168
- else:
169
- self.logger.debug(
170
- f"Failed to apply conversation cache_control to positions {cache_updates['add']}"
171
- )
172
-
173
- return applied_count
174
-
175
- async def _process_structured_output(
176
- self,
177
- content_block: Any,
178
- ) -> Tuple[str, CallToolResult, TextContent]:
179
- """
180
- Process a structured output tool call from Anthropic.
181
-
182
- This handles the special case where Anthropic's model was forced to use
183
- a 'return_structured_output' tool via tool_choice. The tool input contains
184
- the JSON data we want, so we extract it and format it for display.
185
-
186
- Even though we don't call an external tool, we must create a CallToolResult
187
- to satisfy Anthropic's API requirement that every tool_use has a corresponding
188
- tool_result in the next message.
189
-
190
- Returns:
191
- Tuple of (tool_use_id, tool_result, content_block) for the structured data
192
- """
193
- tool_args = content_block.input
194
- tool_use_id = content_block.id
195
-
196
- # Show the formatted JSON response to the user
197
- json_response = json.dumps(tool_args, indent=2)
198
- await self.show_assistant_message(json_response)
199
-
200
- # Create the content for responses
201
- structured_content = TextContent(type="text", text=json.dumps(tool_args))
202
-
203
- # Create a CallToolResult to satisfy Anthropic's API requirements
204
- # This represents the "result" of our structured output "tool"
205
- tool_result = CallToolResult(isError=False, content=[structured_content])
206
-
207
- return tool_use_id, tool_result, structured_content
208
-
209
- async def _process_regular_tool_call(
210
- self,
211
- content_block: Any,
212
- available_tools: List[ToolParam],
213
- is_first_tool: bool,
214
- message_text: str | Text,
215
- ) -> Tuple[str, CallToolResult]:
216
- """
217
- Process a regular MCP tool call.
218
-
219
- This handles actual tool execution via the MCP aggregator.
220
- """
221
- tool_name = content_block.name
222
- tool_args = content_block.input
223
- tool_use_id = content_block.id
224
-
225
- if is_first_tool:
226
- await self.show_assistant_message(message_text, tool_name)
227
-
228
- self.show_tool_call(available_tools, tool_name, tool_args)
229
- tool_call_request = CallToolRequest(
230
- method="tools/call",
231
- params=CallToolRequestParams(name=tool_name, arguments=tool_args),
232
- )
233
- result = await self.call_tool(request=tool_call_request, tool_call_id=tool_use_id)
234
- self.show_tool_result(result)
235
- return tool_use_id, result
236
-
237
- async def _process_tool_calls(
238
- self,
239
- tool_uses: List[Any],
240
- available_tools: List[ToolParam],
241
- message_text: str | Text,
242
- structured_model: Type[ModelT] | None = None,
243
- ) -> Tuple[List[Tuple[str, CallToolResult]], List[ContentBlock]]:
244
- """
245
- Process tool calls, handling both structured output and regular MCP tools.
246
-
247
- For structured output mode:
248
- - Extracts JSON data from the forced 'return_structured_output' tool
249
- - Does NOT create fake CallToolResults
250
- - Returns the JSON content directly
251
-
252
- For regular tools:
253
- - Calls actual MCP tools via the aggregator
254
- - Returns real CallToolResults
255
- """
256
- tool_results = []
257
- responses = []
258
-
259
- for tool_idx, content_block in enumerate(tool_uses):
260
- tool_name = content_block.name
261
- is_first_tool = tool_idx == 0
262
-
263
- if tool_name == "return_structured_output" and structured_model:
264
- # Structured output: extract JSON, don't call external tools
265
- (
266
- tool_use_id,
267
- tool_result,
268
- structured_content,
269
- ) = await self._process_structured_output(content_block)
270
- responses.append(structured_content)
271
- # Add to tool_results to satisfy Anthropic's API requirement for tool_result messages
272
- tool_results.append((tool_use_id, tool_result))
273
- else:
274
- # Regular tool: call external MCP tool
275
- tool_use_id, tool_result = await self._process_regular_tool_call(
276
- content_block, available_tools, is_first_tool, message_text
277
- )
278
- tool_results.append((tool_use_id, tool_result))
279
- responses.extend(tool_result.content)
280
-
281
- return tool_results, responses
282
-
283
- async def _process_stream(self, stream: AsyncMessageStream, model: str) -> Message:
284
- """Process the streaming response and display real-time token usage."""
285
- # Track estimated output tokens by counting text chunks
286
- estimated_tokens = 0
287
-
288
- # Process the raw event stream to get token counts
289
- async for event in stream:
290
- # Count tokens in real-time from content_block_delta events
291
- if (
292
- event.type == "content_block_delta"
293
- and hasattr(event, "delta")
294
- and event.delta.type == "text_delta"
295
- ):
296
- # Use base class method for token estimation and progress emission
297
- estimated_tokens = self._update_streaming_progress(
298
- event.delta.text, model, estimated_tokens
299
- )
300
-
301
- # Also check for final message_delta events with actual usage info
302
- elif (
303
- event.type == "message_delta"
304
- and hasattr(event, "usage")
305
- and event.usage.output_tokens
306
- ):
307
- actual_tokens = event.usage.output_tokens
308
- # Emit final progress with actual token count
309
- token_str = str(actual_tokens).rjust(5)
310
- data = {
311
- "progress_action": ProgressAction.STREAMING,
312
- "model": model,
313
- "agent_name": self.name,
314
- "chat_turn": self.chat_turn(),
315
- "details": token_str.strip(),
316
- }
317
- self.logger.info("Streaming progress", data=data)
318
-
319
- # Get the final message with complete usage data
320
- message = await stream.get_final_message()
321
-
322
- # Log final usage information
323
- if hasattr(message, "usage") and message.usage:
324
- self.logger.info(
325
- f"Streaming complete - Model: {model}, Input tokens: {message.usage.input_tokens}, Output tokens: {message.usage.output_tokens}"
326
- )
327
-
328
- return message
329
-
330
- async def _anthropic_completion(
331
- self,
332
- message_param,
333
- request_params: RequestParams | None = None,
334
- structured_model: Type[ModelT] | None = None,
335
- ) -> list[ContentBlock]:
336
- """
337
- Process a query using an LLM and available tools.
338
- Override this method to use a different LLM.
339
- """
340
-
341
- api_key = self._api_key()
342
- base_url = self._base_url()
343
- if base_url and base_url.endswith("/v1"):
344
- base_url = base_url.rstrip("/v1")
345
-
346
- try:
347
- anthropic = AsyncAnthropic(api_key=api_key, base_url=base_url)
348
- messages: List[MessageParam] = []
349
- params = self.get_request_params(request_params)
350
- except AuthenticationError as e:
351
- raise ProviderKeyError(
352
- "Invalid Anthropic API key",
353
- "The configured Anthropic API key was rejected.\nPlease check that your API key is valid and not expired.",
354
- ) from e
355
-
356
- # Always include prompt messages, but only include conversation history
357
- # if use_history is True
358
- messages.extend(self.history.get(include_completion_history=params.use_history))
359
-
360
- messages.append(message_param) # message_param is the current user turn
361
-
362
- # Get cache mode configuration
363
- cache_mode = self._get_cache_mode()
364
- self.logger.debug(f"Anthropic cache_mode: {cache_mode}")
365
-
366
- available_tools = await self._prepare_tools(structured_model)
367
-
368
- responses: List[ContentBlock] = []
369
-
370
- model = self.default_request_params.model
371
-
372
- # Note: We'll cache tools+system together by putting cache_control only on system prompt
373
-
374
- for i in range(params.max_iterations):
375
- self._log_chat_progress(self.chat_turn(), model=model)
376
-
377
- # Create base arguments dictionary
378
- base_args = {
379
- "model": model,
380
- "messages": messages,
381
- "system": self.instruction or params.systemPrompt,
382
- "stop_sequences": params.stopSequences,
383
- "tools": available_tools,
384
- }
385
-
386
- # Add tool_choice for structured output mode
387
- if structured_model:
388
- base_args["tool_choice"] = {"type": "tool", "name": "return_structured_output"}
389
-
390
- # Apply cache control to system prompt
391
- self._apply_system_cache(base_args, cache_mode)
392
-
393
- # Apply conversation caching
394
- applied_count = await self._apply_conversation_cache(messages, cache_mode)
395
-
396
- # Verify we don't exceed Anthropic's 4 cache block limit
397
- if applied_count > 0:
398
- total_cache_blocks = applied_count
399
- if cache_mode != "off" and base_args["system"]:
400
- total_cache_blocks += 1 # tools+system cache block
401
- if total_cache_blocks > 4:
402
- self.logger.warning(
403
- f"Total cache blocks ({total_cache_blocks}) exceeds Anthropic limit of 4"
404
- )
405
-
406
- if params.maxTokens is not None:
407
- base_args["max_tokens"] = params.maxTokens
408
-
409
- # Use the base class method to prepare all arguments with Anthropic-specific exclusions
410
- arguments = self.prepare_provider_arguments(
411
- base_args, params, self.ANTHROPIC_EXCLUDE_FIELDS
412
- )
413
-
414
- self.logger.debug(f"{arguments}")
415
-
416
- # Use streaming API with helper
417
- async with anthropic.messages.stream(**arguments) as stream:
418
- # Process the stream
419
- response = await self._process_stream(stream, model)
420
-
421
- # Track usage if response is valid and has usage data
422
- if (
423
- hasattr(response, "usage")
424
- and response.usage
425
- and not isinstance(response, BaseException)
426
- ):
427
- try:
428
- turn_usage = TurnUsage.from_anthropic(
429
- response.usage, model or DEFAULT_ANTHROPIC_MODEL
430
- )
431
- self._finalize_turn_usage(turn_usage)
432
- # self._show_usage(response.usage, turn_usage)
433
- except Exception as e:
434
- self.logger.warning(f"Failed to track usage: {e}")
435
-
436
- if isinstance(response, AuthenticationError):
437
- raise ProviderKeyError(
438
- "Invalid Anthropic API key",
439
- "The configured Anthropic API key was rejected.\nPlease check that your API key is valid and not expired.",
440
- ) from response
441
- elif isinstance(response, BaseException):
442
- error_details = str(response)
443
- self.logger.error(f"Error: {error_details}", data=BaseException)
444
-
445
- # Try to extract more useful information for API errors
446
- if hasattr(response, "status_code") and hasattr(response, "response"):
447
- try:
448
- error_json = response.response.json()
449
- error_details = f"Error code: {response.status_code} - {error_json}"
450
- except: # noqa: E722
451
- error_details = f"Error code: {response.status_code} - {str(response)}"
452
-
453
- # Convert other errors to text response
454
- error_message = f"Error during generation: {error_details}"
455
- response = Message(
456
- id="error",
457
- model="error",
458
- role="assistant",
459
- type="message",
460
- content=[TextBlock(type="text", text=error_message)],
461
- stop_reason="end_turn",
462
- usage=Usage(input_tokens=0, output_tokens=0),
463
- )
464
-
465
- self.logger.debug(
466
- f"{model} response:",
467
- data=response,
468
- )
469
-
470
- response_as_message = self.convert_message_to_message_param(response)
471
- messages.append(response_as_message)
472
- if response.content and response.content[0].type == "text":
473
- responses.append(TextContent(type="text", text=response.content[0].text))
474
-
475
- if response.stop_reason == "end_turn":
476
- message_text = ""
477
- for block in response_as_message["content"]:
478
- if isinstance(block, dict) and block.get("type") == "text":
479
- message_text += block.get("text", "")
480
- elif hasattr(block, "type") and block.type == "text":
481
- message_text += block.text
482
-
483
- await self.show_assistant_message(message_text)
484
-
485
- self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'end_turn'")
486
- break
487
- elif response.stop_reason == "stop_sequence":
488
- # We have reached a stop sequence
489
- self.logger.debug(
490
- f"Iteration {i}: Stopping because finish_reason is 'stop_sequence'"
491
- )
492
- break
493
- elif response.stop_reason == "max_tokens":
494
- # We have reached the max tokens limit
495
-
496
- self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'max_tokens'")
497
- if params.maxTokens is not None:
498
- message_text = Text(
499
- f"the assistant has reached the maximum token limit ({params.maxTokens})",
500
- style="dim green italic",
501
- )
502
- else:
503
- message_text = Text(
504
- "the assistant has reached the maximum token limit",
505
- style="dim green italic",
506
- )
507
-
508
- await self.show_assistant_message(message_text)
509
-
510
- break
511
- else:
512
- message_text = ""
513
- for block in response_as_message["content"]:
514
- if isinstance(block, dict) and block.get("type") == "text":
515
- message_text += block.get("text", "")
516
- elif hasattr(block, "type") and block.type == "text":
517
- message_text += block.text
518
-
519
- # response.stop_reason == "tool_use":
520
- # First, collect all tool uses in this turn
521
- tool_uses = [c for c in response.content if c.type == "tool_use"]
522
-
523
- if tool_uses:
524
- if message_text == "":
525
- message_text = Text(
526
- "the assistant requested tool calls",
527
- style="dim green italic",
528
- )
529
-
530
- # Process all tool calls using the helper method
531
- tool_results, tool_responses = await self._process_tool_calls(
532
- tool_uses, available_tools, message_text, structured_model
533
- )
534
- responses.extend(tool_responses)
535
-
536
- # Always add tool_results_message first (required by Anthropic API)
537
- messages.append(AnthropicConverter.create_tool_results_message(tool_results))
538
-
539
- # For structured output, we have our result and should exit after sending tool_result
540
- if structured_model and any(
541
- tool.name == "return_structured_output" for tool in tool_uses
542
- ):
543
- self.logger.debug("Structured output received, breaking iteration loop")
544
- break
545
-
546
- # Only save the new conversation messages to history if use_history is true
547
- # Keep the prompt messages separate
548
- if params.use_history:
549
- # Get current prompt messages
550
- prompt_messages = self.history.get(include_completion_history=False)
551
- new_messages = messages[len(prompt_messages) :]
552
- self.history.set(new_messages)
553
-
554
- self._log_chat_finished(model=model)
555
-
556
- return responses
557
-
558
- async def generate_messages(
559
- self,
560
- message_param,
561
- request_params: RequestParams | None = None,
562
- ) -> PromptMessageMultipart:
563
- """
564
- Process a query using an LLM and available tools.
565
- The default implementation uses Claude as the LLM.
566
- Override this method to use a different LLM.
567
-
568
- """
569
- # Reset tool call counter for new turn
570
- self._reset_turn_tool_calls()
571
-
572
- res = await self._anthropic_completion(
573
- message_param=message_param,
574
- request_params=request_params,
575
- )
576
- return Prompt.assistant(*res)
577
-
578
- async def _apply_prompt_provider_specific(
579
- self,
580
- multipart_messages: List["PromptMessageMultipart"],
581
- request_params: RequestParams | None = None,
582
- is_template: bool = False,
583
- ) -> PromptMessageMultipart:
584
- # Check the last message role
585
- last_message = multipart_messages[-1]
586
-
587
- # Add all previous messages to history (or all messages if last is from assistant)
588
- messages_to_add = (
589
- multipart_messages[:-1] if last_message.role == "user" else multipart_messages
590
- )
591
- converted = []
592
-
593
- # Get cache mode configuration
594
- cache_mode = self._get_cache_mode()
595
-
596
- for msg in messages_to_add:
597
- anthropic_msg = AnthropicConverter.convert_to_anthropic(msg)
598
-
599
- # Apply caching to template messages if cache_mode is "prompt" or "auto"
600
- if is_template and cache_mode in ["prompt", "auto"] and anthropic_msg.get("content"):
601
- content_list = anthropic_msg["content"]
602
- if isinstance(content_list, list) and content_list:
603
- # Apply cache control to the last content block
604
- last_block = content_list[-1]
605
- if isinstance(last_block, dict):
606
- last_block["cache_control"] = {"type": "ephemeral"}
607
- self.logger.debug(
608
- f"Applied cache_control to template message with role {anthropic_msg.get('role')}"
609
- )
610
-
611
- converted.append(anthropic_msg)
612
-
613
- self.history.extend(converted, is_prompt=is_template)
614
-
615
- if last_message.role == "user":
616
- self.logger.debug("Last message in prompt is from user, generating assistant response")
617
- message_param = AnthropicConverter.convert_to_anthropic(last_message)
618
- return await self.generate_messages(message_param, request_params)
619
- else:
620
- # For assistant messages: Return the last message content as text
621
- self.logger.debug("Last message in prompt is from assistant, returning it directly")
622
- return last_message
623
-
624
- async def _apply_prompt_provider_specific_structured(
625
- self,
626
- multipart_messages: List[PromptMessageMultipart],
627
- model: Type[ModelT],
628
- request_params: RequestParams | None = None,
629
- ) -> Tuple[ModelT | None, PromptMessageMultipart]: # noqa: F821
630
- request_params = self.get_request_params(request_params)
631
-
632
- # Check the last message role
633
- last_message = multipart_messages[-1]
634
-
635
- # Add all previous messages to history (or all messages if last is from assistant)
636
- messages_to_add = (
637
- multipart_messages[:-1] if last_message.role == "user" else multipart_messages
638
- )
639
- converted = []
640
-
641
- for msg in messages_to_add:
642
- anthropic_msg = AnthropicConverter.convert_to_anthropic(msg)
643
- converted.append(anthropic_msg)
644
-
645
- self.history.extend(converted, is_prompt=False)
646
-
647
- if last_message.role == "user":
648
- self.logger.debug("Last message in prompt is from user, generating structured response")
649
- message_param = AnthropicConverter.convert_to_anthropic(last_message)
650
-
651
- # Call _anthropic_completion with the structured model
652
- response_content = await self._anthropic_completion(
653
- message_param, request_params, structured_model=model
654
- )
655
-
656
- # Extract the structured data from the response
657
- for content in response_content:
658
- if content.type == "text":
659
- try:
660
- # Parse the JSON response from the tool
661
- data = json.loads(content.text)
662
- parsed_model = model(**data)
663
- # Create assistant response
664
- assistant_response = Prompt.assistant(content)
665
- return parsed_model, assistant_response
666
- except (json.JSONDecodeError, ValueError) as e:
667
- self.logger.error(f"Failed to parse structured output: {e}")
668
- assistant_response = Prompt.assistant(content)
669
- return None, assistant_response
670
-
671
- # If no valid response found
672
- return None, Prompt.assistant()
673
- else:
674
- # For assistant messages: Return the last message content
675
- self.logger.debug("Last message in prompt is from assistant, returning it directly")
676
- return None, last_message
677
-
678
- def _show_usage(self, raw_usage: Usage, turn_usage: TurnUsage) -> None:
679
- # Print raw usage for debugging
680
- print(f"\n=== USAGE DEBUG ({turn_usage.model}) ===")
681
- print(f"Raw usage: {raw_usage}")
682
- print(
683
- f"Turn usage: input={turn_usage.input_tokens}, output={turn_usage.output_tokens}, current_context={turn_usage.current_context_tokens}"
684
- )
685
- print(
686
- f"Cache: read={turn_usage.cache_usage.cache_read_tokens}, write={turn_usage.cache_usage.cache_write_tokens}"
687
- )
688
- print(f"Effective input: {turn_usage.effective_input_tokens}")
689
- print(
690
- 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}"
691
- )
692
- if self.usage_accumulator.context_usage_percentage:
693
- print(
694
- f"Context usage: {self.usage_accumulator.context_usage_percentage:.1f}% of {self.usage_accumulator.context_window_size}"
695
- )
696
- if self.usage_accumulator.cache_hit_rate:
697
- print(f"Cache hit rate: {self.usage_accumulator.cache_hit_rate:.1f}%")
698
- print("===========================\n")
699
-
700
- @classmethod
701
- def convert_message_to_message_param(cls, message: Message, **kwargs) -> MessageParam:
702
- """Convert a response object to an input parameter object to allow LLM calls to be chained."""
703
- content = []
704
-
705
- for content_block in message.content:
706
- if content_block.type == "text":
707
- content.append(TextBlockParam(type="text", text=content_block.text))
708
- elif content_block.type == "tool_use":
709
- content.append(
710
- ToolUseBlockParam(
711
- type="tool_use",
712
- name=content_block.name,
713
- input=content_block.input,
714
- id=content_block.id,
715
- )
716
- )
717
-
718
- return MessageParam(role="assistant", content=content, **kwargs)