| """Pluggable agent orchestration runtimes for the Gradio agentic UI.""" |
|
|
| from __future__ import annotations |
|
|
| import os |
| import queue |
| import sys |
| import threading |
| from abc import ABC, abstractmethod |
| from collections.abc import Iterator |
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Any |
|
|
| _AGENT_REDACT_ROOT = Path(__file__).resolve().parents[1] |
| if str(_AGENT_REDACT_ROOT) not in sys.path: |
| sys.path.insert(0, str(_AGENT_REDACT_ROOT)) |
|
|
|
|
| class AgentRuntimeError(RuntimeError): |
| """Base error for agent runtime failures.""" |
|
|
|
|
| @dataclass |
| class AgentStreamEvent: |
| """Normalized streaming event for Gradio chat/activity panels.""" |
|
|
| kind: str |
| text: str = "" |
| tool_name: str | None = None |
| tool_call_id: str | None = None |
| tool_args: dict[str, Any] | None = None |
| tool_output: str | None = None |
| is_error: bool = False |
| meta: dict[str, Any] = field(default_factory=dict) |
|
|
|
|
| def normalize_orchestrator(raw: str | None = None) -> str: |
| """Return a supported orchestrator id: pi | langgraph | agentcore | agentcore-harness.""" |
| value = (raw or os.environ.get("AGENT_ORCHESTRATOR") or "pi").strip().lower() |
| if value == "harness": |
| value = "agentcore-harness" |
| if value in {"pi", "langgraph", "agentcore", "agentcore-harness"}: |
| return value |
| return "pi" |
|
|
|
|
| def orchestrator_label(orchestrator: str | None = None) -> str: |
| labels = { |
| "pi": "Pi coding agent", |
| "langgraph": "LangGraph", |
| "agentcore": "Bedrock AgentCore Runtime", |
| "agentcore-harness": "Bedrock AgentCore Harness", |
| } |
| return labels.get(normalize_orchestrator(orchestrator), "Agent") |
|
|
|
|
| class AgentRuntime(ABC): |
| """Common interface consumed by ``gradio_app.py``.""" |
|
|
| @property |
| @abstractmethod |
| def orchestrator(self) -> str: |
| """Runtime id: pi | langgraph | agentcore | agentcore-harness.""" |
|
|
| @property |
| @abstractmethod |
| def running(self) -> bool: |
| """True when the runtime is ready to accept prompts.""" |
|
|
| @property |
| def prompt_stream_active(self) -> bool: |
| """True while :meth:`prompt_events` is consuming a prompt stream.""" |
| return False |
|
|
| @abstractmethod |
| def start(self) -> None: |
| """Start or warm the runtime.""" |
|
|
| @abstractmethod |
| def close(self) -> None: |
| """Shut down the runtime.""" |
|
|
| @abstractmethod |
| def abort(self) -> None: |
| """Request cancellation of the active turn.""" |
|
|
| @abstractmethod |
| def prompt_events(self, message: str) -> Iterator[AgentStreamEvent]: |
| """Stream normalized events for one user prompt.""" |
|
|
| def get_state(self) -> dict[str, Any]: |
| return {} |
|
|
| def get_messages(self) -> list[dict[str, Any]]: |
| return [] |
|
|
| def get_session_stats(self) -> dict[str, Any]: |
| return {} |
|
|
| def set_model(self, provider: str, model_id: str) -> dict[str, Any]: |
| return {} |
|
|
| def new_session(self) -> None: |
| return None |
|
|
| def steer(self, message: str) -> None: |
| return None |
|
|
| def follow_up(self, message: str) -> None: |
| return None |
|
|
| def stage_ui_chat_notice(self, label: str, message: str) -> None: |
| return None |
|
|
| def take_pending_ui_chat_notices(self) -> list[dict[str, Any]]: |
| return [] |
|
|
| def drain_pending_ui_history(self) -> list[dict[str, Any]]: |
| return [] |
|
|
| def apply_backend(self, provider: str, model_id: str) -> None: |
| """Reconfigure the orchestration model after UI **Apply backend**.""" |
| self.set_model(provider, model_id) |
| self.new_session() |
|
|
|
|
| class PiAgentRuntime(AgentRuntime): |
| """Adapter around :class:`pi_rpc_client.PiRpcClient`.""" |
|
|
| def __init__(self, client: Any) -> None: |
| self._client = client |
|
|
| @property |
| def orchestrator(self) -> str: |
| return "pi" |
|
|
| @property |
| def client(self) -> Any: |
| return self._client |
|
|
| @property |
| def running(self) -> bool: |
| return bool(self._client.running) |
|
|
| @property |
| def prompt_stream_active(self) -> bool: |
| return bool(self._client.prompt_stream_active) |
|
|
| def start(self) -> None: |
| self._client.start() |
|
|
| def close(self) -> None: |
| self._client.close() |
|
|
| def abort(self) -> None: |
| self._client.abort() |
|
|
| def prompt_events(self, message: str) -> Iterator[AgentStreamEvent]: |
| from pi_rpc_client import PiStreamEvent |
|
|
| for event in self._client.prompt_events(message): |
| if isinstance(event, PiStreamEvent): |
| yield _pi_event_to_agent_event(event) |
| elif isinstance(event, AgentStreamEvent): |
| yield event |
| else: |
| yield AgentStreamEvent(kind="status", text=str(event)) |
|
|
| def get_state(self) -> dict[str, Any]: |
| return dict(self._client.get_state()) |
|
|
| def get_messages(self) -> list[dict[str, Any]]: |
| return list(self._client.get_messages()) |
|
|
| def get_session_stats(self) -> dict[str, Any]: |
| return dict(self._client.get_session_stats()) |
|
|
| def set_model(self, provider: str, model_id: str) -> dict[str, Any]: |
| return dict(self._client.set_model(provider, model_id)) |
|
|
| def new_session(self) -> None: |
| self._client.new_session() |
|
|
| def steer(self, message: str) -> None: |
| self._client.steer(message) |
|
|
| def follow_up(self, message: str) -> None: |
| self._client.follow_up(message) |
|
|
| def stage_ui_chat_notice(self, label: str, message: str) -> None: |
| self._client.stage_ui_chat_notice(label, message) |
|
|
| def take_pending_ui_chat_notices(self) -> list[dict[str, Any]]: |
| return [] |
|
|
| def drain_pending_ui_history(self) -> list[dict[str, Any]]: |
| return list(self._client.drain_pending_ui_history()) |
|
|
|
|
| def _pi_event_to_agent_event(event: Any) -> AgentStreamEvent: |
| return AgentStreamEvent( |
| kind=str(event.kind), |
| text=str(event.text or ""), |
| tool_name=event.tool_name, |
| tool_call_id=event.tool_call_id, |
| tool_args=event.tool_args, |
| tool_output=event.tool_output, |
| is_error=bool(event.is_error), |
| meta=dict(event.meta or {}), |
| ) |
|
|
|
|
| def create_agent_runtime(session_hash: str | None = None) -> AgentRuntime: |
| """Factory for the configured orchestration backend.""" |
| orchestrator = normalize_orchestrator() |
| if orchestrator == "langgraph": |
| from langgraph_runtime import LangGraphAgentRuntime |
|
|
| return LangGraphAgentRuntime(session_hash=session_hash) |
| if orchestrator == "agentcore": |
| from agentcore_runtime import AgentCoreAgentRuntime |
|
|
| return AgentCoreAgentRuntime(session_hash=session_hash) |
| if orchestrator == "agentcore-harness": |
| from agentcore_harness_runtime import AgentCoreHarnessRuntime |
|
|
| return AgentCoreHarnessRuntime(session_hash=session_hash) |
| from pi_rpc_client import default_client |
|
|
| return PiAgentRuntime(default_client(session_hash)) |
|
|
|
|
| def start_agent_prompt_event_worker( |
| runtime: AgentRuntime, |
| event_queue: queue.Queue[AgentStreamEvent | None], |
| prompt: str, |
| ) -> None: |
| """Run ``runtime.prompt_events`` on a background thread, feeding *event_queue*.""" |
|
|
| def _worker() -> None: |
| try: |
| for event in runtime.prompt_events(prompt): |
| event_queue.put(event) |
| except Exception as exc: |
| event_queue.put( |
| AgentStreamEvent(kind="error", text=str(exc), is_error=True) |
| ) |
| finally: |
| event_queue.put(None) |
|
|
| threading.Thread(target=_worker, daemon=True).start() |
|
|
|
|
| def coerce_agent_runtime(client: Any) -> AgentRuntime | None: |
| if client is None: |
| return None |
| if isinstance(client, AgentRuntime): |
| return client |
| if isinstance(client, PiAgentRuntime): |
| return client |
| |
| from pi_rpc_client import PiRpcClient |
|
|
| if isinstance(client, PiRpcClient): |
| return PiAgentRuntime(client) |
| return None |
|
|