fast-agent-mcp 0.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,462 @@
1
+ import re
2
+ from typing import Sequence, Union
3
+
4
+ from anthropic.types import (
5
+ Base64ImageSourceParam,
6
+ Base64PDFSourceParam,
7
+ ContentBlockParam,
8
+ DocumentBlockParam,
9
+ ImageBlockParam,
10
+ MessageParam,
11
+ PlainTextSourceParam,
12
+ TextBlockParam,
13
+ ToolResultBlockParam,
14
+ ToolUseBlockParam,
15
+ URLImageSourceParam,
16
+ URLPDFSourceParam,
17
+ )
18
+ from mcp.types import (
19
+ BlobResourceContents,
20
+ CallToolResult,
21
+ ContentBlock,
22
+ EmbeddedResource,
23
+ ImageContent,
24
+ PromptMessage,
25
+ TextContent,
26
+ TextResourceContents,
27
+ )
28
+
29
+ from fast_agent.core.logging.logger import get_logger
30
+ from fast_agent.mcp.helpers.content_helpers import (
31
+ get_image_data,
32
+ get_resource_uri,
33
+ get_text,
34
+ is_image_content,
35
+ is_resource_content,
36
+ is_text_content,
37
+ )
38
+ from fast_agent.mcp.mime_utils import (
39
+ guess_mime_type,
40
+ is_image_mime_type,
41
+ is_text_mime_type,
42
+ )
43
+ from fast_agent.types import PromptMessageExtended
44
+
45
+ _logger = get_logger("multipart_converter_anthropic")
46
+
47
+ # List of image MIME types supported by Anthropic API
48
+ SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
49
+
50
+
51
+ class AnthropicConverter:
52
+ """Converts MCP message types to Anthropic API format."""
53
+
54
+ @staticmethod
55
+ def _is_supported_image_type(mime_type: str) -> bool:
56
+ """Check if the given MIME type is supported by Anthropic's image API.
57
+
58
+ Args:
59
+ mime_type: The MIME type to check
60
+
61
+ Returns:
62
+ True if the MIME type is supported, False otherwise
63
+ """
64
+ return mime_type in SUPPORTED_IMAGE_MIME_TYPES
65
+
66
+ @staticmethod
67
+ def convert_to_anthropic(multipart_msg: PromptMessageExtended) -> MessageParam:
68
+ """
69
+ Convert a PromptMessageExtended message to Anthropic API format.
70
+
71
+ Args:
72
+ multipart_msg: The PromptMessageExtended message to convert
73
+
74
+ Returns:
75
+ An Anthropic API MessageParam object
76
+ """
77
+ role = multipart_msg.role
78
+ all_content_blocks = []
79
+
80
+ # If this is an assistant message that contains tool_calls, convert
81
+ # those into Anthropic tool_use blocks so the next user message can
82
+ # legally include corresponding tool_result blocks.
83
+ if role == "assistant" and multipart_msg.tool_calls:
84
+ for tool_use_id, req in multipart_msg.tool_calls.items():
85
+ sanitized_id = AnthropicConverter._sanitize_tool_id(tool_use_id)
86
+ name = None
87
+ args = None
88
+ try:
89
+ params = getattr(req, "params", None)
90
+ if params is not None:
91
+ name = getattr(params, "name", None)
92
+ args = getattr(params, "arguments", None)
93
+ except Exception:
94
+ pass
95
+
96
+ all_content_blocks.append(
97
+ ToolUseBlockParam(
98
+ type="tool_use",
99
+ id=sanitized_id,
100
+ name=name or "unknown_tool",
101
+ input=args or {},
102
+ )
103
+ )
104
+
105
+ return MessageParam(role=role, content=all_content_blocks)
106
+
107
+ # Handle tool_results if present (for user messages with tool results)
108
+ # Tool results must come FIRST in the content array per Anthropic API requirements
109
+ if multipart_msg.tool_results:
110
+ # Convert dict to list of tuples for create_tool_results_message
111
+ tool_results_list = list(multipart_msg.tool_results.items())
112
+ tool_msg = AnthropicConverter.create_tool_results_message(tool_results_list)
113
+ # Extract the content blocks from the tool results message
114
+ all_content_blocks.extend(tool_msg["content"])
115
+
116
+ # Then handle regular content blocks if present
117
+ if multipart_msg.content:
118
+ # Convert content blocks
119
+ anthropic_blocks = AnthropicConverter._convert_content_items(
120
+ multipart_msg.content, document_mode=True
121
+ )
122
+
123
+ # Filter blocks based on role (assistant can only have text blocks)
124
+ if role == "assistant":
125
+ text_blocks = []
126
+ for block in anthropic_blocks:
127
+ if block.get("type") == "text":
128
+ text_blocks.append(block)
129
+ else:
130
+ _logger.warning(
131
+ f"Removing non-text block from assistant message: {block.get('type')}"
132
+ )
133
+ anthropic_blocks = text_blocks
134
+
135
+ all_content_blocks.extend(anthropic_blocks)
136
+
137
+ # Handle empty content case
138
+ if not all_content_blocks:
139
+ return MessageParam(role=role, content=[])
140
+
141
+ # Create the Anthropic message
142
+ return MessageParam(role=role, content=all_content_blocks)
143
+
144
+ @staticmethod
145
+ def convert_prompt_message_to_anthropic(message: PromptMessage) -> MessageParam:
146
+ """
147
+ Convert a standard PromptMessage to Anthropic API format.
148
+
149
+ Args:
150
+ message: The PromptMessage to convert
151
+
152
+ Returns:
153
+ An Anthropic API MessageParam object
154
+ """
155
+ # Convert the PromptMessage to a PromptMessageExtended containing a single content item
156
+ multipart = PromptMessageExtended(role=message.role, content=[message.content])
157
+
158
+ # Use the existing conversion method
159
+ return AnthropicConverter.convert_to_anthropic(multipart)
160
+
161
+ @staticmethod
162
+ def _convert_content_items(
163
+ content_items: Sequence[ContentBlock],
164
+ document_mode: bool = True,
165
+ ) -> list[ContentBlockParam]:
166
+ """
167
+ Convert a list of content items to Anthropic content blocks.
168
+
169
+ Args:
170
+ content_items: Sequence of MCP content items
171
+ document_mode: Whether to convert text resources to document blocks (True) or text blocks (False)
172
+
173
+ Returns:
174
+ List of Anthropic content blocks
175
+ """
176
+ anthropic_blocks: list[ContentBlockParam] = []
177
+
178
+ for content_item in content_items:
179
+ if is_text_content(content_item):
180
+ # Handle text content
181
+ text = get_text(content_item)
182
+ anthropic_blocks.append(TextBlockParam(type="text", text=text))
183
+
184
+ elif is_image_content(content_item):
185
+ # Handle image content
186
+ image_content = content_item # type: ImageContent
187
+ # Check if image MIME type is supported
188
+ if not AnthropicConverter._is_supported_image_type(image_content.mimeType):
189
+ data_size = len(image_content.data) if image_content.data else 0
190
+ anthropic_blocks.append(
191
+ TextBlockParam(
192
+ type="text",
193
+ text=f"Image with unsupported format '{image_content.mimeType}' ({data_size} bytes)",
194
+ )
195
+ )
196
+ else:
197
+ image_data = get_image_data(image_content)
198
+ anthropic_blocks.append(
199
+ ImageBlockParam(
200
+ type="image",
201
+ source=Base64ImageSourceParam(
202
+ type="base64",
203
+ media_type=image_content.mimeType,
204
+ data=image_data,
205
+ ),
206
+ )
207
+ )
208
+
209
+ elif is_resource_content(content_item):
210
+ # Handle embedded resource
211
+ block = AnthropicConverter._convert_embedded_resource(content_item, document_mode)
212
+ anthropic_blocks.append(block)
213
+
214
+ return anthropic_blocks
215
+
216
+ @staticmethod
217
+ def _convert_embedded_resource(
218
+ resource: EmbeddedResource,
219
+ document_mode: bool = True,
220
+ ) -> ContentBlockParam:
221
+ """
222
+ Convert EmbeddedResource to appropriate Anthropic block type.
223
+
224
+ Args:
225
+ resource: The embedded resource to convert
226
+ document_mode: Whether to convert text resources to Document blocks (True) or Text blocks (False)
227
+
228
+ Returns:
229
+ An appropriate ContentBlockParam for the resource
230
+ """
231
+ resource_content = resource.resource
232
+ uri_str = get_resource_uri(resource)
233
+ uri = getattr(resource_content, "uri", None)
234
+ is_url: bool = uri and uri.scheme in ("http", "https")
235
+
236
+ # Determine MIME type
237
+ mime_type = AnthropicConverter._determine_mime_type(resource_content)
238
+
239
+ # Extract title from URI
240
+ from fast_agent.mcp.resource_utils import extract_title_from_uri
241
+
242
+ title = extract_title_from_uri(uri) if uri else "resource"
243
+
244
+ # Convert based on MIME type
245
+ if mime_type == "image/svg+xml":
246
+ return AnthropicConverter._convert_svg_resource(resource_content)
247
+
248
+ elif is_image_mime_type(mime_type):
249
+ if not AnthropicConverter._is_supported_image_type(mime_type):
250
+ return AnthropicConverter._create_fallback_text(
251
+ f"Image with unsupported format '{mime_type}'", resource
252
+ )
253
+
254
+ if is_url and uri_str:
255
+ return ImageBlockParam(
256
+ type="image", source=URLImageSourceParam(type="url", url=uri_str)
257
+ )
258
+
259
+ # Try to get image data
260
+ image_data = get_image_data(resource)
261
+ if image_data:
262
+ return ImageBlockParam(
263
+ type="image",
264
+ source=Base64ImageSourceParam(
265
+ type="base64", media_type=mime_type, data=image_data
266
+ ),
267
+ )
268
+
269
+ return AnthropicConverter._create_fallback_text("Image missing data", resource)
270
+
271
+ elif mime_type == "application/pdf":
272
+ if is_url and uri_str:
273
+ return DocumentBlockParam(
274
+ type="document",
275
+ title=title,
276
+ source=URLPDFSourceParam(type="url", url=uri_str),
277
+ )
278
+ elif isinstance(resource_content, BlobResourceContents):
279
+ return DocumentBlockParam(
280
+ type="document",
281
+ title=title,
282
+ source=Base64PDFSourceParam(
283
+ type="base64",
284
+ media_type="application/pdf",
285
+ data=resource_content.blob,
286
+ ),
287
+ )
288
+ return TextBlockParam(type="text", text=f"[PDF resource missing data: {title}]")
289
+
290
+ elif is_text_mime_type(mime_type):
291
+ text = get_text(resource)
292
+ if not text:
293
+ return TextBlockParam(
294
+ type="text",
295
+ text=f"[Text content could not be extracted from {title}]",
296
+ )
297
+
298
+ # Create document block when in document mode
299
+ if document_mode:
300
+ return DocumentBlockParam(
301
+ type="document",
302
+ title=title,
303
+ source=PlainTextSourceParam(
304
+ type="text",
305
+ media_type="text/plain",
306
+ data=text,
307
+ ),
308
+ )
309
+
310
+ # Return as simple text block when not in document mode
311
+ return TextBlockParam(type="text", text=text)
312
+
313
+ # Default fallback - convert to text if possible
314
+ text = get_text(resource)
315
+ if text:
316
+ return TextBlockParam(type="text", text=text)
317
+
318
+ # This is for binary resources - match the format expected by the test
319
+ if isinstance(resource.resource, BlobResourceContents) and hasattr(
320
+ resource.resource, "blob"
321
+ ):
322
+ blob_length = len(resource.resource.blob)
323
+ return TextBlockParam(
324
+ type="text",
325
+ text=f"Embedded Resource {uri._url} with unsupported format {mime_type} ({blob_length} characters)",
326
+ )
327
+
328
+ return AnthropicConverter._create_fallback_text(
329
+ f"Unsupported resource ({mime_type})", resource
330
+ )
331
+
332
+ @staticmethod
333
+ def _determine_mime_type(
334
+ resource: Union[TextResourceContents, BlobResourceContents],
335
+ ) -> str:
336
+ """
337
+ Determine the MIME type of a resource.
338
+
339
+ Args:
340
+ resource: The resource to check
341
+
342
+ Returns:
343
+ The MIME type as a string
344
+ """
345
+ if getattr(resource, "mimeType", None):
346
+ return resource.mimeType
347
+
348
+ if getattr(resource, "uri", None):
349
+ return guess_mime_type(resource.uri.serialize_url)
350
+
351
+ if hasattr(resource, "blob"):
352
+ return "application/octet-stream"
353
+
354
+ return "text/plain"
355
+
356
+ @staticmethod
357
+ def _convert_svg_resource(resource_content) -> TextBlockParam:
358
+ """
359
+ Convert SVG resource to text block with XML code formatting.
360
+
361
+ Args:
362
+ resource_content: The resource content containing SVG data
363
+
364
+ Returns:
365
+ A TextBlockParam with formatted SVG content
366
+ """
367
+ # Use get_text helper to extract text from various content types
368
+ svg_content = get_text(resource_content)
369
+ if svg_content:
370
+ return TextBlockParam(type="text", text=f"```xml\n{svg_content}\n```")
371
+ return TextBlockParam(type="text", text="[SVG content could not be extracted]")
372
+
373
+ @staticmethod
374
+ def _create_fallback_text(
375
+ message: str, resource: Union[TextContent, ImageContent, EmbeddedResource]
376
+ ) -> TextBlockParam:
377
+ """
378
+ Create a fallback text block for unsupported resource types.
379
+
380
+ Args:
381
+ message: The fallback message
382
+ resource: The resource that couldn't be converted
383
+
384
+ Returns:
385
+ A TextBlockParam with the fallback message
386
+ """
387
+ if isinstance(resource, EmbeddedResource) and hasattr(resource.resource, "uri"):
388
+ uri = resource.resource.uri
389
+ return TextBlockParam(type="text", text=f"[{message}: {uri._url}]")
390
+
391
+ return TextBlockParam(type="text", text=f"[{message}]")
392
+
393
+ @staticmethod
394
+ def create_tool_results_message(
395
+ tool_results: list[tuple[str, CallToolResult]],
396
+ ) -> MessageParam:
397
+ """
398
+ Create a user message containing tool results.
399
+
400
+ Args:
401
+ tool_results: List of (tool_use_id, tool_result) tuples
402
+
403
+ Returns:
404
+ A MessageParam with role='user' containing all tool results
405
+ """
406
+ content_blocks = []
407
+
408
+ for tool_use_id, result in tool_results:
409
+ sanitized_id = AnthropicConverter._sanitize_tool_id(tool_use_id)
410
+ # Process each tool result
411
+ tool_result_blocks = []
412
+
413
+ # Process each content item in the result
414
+ for item in result.content:
415
+ if isinstance(item, (TextContent, ImageContent)):
416
+ blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
417
+ tool_result_blocks.extend(blocks)
418
+ elif isinstance(item, EmbeddedResource):
419
+ resource_content = item.resource
420
+ document_mode: bool = not isinstance(resource_content, TextResourceContents)
421
+ # With Anthropic SDK 0.66, documents can be inside tool results
422
+ # Text resources remain inline within the tool_result
423
+ block = AnthropicConverter._convert_embedded_resource(
424
+ item, document_mode=document_mode
425
+ )
426
+ tool_result_blocks.append(block)
427
+
428
+ # Create the tool result block if we have content
429
+ if tool_result_blocks:
430
+ content_blocks.append(
431
+ ToolResultBlockParam(
432
+ type="tool_result",
433
+ tool_use_id=sanitized_id,
434
+ content=tool_result_blocks,
435
+ is_error=result.isError,
436
+ )
437
+ )
438
+ else:
439
+ # If there's no content, still create a placeholder
440
+ content_blocks.append(
441
+ ToolResultBlockParam(
442
+ type="tool_result",
443
+ tool_use_id=sanitized_id,
444
+ content=[TextBlockParam(type="text", text="[No content in tool result]")],
445
+ is_error=result.isError,
446
+ )
447
+ )
448
+
449
+ # All content is now included within the tool_result block.
450
+
451
+ return MessageParam(role="user", content=content_blocks)
452
+
453
+ @staticmethod
454
+ def _sanitize_tool_id(tool_id: str | None) -> str:
455
+ """
456
+ Anthropic tool_use ids must match ^[a-zA-Z0-9_-]+$.
457
+ Clean any other characters to underscores and provide a stable fallback.
458
+ """
459
+ if not tool_id:
460
+ return "tool"
461
+ cleaned = re.sub(r"[^a-zA-Z0-9_-]", "_", tool_id)
462
+ return cleaned or "tool"
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Collection, Literal, TypedDict, cast
4
+
5
+ # Lightweight, runtime-only loader for AWS Bedrock models.
6
+ # - Fetches once per process via boto3 (region from session; env override supported)
7
+ # - Memory cache only; no disk persistence
8
+ # - Provides filtering and optional prefixing (default 'bedrock.') for model IDs
9
+
10
+ try:
11
+ import boto3
12
+ except Exception: # pragma: no cover - import error path
13
+ boto3 = None # type: ignore[assignment]
14
+
15
+
16
+ Modality = Literal["TEXT", "IMAGE", "VIDEO", "SPEECH", "EMBEDDING"]
17
+ Lifecycle = Literal["ACTIVE", "LEGACY"]
18
+ InferenceType = Literal["ON_DEMAND", "PROVISIONED", "INFERENCE_PROFILE"]
19
+
20
+
21
+ class ModelSummary(TypedDict, total=False):
22
+ modelId: str
23
+ modelName: str
24
+ providerName: str
25
+ inputModalities: list[Modality]
26
+ outputModalities: list[Modality]
27
+ responseStreamingSupported: bool
28
+ customizationsSupported: list[str]
29
+ inferenceTypesSupported: list[InferenceType]
30
+ modelLifecycle: dict[str, Lifecycle]
31
+
32
+
33
+ _MODELS_CACHE_BY_REGION: dict[str, dict[str, ModelSummary]] = {}
34
+
35
+
36
+ def _resolve_region(region: str | None) -> str:
37
+ if region:
38
+ return region
39
+ import os
40
+
41
+ env_region = os.getenv("BEDROCK_REGION")
42
+ if env_region:
43
+ return env_region
44
+ if boto3 is None:
45
+ raise RuntimeError(
46
+ "boto3 is required to load Bedrock models. Install boto3 or provide a static list."
47
+ )
48
+ session = boto3.Session()
49
+ if not session.region_name:
50
+ raise RuntimeError(
51
+ "AWS region could not be resolved. Configure your AWS SSO/profile or set BEDROCK_REGION."
52
+ )
53
+ return session.region_name
54
+
55
+
56
+ def _strip_prefix(model_id: str, prefix: str) -> str:
57
+ return model_id[len(prefix) :] if prefix and model_id.startswith(prefix) else model_id
58
+
59
+
60
+ def _ensure_loaded(region: str | None = None) -> dict[str, ModelSummary]:
61
+ resolved_region = _resolve_region(region)
62
+ cache = _MODELS_CACHE_BY_REGION.get(resolved_region)
63
+ if cache is not None:
64
+ return cache
65
+
66
+ if boto3 is None:
67
+ raise RuntimeError("boto3 is required to load Bedrock models. Install boto3.")
68
+
69
+ try:
70
+ client = boto3.client("bedrock", region_name=resolved_region)
71
+ resp = client.list_foundation_models()
72
+ summaries: list[ModelSummary] = resp.get("modelSummaries", []) # type: ignore[assignment]
73
+ except Exception as exc: # keep error simple and actionable
74
+ raise RuntimeError(
75
+ f"Failed to list Bedrock foundation models in region '{resolved_region}'. "
76
+ f"Ensure AWS credentials (SSO) and permissions (bedrock:ListFoundationModels) are configured. "
77
+ f"Original error: {exc}"
78
+ )
79
+
80
+ cache = {s.get("modelId", ""): s for s in summaries if s.get("modelId")}
81
+ _MODELS_CACHE_BY_REGION[resolved_region] = cache
82
+ return cache
83
+
84
+
85
+ def refresh_bedrock_models(region: str | None = None) -> None:
86
+ resolved_region = _resolve_region(region)
87
+ # drop and reload on next access
88
+ _MODELS_CACHE_BY_REGION.pop(resolved_region, None)
89
+ _ensure_loaded(resolved_region)
90
+
91
+
92
+ def _matches_modalities(model_modalities: list[Modality], requested: Collection[Modality]) -> bool:
93
+ # include if all requested are present in the model's modalities
94
+ return set(requested).issubset(set(model_modalities))
95
+
96
+
97
+ def all_model_summaries(
98
+ input_modalities: Collection[Modality] | None = None,
99
+ output_modalities: Collection[Modality] | None = None,
100
+ include_legacy: bool = False,
101
+ providers: Collection[str] | None = None,
102
+ inference_types: Collection[InferenceType] | None = None,
103
+ direct_invocation_only: bool = True,
104
+ region: str | None = None,
105
+ ) -> list[ModelSummary]:
106
+ """Return filtered Bedrock model summaries.
107
+
108
+ Defaults: input_modalities={"TEXT"}, output_modalities={"TEXT"}, include_legacy=False,
109
+ inference_types={"ON_DEMAND"}, direct_invocation_only=True.
110
+ """
111
+
112
+ cache = _ensure_loaded(region)
113
+ results: list[ModelSummary] = []
114
+
115
+ effective_output: set[Modality] = (
116
+ set(output_modalities) if output_modalities is not None else {cast("Modality", "TEXT")}
117
+ )
118
+ effective_input: set[Modality] | None = (
119
+ set(input_modalities) if input_modalities is not None else {cast("Modality", "TEXT")}
120
+ )
121
+ provider_filter: set[str] | None = set(providers) if providers is not None else None
122
+ effective_inference: set[InferenceType] = (
123
+ set(inference_types)
124
+ if inference_types is not None
125
+ else {cast("InferenceType", "ON_DEMAND")}
126
+ )
127
+
128
+ for summary in cache.values():
129
+ lifecycle = (summary.get("modelLifecycle") or {}).get("status")
130
+ if not include_legacy and lifecycle == "LEGACY":
131
+ continue
132
+
133
+ if provider_filter is not None and summary.get("providerName") not in provider_filter:
134
+ continue
135
+
136
+ # direct invocation only: exclude profile variants like :0:24k or :mm
137
+ if direct_invocation_only:
138
+ mid = summary.get("modelId") or ""
139
+ if mid.count(":") > 1:
140
+ continue
141
+
142
+ # modalities
143
+ model_inputs: list[Modality] = summary.get("inputModalities", []) # type: ignore[assignment]
144
+ model_outputs: list[Modality] = summary.get("outputModalities", []) # type: ignore[assignment]
145
+
146
+ if effective_input is not None and not _matches_modalities(model_inputs, effective_input):
147
+ continue
148
+ if effective_output and not _matches_modalities(model_outputs, effective_output):
149
+ continue
150
+
151
+ # inference types
152
+ model_inference: list[InferenceType] = summary.get("inferenceTypesSupported", []) # type: ignore[assignment]
153
+ if effective_inference and not set(effective_inference).issubset(set(model_inference)):
154
+ continue
155
+
156
+ results.append(summary)
157
+
158
+ return results
159
+
160
+
161
+ def all_bedrock_models(
162
+ input_modalities: Collection[Modality] | None = None,
163
+ output_modalities: Collection[Modality] | None = None,
164
+ include_legacy: bool = False,
165
+ providers: Collection[str] | None = None,
166
+ prefix: str = "bedrock.",
167
+ inference_types: Collection[InferenceType] | None = None,
168
+ direct_invocation_only: bool = True,
169
+ region: str | None = None,
170
+ ) -> list[str]:
171
+ """Return model IDs (optionally prefixed) filtered by the given criteria.
172
+
173
+ Defaults: output_modalities={"TEXT"}, exclude LEGACY,
174
+ inference_types={"ON_DEMAND"}, direct_invocation_only=True.
175
+ """
176
+
177
+ summaries = all_model_summaries(
178
+ input_modalities=input_modalities,
179
+ output_modalities=output_modalities,
180
+ include_legacy=include_legacy,
181
+ providers=providers,
182
+ inference_types=inference_types,
183
+ direct_invocation_only=direct_invocation_only,
184
+ region=region,
185
+ )
186
+ ids: list[str] = []
187
+ for s in summaries:
188
+ mid = s.get("modelId")
189
+ if mid:
190
+ ids.append(mid)
191
+ if prefix:
192
+ return [f"{prefix}{mid}" for mid in ids]
193
+ return ids
194
+
195
+
196
+ def get_model_metadata(model_id: str, region: str | None = None) -> ModelSummary | None:
197
+ cache = _ensure_loaded(region)
198
+ # Accept either prefixed or plain model IDs
199
+ plain_id = _strip_prefix(model_id, "bedrock.")
200
+ return cache.get(plain_id)
201
+
202
+
203
+ def list_providers(region: str | None = None) -> list[str]:
204
+ cache = _ensure_loaded(region)
205
+ providers = {s.get("providerName") for s in cache.values() if s.get("providerName")}
206
+ return sorted(providers) # type: ignore[arg-type]
207
+
208
+
209
+ __all__ = [
210
+ "Modality",
211
+ "Lifecycle",
212
+ "ModelSummary",
213
+ "all_bedrock_models",
214
+ "all_model_summaries",
215
+ "get_model_metadata",
216
+ "list_providers",
217
+ "refresh_bedrock_models",
218
+ ]