fast-agent-mcp 0.2.57__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/__init__.py +127 -0
- fast_agent/agents/__init__.py +36 -0
- {mcp_agent/core → fast_agent/agents}/agent_types.py +2 -1
- fast_agent/agents/llm_agent.py +217 -0
- fast_agent/agents/llm_decorator.py +486 -0
- mcp_agent/agents/base_agent.py → fast_agent/agents/mcp_agent.py +377 -385
- fast_agent/agents/tool_agent.py +168 -0
- {mcp_agent → fast_agent}/agents/workflow/chain_agent.py +43 -33
- {mcp_agent → fast_agent}/agents/workflow/evaluator_optimizer.py +31 -35
- {mcp_agent → fast_agent}/agents/workflow/iterative_planner.py +56 -47
- {mcp_agent → fast_agent}/agents/workflow/orchestrator_models.py +4 -4
- {mcp_agent → fast_agent}/agents/workflow/parallel_agent.py +34 -41
- {mcp_agent → fast_agent}/agents/workflow/router_agent.py +54 -39
- {mcp_agent → fast_agent}/cli/__main__.py +5 -3
- {mcp_agent → fast_agent}/cli/commands/check_config.py +95 -66
- {mcp_agent → fast_agent}/cli/commands/go.py +20 -11
- {mcp_agent → fast_agent}/cli/commands/quickstart.py +4 -4
- {mcp_agent → fast_agent}/cli/commands/server_helpers.py +1 -1
- {mcp_agent → fast_agent}/cli/commands/setup.py +64 -134
- {mcp_agent → fast_agent}/cli/commands/url_parser.py +9 -8
- {mcp_agent → fast_agent}/cli/main.py +36 -16
- {mcp_agent → fast_agent}/cli/terminal.py +2 -2
- {mcp_agent → fast_agent}/config.py +13 -2
- fast_agent/constants.py +8 -0
- {mcp_agent → fast_agent}/context.py +24 -19
- {mcp_agent → fast_agent}/context_dependent.py +9 -5
- fast_agent/core/__init__.py +17 -0
- {mcp_agent → fast_agent}/core/agent_app.py +39 -36
- fast_agent/core/core_app.py +135 -0
- {mcp_agent → fast_agent}/core/direct_decorators.py +12 -26
- {mcp_agent → fast_agent}/core/direct_factory.py +95 -73
- {mcp_agent → fast_agent/core}/executor/executor.py +4 -5
- {mcp_agent → fast_agent}/core/fastagent.py +32 -32
- fast_agent/core/logging/__init__.py +5 -0
- {mcp_agent → fast_agent/core}/logging/events.py +3 -3
- {mcp_agent → fast_agent/core}/logging/json_serializer.py +1 -1
- {mcp_agent → fast_agent/core}/logging/listeners.py +85 -7
- {mcp_agent → fast_agent/core}/logging/logger.py +7 -7
- {mcp_agent → fast_agent/core}/logging/transport.py +10 -11
- fast_agent/core/prompt.py +9 -0
- {mcp_agent → fast_agent}/core/validation.py +4 -4
- fast_agent/event_progress.py +61 -0
- fast_agent/history/history_exporter.py +44 -0
- {mcp_agent → fast_agent}/human_input/__init__.py +9 -12
- {mcp_agent → fast_agent}/human_input/elicitation_handler.py +26 -8
- {mcp_agent → fast_agent}/human_input/elicitation_state.py +7 -7
- {mcp_agent → fast_agent}/human_input/simple_form.py +6 -4
- {mcp_agent → fast_agent}/human_input/types.py +1 -18
- fast_agent/interfaces.py +228 -0
- fast_agent/llm/__init__.py +9 -0
- mcp_agent/llm/augmented_llm.py → fast_agent/llm/fastagent_llm.py +128 -218
- fast_agent/llm/internal/passthrough.py +137 -0
- mcp_agent/llm/augmented_llm_playback.py → fast_agent/llm/internal/playback.py +29 -25
- mcp_agent/llm/augmented_llm_silent.py → fast_agent/llm/internal/silent.py +10 -17
- fast_agent/llm/internal/slow.py +38 -0
- {mcp_agent → fast_agent}/llm/memory.py +40 -30
- {mcp_agent → fast_agent}/llm/model_database.py +35 -2
- {mcp_agent → fast_agent}/llm/model_factory.py +103 -77
- fast_agent/llm/model_info.py +126 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/anthropic_utils.py +7 -7
- fast_agent/llm/provider/anthropic/llm_anthropic.py +603 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/multipart_converter_anthropic.py +79 -86
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2192 -0
- {mcp_agent/llm/providers → fast_agent/llm/provider/google}/google_converter.py +66 -14
- fast_agent/llm/provider/google/llm_google_native.py +431 -0
- mcp_agent/llm/providers/augmented_llm_aliyun.py → fast_agent/llm/provider/openai/llm_aliyun.py +6 -7
- mcp_agent/llm/providers/augmented_llm_azure.py → fast_agent/llm/provider/openai/llm_azure.py +4 -4
- mcp_agent/llm/providers/augmented_llm_deepseek.py → fast_agent/llm/provider/openai/llm_deepseek.py +10 -11
- mcp_agent/llm/providers/augmented_llm_generic.py → fast_agent/llm/provider/openai/llm_generic.py +4 -4
- mcp_agent/llm/providers/augmented_llm_google_oai.py → fast_agent/llm/provider/openai/llm_google_oai.py +4 -4
- mcp_agent/llm/providers/augmented_llm_groq.py → fast_agent/llm/provider/openai/llm_groq.py +14 -16
- mcp_agent/llm/providers/augmented_llm_openai.py → fast_agent/llm/provider/openai/llm_openai.py +133 -206
- mcp_agent/llm/providers/augmented_llm_openrouter.py → fast_agent/llm/provider/openai/llm_openrouter.py +6 -6
- mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py → fast_agent/llm/provider/openai/llm_tensorzero_openai.py +17 -16
- mcp_agent/llm/providers/augmented_llm_xai.py → fast_agent/llm/provider/openai/llm_xai.py +6 -6
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/multipart_converter_openai.py +125 -63
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_multipart.py +12 -12
- {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_utils.py +18 -16
- {mcp_agent → fast_agent}/llm/provider_key_manager.py +2 -2
- {mcp_agent → fast_agent}/llm/provider_types.py +2 -0
- {mcp_agent → fast_agent}/llm/sampling_converter.py +15 -12
- {mcp_agent → fast_agent}/llm/usage_tracking.py +23 -5
- fast_agent/mcp/__init__.py +43 -0
- {mcp_agent → fast_agent}/mcp/elicitation_factory.py +3 -3
- {mcp_agent → fast_agent}/mcp/elicitation_handlers.py +19 -10
- {mcp_agent → fast_agent}/mcp/gen_client.py +3 -3
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +183 -0
- {mcp_agent → fast_agent}/mcp/helpers/server_config_helpers.py +8 -8
- {mcp_agent → fast_agent}/mcp/hf_auth.py +25 -23
- fast_agent/mcp/interfaces.py +93 -0
- {mcp_agent → fast_agent}/mcp/logger_textio.py +4 -4
- {mcp_agent → fast_agent}/mcp/mcp_agent_client_session.py +49 -44
- {mcp_agent → fast_agent}/mcp/mcp_aggregator.py +66 -115
- {mcp_agent → fast_agent}/mcp/mcp_connection_manager.py +16 -23
- {mcp_agent/core → fast_agent/mcp}/mcp_content.py +23 -15
- {mcp_agent → fast_agent}/mcp/mime_utils.py +39 -0
- fast_agent/mcp/prompt.py +159 -0
- mcp_agent/mcp/prompt_message_multipart.py → fast_agent/mcp/prompt_message_extended.py +27 -20
- {mcp_agent → fast_agent}/mcp/prompt_render.py +21 -19
- {mcp_agent → fast_agent}/mcp/prompt_serialization.py +46 -46
- fast_agent/mcp/prompts/__main__.py +7 -0
- {mcp_agent → fast_agent}/mcp/prompts/prompt_helpers.py +31 -30
- {mcp_agent → fast_agent}/mcp/prompts/prompt_load.py +8 -8
- {mcp_agent → fast_agent}/mcp/prompts/prompt_server.py +11 -19
- {mcp_agent → fast_agent}/mcp/prompts/prompt_template.py +18 -18
- {mcp_agent → fast_agent}/mcp/resource_utils.py +1 -1
- {mcp_agent → fast_agent}/mcp/sampling.py +31 -26
- {mcp_agent/mcp_server → fast_agent/mcp/server}/__init__.py +1 -1
- {mcp_agent/mcp_server → fast_agent/mcp/server}/agent_server.py +5 -6
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +90 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis-campaign.py +5 -4
- {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +25 -3
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
- {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
- {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
- {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
- {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
- fast_agent/resources/setup/.gitignore +24 -0
- fast_agent/resources/setup/agent.py +18 -0
- fast_agent/resources/setup/fastagent.config.yaml +44 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/types/__init__.py +32 -0
- fast_agent/types/llm_stop_reason.py +77 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console_display.py +1005 -0
- {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +56 -39
- mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
- {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
- {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
- fast_agent/ui/mcp_ui_utils.py +224 -0
- {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
- {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
- {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/METADATA +7 -7
- fast_agent_mcp-0.3.0.dist-info/RECORD +202 -0
- fast_agent_mcp-0.3.0.dist-info/entry_points.txt +5 -0
- fast_agent_mcp-0.2.57.dist-info/RECORD +0 -192
- fast_agent_mcp-0.2.57.dist-info/entry_points.txt +0 -6
- mcp_agent/__init__.py +0 -114
- mcp_agent/agents/agent.py +0 -92
- mcp_agent/agents/workflow/__init__.py +0 -1
- mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
- mcp_agent/app.py +0 -175
- mcp_agent/core/__init__.py +0 -26
- mcp_agent/core/prompt.py +0 -191
- mcp_agent/event_progress.py +0 -134
- mcp_agent/human_input/handler.py +0 -81
- mcp_agent/llm/__init__.py +0 -2
- mcp_agent/llm/augmented_llm_passthrough.py +0 -232
- mcp_agent/llm/augmented_llm_slow.py +0 -53
- mcp_agent/llm/providers/__init__.py +0 -8
- mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -717
- mcp_agent/llm/providers/augmented_llm_bedrock.py +0 -1788
- mcp_agent/llm/providers/augmented_llm_google_native.py +0 -495
- mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
- mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
- mcp_agent/llm/sampling_format_converter.py +0 -37
- mcp_agent/logging/__init__.py +0 -0
- mcp_agent/mcp/__init__.py +0 -50
- mcp_agent/mcp/helpers/__init__.py +0 -25
- mcp_agent/mcp/helpers/content_helpers.py +0 -187
- mcp_agent/mcp/interfaces.py +0 -266
- mcp_agent/mcp/prompts/__init__.py +0 -0
- mcp_agent/mcp/prompts/__main__.py +0 -10
- mcp_agent/mcp_server_registry.py +0 -343
- mcp_agent/tools/tool_definition.py +0 -14
- mcp_agent/ui/console_display.py +0 -790
- mcp_agent/ui/console_display_legacy.py +0 -401
- {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
- {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
- {mcp_agent → fast_agent}/cli/constants.py +0 -0
- {mcp_agent → fast_agent}/core/error_handling.py +0 -0
- {mcp_agent → fast_agent}/core/exceptions.py +0 -0
- {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
- {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
- {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
- {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
- {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
- {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
- {mcp_agent → fast_agent}/mcp/common.py +0 -0
- {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
- {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
- {mcp_agent → fast_agent}/py.typed +0 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
- {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
- {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
- {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
- {mcp_agent → fast_agent/ui}/console.py +0 -0
- {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.57.dist-info → fast_agent_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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 ""
|