feat(agent): add interrupt-aware tool dispatcher
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class InterruptResult(BaseModel):
|
||||||
|
interrupt_type: str
|
||||||
|
tool_name: str
|
||||||
|
tool_args: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class BackendExecutionResult(BaseModel):
|
||||||
|
tool_name: str
|
||||||
|
tool_args: dict[str, Any]
|
||||||
|
result: Any | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ToolDispatcher:
|
||||||
|
def dispatch(
|
||||||
|
self, tool: dict[str, Any]
|
||||||
|
) -> InterruptResult | BackendExecutionResult:
|
||||||
|
return dispatch_tool_call(tool)
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_tool_call(
|
||||||
|
tool: dict[str, Any],
|
||||||
|
) -> InterruptResult | BackendExecutionResult:
|
||||||
|
name = tool["name"]
|
||||||
|
target = tool["execution_target"]
|
||||||
|
args = tool.get("args", {})
|
||||||
|
|
||||||
|
if target == "frontend":
|
||||||
|
return InterruptResult(
|
||||||
|
interrupt_type="tool_execution",
|
||||||
|
tool_name=name,
|
||||||
|
tool_args=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
if target == "backend":
|
||||||
|
requires_approval = tool.get("requires_approval", False)
|
||||||
|
if requires_approval:
|
||||||
|
return InterruptResult(
|
||||||
|
interrupt_type="approval_required",
|
||||||
|
tool_name=name,
|
||||||
|
tool_args=args,
|
||||||
|
)
|
||||||
|
return BackendExecutionResult(
|
||||||
|
tool_name=name,
|
||||||
|
tool_args=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown execution_target: {target}")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from v1.agent.tool_dispatcher import (
|
||||||
|
BackendExecutionResult,
|
||||||
|
InterruptResult,
|
||||||
|
ToolDispatcher,
|
||||||
|
dispatch_tool_call,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestToolDispatcher:
|
||||||
|
def test_frontend_tool_returns_interrupt(self):
|
||||||
|
tool = {
|
||||||
|
"name": "ui.navigate_to",
|
||||||
|
"execution_target": "frontend",
|
||||||
|
"args": {"path": "/home"},
|
||||||
|
}
|
||||||
|
result = dispatch_tool_call(tool)
|
||||||
|
assert isinstance(result, InterruptResult)
|
||||||
|
assert result.interrupt_type == "tool_execution"
|
||||||
|
assert result.tool_name == "ui.navigate_to"
|
||||||
|
|
||||||
|
def test_backend_tool_executes_directly(self):
|
||||||
|
tool = {
|
||||||
|
"name": "srv.get_user_info",
|
||||||
|
"execution_target": "backend",
|
||||||
|
"args": {"user_id": "u1"},
|
||||||
|
"requires_approval": False,
|
||||||
|
}
|
||||||
|
result = dispatch_tool_call(tool)
|
||||||
|
assert isinstance(result, BackendExecutionResult)
|
||||||
|
assert result.tool_name == "srv.get_user_info"
|
||||||
|
|
||||||
|
def test_backend_tool_with_approval_returns_interrupt(self):
|
||||||
|
tool = {
|
||||||
|
"name": "srv.transfer_funds",
|
||||||
|
"execution_target": "backend",
|
||||||
|
"args": {"to": "u2", "amount": 100},
|
||||||
|
"requires_approval": True,
|
||||||
|
}
|
||||||
|
result = dispatch_tool_call(tool)
|
||||||
|
assert isinstance(result, InterruptResult)
|
||||||
|
assert result.interrupt_type == "approval_required"
|
||||||
|
assert result.tool_name == "srv.transfer_funds"
|
||||||
|
|
||||||
|
def test_dispatcher_class_can_dispatch(self):
|
||||||
|
dispatcher = ToolDispatcher()
|
||||||
|
tool = {
|
||||||
|
"name": "ui.show_dialog",
|
||||||
|
"execution_target": "frontend",
|
||||||
|
"args": {"message": "Hello"},
|
||||||
|
}
|
||||||
|
result = dispatcher.dispatch(tool)
|
||||||
|
assert isinstance(result, InterruptResult)
|
||||||
Reference in New Issue
Block a user