This commit is contained in:
+11
-1
@@ -57,8 +57,15 @@ class AIPlanner(Planner):
|
||||
return [
|
||||
PlannedStep(
|
||||
action=decision.tool_name,
|
||||
description=f"AI planner: {decision.tool_name}({decision.arguments})",
|
||||
description=(
|
||||
f"AI planner: {decision.purpose}"
|
||||
if decision.purpose
|
||||
else f"AI planner: {decision.tool_name}({decision.arguments})"
|
||||
),
|
||||
args=dict(decision.arguments),
|
||||
expected_text=decision.expected_outcome,
|
||||
purpose=decision.purpose,
|
||||
expected_outcome=decision.expected_outcome,
|
||||
prompt=decision.user_prompt or user_prompt,
|
||||
rationale=decision.text_output,
|
||||
thinking=decision.thinking,
|
||||
@@ -78,7 +85,10 @@ def _history_summary(world: "WorldState | None") -> list[dict[str, Any]]:
|
||||
{
|
||||
"page": event.page,
|
||||
"action": event.action,
|
||||
"arguments": dict(event.arguments),
|
||||
"rationale": event.rationale,
|
||||
"purpose": event.purpose,
|
||||
"expected_outcome": event.expected_outcome,
|
||||
"success": event.success,
|
||||
}
|
||||
for event in world.history
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
@@ -28,6 +28,8 @@ class StepResult:
|
||||
"description": self.step.description,
|
||||
"args": dict(self.step.args),
|
||||
"expected_text": self.step.expected_text,
|
||||
"purpose": self.step.purpose,
|
||||
"expected_outcome": self.step.expected_outcome,
|
||||
},
|
||||
"success": self.success,
|
||||
"attempts": self.attempts,
|
||||
|
||||
@@ -16,6 +16,10 @@ class PlannedStep:
|
||||
description: str
|
||||
args: dict[str, Any] = field(default_factory=dict)
|
||||
expected_text: str | None = None
|
||||
# Structured action semantics, populated by AI planner tool calls and
|
||||
# retained independently from executable arguments for later reuse.
|
||||
purpose: str | None = None
|
||||
expected_outcome: str | None = None
|
||||
# The actual prompt sent to the LLM for this step (AI planners only).
|
||||
# ``None`` for non-LLM planners; TaskRunner falls back to the task goal.
|
||||
prompt: str | None = None
|
||||
|
||||
@@ -382,6 +382,8 @@ class TaskRunner:
|
||||
"action": step.action,
|
||||
"description": step.description,
|
||||
"args": step.args,
|
||||
"purpose": step.purpose,
|
||||
"expected_outcome": step.expected_outcome,
|
||||
},
|
||||
result=result.to_dict()
|
||||
if hasattr(result, "to_dict")
|
||||
|
||||
@@ -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)
|
||||
|
||||
+59
-32
@@ -11,18 +11,51 @@ class ToolSpec:
|
||||
parameters: dict[str, Any]
|
||||
|
||||
|
||||
ACTION_METADATA_PROPERTIES: dict[str, dict[str, Any]] = {
|
||||
"purpose": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 240,
|
||||
"description": "Concise intent for this action, used by later reuse.",
|
||||
},
|
||||
"expected_outcome": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 240,
|
||||
"description": "Observable screen state expected after this action.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _action_parameters(
|
||||
*,
|
||||
required: list[str],
|
||||
properties: dict[str, dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": [*required, "purpose", "expected_outcome"],
|
||||
"properties": {**properties, **ACTION_METADATA_PROPERTIES},
|
||||
}
|
||||
|
||||
|
||||
TAP_SPEC = ToolSpec(
|
||||
name="tap",
|
||||
description="Tap a point on the screen, given in Scene pixel coordinates.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["x", "y"],
|
||||
"properties": {
|
||||
"x": {"type": "number", "description": "X coordinate in Scene pixel space."},
|
||||
"y": {"type": "number", "description": "Y coordinate in Scene pixel space."},
|
||||
parameters=_action_parameters(
|
||||
required=["x", "y"],
|
||||
properties={
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "X coordinate in Scene pixel space.",
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": "Y coordinate in Scene pixel space.",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
SWIPE_SPEC = ToolSpec(
|
||||
@@ -31,11 +64,9 @@ SWIPE_SPEC = ToolSpec(
|
||||
"Swipe from a start point to an end point on the screen, given in "
|
||||
"Scene pixel coordinates."
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["start_x", "start_y", "end_x", "end_y"],
|
||||
"properties": {
|
||||
parameters=_action_parameters(
|
||||
required=["start_x", "start_y", "end_x", "end_y"],
|
||||
properties={
|
||||
"start_x": {"type": "number", "description": "Start X coordinate."},
|
||||
"start_y": {"type": "number", "description": "Start Y coordinate."},
|
||||
"end_x": {"type": "number", "description": "End X coordinate."},
|
||||
@@ -46,52 +77,46 @@ SWIPE_SPEC = ToolSpec(
|
||||
"default": 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
INPUT_TEXT_SPEC = ToolSpec(
|
||||
name="input_text",
|
||||
description="Type text into the currently focused input field.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["text"],
|
||||
"properties": {
|
||||
parameters=_action_parameters(
|
||||
required=["text"],
|
||||
properties={
|
||||
"text": {"type": "string", "description": "Text to type."},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
LAUNCH_APP_SPEC = ToolSpec(
|
||||
name="launch_app",
|
||||
description="Launch (foreground) an app by its bundle/package identifier.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["app_id"],
|
||||
"properties": {
|
||||
parameters=_action_parameters(
|
||||
required=["app_id"],
|
||||
properties={
|
||||
"app_id": {
|
||||
"type": "string",
|
||||
"description": "App bundle/package identifier.",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
TERMINATE_APP_SPEC = ToolSpec(
|
||||
name="terminate_app",
|
||||
description="Terminate a running app by its bundle/package identifier.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["app_id"],
|
||||
"properties": {
|
||||
parameters=_action_parameters(
|
||||
required=["app_id"],
|
||||
properties={
|
||||
"app_id": {
|
||||
"type": "string",
|
||||
"description": "App bundle/package identifier.",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
FINISH_TASK_SPEC = ToolSpec(
|
||||
@@ -126,4 +151,6 @@ ACTION_TOOL_SPECS: list[ToolSpec] = [
|
||||
TERMINATE_APP_SPEC,
|
||||
]
|
||||
|
||||
ACTION_TOOL_NAMES = frozenset(spec.name for spec in ACTION_TOOL_SPECS)
|
||||
|
||||
ALL_TOOL_SPECS: list[ToolSpec] = [*ACTION_TOOL_SPECS, FINISH_TASK_SPEC]
|
||||
|
||||
Reference in New Issue
Block a user