import json import os import subprocess from collections.abc import Awaitable, Callable from pathlib import Path from typing import Any, Literal, TypeAlias, cast from openai.types.responses import ( ResponseComputerToolCall, ResponseFileSearchToolCall, ResponseFunctionToolCall, ResponseFunctionWebSearch, ) from openai.types.responses.response_code_interpreter_tool_call import ( ResponseCodeInterpreterToolCall, ) from openai.types.responses.response_output_item import ImageGenerationCall, LocalShellCall, McpCall from pydantic import BaseModel, Field from rich import box from rich.console import Console, Group from rich.markdown import Markdown from rich.panel import Panel from rich.pretty import Pretty from rich.prompt import Prompt from rich.syntax import Syntax from rich.text import Text from typing_extensions import TypedDict from agents import ItemHelpers, TResponseInputItem from agents.items import ( CompactionItem, HandoffCallItem, HandoffOutputItem, MCPApprovalRequestItem, MCPApprovalResponseItem, MCPListToolsItem, MessageOutputItem, ReasoningItem, ToolApprovalItem, ToolCallItem, ToolCallOutputItem, ToolSearchCallItem, ToolSearchOutputItem, ) from agents.sandbox import Manifest from agents.sandbox.sandboxes.docker import DockerSandboxClient, DockerSandboxClientOptions from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient from agents.sandbox.session import BaseSandboxClient, SandboxSession from agents.stream_events import ( AgentUpdatedStreamEvent, RawResponsesStreamEvent, StreamEvent, ) from examples.auto_mode import input_with_fallback, is_auto_mode DEFAULT_SANDBOX_IMAGE = "sandbox-tutorials:latest" console = Console() PanelBody = Group | Pretty | Text PrintableEvent: TypeAlias = StreamEvent | str SandboxClient: TypeAlias = BaseSandboxClient[Any] InteractiveTurnRunner: TypeAlias = Callable[ [list[TResponseInputItem]], Awaitable[list[TResponseInputItem]] ] class ApplyPatchOperationPayload(TypedDict): path: str type: Literal["create_file", "update_file", "delete_file"] diff: str class ApplyPatchCallPayload(TypedDict): type: Literal["apply_patch_call"] call_id: str operation: ApplyPatchOperationPayload class Question(BaseModel): query: str = Field(description="User-facing question to ask.") options: list[str] = Field( default_factory=list, description="Suggested answer options. The UI always adds a custom free-text choice.", ) class QuestionAnswer(BaseModel): question: str = Field(description="The question that was asked.") answer: str = Field(description="The user's selected or free-text answer.") def load_env_defaults(env_path: Path) -> None: if not env_path.exists(): return for raw_line in env_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) normalized_key = key.strip() normalized_value = value.strip().strip('"').strip("'") if normalized_key: os.environ.setdefault(normalized_key, normalized_value) async def create_sandbox_client_and_session( *, manifest: Manifest, use_docker: bool, image: str = DEFAULT_SANDBOX_IMAGE, ) -> tuple[SandboxClient, SandboxSession]: if use_docker: try: from docker import from_env as docker_from_env # type: ignore[import-untyped] except ImportError as exc: raise SystemExit( "Docker-backed runs require the Docker SDK. Install repo dependencies with `make sync`." ) from exc client: SandboxClient = DockerSandboxClient( docker_from_env(environment=build_docker_environment()) ) sandbox = await client.create( manifest=manifest, options=DockerSandboxClientOptions(image=image), ) return client, sandbox client = UnixLocalSandboxClient() sandbox = await client.create(manifest=manifest) return client, sandbox def build_docker_environment() -> dict[str, str]: environment = os.environ.copy() if environment.get("DOCKER_HOST") or environment.get("DOCKER_CONTEXT"): return environment # Respect whichever Docker context the CLI is currently using, including Docker Desktop # and Colima, without taking a direct dependency on a specific daemon provider. try: result = subprocess.run( ["docker", "context", "inspect", "--format", "{{json .Endpoints.docker.Host}}"], capture_output=True, check=True, text=True, ) docker_host = json.loads(result.stdout.strip() or "null") except (OSError, subprocess.SubprocessError, json.JSONDecodeError): return environment if isinstance(docker_host, str) and docker_host: environment["DOCKER_HOST"] = docker_host return environment def prompt_with_fallback(prompt: str, fallback: str) -> str: if is_auto_mode(): return input_with_fallback(prompt, fallback).strip() try: return Prompt.ask(prompt).strip() except (EOFError, KeyboardInterrupt): return fallback def ask_user_questions(questions: list[Question]) -> list[QuestionAnswer]: answers: list[QuestionAnswer] = [] for question_index, question in enumerate(questions, start=1): suggested_options = [option.strip() for option in question.options if option.strip()] custom_choice_index = len(suggested_options) + 1 options_text = Text.from_markup( "\n".join( [ *( f"[cyan]{index}.[/cyan] {option}" for index, option in enumerate( suggested_options, start=1, ) ), f"[cyan]{custom_choice_index}.[/cyan] Use your own text", ] ) ) console.print( Panel( Group( Text(question.query), options_text, ), title=f"Question {question_index}", border_style="magenta", box=box.ROUNDED, expand=False, ) ) while True: choice = prompt_with_fallback( f"[bold cyan]Select[/bold cyan] 1-{custom_choice_index}", "1" if suggested_options else str(custom_choice_index), ) if choice.isdigit() and 1 <= int(choice) <= len(suggested_options): answer = suggested_options[int(choice) - 1] break if choice.isdigit() and int(choice) == custom_choice_index: answer = prompt_with_fallback( "[bold cyan]Your answer[/bold cyan]", suggested_options[0] if suggested_options else "Use a conservative assumption.", ) if answer: break continue if choice and not choice.isdigit(): answer = choice break console.print( f"[red]Please enter a number from 1 to {custom_choice_index}, or custom text.[/red]" ) answers.append(QuestionAnswer(question=question.query, answer=answer)) console.print( Panel( Pretty([answer.model_dump(mode="json") for answer in answers], expand_all=True), title="Question answers", border_style="magenta", box=box.ROUNDED, expand=False, ) ) return answers async def run_interactive_loop( *, conversation: list[TResponseInputItem], no_interactive: bool, run_turn: InteractiveTurnRunner, ) -> list[TResponseInputItem]: if no_interactive or is_auto_mode(): return conversation console.print("[dim]Enter follow-up prompts. Press Ctrl-D or Ctrl-C to finish.[/dim]") while True: try: next_message = Prompt.ask("[bold cyan]user[/bold cyan]").strip() except (EOFError, KeyboardInterrupt): break if not next_message: continue conversation.append({"role": "user", "content": next_message}) conversation = await run_turn(conversation) return conversation def print_event(event: PrintableEvent) -> None: if isinstance(event, str): console.print() console.rule("[bold green]Final output[/bold green]", style="green") console.print( Panel( Markdown(event or "_No final output returned._"), border_style="green", box=box.ROUNDED, expand=False, ) ) return if isinstance(event, AgentUpdatedStreamEvent): console.print( Panel( Pretty(event.new_agent.name, expand_all=True), title="Agent updated", border_style="cyan", box=box.ROUNDED, expand=False, ) ) return if isinstance(event, RawResponsesStreamEvent): return body: PanelBody match event.item: case ReasoningItem() as item: body = Pretty(item, expand_all=True) title = f"Reasoning item: {event.name.replace('_', ' ')}" case ToolCallItem() as item: tool_name = "tool" body = Pretty(item.raw_item, expand_all=True) match item.raw_item: case ResponseFunctionToolCall() as raw_item: tool_name = raw_item.name payload = json.loads(raw_item.arguments) if raw_item.arguments else {} if tool_name == "exec_command": command = payload["cmd"] if "\\n" in command and "\n" not in command: command = command.replace("\\n", "\n") body = Group( Pretty( {key: value for key, value in payload.items() if key != "cmd"}, expand_all=True, ), Syntax(command, "bash", theme="ansi_dark", word_wrap=True), ) else: body = Pretty(payload, expand_all=True) case ResponseComputerToolCall() as raw_item: tool_name = "computer" body = Pretty(raw_item, expand_all=True) case ResponseFileSearchToolCall() as raw_item: tool_name = "file_search" body = Pretty(raw_item, expand_all=True) case ResponseFunctionWebSearch() as raw_item: tool_name = "web_search" body = Pretty(raw_item, expand_all=True) case McpCall() as raw_item: tool_name = "mcp" body = Pretty(raw_item, expand_all=True) case ResponseCodeInterpreterToolCall() as raw_item: tool_name = "code_interpreter" body = Pretty(raw_item, expand_all=True) case ImageGenerationCall() as raw_item: tool_name = "image_generation" body = Pretty(raw_item, expand_all=True) case LocalShellCall() as raw_item: tool_name = "local_shell" body = Pretty(raw_item, expand_all=True) case dict() as raw_item: tool_name = "apply_patch" payload = cast(ApplyPatchCallPayload, raw_item)["operation"] body = Group( Pretty( { "path": payload["path"], "type": payload["type"], }, expand_all=True, ), Syntax(payload["diff"], "diff", theme="ansi_dark", word_wrap=True), ) title = f"Tool call: {tool_name}" case ToolCallOutputItem() as item: body = Text(item.output) if isinstance(item.output, str) else Pretty(item.output) title = "Tool output" case MessageOutputItem() as item: output = ItemHelpers.text_message_output(item) body = Text(output) if isinstance(output, str) else Pretty(output, expand_all=True) title = "Message output" case ToolSearchCallItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "Tool search call" case ToolSearchOutputItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "Tool search output" case HandoffCallItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "Handoff call" case HandoffOutputItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "Handoff output" case MCPListToolsItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "MCP list tools" case MCPApprovalRequestItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "MCP approval request" case MCPApprovalResponseItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "MCP approval response" case CompactionItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "Compaction" case ToolApprovalItem() as item: body = Pretty(item.raw_item, expand_all=True) title = "Tool approval" console.print( Panel( body, title=title, border_style="cyan", box=box.ROUNDED, expand=False, ) )