153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
"""
|
|
File-backed session example with human-in-the-loop tool approval.
|
|
|
|
This mirrors the JS `file-hitl.ts` sample: a session persisted on disk and tools that
|
|
require approval before execution.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Any
|
|
|
|
from agents import Agent, Runner, function_tool
|
|
from agents.run_context import RunContextWrapper
|
|
from agents.run_state import RunState
|
|
from examples.auto_mode import confirm_with_fallback, input_with_fallback, is_auto_mode
|
|
|
|
from .file_session import FileSession
|
|
|
|
|
|
async def main() -> None:
|
|
user_context = {"user_id": "101"}
|
|
|
|
customer_directory: dict[str, str] = {
|
|
"101": (
|
|
"Customer Kaz S. (tier gold) can be reached at +1-415-555-AAAA. "
|
|
"Notes: Prefers SMS follow ups and values concise summaries."
|
|
),
|
|
"104": (
|
|
"Customer Yu S. (tier platinum) can be reached at +1-415-555-BBBB. "
|
|
"Notes: Recently reported sync issues. Flagged for a proactive onboarding call."
|
|
),
|
|
"205": (
|
|
"Customer Ken S. (tier standard) can be reached at +1-415-555-CCCC. "
|
|
"Notes: Interested in automation tutorials sent last week."
|
|
),
|
|
}
|
|
|
|
lookup_customer_profile = create_lookup_customer_profile_tool(directory=customer_directory)
|
|
|
|
instructions = (
|
|
"You assist support agents. For every user turn you must call lookup_customer_profile. "
|
|
"If a tool reports a transient failure, request approval and retry the same call once before "
|
|
"responding. Keep responses under three sentences."
|
|
)
|
|
|
|
agent = Agent(
|
|
name="File HITL assistant",
|
|
instructions=instructions,
|
|
tools=[lookup_customer_profile],
|
|
)
|
|
|
|
session = FileSession(dir="examples/memory/tmp")
|
|
session_id = await session.get_session_id()
|
|
print(f"Session id: {session_id}")
|
|
print("Enter a message to chat with the agent. Submit an empty line to exit.")
|
|
auto_mode = is_auto_mode()
|
|
|
|
saved_state = await session.load_state_json()
|
|
if saved_state:
|
|
print("Found saved run state. Resuming pending interruptions before new input.")
|
|
try:
|
|
state = await RunState.from_json(agent, saved_state, context_override=user_context)
|
|
result = await Runner.run(agent, state, session=session)
|
|
while result.interruptions:
|
|
state = result.to_state()
|
|
for interruption in result.interruptions:
|
|
args = format_tool_arguments(interruption)
|
|
approved = await prompt_yes_no(
|
|
f"Agent {interruption.agent.name} wants to call {interruption.name} with {args or 'no arguments'}"
|
|
)
|
|
if approved:
|
|
state.approve(interruption)
|
|
print("Approved tool call.")
|
|
else:
|
|
state.reject(interruption)
|
|
print("Rejected tool call.")
|
|
result = await Runner.run(agent, state, session=session)
|
|
await session.save_state_json(result.to_state().to_json())
|
|
reply = result.final_output or "[No final output produced]"
|
|
print(f"Assistant (resumed): {reply}\n")
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"Failed to resume saved state: {exc}. Starting a new session.")
|
|
|
|
while True:
|
|
if auto_mode:
|
|
user_message = input_with_fallback("You: ", "Summarize the customer profile.")
|
|
else:
|
|
print("You: ", end="", flush=True)
|
|
loop = asyncio.get_event_loop()
|
|
user_message = await loop.run_in_executor(None, input)
|
|
if not user_message.strip():
|
|
break
|
|
|
|
result = await Runner.run(agent, user_message, session=session, context=user_context)
|
|
while result.interruptions:
|
|
state = result.to_state()
|
|
for interruption in result.interruptions:
|
|
args = format_tool_arguments(interruption)
|
|
approved = await prompt_yes_no(
|
|
f"Agent {interruption.agent.name} wants to call {interruption.name} with {args or 'no arguments'}"
|
|
)
|
|
if approved:
|
|
state.approve(interruption)
|
|
print("Approved tool call.")
|
|
else:
|
|
state.reject(interruption)
|
|
print("Rejected tool call.")
|
|
result = await Runner.run(agent, state, session=session)
|
|
await session.save_state_json(result.to_state().to_json())
|
|
|
|
reply = result.final_output or "[No final output produced]"
|
|
print(f"Assistant: {reply}\n")
|
|
if auto_mode:
|
|
break
|
|
|
|
|
|
def create_lookup_customer_profile_tool(
|
|
*,
|
|
directory: dict[str, str],
|
|
missing_customer_message: str = "No customer found for that id.",
|
|
):
|
|
@function_tool(
|
|
name_override="lookup_customer_profile",
|
|
description_override="Look up stored profile details for a customer by their internal id.",
|
|
needs_approval=True,
|
|
)
|
|
def lookup_customer_profile(ctx: RunContextWrapper[Any]) -> str:
|
|
return directory.get(ctx.context.get("user_id"), missing_customer_message)
|
|
|
|
return lookup_customer_profile
|
|
|
|
|
|
def format_tool_arguments(interruption: Any) -> str:
|
|
args = getattr(interruption, "arguments", None)
|
|
if args is None:
|
|
return ""
|
|
if isinstance(args, str):
|
|
return args
|
|
try:
|
|
return json.dumps(args)
|
|
except Exception:
|
|
return str(args)
|
|
|
|
|
|
async def prompt_yes_no(question: str) -> bool:
|
|
return confirm_with_fallback(f"{question} (y/n): ", default=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|