Skip to content

Commit 3d52667

Browse files
committed
fix: Bring up coverage to minimum and add session hitl examples
1 parent 351742d commit 3d52667

File tree

5 files changed

+974
-6
lines changed

5 files changed

+974
-6
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Example demonstrating SQLite in-memory session with human-in-the-loop (HITL) tool approval.
3+
4+
This example shows how to use SQLite in-memory session memory combined with
5+
human-in-the-loop tool approval. The session maintains conversation history while
6+
requiring approval for specific tool calls.
7+
"""
8+
9+
import asyncio
10+
11+
from agents import Agent, Runner, SQLiteSession, function_tool
12+
13+
14+
async def _needs_approval(_ctx, _params, _call_id) -> bool:
15+
"""Always require approval for weather tool."""
16+
return True
17+
18+
19+
@function_tool(needs_approval=_needs_approval)
20+
def get_weather(location: str) -> str:
21+
"""Get weather for a location.
22+
23+
Args:
24+
location: The location to get weather for
25+
26+
Returns:
27+
Weather information as a string
28+
"""
29+
# Simulated weather data
30+
weather_data = {
31+
"san francisco": "Foggy, 58°F",
32+
"oakland": "Sunny, 72°F",
33+
"new york": "Rainy, 65°F",
34+
}
35+
# Check if any city name is in the provided location string
36+
location_lower = location.lower()
37+
for city, weather in weather_data.items():
38+
if city in location_lower:
39+
return weather
40+
return f"Weather data not available for {location}"
41+
42+
43+
async def prompt_yes_no(question: str) -> bool:
44+
"""Prompt user for yes/no answer.
45+
46+
Args:
47+
question: The question to ask
48+
49+
Returns:
50+
True if user answered yes, False otherwise
51+
"""
52+
print(f"\n{question} (y/n): ", end="", flush=True)
53+
loop = asyncio.get_event_loop()
54+
answer = await loop.run_in_executor(None, input)
55+
normalized = answer.strip().lower()
56+
return normalized in ("y", "yes")
57+
58+
59+
async def main():
60+
# Create an agent with a tool that requires approval
61+
agent = Agent(
62+
name="HITL Assistant",
63+
instructions="You help users with information. Always use available tools when appropriate. Keep responses concise.",
64+
tools=[get_weather],
65+
)
66+
67+
# Create an in-memory SQLite session instance that will persist across runs
68+
session = SQLiteSession(":memory:")
69+
session_id = session.session_id
70+
71+
print("=== Memory Session + HITL Example ===")
72+
print(f"Session id: {session_id}")
73+
print("Enter a message to chat with the agent. Submit an empty line to exit.")
74+
print("The agent will ask for approval before using tools.\n")
75+
76+
while True:
77+
# Get user input
78+
print("You: ", end="", flush=True)
79+
loop = asyncio.get_event_loop()
80+
user_message = await loop.run_in_executor(None, input)
81+
82+
if not user_message.strip():
83+
break
84+
85+
# Run the agent
86+
result = await Runner.run(agent, user_message, session=session)
87+
88+
# Handle interruptions (tool approvals)
89+
while result.interruptions:
90+
# Get the run state
91+
state = result.to_state()
92+
93+
for interruption in result.interruptions:
94+
tool_name = interruption.raw_item.name # type: ignore[union-attr]
95+
args = interruption.raw_item.arguments or "(no arguments)" # type: ignore[union-attr]
96+
97+
approved = await prompt_yes_no(
98+
f"Agent {interruption.agent.name} wants to call '{tool_name}' with {args}. Approve?"
99+
)
100+
101+
if approved:
102+
state.approve(interruption)
103+
print("Approved tool call.")
104+
else:
105+
state.reject(interruption)
106+
print("Rejected tool call.")
107+
108+
# Resume the run with the updated state
109+
result = await Runner.run(agent, state, session=session)
110+
111+
# Display the response
112+
reply = result.final_output or "[No final output produced]"
113+
print(f"Assistant: {reply}\n")
114+
115+
116+
if __name__ == "__main__":
117+
asyncio.run(main())
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""
2+
Example demonstrating OpenAI Conversations session with human-in-the-loop (HITL) tool approval.
3+
4+
This example shows how to use OpenAI Conversations session memory combined with
5+
human-in-the-loop tool approval. The session maintains conversation history while
6+
requiring approval for specific tool calls.
7+
"""
8+
9+
import asyncio
10+
11+
from agents import Agent, OpenAIConversationsSession, Runner, function_tool
12+
13+
14+
async def _needs_approval(_ctx, _params, _call_id) -> bool:
15+
"""Always require approval for weather tool."""
16+
return True
17+
18+
19+
@function_tool(needs_approval=_needs_approval)
20+
def get_weather(location: str) -> str:
21+
"""Get weather for a location.
22+
23+
Args:
24+
location: The location to get weather for
25+
26+
Returns:
27+
Weather information as a string
28+
"""
29+
# Simulated weather data
30+
weather_data = {
31+
"san francisco": "Foggy, 58°F",
32+
"oakland": "Sunny, 72°F",
33+
"new york": "Rainy, 65°F",
34+
}
35+
# Check if any city name is in the provided location string
36+
location_lower = location.lower()
37+
for city, weather in weather_data.items():
38+
if city in location_lower:
39+
return weather
40+
return f"Weather data not available for {location}"
41+
42+
43+
async def prompt_yes_no(question: str) -> bool:
44+
"""Prompt user for yes/no answer.
45+
46+
Args:
47+
question: The question to ask
48+
49+
Returns:
50+
True if user answered yes, False otherwise
51+
"""
52+
print(f"\n{question} (y/n): ", end="", flush=True)
53+
loop = asyncio.get_event_loop()
54+
answer = await loop.run_in_executor(None, input)
55+
normalized = answer.strip().lower()
56+
return normalized in ("y", "yes")
57+
58+
59+
async def main():
60+
# Create an agent with a tool that requires approval
61+
agent = Agent(
62+
name="HITL Assistant",
63+
instructions="You help users with information. Always use available tools when appropriate. Keep responses concise.",
64+
tools=[get_weather],
65+
)
66+
67+
# Create a session instance that will persist across runs
68+
session = OpenAIConversationsSession()
69+
70+
print("=== OpenAI Session + HITL Example ===")
71+
print("Enter a message to chat with the agent. Submit an empty line to exit.")
72+
print("The agent will ask for approval before using tools.\n")
73+
74+
while True:
75+
# Get user input
76+
print("You: ", end="", flush=True)
77+
loop = asyncio.get_event_loop()
78+
user_message = await loop.run_in_executor(None, input)
79+
80+
if not user_message.strip():
81+
break
82+
83+
# Run the agent
84+
result = await Runner.run(agent, user_message, session=session)
85+
86+
# Handle interruptions (tool approvals)
87+
while result.interruptions:
88+
# Get the run state
89+
state = result.to_state()
90+
91+
for interruption in result.interruptions:
92+
tool_name = interruption.raw_item.name # type: ignore[union-attr]
93+
args = interruption.raw_item.arguments or "(no arguments)" # type: ignore[union-attr]
94+
95+
approved = await prompt_yes_no(
96+
f"Agent {interruption.agent.name} wants to call '{tool_name}' with {args}. Approve?"
97+
)
98+
99+
if approved:
100+
state.approve(interruption)
101+
print("Approved tool call.")
102+
else:
103+
state.reject(interruption)
104+
print("Rejected tool call.")
105+
106+
# Resume the run with the updated state
107+
result = await Runner.run(agent, state, session=session)
108+
109+
# Display the response
110+
reply = result.final_output or "[No final output produced]"
111+
print(f"Assistant: {reply}\n")
112+
113+
114+
if __name__ == "__main__":
115+
asyncio.run(main())

