mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-20 00:22:31 +00:00
fix(provider): preserve Bedrock tool config for history
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ef268f47d2
commit
07f9ab580a
@ -18,6 +18,7 @@ _IMAGE_DATA_URL = re.compile(r"^data:image/([a-zA-Z0-9.+-]+);base64,(.*)$", re.D
|
|||||||
_TEXT_BLOCK_TYPES = {"text", "input_text", "output_text"}
|
_TEXT_BLOCK_TYPES = {"text", "input_text", "output_text"}
|
||||||
_TEMPERATURE_UNSUPPORTED_MODEL_TOKENS = ("claude-opus-4-7",)
|
_TEMPERATURE_UNSUPPORTED_MODEL_TOKENS = ("claude-opus-4-7",)
|
||||||
_ADAPTIVE_THINKING_ONLY_MODEL_TOKENS = ("claude-opus-4-7",)
|
_ADAPTIVE_THINKING_ONLY_MODEL_TOKENS = ("claude-opus-4-7",)
|
||||||
|
_NOOP_TOOL_NAME = "nanobot_noop"
|
||||||
|
|
||||||
|
|
||||||
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
||||||
@ -325,6 +326,27 @@ class BedrockProvider(LLMProvider):
|
|||||||
result.append({"toolSpec": spec})
|
result.append({"toolSpec": spec})
|
||||||
return result or None
|
return result or None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _contains_tool_blocks(messages: list[dict[str, Any]]) -> bool:
|
||||||
|
for msg in messages:
|
||||||
|
content = msg.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
continue
|
||||||
|
for block in content:
|
||||||
|
if isinstance(block, dict) and ("toolUse" in block or "toolResult" in block):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _noop_tool() -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"toolSpec": {
|
||||||
|
"name": _NOOP_TOOL_NAME,
|
||||||
|
"description": "Internal placeholder for Bedrock tool history validation.",
|
||||||
|
"inputSchema": {"json": {"type": "object", "properties": {}}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_tool_choice(
|
def _convert_tool_choice(
|
||||||
tool_choice: str | dict[str, Any] | None,
|
tool_choice: str | dict[str, Any] | None,
|
||||||
@ -389,11 +411,16 @@ class BedrockProvider(LLMProvider):
|
|||||||
kwargs["additionalModelRequestFields"] = additional
|
kwargs["additionalModelRequestFields"] = additional
|
||||||
|
|
||||||
bedrock_tools = self._convert_tools(tools)
|
bedrock_tools = self._convert_tools(tools)
|
||||||
|
tool_config: dict[str, Any] | None = None
|
||||||
if bedrock_tools:
|
if bedrock_tools:
|
||||||
tool_config: dict[str, Any] = {"tools": bedrock_tools}
|
tool_config = {"tools": bedrock_tools}
|
||||||
choice = self._convert_tool_choice(tool_choice)
|
choice = self._convert_tool_choice(tool_choice)
|
||||||
if choice:
|
if choice:
|
||||||
tool_config["toolChoice"] = choice
|
tool_config["toolChoice"] = choice
|
||||||
|
elif self._contains_tool_blocks(bedrock_messages):
|
||||||
|
tool_config = {"tools": [self._noop_tool()]}
|
||||||
|
|
||||||
|
if tool_config:
|
||||||
kwargs["toolConfig"] = tool_config
|
kwargs["toolConfig"] = tool_config
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|||||||
@ -106,6 +106,7 @@ def test_generic_bedrock_model_keeps_temperature_and_skips_anthropic_thinking()
|
|||||||
assert kwargs["modelId"] == "amazon.nova-lite-v1:0"
|
assert kwargs["modelId"] == "amazon.nova-lite-v1:0"
|
||||||
assert kwargs["inferenceConfig"] == {"maxTokens": 1024, "temperature": 0.3}
|
assert kwargs["inferenceConfig"] == {"maxTokens": 1024, "temperature": 0.3}
|
||||||
assert "additionalModelRequestFields" not in kwargs
|
assert "additionalModelRequestFields" not in kwargs
|
||||||
|
assert "toolConfig" not in kwargs
|
||||||
|
|
||||||
|
|
||||||
def test_build_kwargs_converts_messages_tools_and_tool_results() -> None:
|
def test_build_kwargs_converts_messages_tools_and_tool_results() -> None:
|
||||||
@ -160,6 +161,39 @@ def test_build_kwargs_converts_messages_tools_and_tool_results() -> None:
|
|||||||
assert kwargs["toolConfig"]["toolChoice"] == {"any": {}}
|
assert kwargs["toolConfig"]["toolChoice"] == {"any": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_kwargs_keeps_tool_config_for_historical_tool_blocks_without_tools() -> None:
|
||||||
|
provider = BedrockProvider(region="us-east-1", client=FakeClient())
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": "read x"},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": "toolu_1",
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": "read_file", "arguments": '{"path": "x"}'},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{"role": "tool", "tool_call_id": "toolu_1", "name": "read_file", "content": "ok"},
|
||||||
|
{"role": "user", "content": "continue"},
|
||||||
|
]
|
||||||
|
|
||||||
|
kwargs = provider._build_kwargs(
|
||||||
|
messages=messages,
|
||||||
|
tools=[],
|
||||||
|
model="bedrock/anthropic.claude-opus-4-7",
|
||||||
|
max_tokens=1024,
|
||||||
|
temperature=0.7,
|
||||||
|
reasoning_effort=None,
|
||||||
|
tool_choice=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert any("toolUse" in block for msg in kwargs["messages"] for block in msg["content"])
|
||||||
|
assert any("toolResult" in block for msg in kwargs["messages"] for block in msg["content"])
|
||||||
|
assert kwargs["toolConfig"]["tools"][0]["toolSpec"]["name"] == "nanobot_noop"
|
||||||
|
assert "toolChoice" not in kwargs["toolConfig"]
|
||||||
|
|
||||||
|
|
||||||
def test_parse_response_maps_text_tools_reasoning_usage_and_stop_reason() -> None:
|
def test_parse_response_maps_text_tools_reasoning_usage_and_stop_reason() -> None:
|
||||||
response = {
|
response = {
|
||||||
"output": {
|
"output": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user