This commit is contained in:
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from runtime.planner_config import PlannerConfig
|
||||
from runtime.tool_specs import ToolSpec
|
||||
from runtime.tool_specs import ACTION_TOOL_NAMES, ToolSpec
|
||||
|
||||
|
||||
class ToolCallUnavailable(Exception):
|
||||
@@ -36,6 +36,10 @@ class ToolCallDecision:
|
||||
# Extended thinking block (Anthropic) or reasoning_content (OpenAI o-series).
|
||||
# None when not enabled or not present in the response.
|
||||
thinking: str | None = None
|
||||
# Required structured metadata for device actions. These fields are removed
|
||||
# from ``arguments`` before the Runtime invokes the physical device tool.
|
||||
purpose: str | None = None
|
||||
expected_outcome: str | None = None
|
||||
|
||||
|
||||
class ToolCallingClient(Protocol):
|
||||
@@ -352,14 +356,19 @@ def _decision_from_anthropic_response(
|
||||
name = _value(block, "name")
|
||||
arguments = _value(block, "input")
|
||||
if isinstance(name, str) and isinstance(arguments, dict):
|
||||
executable_arguments, purpose, expected_outcome = (
|
||||
_split_action_metadata(name, arguments)
|
||||
)
|
||||
return ToolCallDecision(
|
||||
tool_name=name,
|
||||
arguments=arguments,
|
||||
arguments=executable_arguments,
|
||||
usage=_anthropic_usage(response),
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
text_output="\n".join(text_parts) if text_parts else None,
|
||||
thinking=thinking,
|
||||
purpose=purpose,
|
||||
expected_outcome=expected_outcome,
|
||||
)
|
||||
raise ValueError("anthropic response did not include a tool_use block")
|
||||
|
||||
@@ -418,6 +427,9 @@ def _decision_from_openai_response(
|
||||
if not isinstance(name, str):
|
||||
raise ValueError("openai tool call missing a function name")
|
||||
arguments = _decode_openai_arguments(_value(function, "arguments"))
|
||||
executable_arguments, purpose, expected_outcome = _split_action_metadata(
|
||||
name, arguments
|
||||
)
|
||||
# Extract reasoning_content from o-series models when present.
|
||||
raw_reasoning = _value(message, "reasoning_content")
|
||||
thinking: str | None = raw_reasoning if isinstance(raw_reasoning, str) else None
|
||||
@@ -427,12 +439,14 @@ def _decision_from_openai_response(
|
||||
text_output: str | None = raw_content if isinstance(raw_content, str) else None
|
||||
return ToolCallDecision(
|
||||
tool_name=name,
|
||||
arguments=arguments,
|
||||
arguments=executable_arguments,
|
||||
usage=_openai_usage(response),
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
thinking=thinking,
|
||||
text_output=text_output,
|
||||
purpose=purpose,
|
||||
expected_outcome=expected_outcome,
|
||||
)
|
||||
|
||||
|
||||
@@ -475,6 +489,30 @@ def _decode_openai_arguments(raw_arguments: Any) -> dict[str, Any]:
|
||||
raise ValueError("openai tool call arguments must decode to a JSON object")
|
||||
|
||||
|
||||
def _split_action_metadata(
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], str | None, str | None]:
|
||||
executable_arguments = dict(arguments)
|
||||
if tool_name not in ACTION_TOOL_NAMES:
|
||||
return executable_arguments, None, None
|
||||
return (
|
||||
{
|
||||
name: value
|
||||
for name, value in executable_arguments.items()
|
||||
if name not in {"purpose", "expected_outcome"}
|
||||
},
|
||||
_metadata_text(executable_arguments.get("purpose")),
|
||||
_metadata_text(executable_arguments.get("expected_outcome")),
|
||||
)
|
||||
|
||||
|
||||
def _metadata_text(value: Any) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
return value.strip() or None
|
||||
|
||||
|
||||
def _value(source: Any, key: str) -> Any:
|
||||
if isinstance(source, dict):
|
||||
return source.get(key)
|
||||
|
||||
Reference in New Issue
Block a user