src/agents/run.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,9 @@ async def run(
593593
should_run_agent_start_hooks = True
594594

595595
# save only the new user input to the session, not the combined history
596-
await self._save_result_to_session(session, original_user_input, [])
596+
# Skip saving if resuming from state - input is already in session
597+
if not is_resumed_state:
598+
await self._save_result_to_session(session, original_user_input, [])
597599

598600
# If resuming from an interrupted state, execute approved tools first
599601
if is_resumed_state and run_state is not None and run_state._current_step is not None:

src/agents/run_state.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,12 @@ def to_json(self) -> dict[str, Any]:
202202
},
203203
"approvals": approvals_dict,
204204
"context": self._context.context
205-
if hasattr(self._context.context, "__dict__")
206-
else {},
205+
if isinstance(self._context.context, dict)
206+
else (
207+
self._context.context.__dict__
208+
if hasattr(self._context.context, "__dict__")
209+
else {}
210+
),
207211
},
208212
"maxTurns": self._max_turns,
209213
"inputGuardrailResults": [
@@ -491,9 +495,10 @@ def _build_agent_map(initial_agent: Agent[Any]) -> dict[str, Agent[Any]]:
491495

492496
# Add handoff agents to the queue
493497
for handoff in current.handoffs:
494-
if hasattr(handoff, "agent") and handoff.agent:
495-
if handoff.agent.name not in agent_map:
496-
queue.append(handoff.agent)
498+
# Handoff can be either an Agent or a Handoff object with an .agent attribute
499+
handoff_agent = handoff if not hasattr(handoff, "agent") else handoff.agent
500+
if handoff_agent and handoff_agent.name not in agent_map: # type: ignore[union-attr]
501+
queue.append(handoff_agent) # type: ignore[arg-type]
497502

498503
return agent_map
499504

0 commit comments

Comments
 (0)