Skip to content

Commit df3fa77

Browse files
Python: fix(anthropic): harden tool-choice reset and tool-call argument serialization
Fixes #9853 - Keep Anthropic tool definitions and set tool_choice to {"type":"none"} when auto-invoke retries are exhausted - Add explicit Anthropic tool_choice validation with clear allowed values and invalid-type guard - Serialize ToolUseBlock input with an is-not-None check so empty dict arguments are preserved as "{}" - Add regression tests for exhausted auto-invoke payload behavior, empty-argument serialization, and invalid tool_choice handling
1 parent dff41d4 commit df3fa77

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

python/semantic_kernel/connectors/ai/anthropic/prompt_execution_settings/anthropic_prompt_execution_settings.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
from pydantic import Field, model_validator
77

8+
from semantic_kernel.connectors.ai.function_choice_type import FunctionChoiceType
89
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
10+
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
911

1012
logger = logging.getLogger(__name__)
1113

@@ -44,5 +46,25 @@ class AnthropicChatPromptExecutionSettings(AnthropicPromptExecutionSettings):
4446

4547
@model_validator(mode="after")
4648
def validate_tool_choice(self) -> "AnthropicChatPromptExecutionSettings":
47-
"""Validate tool choice payload."""
49+
"""Validate tool choice payload.
50+
51+
Anthropic supports disabling tool calls by setting {"type": "none"},
52+
which is used when auto-invocation attempts are exhausted.
53+
"""
54+
if self.tool_choice is None:
55+
return self
56+
57+
allowed_tool_choice_types = {
58+
FunctionChoiceType.AUTO.value,
59+
FunctionChoiceType.NONE.value,
60+
FunctionChoiceType.REQUIRED.value,
61+
"any",
62+
"tool",
63+
}
64+
tool_choice_type = self.tool_choice.get("type")
65+
if tool_choice_type not in allowed_tool_choice_types:
66+
raise ServiceInvalidExecutionSettingsError(
67+
f"Invalid Anthropic tool_choice type '{tool_choice_type}'. "
68+
f"Expected one of: {sorted(allowed_tool_choice_types)}."
69+
)
4870
return self

python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ def _update_function_choice_settings_callback(
144144
@override
145145
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
146146
if hasattr(settings, "tool_choice") and getattr(settings, "tools", None):
147+
# Anthropic supports disabling tool calls while keeping tool definitions:
148+
# https://docs.anthropic.com/en/api/messages#body-tool_choice
147149
settings.tool_choice = {"type": FunctionChoiceType.NONE.value}
148150

149151
@override
@@ -302,7 +304,7 @@ def _create_streaming_chat_message_content(
302304
id=tool_use_block.id,
303305
index=stream_event.index,
304306
name=tool_use_block.name,
305-
arguments=json.dumps(tool_use_block.input) if tool_use_block.input else None,
307+
arguments=json.dumps(tool_use_block.input) if tool_use_block.input is not None else None,
306308
)
307309
)
308310
elif isinstance(stream_event, RawMessageDeltaEvent):

python/tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,3 +645,33 @@ def test_get_tool_calls_from_message_serializes_arguments(
645645

646646
assert len(tool_calls) == 1
647647
assert tool_calls[0].arguments == '{"input": 3, "amount": 3}'
648+
649+
650+
def test_get_tool_calls_from_message_serializes_empty_arguments_dict(
651+
mock_anthropic_client_completion: MagicMock,
652+
):
653+
chat_completion = AnthropicChatCompletion(
654+
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_anthropic_client_completion
655+
)
656+
message = Message(
657+
id="test_message_id",
658+
content=[
659+
ToolUseBlock(
660+
id="test_tool_use_block_id",
661+
input={},
662+
name="math-Add",
663+
type="tool_use",
664+
),
665+
],
666+
model="claude-3-opus-20240229",
667+
role="assistant",
668+
stop_reason="tool_use",
669+
stop_sequence=None,
670+
type="message",
671+
usage=Usage(input_tokens=100, output_tokens=100),
672+
)
673+
674+
tool_calls = chat_completion._get_tool_calls_from_message(message)
675+
676+
assert len(tool_calls) == 1
677+
assert tool_calls[0].arguments == "{}"

python/tests/unit/connectors/ai/anthropic/test_anthropic_request_settings.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

3+
import pytest
4+
35
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
46
AnthropicChatPromptExecutionSettings,
57
)
68
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
79
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
10+
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
811

912

1013
def test_default_anthropic_chat_prompt_execution_settings():
@@ -124,3 +127,14 @@ def test_tool_choice_none():
124127
function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
125128
)
126129
assert settings.tool_choice == {"type": "none"}
130+
131+
132+
def test_tool_choice_invalid_type_raises():
133+
with pytest.raises(ServiceInvalidExecutionSettingsError, match="Invalid Anthropic tool_choice type"):
134+
AnthropicChatPromptExecutionSettings(
135+
service_id="test_service",
136+
extension_data={
137+
"tool_choice": {"type": "invalid"},
138+
"messages": [{"role": "system", "content": "Hello"}],
139+
},
140+
)

0 commit comments

Comments
 (0)