|
| 1 | +--- |
| 2 | +search: |
| 3 | + exclude: true |
| 4 | +--- |
| 5 | +# Human-in-the-loop |
| 6 | + |
| 7 | +human-in-the-loop (HITL) フローを使用すると、人が機微なツール呼び出しを承認または拒否するまで、エージェントの実行を一時停止できます。ツールは承認が必要なタイミングを宣言し、実行結果は保留中の承認を割り込みとして提示し、`RunState` により決定後に実行をシリアライズして再開できます。 |
| 8 | + |
| 9 | +## 承認が必要なツールのマーキング |
| 10 | + |
| 11 | +常に承認を要求するには `needs_approval` を `True` に設定するか、呼び出しごとに判定する async 関数を指定します。この callable は実行コンテキスト、解析済みのツール パラメーター、ツール呼び出し ID を受け取ります。 |
| 12 | + |
| 13 | +```python |
| 14 | +from agents import Agent, Runner, function_tool |
| 15 | + |
| 16 | + |
| 17 | +@function_tool(needs_approval=True) |
| 18 | +async def cancel_order(order_id: int) -> str: |
| 19 | + return f"Cancelled order {order_id}" |
| 20 | + |
| 21 | + |
| 22 | +async def requires_review(_ctx, params, _call_id) -> bool: |
| 23 | + return "refund" in params.get("subject", "").lower() |
| 24 | + |
| 25 | + |
| 26 | +@function_tool(needs_approval=requires_review) |
| 27 | +async def send_email(subject: str, body: str) -> str: |
| 28 | + return f"Sent '{subject}'" |
| 29 | + |
| 30 | + |
| 31 | +agent = Agent( |
| 32 | + name="Support agent", |
| 33 | + instructions="Handle tickets and ask for approval when needed.", |
| 34 | + tools=[cancel_order, send_email], |
| 35 | +) |
| 36 | +``` |
| 37 | + |
| 38 | +`needs_approval` は [`function_tool`][agents.tool.function_tool]、[`Agent.as_tool`][agents.agent.Agent.as_tool]、[`ShellTool`][agents.tool.ShellTool]、[`ApplyPatchTool`][agents.tool.ApplyPatchTool] で利用できます。ローカル MCP サーバーも、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio]、[`MCPServerSse`][agents.mcp.server.MCPServerSse]、[`MCPServerStreamableHttp`][agents.mcp.server.MCPServerStreamableHttp] の `require_approval` を通じて承認をサポートします。Hosted MCP サーバーは、`tool_config={"require_approval": "always"}` と任意の `on_approval_request` コールバックを指定した [`HostedMCPTool`][agents.tool.HostedMCPTool] を介して承認をサポートします。Shell および apply_patch ツールは、割り込みを出さずに自動承認または自動拒否したい場合に `on_approval` コールバックを受け付けます。 |
| 39 | + |
| 40 | +## 承認フローの仕組み |
| 41 | + |
| 42 | +1. モデルがツール呼び出しを出力すると、runner が `needs_approval` を評価します。 |
| 43 | +2. そのツール呼び出しに対する承認判断がすでに [`RunContextWrapper`][agents.run_context.RunContextWrapper] に保存されている場合(例: `always_approve=True` によるもの)、runner はプロンプトを出さずに続行します。呼び出しごとの承認は特定の呼び出し ID にスコープされます。将来の呼び出しを自動的に許可するには `always_approve=True` を使用します。 |
| 44 | +3. それ以外の場合、実行は一時停止し、`RunResult.interruptions`(または `RunResultStreaming.interruptions`)に `agent.name`、`name`、`arguments` などの詳細を含む `ToolApprovalItem` エントリが入ります。 |
| 45 | +4. `result.to_state()` で結果を `RunState` に変換し、`state.approve(...)` または `state.reject(...)` を呼び出し(任意で `always_approve` または `always_reject` を渡します)、その後 `Runner.run(agent, state)` または `Runner.run_streamed(agent, state)` で再開します。 |
| 46 | +5. 再開された実行は中断した箇所から継続し、新たに承認が必要になればこのフローに再度入ります。 |
| 47 | + |
| 48 | +## 例: 一時停止、承認、再開 |
| 49 | + |
| 50 | +以下のスニペットは JavaScript HITL ガイドを踏襲しています。ツールが承認を必要としたときに一時停止し、状態をディスクに永続化し、それを再読み込みして、判断を収集した後に再開します。 |
| 51 | + |
| 52 | +```python |
| 53 | +import asyncio |
| 54 | +import json |
| 55 | +from pathlib import Path |
| 56 | + |
| 57 | +from agents import Agent, Runner, RunState, function_tool |
| 58 | + |
| 59 | + |
| 60 | +async def needs_oakland_approval(_ctx, params, _call_id) -> bool: |
| 61 | + return "Oakland" in params.get("city", "") |
| 62 | + |
| 63 | + |
| 64 | +@function_tool(needs_approval=needs_oakland_approval) |
| 65 | +async def get_temperature(city: str) -> str: |
| 66 | + return f"The temperature in {city} is 20° Celsius" |
| 67 | + |
| 68 | + |
| 69 | +agent = Agent( |
| 70 | + name="Weather assistant", |
| 71 | + instructions="Answer weather questions with the provided tools.", |
| 72 | + tools=[get_temperature], |
| 73 | +) |
| 74 | + |
| 75 | +STATE_PATH = Path(".cache/hitl_state.json") |
| 76 | + |
| 77 | + |
| 78 | +def prompt_approval(tool_name: str, arguments: str | None) -> bool: |
| 79 | + answer = input(f"Approve {tool_name} with {arguments}? [y/N]: ").strip().lower() |
| 80 | + return answer in {"y", "yes"} |
| 81 | + |
| 82 | + |
| 83 | +async def main() -> None: |
| 84 | + result = await Runner.run(agent, "What is the temperature in Oakland?") |
| 85 | + |
| 86 | + while result.interruptions: |
| 87 | + # Persist the paused state. |
| 88 | + state = result.to_state() |
| 89 | + STATE_PATH.parent.mkdir(parents=True, exist_ok=True) |
| 90 | + STATE_PATH.write_text(state.to_string()) |
| 91 | + |
| 92 | + # Load the state later (could be a different process). |
| 93 | + stored = json.loads(STATE_PATH.read_text()) |
| 94 | + state = await RunState.from_json(agent, stored) |
| 95 | + |
| 96 | + for interruption in result.interruptions: |
| 97 | + approved = await asyncio.get_running_loop().run_in_executor( |
| 98 | + None, prompt_approval, interruption.name or "unknown_tool", interruption.arguments |
| 99 | + ) |
| 100 | + if approved: |
| 101 | + state.approve(interruption, always_approve=False) |
| 102 | + else: |
| 103 | + state.reject(interruption) |
| 104 | + |
| 105 | + result = await Runner.run(agent, state) |
| 106 | + |
| 107 | + print(result.final_output) |
| 108 | + |
| 109 | + |
| 110 | +if __name__ == "__main__": |
| 111 | + asyncio.run(main()) |
| 112 | +``` |
| 113 | + |
| 114 | +この例では、`prompt_approval` は `input()` を使用し、`run_in_executor(...)` で実行されるため同期的です。承認元がすでに非同期(例: HTTP リクエストや async データベース クエリ)であれば、`async def` 関数を使い、代わりに直接 `await` できます。 |
| 115 | + |
| 116 | +承認待ちの間も出力をストリーミングするには `Runner.run_streamed` を呼び出し、完了するまで `result.stream_events()` を消費してから、上記と同じ `result.to_state()` と再開手順に従ってください。 |
| 117 | + |
| 118 | +## このリポジトリの他のパターン |
| 119 | + |
| 120 | +- **ストリーミング承認**: `examples/agent_patterns/human_in_the_loop_stream.py` は、`stream_events()` を最後まで取り出してから、保留中のツール呼び出しを承認し、`Runner.run_streamed(agent, state)` で再開する方法を示します。 |
| 121 | +- **ツールとしてのエージェントの承認**: `Agent.as_tool(..., needs_approval=...)` は、委譲されたエージェント タスクにレビューが必要な場合に同じ割り込みフローを適用します。 |
| 122 | +- **Shell および apply_patch ツール**: `ShellTool` と `ApplyPatchTool` も `needs_approval` をサポートします。将来の呼び出しに対して判断をキャッシュするには `state.approve(interruption, always_approve=True)` または `state.reject(..., always_reject=True)` を使用します。自動判断には `on_approval` を指定します(`examples/tools/shell.py` を参照)。手動判断には割り込みを処理します(`examples/tools/shell_human_in_the_loop.py` を参照)。 |
| 123 | +- **ローカル MCP サーバー**: `MCPServerStdio` / `MCPServerSse` / `MCPServerStreamableHttp` の `require_approval` を使用して MCP ツール呼び出しをゲートします(`examples/mcp/get_all_mcp_tools_example/main.py` と `examples/mcp/tool_filter_example/main.py` を参照)。 |
| 124 | +- **Hosted MCP サーバー**: `HostedMCPTool` の `require_approval` を `"always"` に設定して HITL を強制し、必要に応じて `on_approval_request` を指定して自動承認または拒否を行います(`examples/hosted_mcp/human_in_the_loop.py` と `examples/hosted_mcp/on_approval.py` を参照)。信頼できるサーバーには `"never"` を使用します(`examples/hosted_mcp/simple.py`)。 |
| 125 | +- **セッションとメモリ**: `Runner.run` にセッションを渡すことで、承認と会話履歴が複数ターンにわたって維持されます。SQLite と OpenAI Conversations のセッション バリアントは `examples/memory/memory_session_hitl_example.py` と `examples/memory/openai_session_hitl_example.py` にあります。 |
| 126 | +- **Realtime エージェント**: realtime デモは、`RealtimeSession` の `approve_tool_call` / `reject_tool_call` を介してツール呼び出しを承認または拒否する WebSocket メッセージを公開します(サーバー側ハンドラーは `examples/realtime/app/server.py` を参照)。 |
| 127 | + |
| 128 | +## 長時間にわたる承認 |
| 129 | + |
| 130 | +`RunState` は永続性を意図して設計されています。`state.to_json()` または `state.to_string()` を使用して保留中の作業をデータベースやキューに保存し、後で `RunState.from_json(...)` または `RunState.from_string(...)` で再作成できます。シリアライズされたペイロードに機微なコンテキスト データを永続化したくない場合は `context_override` を渡してください。 |
| 131 | + |
| 132 | +## 保留タスクのバージョニング |
| 133 | + |
| 134 | +承認がしばらく滞留する可能性がある場合は、エージェント定義または SDK のバージョン マーカーを、シリアライズされた状態と一緒に保存してください。そうすれば、デシリアライズを一致するコード パスにルーティングでき、モデル、プロンプト、ツール定義が変更された際の非互換を回避できます。 |
0 commit comments