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