fast-agent-mcp 0.2.57__py3-none-any.whl → 0.3.0__py3-none-any.whl

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

Potentially problematic release.


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

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 +64 -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 +13 -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 +17 -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 +128 -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. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  64. fast_agent/llm/provider/bedrock/llm_bedrock.py +2192 -0
  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 -206
  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 +43 -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/elicitation_forms_server.py +25 -3
  118. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
  119. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
  120. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
  121. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
  122. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
  123. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  124. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
  125. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
  126. {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
  127. {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
  128. {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
  129. {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
  130. {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
  131. {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
  132. {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
  133. {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
  134. {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
  135. {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
  136. fast_agent/resources/setup/.gitignore +24 -0
  137. fast_agent/resources/setup/agent.py +18 -0
  138. fast_agent/resources/setup/fastagent.config.yaml +44 -0
  139. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -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 +56 -39
  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.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/METADATA +7 -7
  154. fast_agent_mcp-0.3.0.dist-info/RECORD +202 -0
  155. fast_agent_mcp-0.3.0.dist-info/entry_points.txt +5 -0
  156. fast_agent_mcp-0.2.57.dist-info/RECORD +0 -192
  157. fast_agent_mcp-0.2.57.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 -717
  172. mcp_agent/llm/providers/augmented_llm_bedrock.py +0 -1788
  173. mcp_agent/llm/providers/augmented_llm_google_native.py +0 -495
  174. mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
  175. mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
  176. mcp_agent/llm/sampling_format_converter.py +0 -37
  177. mcp_agent/logging/__init__.py +0 -0
  178. mcp_agent/mcp/__init__.py +0 -50
  179. mcp_agent/mcp/helpers/__init__.py +0 -25
  180. mcp_agent/mcp/helpers/content_helpers.py +0 -187
  181. mcp_agent/mcp/interfaces.py +0 -266
  182. mcp_agent/mcp/prompts/__init__.py +0 -0
  183. mcp_agent/mcp/prompts/__main__.py +0 -10
  184. mcp_agent/mcp_server_registry.py +0 -343
  185. mcp_agent/tools/tool_definition.py +0 -14
  186. mcp_agent/ui/console_display.py +0 -790
  187. mcp_agent/ui/console_display_legacy.py +0 -401
  188. {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
  189. {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
  190. {mcp_agent → fast_agent}/cli/constants.py +0 -0
  191. {mcp_agent → fast_agent}/core/error_handling.py +0 -0
  192. {mcp_agent → fast_agent}/core/exceptions.py +0 -0
  193. {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
  194. {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
  195. {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
  196. {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
  197. {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
  198. {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
  199. {mcp_agent → fast_agent}/mcp/common.py +0 -0
  200. {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
  201. {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
  202. {mcp_agent → fast_agent}/py.typed +0 -0
  203. {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
  204. {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
  205. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_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.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/WHEEL +0 -0
  234. {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1788 +0,0 @@
1
- import json
2
- import os
3
- import re
4
- from enum import Enum
5
- from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, Union
6
-
7
- from mcp.types import ContentBlock, TextContent
8
- from rich.text import Text
9
-
10
- from mcp_agent.core.exceptions import ProviderKeyError
11
- from mcp_agent.core.request_params import RequestParams
12
- from mcp_agent.event_progress import ProgressAction
13
- from mcp_agent.llm.augmented_llm import AugmentedLLM
14
- from mcp_agent.llm.provider_types import Provider
15
- from mcp_agent.llm.usage_tracking import TurnUsage
16
- from mcp_agent.logging.logger import get_logger
17
- from mcp_agent.mcp.interfaces import ModelT
18
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
19
-
20
- if TYPE_CHECKING:
21
- from mcp import ListToolsResult
22
-
23
- try:
24
- import boto3
25
- from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError
26
- except ImportError:
27
- boto3 = None
28
- BotoCoreError = Exception
29
- ClientError = Exception
30
- NoCredentialsError = Exception
31
-
32
- try:
33
- from anthropic.types import ToolParam
34
- except ImportError:
35
- ToolParam = None
36
-
37
- from mcp.types import (
38
- CallToolRequest,
39
- CallToolRequestParams,
40
- )
41
-
42
- DEFAULT_BEDROCK_MODEL = "amazon.nova-lite-v1:0"
43
-
44
- # Bedrock message format types
45
- BedrockMessage = Dict[str, Any] # Bedrock message format
46
- BedrockMessageParam = Dict[str, Any] # Bedrock message parameter format
47
-
48
-
49
- class ToolSchemaType(Enum):
50
- """Enum for different tool schema formats used by different model families."""
51
-
52
- DEFAULT = "default" # Default toolSpec format used by most models (formerly Nova)
53
- SYSTEM_PROMPT = "system_prompt" # System prompt-based tool calling format
54
- ANTHROPIC = "anthropic" # Native Anthropic tool calling format
55
-
56
-
57
- class BedrockAugmentedLLM(AugmentedLLM[BedrockMessageParam, BedrockMessage]):
58
- """
59
- AWS Bedrock implementation of AugmentedLLM using the Converse API.
60
- Supports all Bedrock models including Nova, Claude, Meta, etc.
61
- """
62
-
63
- # Bedrock-specific parameter exclusions
64
- BEDROCK_EXCLUDE_FIELDS = {
65
- AugmentedLLM.PARAM_MESSAGES,
66
- AugmentedLLM.PARAM_MODEL,
67
- AugmentedLLM.PARAM_SYSTEM_PROMPT,
68
- AugmentedLLM.PARAM_STOP_SEQUENCES,
69
- AugmentedLLM.PARAM_MAX_TOKENS,
70
- AugmentedLLM.PARAM_METADATA,
71
- AugmentedLLM.PARAM_USE_HISTORY,
72
- AugmentedLLM.PARAM_MAX_ITERATIONS,
73
- AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
74
- AugmentedLLM.PARAM_TEMPLATE_VARS,
75
- }
76
-
77
- @classmethod
78
- def matches_model_pattern(cls, model_name: str) -> bool:
79
- """Check if a model name matches Bedrock model patterns."""
80
- # Bedrock model patterns
81
- bedrock_patterns = [
82
- r"^amazon\.nova.*", # Amazon Nova models
83
- r"^anthropic\.claude.*", # Anthropic Claude models
84
- r"^meta\.llama.*", # Meta Llama models
85
- r"^mistral\..*", # Mistral models
86
- r"^cohere\..*", # Cohere models
87
- r"^ai21\..*", # AI21 models
88
- r"^stability\..*", # Stability AI models
89
- r"^openai\..*", # OpenAI models
90
- ]
91
-
92
- import re
93
-
94
- return any(re.match(pattern, model_name) for pattern in bedrock_patterns)
95
-
96
- def __init__(self, *args, **kwargs) -> None:
97
- """Initialize the Bedrock LLM with AWS credentials and region."""
98
- if boto3 is None:
99
- raise ImportError(
100
- "boto3 is required for Bedrock support. Install with: pip install boto3"
101
- )
102
-
103
- # Initialize logger
104
- self.logger = get_logger(__name__)
105
-
106
- # Extract AWS configuration from kwargs first
107
- self.aws_region = kwargs.pop("region", None)
108
- self.aws_profile = kwargs.pop("profile", None)
109
-
110
- super().__init__(*args, provider=Provider.BEDROCK, **kwargs)
111
-
112
- # Use config values if not provided in kwargs (after super().__init__)
113
- if self.context.config and self.context.config.bedrock:
114
- if not self.aws_region:
115
- self.aws_region = self.context.config.bedrock.region
116
- if not self.aws_profile:
117
- self.aws_profile = self.context.config.bedrock.profile
118
-
119
- # Final fallback to environment variables
120
- if not self.aws_region:
121
- # Support both AWS_REGION and AWS_DEFAULT_REGION
122
- self.aws_region = os.environ.get("AWS_REGION") or os.environ.get(
123
- "AWS_DEFAULT_REGION", "us-east-1"
124
- )
125
-
126
- if not self.aws_profile:
127
- # Support AWS_PROFILE environment variable
128
- self.aws_profile = os.environ.get("AWS_PROFILE")
129
-
130
- # Initialize AWS clients
131
- self._bedrock_client = None
132
- self._bedrock_runtime_client = None
133
-
134
- def _initialize_default_params(self, kwargs: dict) -> RequestParams:
135
- """Initialize Bedrock-specific default parameters"""
136
- # Get base defaults from parent (includes ModelDatabase lookup)
137
- base_params = super()._initialize_default_params(kwargs)
138
-
139
- # Override with Bedrock-specific settings
140
- chosen_model = kwargs.get("model", DEFAULT_BEDROCK_MODEL)
141
- base_params.model = chosen_model
142
-
143
- return base_params
144
-
145
- def _get_bedrock_client(self):
146
- """Get or create Bedrock client."""
147
- if self._bedrock_client is None:
148
- try:
149
- session = boto3.Session(profile_name=self.aws_profile)
150
- self._bedrock_client = session.client("bedrock", region_name=self.aws_region)
151
- except NoCredentialsError as e:
152
- raise ProviderKeyError(
153
- "AWS credentials not found",
154
- "Please configure AWS credentials using AWS CLI, environment variables, or IAM roles.",
155
- ) from e
156
- return self._bedrock_client
157
-
158
- def _get_bedrock_runtime_client(self):
159
- """Get or create Bedrock Runtime client."""
160
- if self._bedrock_runtime_client is None:
161
- try:
162
- session = boto3.Session(profile_name=self.aws_profile)
163
- self._bedrock_runtime_client = session.client(
164
- "bedrock-runtime", region_name=self.aws_region
165
- )
166
- except NoCredentialsError as e:
167
- raise ProviderKeyError(
168
- "AWS credentials not found",
169
- "Please configure AWS credentials using AWS CLI, environment variables, or IAM roles.",
170
- ) from e
171
- return self._bedrock_runtime_client
172
-
173
- def _get_tool_schema_type(self, model_id: str) -> ToolSchemaType:
174
- """
175
- Determine which tool schema format to use based on model family.
176
-
177
- Args:
178
- model_id: The model ID (e.g., "bedrock.meta.llama3-1-8b-instruct-v1:0")
179
-
180
- Returns:
181
- ToolSchemaType indicating which format to use
182
- """
183
- # Remove any "bedrock." prefix for pattern matching
184
- clean_model = model_id.replace("bedrock.", "")
185
-
186
- # Anthropic models use native Anthropic format
187
- if re.search(r"anthropic\.claude", clean_model):
188
- self.logger.debug(
189
- f"Model {model_id} detected as Anthropic - using native Anthropic format"
190
- )
191
- return ToolSchemaType.ANTHROPIC
192
-
193
- # Scout models use SYSTEM_PROMPT format
194
- if re.search(r"meta\.llama4-scout", clean_model):
195
- self.logger.debug(f"Model {model_id} detected as Scout - using SYSTEM_PROMPT format")
196
- return ToolSchemaType.SYSTEM_PROMPT
197
-
198
- # Other Llama 4 models use default toolConfig format
199
- if re.search(r"meta\.llama4", clean_model):
200
- self.logger.debug(
201
- f"Model {model_id} detected as Llama 4 (non-Scout) - using default toolConfig format"
202
- )
203
- return ToolSchemaType.DEFAULT
204
-
205
- # Llama 3.x models use system prompt format
206
- if re.search(r"meta\.llama3", clean_model):
207
- self.logger.debug(
208
- f"Model {model_id} detected as Llama 3.x - using system prompt format"
209
- )
210
- return ToolSchemaType.SYSTEM_PROMPT
211
-
212
- # Future: Add other model-specific formats here
213
- # if re.search(r"mistral\.", clean_model):
214
- # return ToolSchemaType.MISTRAL
215
-
216
- # Default to default format for all other models
217
- self.logger.debug(f"Model {model_id} using default tool format")
218
- return ToolSchemaType.DEFAULT
219
-
220
- def _supports_streaming_with_tools(self, model: str) -> bool:
221
- """
222
- Check if a model supports streaming with tools.
223
-
224
- Some models (like AI21 Jamba) support tools but not in streaming mode.
225
- This method uses regex patterns to identify such models.
226
-
227
- Args:
228
- model: The model name (e.g., "ai21.jamba-1-5-mini-v1:0")
229
-
230
- Returns:
231
- False if the model requires non-streaming for tools, True otherwise
232
- """
233
- # Remove any "bedrock." prefix for pattern matching
234
- clean_model = model.replace("bedrock.", "")
235
-
236
- # Models that don't support streaming with tools
237
- non_streaming_patterns = [
238
- r"ai21\.jamba", # All AI21 Jamba models
239
- r"meta\.llama", # All Meta Llama models
240
- r"mistral\.", # All Mistral models
241
- r"amazon\.titan", # All Amazon Titan models
242
- r"cohere\.command", # All Cohere Command models
243
- r"anthropic\.claude-instant", # Anthropic Claude Instant models
244
- r"anthropic\.claude-v2", # Anthropic Claude v2 models
245
- r"deepseek\.", # All DeepSeek models
246
- ]
247
-
248
- for pattern in non_streaming_patterns:
249
- if re.search(pattern, clean_model, re.IGNORECASE):
250
- self.logger.debug(
251
- f"Model {model} detected as non-streaming for tools (pattern: {pattern})"
252
- )
253
- return False
254
-
255
- return True
256
-
257
- def _supports_tool_use(self, model_id: str) -> bool:
258
- """
259
- Determine if a model supports tool use at all.
260
- Some models don't support tools in any form.
261
- Based on AWS Bedrock documentation: https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html
262
- """
263
- # Models that don't support tool use at all
264
- no_tool_use_patterns = [
265
- r"ai21\.jamba-instruct", # AI21 Jamba-Instruct (but not jamba 1.5)
266
- r"ai21\..*jurassic", # AI21 Labs Jurassic-2 models
267
- r"amazon\.titan", # All Amazon Titan models
268
- r"anthropic\.claude-v2", # Anthropic Claude v2 models
269
- r"anthropic\.claude-instant", # Anthropic Claude Instant models
270
- r"cohere\.command(?!-r)", # Cohere Command (but not Command R/R+)
271
- r"cohere\.command-light", # Cohere Command Light
272
- r"deepseek\.", # All DeepSeek models
273
- r"meta\.llama[23](?![-.])", # Meta Llama 2 and 3 (but not 3.1+, 3.2+, etc.)
274
- r"meta\.llama3-1-8b", # Meta Llama 3.1 8b - doesn't support tool calls
275
- r"meta\.llama3-2-[13]b", # Meta Llama 3.2 1b and 3b (but not 11b/90b)
276
- r"meta\.llama3-2-11b", # Meta Llama 3.2 11b - doesn't support tool calls
277
- r"mistral\..*-instruct", # Mistral AI Instruct (but not Mistral Large)
278
- ]
279
-
280
- for pattern in no_tool_use_patterns:
281
- if re.search(pattern, model_id):
282
- self.logger.info(f"Model {model_id} does not support tool use")
283
- return False
284
-
285
- return True
286
-
287
- def _supports_system_messages(self, model: str) -> bool:
288
- """
289
- Check if a model supports system messages.
290
-
291
- Some models (like Titan and Cohere embedding models) don't support system messages.
292
- This method uses regex patterns to identify such models.
293
-
294
- Args:
295
- model: The model name (e.g., "amazon.titan-embed-text-v1")
296
-
297
- Returns:
298
- False if the model doesn't support system messages, True otherwise
299
- """
300
- # Remove any "bedrock." prefix for pattern matching
301
- clean_model = model.replace("bedrock.", "")
302
-
303
- # DEBUG: Print the model names for debugging
304
- self.logger.info(
305
- f"DEBUG: Checking system message support for model='{model}', clean_model='{clean_model}'"
306
- )
307
-
308
- # Models that don't support system messages (reverse logic as suggested)
309
- no_system_message_patterns = [
310
- r"amazon\.titan", # All Amazon Titan models
311
- r"cohere\.command.*-text", # Cohere command text models (command-text-v14, command-light-text-v14)
312
- r"mistral.*mixtral.*8x7b", # Mistral Mixtral 8x7b models
313
- r"mistral.mistral-7b-instruct", # Mistral 7b instruct models
314
- r"meta\.llama3-2-11b-instruct", # Specific Meta Llama3 model
315
- ]
316
-
317
- for pattern in no_system_message_patterns:
318
- if re.search(pattern, clean_model, re.IGNORECASE):
319
- self.logger.info(
320
- f"DEBUG: Model {model} detected as NOT supporting system messages (pattern: {pattern})"
321
- )
322
- return False
323
-
324
- self.logger.info(f"DEBUG: Model {model} detected as supporting system messages")
325
- return True
326
-
327
- def _convert_tools_nova_format(self, tools: "ListToolsResult") -> List[Dict[str, Any]]:
328
- """Convert MCP tools to Nova-specific toolSpec format.
329
-
330
- Note: Nova models have VERY strict JSON schema requirements:
331
- - Top level schema must be of type Object
332
- - ONLY three fields are supported: type, properties, required
333
- - NO other fields like $schema, description, title, additionalProperties
334
- - Properties can only have type and description
335
- - Tools with no parameters should have empty properties object
336
- """
337
- bedrock_tools = []
338
-
339
- # Create mapping from cleaned names to original names for tool execution
340
- self.tool_name_mapping = {}
341
-
342
- self.logger.debug(f"Converting {len(tools.tools)} MCP tools to Nova format")
343
-
344
- for tool in tools.tools:
345
- self.logger.debug(f"Converting MCP tool: {tool.name}")
346
-
347
- # Extract and validate the input schema
348
- input_schema = tool.inputSchema or {}
349
-
350
- # Create Nova-compliant schema with ONLY the three allowed fields
351
- # Always include type and properties (even if empty)
352
- nova_schema: Dict[str, Any] = {"type": "object", "properties": {}}
353
-
354
- # Properties - clean them strictly
355
- properties: Dict[str, Any] = {}
356
- if "properties" in input_schema and isinstance(input_schema["properties"], dict):
357
- for prop_name, prop_def in input_schema["properties"].items():
358
- # Only include type and description for each property
359
- clean_prop: Dict[str, Any] = {}
360
-
361
- if isinstance(prop_def, dict):
362
- # Only include type (required) and description (optional)
363
- clean_prop["type"] = prop_def.get("type", "string")
364
- # Nova allows description in properties
365
- if "description" in prop_def:
366
- clean_prop["description"] = prop_def["description"]
367
- else:
368
- # Handle simple property definitions
369
- clean_prop["type"] = "string"
370
-
371
- properties[prop_name] = clean_prop
372
-
373
- # Always set properties (even if empty for parameterless tools)
374
- nova_schema["properties"] = properties
375
-
376
- # Required fields - only add if present and not empty
377
- if (
378
- "required" in input_schema
379
- and isinstance(input_schema["required"], list)
380
- and input_schema["required"]
381
- ):
382
- nova_schema["required"] = input_schema["required"]
383
-
384
- # IMPORTANT: Nova tool name compatibility fix
385
- # Problem: Amazon Nova models fail with "Model produced invalid sequence as part of ToolUse"
386
- # when tool names contain hyphens (e.g., "utils-get_current_date_information")
387
- # Solution: Replace hyphens with underscores for Nova (e.g., "utils_get_current_date_information")
388
- # Note: Underscores work fine, simple names work fine, but hyphens cause tool calling to fail
389
- clean_name = tool.name.replace("-", "_")
390
-
391
- # Store mapping from cleaned name back to original MCP name
392
- # This is needed because:
393
- # 1. Nova receives tools with cleaned names (utils_get_current_date_information)
394
- # 2. Nova calls tools using cleaned names
395
- # 3. But MCP server expects original names (utils-get_current_date_information)
396
- # 4. So we map back: utils_get_current_date_information -> utils-get_current_date_information
397
- self.tool_name_mapping[clean_name] = tool.name
398
-
399
- bedrock_tool = {
400
- "toolSpec": {
401
- "name": clean_name,
402
- "description": tool.description or f"Tool: {tool.name}",
403
- "inputSchema": {"json": nova_schema},
404
- }
405
- }
406
-
407
- bedrock_tools.append(bedrock_tool)
408
-
409
- self.logger.debug(f"Converted {len(bedrock_tools)} tools for Nova format")
410
- return bedrock_tools
411
-
412
- def _convert_tools_system_prompt_format(self, tools: "ListToolsResult") -> str:
413
- """Convert MCP tools to system prompt format.
414
-
415
- Uses different formats based on the model:
416
- - Scout models: Comprehensive system prompt format
417
- - Other models: Minimal format
418
- """
419
- if not tools.tools:
420
- return ""
421
-
422
- # Create mapping from tool names to original names (no cleaning needed for Llama)
423
- self.tool_name_mapping = {}
424
-
425
- self.logger.debug(
426
- f"Converting {len(tools.tools)} MCP tools to Llama native system prompt format"
427
- )
428
-
429
- # Check if this is a Scout model
430
- model_id = self.default_request_params.model or DEFAULT_BEDROCK_MODEL
431
- clean_model = model_id.replace("bedrock.", "")
432
- is_scout = re.search(r"meta\.llama4-scout", clean_model)
433
-
434
- if is_scout:
435
- # Use comprehensive system prompt format for Scout models
436
- prompt_parts = [
437
- "You are a helpful assistant with access to the following functions. Use them if required:",
438
- "",
439
- ]
440
-
441
- # Add each tool definition in JSON format
442
- for tool in tools.tools:
443
- self.logger.debug(f"Converting MCP tool: {tool.name}")
444
-
445
- # Use original tool name (no hyphen replacement for Llama)
446
- tool_name = tool.name
447
-
448
- # Store mapping (identity mapping since no name cleaning)
449
- self.tool_name_mapping[tool_name] = tool.name
450
-
451
- # Create tool definition in the format Llama expects
452
- tool_def = {
453
- "type": "function",
454
- "function": {
455
- "name": tool_name,
456
- "description": tool.description or f"Tool: {tool.name}",
457
- "parameters": tool.inputSchema or {"type": "object", "properties": {}},
458
- },
459
- }
460
-
461
- prompt_parts.append(json.dumps(tool_def))
462
-
463
- # Add comprehensive response format instructions for Scout
464
- prompt_parts.extend(
465
- [
466
- "",
467
- "## Rules for Function Calling:",
468
- "1. When you need to call a function, use the following format:",
469
- " [function_name(arguments)]",
470
- "2. You can call multiple functions in a single response if needed",
471
- "3. Always provide the function results in your response to the user",
472
- "4. If a function call fails, explain the error and try an alternative approach",
473
- "5. Only call functions when necessary to answer the user's question",
474
- "",
475
- "## Response Rules:",
476
- "- Always provide a complete answer to the user's question",
477
- "- Include function results in your response",
478
- "- Be helpful and informative",
479
- "- If you cannot answer without calling a function, call the appropriate function first",
480
- "",
481
- "## Boundaries:",
482
- "- Only call functions that are explicitly provided above",
483
- "- Do not make up function names or parameters",
484
- "- Follow the exact function signature provided",
485
- "- Always validate your function calls before making them",
486
- ]
487
- )
488
- else:
489
- # Use minimal format for other Llama models
490
- prompt_parts = [
491
- "You have the following tools available to help answer the user's request. You can call one or more functions at a time. The functions are described here in JSON-schema format:",
492
- "",
493
- ]
494
-
495
- # Add each tool definition in JSON format
496
- for tool in tools.tools:
497
- self.logger.debug(f"Converting MCP tool: {tool.name}")
498
-
499
- # Use original tool name (no hyphen replacement for Llama)
500
- tool_name = tool.name
501
-
502
- # Store mapping (identity mapping since no name cleaning)
503
- self.tool_name_mapping[tool_name] = tool.name
504
-
505
- # Create tool definition in the format Llama expects
506
- tool_def = {
507
- "type": "function",
508
- "function": {
509
- "name": tool_name,
510
- "description": tool.description or f"Tool: {tool.name}",
511
- "parameters": tool.inputSchema or {"type": "object", "properties": {}},
512
- },
513
- }
514
-
515
- prompt_parts.append(json.dumps(tool_def))
516
-
517
- # Add the response format instructions based on community best practices
518
- prompt_parts.extend(
519
- [
520
- "",
521
- "To call one or more tools, provide the tool calls on a new line as a JSON-formatted array. Explain your steps in a neutral tone. Then, only call the tools you can for the first step, then end your turn. If you previously received an error, you can try to call the tool again. Give up after 3 errors.",
522
- "",
523
- "Conform precisely to the single-line format of this example:",
524
- "Tool Call:",
525
- '[{"name": "SampleTool", "arguments": {"foo": "bar"}},{"name": "SampleTool", "arguments": {"foo": "other"}}]',
526
- ]
527
- )
528
-
529
- system_prompt = "\n".join(prompt_parts)
530
- self.logger.debug(f"Generated Llama native system prompt: {system_prompt}")
531
-
532
- return system_prompt
533
-
534
- def _convert_tools_anthropic_format(self, tools: "ListToolsResult") -> List[Dict[str, Any]]:
535
- """Convert MCP tools to Anthropic format wrapped in Bedrock toolSpec - preserves raw schema."""
536
- # No tool name mapping needed for Anthropic (uses original names)
537
- self.tool_name_mapping = {}
538
-
539
- self.logger.debug(
540
- f"Converting {len(tools.tools)} MCP tools to Anthropic format with toolSpec wrapper"
541
- )
542
-
543
- bedrock_tools = []
544
- for tool in tools.tools:
545
- self.logger.debug(f"Converting MCP tool: {tool.name}")
546
-
547
- # Store identity mapping (no name cleaning for Anthropic)
548
- self.tool_name_mapping[tool.name] = tool.name
549
-
550
- # Use raw MCP schema (like native Anthropic provider) - no cleaning
551
- input_schema = tool.inputSchema or {"type": "object", "properties": {}}
552
-
553
- # Wrap in Bedrock toolSpec format but preserve raw Anthropic schema
554
- bedrock_tool = {
555
- "toolSpec": {
556
- "name": tool.name, # Original name, no cleaning
557
- "description": tool.description or f"Tool: {tool.name}",
558
- "inputSchema": {
559
- "json": input_schema # Raw MCP schema, not cleaned
560
- },
561
- }
562
- }
563
- bedrock_tools.append(bedrock_tool)
564
-
565
- self.logger.debug(
566
- f"Converted {len(bedrock_tools)} tools to Anthropic format with toolSpec wrapper"
567
- )
568
- return bedrock_tools
569
-
570
- def _convert_mcp_tools_to_bedrock(
571
- self, tools: "ListToolsResult"
572
- ) -> Union[List[Dict[str, Any]], str]:
573
- """Convert MCP tools to appropriate Bedrock format based on model type."""
574
- model_id = self.default_request_params.model or DEFAULT_BEDROCK_MODEL
575
- schema_type = self._get_tool_schema_type(model_id)
576
-
577
- if schema_type == ToolSchemaType.SYSTEM_PROMPT:
578
- system_prompt = self._convert_tools_system_prompt_format(tools)
579
- # Store the system prompt for later use in system message
580
- self._system_prompt_tools = system_prompt
581
- return system_prompt
582
- elif schema_type == ToolSchemaType.ANTHROPIC:
583
- return self._convert_tools_anthropic_format(tools)
584
- else:
585
- return self._convert_tools_nova_format(tools)
586
-
587
- def _add_tools_to_request(
588
- self,
589
- converse_args: Dict[str, Any],
590
- available_tools: Union[List[Dict[str, Any]], str],
591
- model_id: str,
592
- ) -> None:
593
- """Add tools to the request in the appropriate format based on model type."""
594
- schema_type = self._get_tool_schema_type(model_id)
595
-
596
- if schema_type == ToolSchemaType.SYSTEM_PROMPT:
597
- # System prompt models expect tools in the system prompt, not as API parameters
598
- # Tools are already handled in the system prompt generation
599
- self.logger.debug("System prompt tools handled in system prompt")
600
- elif schema_type == ToolSchemaType.ANTHROPIC:
601
- # Anthropic models expect toolConfig with tools array (like native provider)
602
- converse_args["toolConfig"] = {"tools": available_tools}
603
- self.logger.debug(
604
- f"Added {len(available_tools)} tools to Anthropic request in toolConfig format"
605
- )
606
- else:
607
- # Nova models expect toolConfig with toolSpec format
608
- converse_args["toolConfig"] = {"tools": available_tools}
609
- self.logger.debug(
610
- f"Added {len(available_tools)} tools to Nova request in toolConfig format"
611
- )
612
-
613
- def _parse_nova_tool_response(self, processed_response: Dict[str, Any]) -> List[Dict[str, Any]]:
614
- """Parse Nova-format tool response (toolUse format)."""
615
- tool_uses = [
616
- content_item
617
- for content_item in processed_response.get("content", [])
618
- if "toolUse" in content_item
619
- ]
620
-
621
- parsed_tools = []
622
- for tool_use_item in tool_uses:
623
- tool_use = tool_use_item["toolUse"]
624
- parsed_tools.append(
625
- {
626
- "type": "nova",
627
- "name": tool_use["name"],
628
- "arguments": tool_use["input"],
629
- "id": tool_use["toolUseId"],
630
- }
631
- )
632
-
633
- return parsed_tools
634
-
635
- def _parse_system_prompt_tool_response(
636
- self, processed_response: Dict[str, Any]
637
- ) -> List[Dict[str, Any]]:
638
- """Parse system prompt tool response format: function calls in text."""
639
- # Extract text content from the response
640
- text_content = ""
641
- for content_item in processed_response.get("content", []):
642
- if isinstance(content_item, dict) and "text" in content_item:
643
- text_content += content_item["text"]
644
-
645
- if not text_content:
646
- return []
647
-
648
- # Look for different tool call formats
649
- tool_calls = []
650
-
651
- # First try Scout format: [function_name(arguments)]
652
- scout_pattern = r"\[([^(]+)\(([^)]*)\)\]"
653
- scout_matches = re.findall(scout_pattern, text_content)
654
- if scout_matches:
655
- for i, (func_name, args_str) in enumerate(scout_matches):
656
- func_name = func_name.strip()
657
- args_str = args_str.strip()
658
-
659
- # Parse arguments - could be empty, JSON object, or simple values
660
- arguments = {}
661
- if args_str:
662
- try:
663
- # Try to parse as JSON object first
664
- if args_str.startswith("{") and args_str.endswith("}"):
665
- arguments = json.loads(args_str)
666
- else:
667
- # For simple values, create a basic structure
668
- arguments = {"value": args_str}
669
- except json.JSONDecodeError:
670
- # If JSON parsing fails, treat as string
671
- arguments = {"value": args_str}
672
-
673
- tool_calls.append(
674
- {
675
- "type": "system_prompt",
676
- "name": func_name,
677
- "arguments": arguments,
678
- "id": f"system_prompt_{func_name}_{i}",
679
- }
680
- )
681
-
682
- if tool_calls:
683
- return tool_calls
684
-
685
- # Second try: find the "Tool Call:" format
686
- tool_call_match = re.search(r"Tool Call:\s*(\[.*?\])", text_content, re.DOTALL)
687
- if tool_call_match:
688
- json_str = tool_call_match.group(1)
689
- try:
690
- parsed_calls = json.loads(json_str)
691
- if isinstance(parsed_calls, list):
692
- for i, call in enumerate(parsed_calls):
693
- if isinstance(call, dict) and "name" in call:
694
- tool_calls.append(
695
- {
696
- "type": "system_prompt",
697
- "name": call["name"],
698
- "arguments": call.get("arguments", {}),
699
- "id": f"system_prompt_{call['name']}_{i}",
700
- }
701
- )
702
- return tool_calls
703
- except json.JSONDecodeError as e:
704
- self.logger.warning(f"Failed to parse Tool Call JSON array: {json_str} - {e}")
705
-
706
- # Fallback: try to parse any JSON array in the text
707
- array_match = re.search(r"\[.*?\]", text_content, re.DOTALL)
708
- if array_match:
709
- json_str = array_match.group(0)
710
- try:
711
- parsed_calls = json.loads(json_str)
712
- if isinstance(parsed_calls, list):
713
- for i, call in enumerate(parsed_calls):
714
- if isinstance(call, dict) and "name" in call:
715
- tool_calls.append(
716
- {
717
- "type": "system_prompt",
718
- "name": call["name"],
719
- "arguments": call.get("arguments", {}),
720
- "id": f"system_prompt_{call['name']}_{i}",
721
- }
722
- )
723
- return tool_calls
724
- except json.JSONDecodeError as e:
725
- self.logger.warning(f"Failed to parse JSON array: {json_str} - {e}")
726
-
727
- # Fallback: try to parse as single JSON object (backward compatibility)
728
- try:
729
- json_match = re.search(r'\{[^}]*"name"[^}]*"arguments"[^}]*\}', text_content, re.DOTALL)
730
- if json_match:
731
- json_str = json_match.group(0)
732
- function_call = json.loads(json_str)
733
-
734
- if "name" in function_call:
735
- return [
736
- {
737
- "type": "system_prompt",
738
- "name": function_call["name"],
739
- "arguments": function_call.get("arguments", {}),
740
- "id": f"system_prompt_{function_call['name']}",
741
- }
742
- ]
743
-
744
- except json.JSONDecodeError as e:
745
- self.logger.warning(
746
- f"Failed to parse system prompt tool response as JSON: {text_content} - {e}"
747
- )
748
-
749
- # Fallback to old custom tag format in case some models still use it
750
- function_regex = r"<function=([^>]+)>(.*?)</function>"
751
- match = re.search(function_regex, text_content)
752
-
753
- if match:
754
- function_name = match.group(1)
755
- function_args_json = match.group(2)
756
-
757
- try:
758
- function_args = json.loads(function_args_json)
759
- return [
760
- {
761
- "type": "system_prompt",
762
- "name": function_name,
763
- "arguments": function_args,
764
- "id": f"system_prompt_{function_name}",
765
- }
766
- ]
767
- except json.JSONDecodeError:
768
- self.logger.warning(
769
- f"Failed to parse fallback custom tag format: {function_args_json}"
770
- )
771
-
772
- return []
773
-
774
- def _parse_anthropic_tool_response(
775
- self, processed_response: Dict[str, Any]
776
- ) -> List[Dict[str, Any]]:
777
- """Parse Anthropic tool response format (same as native provider)."""
778
- tool_uses = []
779
-
780
- # Look for toolUse in content items (Bedrock format for Anthropic models)
781
- for content_item in processed_response.get("content", []):
782
- if "toolUse" in content_item:
783
- tool_use = content_item["toolUse"]
784
- tool_uses.append(
785
- {
786
- "type": "anthropic",
787
- "name": tool_use["name"],
788
- "arguments": tool_use["input"],
789
- "id": tool_use["toolUseId"],
790
- }
791
- )
792
-
793
- return tool_uses
794
-
795
- def _parse_tool_response(
796
- self, processed_response: Dict[str, Any], model_id: str
797
- ) -> List[Dict[str, Any]]:
798
- """Parse tool response based on model type."""
799
- schema_type = self._get_tool_schema_type(model_id)
800
-
801
- if schema_type == ToolSchemaType.SYSTEM_PROMPT:
802
- return self._parse_system_prompt_tool_response(processed_response)
803
- elif schema_type == ToolSchemaType.ANTHROPIC:
804
- return self._parse_anthropic_tool_response(processed_response)
805
- else:
806
- return self._parse_nova_tool_response(processed_response)
807
-
808
- def _convert_messages_to_bedrock(
809
- self, messages: List[BedrockMessageParam]
810
- ) -> List[Dict[str, Any]]:
811
- """Convert message parameters to Bedrock format."""
812
- bedrock_messages = []
813
- for message in messages:
814
- bedrock_message = {"role": message.get("role", "user"), "content": []}
815
-
816
- content = message.get("content", [])
817
-
818
- if isinstance(content, str):
819
- bedrock_message["content"].append({"text": content})
820
- elif isinstance(content, list):
821
- for item in content:
822
- item_type = item.get("type")
823
- if item_type == "text":
824
- bedrock_message["content"].append({"text": item.get("text", "")})
825
- elif item_type == "tool_use":
826
- bedrock_message["content"].append(
827
- {
828
- "toolUse": {
829
- "toolUseId": item.get("id", ""),
830
- "name": item.get("name", ""),
831
- "input": item.get("input", {}),
832
- }
833
- }
834
- )
835
- elif item_type == "tool_result":
836
- tool_use_id = item.get("tool_use_id")
837
- raw_content = item.get("content", [])
838
- status = item.get("status", "success")
839
-
840
- bedrock_content_list = []
841
- if raw_content:
842
- for part in raw_content:
843
- # FIX: The content parts are dicts, not TextContent objects.
844
- if isinstance(part, dict) and "text" in part:
845
- bedrock_content_list.append({"text": part.get("text", "")})
846
-
847
- # Bedrock requires content for error statuses.
848
- if not bedrock_content_list and status == "error":
849
- bedrock_content_list.append({"text": "Tool call failed with an error."})
850
-
851
- bedrock_message["content"].append(
852
- {
853
- "toolResult": {
854
- "toolUseId": tool_use_id,
855
- "content": bedrock_content_list,
856
- "status": status,
857
- }
858
- }
859
- )
860
-
861
- # Only add the message if it has content
862
- if bedrock_message["content"]:
863
- bedrock_messages.append(bedrock_message)
864
-
865
- return bedrock_messages
866
-
867
- async def _process_stream(self, stream_response, model: str) -> BedrockMessage:
868
- """Process streaming response from Bedrock."""
869
- estimated_tokens = 0
870
- response_content = []
871
- tool_uses = []
872
- stop_reason = None
873
- usage = {"input_tokens": 0, "output_tokens": 0}
874
-
875
- try:
876
- for event in stream_response["stream"]:
877
- if "messageStart" in event:
878
- # Message started
879
- continue
880
- elif "contentBlockStart" in event:
881
- # Content block started
882
- content_block = event["contentBlockStart"]
883
- if "start" in content_block and "toolUse" in content_block["start"]:
884
- # Tool use block started
885
- tool_use_start = content_block["start"]["toolUse"]
886
- self.logger.debug(f"Tool use block started: {tool_use_start}")
887
- tool_uses.append(
888
- {
889
- "toolUse": {
890
- "toolUseId": tool_use_start.get("toolUseId"),
891
- "name": tool_use_start.get("name"),
892
- "input": tool_use_start.get("input", {}),
893
- "_input_accumulator": "", # For accumulating streamed input
894
- }
895
- }
896
- )
897
- elif "contentBlockDelta" in event:
898
- # Content delta received
899
- delta = event["contentBlockDelta"]["delta"]
900
- if "text" in delta:
901
- text = delta["text"]
902
- response_content.append(text)
903
- # Update streaming progress
904
- estimated_tokens = self._update_streaming_progress(
905
- text, model, estimated_tokens
906
- )
907
- elif "toolUse" in delta:
908
- # Tool use delta - handle tool call
909
- tool_use = delta["toolUse"]
910
- self.logger.debug(f"Tool use delta: {tool_use}")
911
- if tool_use and tool_uses:
912
- # Handle input accumulation for streaming tool arguments
913
- if "input" in tool_use:
914
- input_data = tool_use["input"]
915
-
916
- # If input is a dict, merge it directly
917
- if isinstance(input_data, dict):
918
- tool_uses[-1]["toolUse"]["input"].update(input_data)
919
- # If input is a string, accumulate it for later JSON parsing
920
- elif isinstance(input_data, str):
921
- tool_uses[-1]["toolUse"]["_input_accumulator"] += input_data
922
- self.logger.debug(
923
- f"Accumulated input: {tool_uses[-1]['toolUse']['_input_accumulator']}"
924
- )
925
- else:
926
- self.logger.debug(
927
- f"Tool use input is unexpected type: {type(input_data)}: {input_data}"
928
- )
929
- # Set the input directly if it's not a dict or string
930
- tool_uses[-1]["toolUse"]["input"] = input_data
931
- elif "contentBlockStop" in event:
932
- # Content block stopped - finalize any accumulated tool input
933
- if tool_uses:
934
- for tool_use in tool_uses:
935
- if "_input_accumulator" in tool_use["toolUse"]:
936
- accumulated_input = tool_use["toolUse"]["_input_accumulator"]
937
- if accumulated_input:
938
- self.logger.debug(
939
- f"Processing accumulated input: {accumulated_input}"
940
- )
941
- try:
942
- # Try to parse the accumulated input as JSON
943
- parsed_input = json.loads(accumulated_input)
944
- if isinstance(parsed_input, dict):
945
- tool_use["toolUse"]["input"].update(parsed_input)
946
- else:
947
- tool_use["toolUse"]["input"] = parsed_input
948
- self.logger.debug(
949
- f"Successfully parsed accumulated input: {parsed_input}"
950
- )
951
- except json.JSONDecodeError as e:
952
- self.logger.warning(
953
- f"Failed to parse accumulated input as JSON: {accumulated_input} - {e}"
954
- )
955
- # If it's not valid JSON, treat it as a string value
956
- tool_use["toolUse"]["input"] = accumulated_input
957
- # Clean up the accumulator
958
- del tool_use["toolUse"]["_input_accumulator"]
959
- continue
960
- elif "messageStop" in event:
961
- # Message stopped
962
- if "stopReason" in event["messageStop"]:
963
- stop_reason = event["messageStop"]["stopReason"]
964
- elif "metadata" in event:
965
- # Usage metadata
966
- metadata = event["metadata"]
967
- if "usage" in metadata:
968
- usage = metadata["usage"]
969
- actual_tokens = usage.get("outputTokens", 0)
970
- if actual_tokens > 0:
971
- # Emit final progress with actual token count
972
- token_str = str(actual_tokens).rjust(5)
973
- data = {
974
- "progress_action": ProgressAction.STREAMING,
975
- "model": model,
976
- "agent_name": self.name,
977
- "chat_turn": self.chat_turn(),
978
- "details": token_str.strip(),
979
- }
980
- self.logger.info("Streaming progress", data=data)
981
- except Exception as e:
982
- self.logger.error(f"Error processing stream: {e}")
983
- raise
984
-
985
- # Construct the response message
986
- full_text = "".join(response_content)
987
- response = {
988
- "content": [{"text": full_text}] if full_text else [],
989
- "stop_reason": stop_reason or "end_turn",
990
- "usage": {
991
- "input_tokens": usage.get("inputTokens", 0),
992
- "output_tokens": usage.get("outputTokens", 0),
993
- },
994
- "model": model,
995
- "role": "assistant",
996
- }
997
-
998
- # Add tool uses if any
999
- if tool_uses:
1000
- # Clean up any remaining accumulators before adding to response
1001
- for tool_use in tool_uses:
1002
- if "_input_accumulator" in tool_use["toolUse"]:
1003
- accumulated_input = tool_use["toolUse"]["_input_accumulator"]
1004
- if accumulated_input:
1005
- self.logger.debug(
1006
- f"Final processing of accumulated input: {accumulated_input}"
1007
- )
1008
- try:
1009
- # Try to parse the accumulated input as JSON
1010
- parsed_input = json.loads(accumulated_input)
1011
- if isinstance(parsed_input, dict):
1012
- tool_use["toolUse"]["input"].update(parsed_input)
1013
- else:
1014
- tool_use["toolUse"]["input"] = parsed_input
1015
- self.logger.debug(
1016
- f"Successfully parsed final accumulated input: {parsed_input}"
1017
- )
1018
- except json.JSONDecodeError as e:
1019
- self.logger.warning(
1020
- f"Failed to parse final accumulated input as JSON: {accumulated_input} - {e}"
1021
- )
1022
- # If it's not valid JSON, treat it as a string value
1023
- tool_use["toolUse"]["input"] = accumulated_input
1024
- # Clean up the accumulator
1025
- del tool_use["toolUse"]["_input_accumulator"]
1026
-
1027
- response["content"].extend(tool_uses)
1028
-
1029
- return response
1030
-
1031
- def _process_non_streaming_response(self, response, model: str) -> BedrockMessage:
1032
- """Process non-streaming response from Bedrock."""
1033
- self.logger.debug(f"Processing non-streaming response: {response}")
1034
-
1035
- # Extract response content
1036
- content = response.get("output", {}).get("message", {}).get("content", [])
1037
- usage = response.get("usage", {})
1038
- stop_reason = response.get("stopReason", "end_turn")
1039
-
1040
- # Show progress for non-streaming (single update)
1041
- if usage.get("outputTokens", 0) > 0:
1042
- token_str = str(usage.get("outputTokens", 0)).rjust(5)
1043
- data = {
1044
- "progress_action": ProgressAction.STREAMING,
1045
- "model": model,
1046
- "agent_name": self.name,
1047
- "chat_turn": self.chat_turn(),
1048
- "details": token_str.strip(),
1049
- }
1050
- self.logger.info("Non-streaming progress", data=data)
1051
-
1052
- # Convert to the same format as streaming response
1053
- processed_response = {
1054
- "content": content,
1055
- "stop_reason": stop_reason,
1056
- "usage": {
1057
- "input_tokens": usage.get("inputTokens", 0),
1058
- "output_tokens": usage.get("outputTokens", 0),
1059
- },
1060
- "model": model,
1061
- "role": "assistant",
1062
- }
1063
-
1064
- return processed_response
1065
-
1066
- async def _bedrock_completion(
1067
- self,
1068
- message_param: BedrockMessageParam,
1069
- request_params: RequestParams | None = None,
1070
- ) -> List[ContentBlock | CallToolRequestParams]:
1071
- """
1072
- Process a query using Bedrock and available tools.
1073
- """
1074
- client = self._get_bedrock_runtime_client()
1075
-
1076
- try:
1077
- messages: List[BedrockMessageParam] = []
1078
- params = self.get_request_params(request_params)
1079
- except (ClientError, BotoCoreError) as e:
1080
- error_msg = str(e)
1081
- if "UnauthorizedOperation" in error_msg or "AccessDenied" in error_msg:
1082
- raise ProviderKeyError(
1083
- "AWS Bedrock access denied",
1084
- "Please check your AWS credentials and IAM permissions for Bedrock.",
1085
- ) from e
1086
- else:
1087
- raise ProviderKeyError(
1088
- "AWS Bedrock error",
1089
- f"Error accessing Bedrock: {error_msg}",
1090
- ) from e
1091
-
1092
- # Always include prompt messages, but only include conversation history
1093
- # if use_history is True
1094
- messages.extend(self.history.get(include_completion_history=params.use_history))
1095
- messages.append(message_param)
1096
-
1097
- # Get available tools - but only if model supports tool use
1098
- available_tools = []
1099
- tool_list = None
1100
- model_to_check = self.default_request_params.model or DEFAULT_BEDROCK_MODEL
1101
-
1102
- if self._supports_tool_use(model_to_check):
1103
- try:
1104
- tool_list = await self.aggregator.list_tools()
1105
- self.logger.debug(f"Found {len(tool_list.tools)} MCP tools")
1106
-
1107
- available_tools = self._convert_mcp_tools_to_bedrock(tool_list)
1108
- self.logger.debug(
1109
- f"Successfully converted {len(available_tools)} tools for Bedrock"
1110
- )
1111
-
1112
- except Exception as e:
1113
- self.logger.error(f"Error fetching or converting MCP tools: {e}")
1114
- import traceback
1115
-
1116
- self.logger.debug(f"Traceback: {traceback.format_exc()}")
1117
- available_tools = []
1118
- tool_list = None
1119
- else:
1120
- self.logger.info(
1121
- f"Model {model_to_check} does not support tool use - skipping tool preparation"
1122
- )
1123
-
1124
- responses: List[ContentBlock] = []
1125
- model = self.default_request_params.model
1126
-
1127
- for i in range(params.max_iterations):
1128
- self._log_chat_progress(self.chat_turn(), model=model)
1129
-
1130
- # Process tools BEFORE message conversion for Llama native format
1131
- model_to_check = model or DEFAULT_BEDROCK_MODEL
1132
- schema_type = self._get_tool_schema_type(model_to_check)
1133
-
1134
- # For Llama native format, we need to store tools before message conversion
1135
- if schema_type == ToolSchemaType.SYSTEM_PROMPT and available_tools:
1136
- has_tools = bool(available_tools) and (
1137
- (isinstance(available_tools, list) and len(available_tools) > 0)
1138
- or (isinstance(available_tools, str) and available_tools.strip())
1139
- )
1140
-
1141
- if has_tools:
1142
- self._add_tools_to_request({}, available_tools, model_to_check)
1143
- self.logger.debug("Pre-processed Llama native tools for message injection")
1144
-
1145
- # Convert messages to Bedrock format
1146
- bedrock_messages = self._convert_messages_to_bedrock(messages)
1147
-
1148
- # Prepare Bedrock Converse API arguments
1149
- converse_args = {
1150
- "modelId": model,
1151
- "messages": bedrock_messages,
1152
- }
1153
-
1154
- # Add system prompt if available and supported by the model
1155
- system_text = self.instruction or params.systemPrompt
1156
-
1157
- # For Llama native format, inject tools into system prompt
1158
- if (
1159
- schema_type == ToolSchemaType.SYSTEM_PROMPT
1160
- and hasattr(self, "_system_prompt_tools")
1161
- and self._system_prompt_tools
1162
- ):
1163
- # Combine system prompt with tools for Llama native format
1164
- if system_text:
1165
- system_text = f"{system_text}\n\n{self._system_prompt_tools}"
1166
- else:
1167
- system_text = self._system_prompt_tools
1168
- self.logger.debug("Combined system prompt with system prompt tools")
1169
- elif hasattr(self, "_system_prompt_tools") and self._system_prompt_tools:
1170
- # For other formats, combine system prompt with tools
1171
- if system_text:
1172
- system_text = f"{system_text}\n\n{self._system_prompt_tools}"
1173
- else:
1174
- system_text = self._system_prompt_tools
1175
- self.logger.debug("Combined system prompt with tools system prompt")
1176
-
1177
- self.logger.info(
1178
- f"DEBUG: BEFORE CHECK - model='{model_to_check}', has_system_text={bool(system_text)}"
1179
- )
1180
- self.logger.info(
1181
- f"DEBUG: self.instruction='{self.instruction}', params.systemPrompt='{params.systemPrompt}'"
1182
- )
1183
-
1184
- supports_system = self._supports_system_messages(model_to_check)
1185
- self.logger.info(f"DEBUG: supports_system={supports_system}")
1186
-
1187
- if system_text and supports_system:
1188
- converse_args["system"] = [{"text": system_text}]
1189
- self.logger.info(f"DEBUG: Added system prompt to {model_to_check} request")
1190
- elif system_text:
1191
- # For models that don't support system messages, inject system prompt into the first user message
1192
- self.logger.info(
1193
- f"DEBUG: Injecting system prompt into first user message for {model_to_check} (doesn't support system messages)"
1194
- )
1195
- if bedrock_messages and bedrock_messages[0].get("role") == "user":
1196
- first_message = bedrock_messages[0]
1197
- if first_message.get("content") and len(first_message["content"]) > 0:
1198
- # Prepend system instruction to the first user message
1199
- original_text = first_message["content"][0].get("text", "")
1200
- first_message["content"][0]["text"] = (
1201
- f"System: {system_text}\n\nUser: {original_text}"
1202
- )
1203
- self.logger.info("DEBUG: Injected system prompt into first user message")
1204
- else:
1205
- self.logger.info(f"DEBUG: No system text provided for {model_to_check}")
1206
-
1207
- # Add tools if available - format depends on model type (skip for Llama native as already processed)
1208
- if schema_type != ToolSchemaType.SYSTEM_PROMPT:
1209
- has_tools = bool(available_tools) and (
1210
- (isinstance(available_tools, list) and len(available_tools) > 0)
1211
- or (isinstance(available_tools, str) and available_tools.strip())
1212
- )
1213
-
1214
- if has_tools:
1215
- self._add_tools_to_request(converse_args, available_tools, model_to_check)
1216
- else:
1217
- self.logger.debug(
1218
- "No tools available - omitting tool configuration from request"
1219
- )
1220
-
1221
- # Add inference configuration
1222
- inference_config = {}
1223
- if params.maxTokens is not None:
1224
- inference_config["maxTokens"] = params.maxTokens
1225
- if params.stopSequences:
1226
- inference_config["stopSequences"] = params.stopSequences
1227
-
1228
- # Nova-specific recommended settings for tool calling
1229
- if model and "nova" in model.lower():
1230
- inference_config["topP"] = 1.0
1231
- inference_config["temperature"] = 1.0
1232
- # Add additionalModelRequestFields for topK
1233
- converse_args["additionalModelRequestFields"] = {"inferenceConfig": {"topK": 1}}
1234
-
1235
- if inference_config:
1236
- converse_args["inferenceConfig"] = inference_config
1237
-
1238
- self.logger.debug(f"Bedrock converse args: {converse_args}")
1239
-
1240
- # Debug: Print the actual messages being sent to Bedrock for Llama models
1241
- schema_type = self._get_tool_schema_type(model_to_check)
1242
- if schema_type == ToolSchemaType.SYSTEM_PROMPT:
1243
- self.logger.info("=== SYSTEM PROMPT DEBUG ===")
1244
- self.logger.info("Messages being sent to Bedrock:")
1245
- for i, msg in enumerate(converse_args.get("messages", [])):
1246
- self.logger.info(f"Message {i} ({msg.get('role', 'unknown')}):")
1247
- for j, content in enumerate(msg.get("content", [])):
1248
- if "text" in content:
1249
- self.logger.info(f" Content {j}: {content['text'][:500]}...")
1250
- self.logger.info("=== END SYSTEM PROMPT DEBUG ===")
1251
-
1252
- # Debug: Print the full tool config being sent
1253
- if "toolConfig" in converse_args:
1254
- self.logger.debug(
1255
- f"Tool config being sent to Bedrock: {json.dumps(converse_args['toolConfig'], indent=2)}"
1256
- )
1257
-
1258
- try:
1259
- # Choose streaming vs non-streaming based on model capabilities and tool presence
1260
- # Logic: Only use non-streaming when BOTH conditions are true:
1261
- # 1. Tools are available (available_tools is not empty)
1262
- # 2. Model doesn't support streaming with tools
1263
- # Otherwise, always prefer streaming for better UX
1264
- has_tools = bool(available_tools) and (
1265
- (isinstance(available_tools, list) and len(available_tools) > 0)
1266
- or (isinstance(available_tools, str) and available_tools.strip())
1267
- )
1268
-
1269
- if has_tools and not self._supports_streaming_with_tools(
1270
- model or DEFAULT_BEDROCK_MODEL
1271
- ):
1272
- # Use non-streaming API: model requires it for tool calls
1273
- self.logger.debug(
1274
- f"Using non-streaming API for {model} with tools (model limitation)"
1275
- )
1276
- response = client.converse(**converse_args)
1277
- processed_response = self._process_non_streaming_response(
1278
- response, model or DEFAULT_BEDROCK_MODEL
1279
- )
1280
- else:
1281
- # Use streaming API: either no tools OR model supports streaming with tools
1282
- streaming_reason = (
1283
- "no tools present"
1284
- if not has_tools
1285
- else "model supports streaming with tools"
1286
- )
1287
- self.logger.debug(f"Using streaming API for {model} ({streaming_reason})")
1288
- response = client.converse_stream(**converse_args)
1289
- processed_response = await self._process_stream(
1290
- response, model or DEFAULT_BEDROCK_MODEL
1291
- )
1292
- except (ClientError, BotoCoreError) as e:
1293
- error_msg = str(e)
1294
- self.logger.error(f"Bedrock API error: {error_msg}")
1295
-
1296
- # Create error response
1297
- processed_response = {
1298
- "content": [{"text": f"Error during generation: {error_msg}"}],
1299
- "stop_reason": "error",
1300
- "usage": {"input_tokens": 0, "output_tokens": 0},
1301
- "model": model,
1302
- "role": "assistant",
1303
- }
1304
-
1305
- # Track usage
1306
- if processed_response.get("usage"):
1307
- try:
1308
- usage = processed_response["usage"]
1309
- turn_usage = TurnUsage(
1310
- provider=Provider.BEDROCK.value,
1311
- model=model,
1312
- input_tokens=usage.get("input_tokens", 0),
1313
- output_tokens=usage.get("output_tokens", 0),
1314
- total_tokens=usage.get("input_tokens", 0) + usage.get("output_tokens", 0),
1315
- cache_creation_input_tokens=0,
1316
- cache_read_input_tokens=0,
1317
- raw_usage=usage,
1318
- )
1319
- self.usage_accumulator.add_turn(turn_usage)
1320
- except Exception as e:
1321
- self.logger.warning(f"Failed to track usage: {e}")
1322
-
1323
- self.logger.debug(f"{model} response:", data=processed_response)
1324
-
1325
- # Convert response to message param and add to messages
1326
- response_message_param = self.convert_message_to_message_param(processed_response)
1327
- messages.append(response_message_param)
1328
-
1329
- # Extract text content for responses
1330
- if processed_response.get("content"):
1331
- for content_item in processed_response["content"]:
1332
- if content_item.get("text"):
1333
- responses.append(TextContent(type="text", text=content_item["text"]))
1334
-
1335
- # Handle different stop reasons
1336
- stop_reason = processed_response.get("stop_reason", "end_turn")
1337
-
1338
- # For Llama native format, check for tool calls even if stop_reason is "end_turn"
1339
- schema_type = self._get_tool_schema_type(model or DEFAULT_BEDROCK_MODEL)
1340
- if schema_type == ToolSchemaType.SYSTEM_PROMPT and stop_reason == "end_turn":
1341
- # Check if there's a tool call in the response
1342
- parsed_tools = self._parse_tool_response(
1343
- processed_response, model or DEFAULT_BEDROCK_MODEL
1344
- )
1345
- if parsed_tools:
1346
- # Override stop_reason to handle as tool_use
1347
- stop_reason = "tool_use"
1348
- self.logger.debug(
1349
- "Detected system prompt tool call, overriding stop_reason to 'tool_use'"
1350
- )
1351
-
1352
- if stop_reason == "end_turn":
1353
- # Extract text for display
1354
- message_text = ""
1355
- for content_item in processed_response.get("content", []):
1356
- if content_item.get("text"):
1357
- message_text += content_item["text"]
1358
-
1359
- await self.show_assistant_message(message_text)
1360
- self.logger.debug(f"Iteration {i}: Stopping because stop_reason is 'end_turn'")
1361
- break
1362
- elif stop_reason == "stop_sequence":
1363
- self.logger.debug(f"Iteration {i}: Stopping because stop_reason is 'stop_sequence'")
1364
- break
1365
- elif stop_reason == "max_tokens":
1366
- self.logger.debug(f"Iteration {i}: Stopping because stop_reason is 'max_tokens'")
1367
- if params.maxTokens is not None:
1368
- message_text = Text(
1369
- f"the assistant has reached the maximum token limit ({params.maxTokens})",
1370
- style="dim green italic",
1371
- )
1372
- else:
1373
- message_text = Text(
1374
- "the assistant has reached the maximum token limit",
1375
- style="dim green italic",
1376
- )
1377
- await self.show_assistant_message(message_text)
1378
- break
1379
- elif stop_reason in ["tool_use", "tool_calls"]:
1380
- # Handle tool use/calls - format depends on model type
1381
- message_text = ""
1382
- for content_item in processed_response.get("content", []):
1383
- if content_item.get("text"):
1384
- message_text += content_item["text"]
1385
-
1386
- # Parse tool calls using model-specific method
1387
- self.logger.info(f"DEBUG: About to parse tool response: {processed_response}")
1388
- parsed_tools = self._parse_tool_response(
1389
- processed_response, model or DEFAULT_BEDROCK_MODEL
1390
- )
1391
- self.logger.info(f"DEBUG: Parsed tools: {parsed_tools}")
1392
-
1393
- if parsed_tools:
1394
- # We will comment out showing the assistant's intermediate message
1395
- # to make the output less chatty, as requested by the user.
1396
- # if not message_text:
1397
- # message_text = Text(
1398
- # "the assistant requested tool calls",
1399
- # style="dim green italic",
1400
- # )
1401
- #
1402
- # await self.show_assistant_message(message_text)
1403
-
1404
- # Process tool calls and collect results
1405
- tool_results_for_batch = []
1406
- for tool_idx, parsed_tool in enumerate(parsed_tools):
1407
- # The original name is needed to call the tool, which is in tool_name_mapping.
1408
- tool_name_from_model = parsed_tool["name"]
1409
- tool_name = self.tool_name_mapping.get(
1410
- tool_name_from_model, tool_name_from_model
1411
- )
1412
-
1413
- tool_args = parsed_tool["arguments"]
1414
- tool_use_id = parsed_tool["id"]
1415
-
1416
- self.show_tool_call(tool_list.tools, tool_name, tool_args)
1417
-
1418
- tool_call_request = CallToolRequest(
1419
- method="tools/call",
1420
- params=CallToolRequestParams(name=tool_name, arguments=tool_args),
1421
- )
1422
-
1423
- # Call the tool and get the result
1424
- result = await self.call_tool(
1425
- request=tool_call_request, tool_call_id=tool_use_id
1426
- )
1427
- # We will also comment out showing the raw tool result to reduce verbosity.
1428
- # self.show_tool_result(result)
1429
-
1430
- # Add each result to our collection
1431
- tool_results_for_batch.append((tool_use_id, result, tool_name))
1432
- responses.extend(result.content)
1433
-
1434
- # After processing all tool calls for a turn, clear the intermediate
1435
- # responses. This ensures that the final returned value only contains
1436
- # the model's last message, not the reasoning or raw tool output.
1437
- responses.clear()
1438
-
1439
- # Now, create the message with tool results based on the model's schema type.
1440
- schema_type = self._get_tool_schema_type(model or DEFAULT_BEDROCK_MODEL)
1441
-
1442
- if schema_type == ToolSchemaType.SYSTEM_PROMPT:
1443
- # For system prompt models (like Llama), format results as a simple text message.
1444
- # The model expects to see the results in a human-readable format to continue.
1445
- tool_result_parts = []
1446
- for _, tool_result, tool_name in tool_results_for_batch:
1447
- result_text = "".join(
1448
- [
1449
- part.text
1450
- for part in tool_result.content
1451
- if isinstance(part, TextContent)
1452
- ]
1453
- )
1454
-
1455
- # Create a representation of the tool's output.
1456
- # Using a JSON-like string is a robust way to present this.
1457
- result_payload = {
1458
- "tool_name": tool_name,
1459
- "status": "error" if tool_result.isError else "success",
1460
- "result": result_text,
1461
- }
1462
- tool_result_parts.append(json.dumps(result_payload))
1463
-
1464
- if tool_result_parts:
1465
- # Combine all tool results into a single text block.
1466
- full_result_text = f"Tool Results:\n{', '.join(tool_result_parts)}"
1467
- messages.append(
1468
- {
1469
- "role": "user",
1470
- "content": [{"type": "text", "text": full_result_text}],
1471
- }
1472
- )
1473
- else:
1474
- # For native tool-using models (Anthropic, Nova), use the structured 'tool_result' format.
1475
- tool_result_blocks = []
1476
- for tool_id, tool_result, _ in tool_results_for_batch:
1477
- # Convert tool result content into a list of content blocks
1478
- # This mimics the native Anthropic provider's approach.
1479
- result_content_blocks = []
1480
- if tool_result.content:
1481
- for part in tool_result.content:
1482
- if isinstance(part, TextContent):
1483
- result_content_blocks.append({"text": part.text})
1484
- # Note: This can be extended to handle other content types like images
1485
- # For now, we are focusing on making text-based tools work correctly.
1486
-
1487
- # If there's no content, provide a default message.
1488
- if not result_content_blocks:
1489
- result_content_blocks.append(
1490
- {"text": "[No content in tool result]"}
1491
- )
1492
-
1493
- # This is the format Bedrock expects for tool results in the Converse API
1494
- tool_result_blocks.append(
1495
- {
1496
- "type": "tool_result",
1497
- "tool_use_id": tool_id,
1498
- "content": result_content_blocks,
1499
- "status": "error" if tool_result.isError else "success",
1500
- }
1501
- )
1502
-
1503
- if tool_result_blocks:
1504
- # Append a single user message with all the tool results for this turn
1505
- messages.append(
1506
- {
1507
- "role": "user",
1508
- "content": tool_result_blocks,
1509
- }
1510
- )
1511
-
1512
- continue
1513
- else:
1514
- # No tool uses but stop_reason was tool_use/tool_calls, treat as end_turn
1515
- await self.show_assistant_message(message_text)
1516
- break
1517
- else:
1518
- # Unknown stop reason, continue or break based on content
1519
- message_text = ""
1520
- for content_item in processed_response.get("content", []):
1521
- if content_item.get("text"):
1522
- message_text += content_item["text"]
1523
-
1524
- if message_text:
1525
- await self.show_assistant_message(message_text)
1526
- break
1527
-
1528
- # Update history
1529
- if params.use_history:
1530
- # Get current prompt messages
1531
- prompt_messages = self.history.get(include_completion_history=False)
1532
-
1533
- # Calculate new conversation messages (excluding prompts)
1534
- new_messages = messages[len(prompt_messages) :]
1535
-
1536
- # Remove system prompt from new messages if it was added
1537
- if (self.instruction or params.systemPrompt) and new_messages:
1538
- # System prompt is not added to messages list in Bedrock, so no need to remove it
1539
- pass
1540
-
1541
- self.history.set(new_messages)
1542
-
1543
- # Strip leading whitespace from the *last* non-empty text block of the final response
1544
- # to ensure the output is clean.
1545
- if responses:
1546
- for item in reversed(responses):
1547
- if isinstance(item, TextContent) and item.text:
1548
- item.text = item.text.lstrip()
1549
- break
1550
-
1551
- return responses
1552
-
1553
- async def generate_messages(
1554
- self,
1555
- message_param: BedrockMessageParam,
1556
- request_params: RequestParams | None = None,
1557
- ) -> PromptMessageMultipart:
1558
- """Generate messages using Bedrock."""
1559
- responses = await self._bedrock_completion(message_param, request_params)
1560
-
1561
- # Convert responses to PromptMessageMultipart
1562
- content_list = []
1563
- for response in responses:
1564
- if isinstance(response, TextContent):
1565
- content_list.append(response)
1566
-
1567
- return PromptMessageMultipart(role="assistant", content=content_list)
1568
-
1569
- async def _apply_prompt_provider_specific(
1570
- self,
1571
- multipart_messages: List[PromptMessageMultipart],
1572
- request_params: RequestParams | None = None,
1573
- is_template: bool = False,
1574
- ) -> PromptMessageMultipart:
1575
- """Apply Bedrock-specific prompt formatting."""
1576
- if not multipart_messages:
1577
- return PromptMessageMultipart(role="user", content=[])
1578
-
1579
- # Check the last message role
1580
- last_message = multipart_messages[-1]
1581
-
1582
- # Add all previous messages to history (or all messages if last is from assistant)
1583
- # if the last message is a "user" inference is required
1584
- messages_to_add = (
1585
- multipart_messages[:-1] if last_message.role == "user" else multipart_messages
1586
- )
1587
- converted = []
1588
- for msg in messages_to_add:
1589
- # Convert each message to Bedrock message parameter format
1590
- bedrock_msg = {"role": msg.role, "content": []}
1591
- for content_item in msg.content:
1592
- if isinstance(content_item, TextContent):
1593
- bedrock_msg["content"].append({"type": "text", "text": content_item.text})
1594
- converted.append(bedrock_msg)
1595
-
1596
- # Add messages to history
1597
- self.history.extend(converted, is_prompt=is_template)
1598
-
1599
- if last_message.role == "assistant":
1600
- # For assistant messages: Return the last message (no completion needed)
1601
- return last_message
1602
-
1603
- # Convert the last user message to Bedrock message parameter format
1604
- message_param = {"role": last_message.role, "content": []}
1605
- for content_item in last_message.content:
1606
- if isinstance(content_item, TextContent):
1607
- message_param["content"].append({"type": "text", "text": content_item.text})
1608
-
1609
- # Generate response
1610
- return await self.generate_messages(message_param, request_params)
1611
-
1612
- def _generate_simplified_schema(self, model: Type[ModelT]) -> str:
1613
- """Generates a simplified, human-readable schema with inline enum constraints."""
1614
-
1615
- def get_field_type_representation(field_type: Any) -> Any:
1616
- """Get a string representation for a field type."""
1617
- # Handle Optional types
1618
- if hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
1619
- non_none_types = [t for t in field_type.__args__ if t is not type(None)]
1620
- if non_none_types:
1621
- field_type = non_none_types[0]
1622
-
1623
- # Handle basic types
1624
- if field_type is str:
1625
- return "string"
1626
- elif field_type is int:
1627
- return "integer"
1628
- elif field_type is float:
1629
- return "float"
1630
- elif field_type is bool:
1631
- return "boolean"
1632
-
1633
- # Handle Enum types
1634
- elif hasattr(field_type, "__bases__") and any(
1635
- issubclass(base, Enum) for base in field_type.__bases__ if isinstance(base, type)
1636
- ):
1637
- enum_values = [f'"{e.value}"' for e in field_type]
1638
- return f"string (must be one of: {', '.join(enum_values)})"
1639
-
1640
- # Handle List types
1641
- elif (
1642
- hasattr(field_type, "__origin__")
1643
- and hasattr(field_type, "__args__")
1644
- and field_type.__origin__ is list
1645
- ):
1646
- item_type_repr = "any"
1647
- if field_type.__args__:
1648
- item_type_repr = get_field_type_representation(field_type.__args__[0])
1649
- return [item_type_repr]
1650
-
1651
- # Handle nested Pydantic models
1652
- elif hasattr(field_type, "__bases__") and any(
1653
- hasattr(base, "model_fields") for base in field_type.__bases__
1654
- ):
1655
- nested_schema = _generate_schema_dict(field_type)
1656
- return nested_schema
1657
-
1658
- # Default fallback
1659
- else:
1660
- return "any"
1661
-
1662
- def _generate_schema_dict(model_class: Type) -> Dict[str, Any]:
1663
- """Recursively generate the schema as a dictionary."""
1664
- schema_dict = {}
1665
- if hasattr(model_class, "model_fields"):
1666
- for field_name, field_info in model_class.model_fields.items():
1667
- schema_dict[field_name] = get_field_type_representation(field_info.annotation)
1668
- return schema_dict
1669
-
1670
- schema = _generate_schema_dict(model)
1671
- return json.dumps(schema, indent=2)
1672
-
1673
- async def _apply_prompt_provider_specific_structured(
1674
- self,
1675
- multipart_messages: List[PromptMessageMultipart],
1676
- model: Type[ModelT],
1677
- request_params: RequestParams | None = None,
1678
- ) -> Tuple[ModelT | None, PromptMessageMultipart]:
1679
- """Apply structured output for Bedrock using prompt engineering with a simplified schema."""
1680
- request_params = self.get_request_params(request_params)
1681
-
1682
- # Generate a simplified, human-readable schema
1683
- simplified_schema = self._generate_simplified_schema(model)
1684
-
1685
- # Build the new simplified prompt
1686
- prompt_parts = [
1687
- "You are a JSON generator. Respond with JSON that strictly follows the provided schema. Do not add any commentary or explanation.",
1688
- "",
1689
- "JSON Schema:",
1690
- simplified_schema,
1691
- "",
1692
- "IMPORTANT RULES:",
1693
- "- You MUST respond with only raw JSON data. No other text, commentary, or markdown is allowed.",
1694
- "- All field names and enum values are case-sensitive and must match the schema exactly.",
1695
- "- Do not add any extra fields to the JSON response. Only include the fields specified in the schema.",
1696
- "- Valid JSON requires double quotes for all field names and string values. Other types (int, float, boolean, etc.) should not be quoted.",
1697
- "",
1698
- "Now, generate the valid JSON response for the following request:",
1699
- ]
1700
-
1701
- # Add the new prompt to the last user message
1702
- multipart_messages[-1].add_text("\n".join(prompt_parts))
1703
-
1704
- self.logger.info(f"DEBUG: Prompt messages: {multipart_messages[-1].content}")
1705
-
1706
- result: PromptMessageMultipart = await self._apply_prompt_provider_specific(
1707
- multipart_messages, request_params
1708
- )
1709
- return self._structured_from_multipart(result, model)
1710
-
1711
- def _clean_json_response(self, text: str) -> str:
1712
- """Clean up JSON response by removing text before first { and after last }."""
1713
- if not text:
1714
- return text
1715
-
1716
- # Find the first { and last }
1717
- first_brace = text.find("{")
1718
- last_brace = text.rfind("}")
1719
-
1720
- # If we found both braces, extract just the JSON part
1721
- if first_brace != -1 and last_brace != -1 and first_brace < last_brace:
1722
- return text[first_brace : last_brace + 1]
1723
-
1724
- # Otherwise return the original text
1725
- return text
1726
-
1727
- def _structured_from_multipart(
1728
- self, message: PromptMessageMultipart, model: Type[ModelT]
1729
- ) -> Tuple[ModelT | None, PromptMessageMultipart]:
1730
- """Override to apply JSON cleaning before parsing."""
1731
- # Get the text from the multipart message
1732
- text = message.all_text()
1733
-
1734
- # Clean the JSON response to remove extra text
1735
- cleaned_text = self._clean_json_response(text)
1736
-
1737
- # If we cleaned the text, create a new multipart with the cleaned text
1738
- if cleaned_text != text:
1739
- from mcp.types import TextContent
1740
-
1741
- cleaned_multipart = PromptMessageMultipart(
1742
- role=message.role, content=[TextContent(type="text", text=cleaned_text)]
1743
- )
1744
- else:
1745
- cleaned_multipart = message
1746
-
1747
- # Use the parent class method with the cleaned multipart
1748
- return super()._structured_from_multipart(cleaned_multipart, model)
1749
-
1750
- @classmethod
1751
- def convert_message_to_message_param(
1752
- cls, message: BedrockMessage, **kwargs
1753
- ) -> BedrockMessageParam:
1754
- """Convert a Bedrock message to message parameter format."""
1755
- message_param = {"role": message.get("role", "assistant"), "content": []}
1756
-
1757
- for content_item in message.get("content", []):
1758
- if isinstance(content_item, dict):
1759
- if "text" in content_item:
1760
- message_param["content"].append({"type": "text", "text": content_item["text"]})
1761
- elif "toolUse" in content_item:
1762
- tool_use = content_item["toolUse"]
1763
- tool_input = tool_use.get("input", {})
1764
-
1765
- # Ensure tool_input is a dictionary
1766
- if not isinstance(tool_input, dict):
1767
- if isinstance(tool_input, str):
1768
- try:
1769
- tool_input = json.loads(tool_input) if tool_input else {}
1770
- except json.JSONDecodeError:
1771
- tool_input = {}
1772
- else:
1773
- tool_input = {}
1774
-
1775
- message_param["content"].append(
1776
- {
1777
- "type": "tool_use",
1778
- "id": tool_use.get("toolUseId", ""),
1779
- "name": tool_use.get("name", ""),
1780
- "input": tool_input,
1781
- }
1782
- )
1783
-
1784
- return message_param
1785
-
1786
- def _api_key(self) -> str:
1787
- """Bedrock doesn't use API keys, returns empty string."""
1788
- return ""