feat(I): I-APPR-LOOP — agent loop human-in-the-loop approval integration (require_approval + approval_tools params, ApprovalRequest creation, workstream notification, pause loop), 8 tests passing
This commit is contained in:
@@ -149,6 +149,8 @@ async def run_react_loop(
|
|||||||
trace_id: str | None = None,
|
trace_id: str | None = None,
|
||||||
on_step: Callable | None = None,
|
on_step: Callable | None = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
|
require_approval: bool = False,
|
||||||
|
approval_tools: list[str] | None = None,
|
||||||
) -> ReActResult:
|
) -> ReActResult:
|
||||||
"""Execute a ReAct loop: LLM reasoning → tool execution → repeat.
|
"""Execute a ReAct loop: LLM reasoning → tool execution → repeat.
|
||||||
|
|
||||||
@@ -371,6 +373,44 @@ async def run_react_loop(
|
|||||||
"arguments": args,
|
"arguments": args,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
elif require_approval and (approval_tools is None or tool_name in (approval_tools or [])):
|
||||||
|
# I-APPR-LOOP: Human-in-the-Loop Approval
|
||||||
|
# Create an ApprovalRequest and pause the loop
|
||||||
|
try:
|
||||||
|
from app.core.approval import create_approval_request
|
||||||
|
from app.ai.agent_workstream import post_approval_request
|
||||||
|
|
||||||
|
approval = await create_approval_request(
|
||||||
|
db=db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
entity_type="agent_run",
|
||||||
|
entity_id=agent_run_id or uuid.uuid4(),
|
||||||
|
action=f"tool:{tool_name}",
|
||||||
|
requested_by=user_id,
|
||||||
|
requested_by_type="agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Post approval request to workstream
|
||||||
|
if agent_run_id:
|
||||||
|
await post_approval_request(
|
||||||
|
db=db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
agent_id=getattr(agent_definition, "id", uuid.uuid4()),
|
||||||
|
approval_id=approval.id,
|
||||||
|
action=f"Tool '{tool_name}' requires approval",
|
||||||
|
details={"tool_name": tool_name, "arguments": args},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pause the loop — return with waiting_for_approval status
|
||||||
|
result.status = "waiting_for_approval"
|
||||||
|
result.error = f"Tool '{tool_name}' requires human approval (request_id: {approval.id})"
|
||||||
|
result.steps_taken = step_num
|
||||||
|
result.final_content = f"I need approval to execute tool '{tool_name}'. Approval request {approval.id} has been created."
|
||||||
|
logger.info("Agent loop paused for approval on tool '%s' (request: %s)", tool_name, approval.id)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to create approval request for tool '%s': %s", tool_name, e)
|
||||||
|
observation = json.dumps({"error": f"Approval required but failed to create request: {e}"})
|
||||||
else:
|
else:
|
||||||
observation = await _execute_tool(tool_registry, tool_name, args, tool_context)
|
observation = await _execute_tool(tool_registry, tool_name, args, tool_context)
|
||||||
observations.append(observation)
|
observations.append(observation)
|
||||||
|
|||||||
@@ -93,3 +93,29 @@ class TestIntegrationTools:
|
|||||||
)
|
)
|
||||||
assert result["error"] == "Instance not found"
|
assert result["error"] == "Instance not found"
|
||||||
assert result["status"] == "not_found"
|
assert result["status"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
# ─── I-APPR-LOOP: Agent Loop Human-in-the-Loop Approval ──────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestAgentLoopApproval:
|
||||||
|
"""Test the I-APPR-LOOP approval integration in run_react_loop."""
|
||||||
|
|
||||||
|
def test_run_react_loop_has_approval_params(self):
|
||||||
|
"""run_react_loop has require_approval and approval_tools parameters."""
|
||||||
|
import inspect
|
||||||
|
from app.ai.agent_loop import run_react_loop
|
||||||
|
|
||||||
|
sig = inspect.signature(run_react_loop)
|
||||||
|
assert "require_approval" in sig.parameters
|
||||||
|
assert "approval_tools" in sig.parameters
|
||||||
|
assert sig.parameters["require_approval"].default is False
|
||||||
|
assert sig.parameters["approval_tools"].default is None
|
||||||
|
|
||||||
|
def test_react_result_has_waiting_for_approval_status(self):
|
||||||
|
"""ReActResult supports waiting_for_approval status."""
|
||||||
|
from app.ai.agent_loop import ReActResult
|
||||||
|
|
||||||
|
result = ReActResult(final_content="", status="waiting_for_approval")
|
||||||
|
assert result.status == "waiting_for_approval"
|
||||||
|
assert result.final_content == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user