Human-in-the-Loop: OpenAI Implementation
Overview
Section titled âOverviewâOpenAI provides two main approaches for human-in-the-loop:
- Function Calling (Chat Completions API) - Manual implementation with full control
- Agents SDK - Built-in approval workflows with
needsApproval
graph TB A[OpenAI HITL] --> B[Function Calling] A --> C[Agents SDK]
B --> D[Custom Implementation] B --> E[Full Control]
C --> F[needsApproval Flag] C --> G[Automatic Pausing]Approach 1: Function Calling
Section titled âApproach 1: Function CallingâHow It Works
Section titled âHow It WorksâYou define custom functions (tools) that GPT can call:
sequenceDiagram participant U as User participant App as Your App participant API as OpenAI API participant GPT as GPT-4
U->>App: "Add authentication" App->>API: Request + tools API->>GPT: Process
GPT->>API: Generate function call API->>App: Response with tool_calls
App->>App: Detect ask_user_question App->>U: Render UI
U->>App: Select option App->>API: Tool result API->>GPT: Continue
GPT->>API: Final response API->>App: Complete App->>U: Show resultDefine the Tool
Section titled âDefine the Toolâimport openaiimport json
# Define the ask_user_question functiontools = [ { "type": "function", "function": { "name": "ask_user_question", "description": "Ask the user a multiple choice question and wait for their response", "parameters": { "type": "object", "properties": { "question": { "type": "string", "description": "The question to ask the user" }, "options": { "type": "array", "description": "Available answer choices", "items": { "type": "object", "properties": { "label": { "type": "string", "description": "Display text for this option" }, "value": { "type": "string", "description": "Value to return if selected" }, "description": { "type": "string", "description": "Explanation of this option" } }, "required": ["label", "value", "description"] }, "minItems": 2, "maxItems": 5 }, "allow_multiple": { "type": "boolean", "description": "Whether user can select multiple options" } }, "required": ["question", "options"] } } }]Make the Request
Section titled âMake the Requestâ# Send request to OpenAIresponse = openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "You are a helpful assistant that asks clarifying questions."}, {"role": "user", "content": "Help me set up authentication for my app"} ], tools=tools, tool_choice="auto" # Let model decide when to use tools)Handle the Response
Section titled âHandle the Responseâ# Check for tool callsif response.choices[0].message.tool_calls: tool_call = response.choices[0].message.tool_calls[0]
if tool_call.function.name == "ask_user_question": # Parse arguments args = json.loads(tool_call.function.arguments)
# Display to user (your custom UI logic) user_answer = display_question_ui( question=args["question"], options=args["options"], allow_multiple=args.get("allow_multiple", False) )
# Return result to GPT messages.append(response.choices[0].message) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"selected": user_answer}) })
# Continue conversation response = openai.chat.completions.create( model="gpt-4o", messages=messages, tools=tools )Example: Full Implementation
Section titled âExample: Full Implementationâdef interactive_agent(user_request: str): """Run an interactive agent with human-in-the-loop"""
messages = [ {"role": "system", "content": "You are a helpful assistant. Use ask_user_question when you need clarification."}, {"role": "user", "content": user_request} ]
max_iterations = 10
for iteration in range(max_iterations): # Call OpenAI response = openai.chat.completions.create( model="gpt-4o", messages=messages, tools=tools )
message = response.choices[0].message
# Check if done if not message.tool_calls: return message.content
# Handle tool calls for tool_call in message.tool_calls: if tool_call.function.name == "ask_user_question": # Ask user args = json.loads(tool_call.function.arguments) user_answer = ask_user_in_terminal(args)
# Add to conversation messages.append(message) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"answer": user_answer}) })
return "Max iterations reached"
def ask_user_in_terminal(args): """Simple terminal UI""" print(f"\nâ {args['question']}") print("â" * 70)
for i, option in enumerate(args['options'], 1): print(f" {i}. {option['label']}") print(f" {option['description']}") print()
choice = input(f"Select option (1-{len(args['options'])}): ").strip() idx = int(choice) - 1 return args['options'][idx]['value']Structured Outputs (Guaranteed Compliance)
Section titled âStructured Outputs (Guaranteed Compliance)âUse strict: true for 100% schema compliance:
tools = [ { "type": "function", "function": { "name": "ask_user_question", "strict": True, # â Enables Structured Outputs "parameters": { "type": "object", "properties": { "question": {"type": "string"}, "options": { "type": "array", "items": { "type": "object", "properties": { "label": {"type": "string"}, "value": {"type": "string"} }, "required": ["label", "value"], "additionalProperties": False } } }, "required": ["question", "options"], "additionalProperties": False } } }]Benefits:
- đŻ 100% schema compliance
- đĄď¸ Type safety guaranteed
- đŤ No hallucinated fields
- â Better reliability
Approach 2: OpenAI Agents SDK
Section titled âApproach 2: OpenAI Agents SDKâOverview
Section titled âOverviewâThe Agents SDK provides built-in approval workflows:
graph LR A[Tool Definition] --> B{needsApproval?} B -->|true| C[Always Pause] B -->|function| D[Conditional Pause] B -->|false| E[Auto Execute] C --> F[Wait for User] D --> F F --> G[Approved?] G -->|Yes| H[Execute] G -->|No| I[Reject]Installation
Section titled âInstallationânpm install openai @openai/agentsBasic Usage
Section titled âBasic Usageâimport { Agent } from '@openai/agents';
const agent = new Agent({ name: 'My Agent', model: 'gpt-4o', instructions: 'You are a helpful assistant', tools: [ { name: 'send_email', description: 'Send an email to customers', needsApproval: true, // â Always requires approval execute: async ({ to, subject, body }) => { // This only runs after approval return await sendEmail(to, subject, body); }, }, ],});Run with Approval Flow
Section titled âRun with Approval Flowâ// Run the agentconst result = await agent.run('Send welcome email to new customers');
// Check for interruptions (approval requests)if (result.interruptions && result.interruptions.length > 0) { for (const interruption of result.interruptions) { // Show approval UI to user const approved = await showApprovalUI({ action: interruption.tool.name, arguments: interruption.arguments, description: interruption.tool.description, });
if (approved) { result.state.approve(interruption); } else { result.state.reject(interruption); } }
// Resume execution after approvals const finalResult = await agent.resume(result.state); console.log(finalResult.content);}Conditional Approval
Section titled âConditional ApprovalâUse a function to decide when approval is needed:
const agent = new Agent({ tools: [ { name: 'delete_data', description: 'Delete data from database', needsApproval: async ({ table, where }) => { // Require approval only for sensitive tables const sensitiveTables = ['users', 'payments', 'accounts']; return sensitiveTables.includes(table); }, execute: async ({ table, where }) => { return await db.delete(table, where); }, }, { name: 'send_email', description: 'Send email', needsApproval: async ({ recipients }) => { // Require approval for bulk emails return recipients.length > 100; }, execute: async ({ recipients, subject, body }) => { return await sendBulkEmail(recipients, subject, body); }, }, ],});Complete Example
Section titled âComplete Exampleâimport { Agent } from '@openai/agents';
// Create agent with approval workflowconst deploymentAgent = new Agent({ name: 'Deployment Assistant', model: 'gpt-4o', instructions: `You help users deploy applications. Always use appropriate tools for each environment.`,
tools: [ // Production - always needs approval { name: 'deploy_to_production', description: 'Deploy to production environment', needsApproval: true, execute: async ({ version }) => { await deployToProduction(version); return { status: 'deployed', environment: 'production', version }; }, },
// Staging - no approval needed { name: 'deploy_to_staging', description: 'Deploy to staging environment', needsApproval: false, execute: async ({ version }) => { await deployToStaging(version); return { status: 'deployed', environment: 'staging', version }; }, },
// Rollback - conditional approval { name: 'rollback', description: 'Rollback to previous version', needsApproval: async ({ environment }) => { // Approval only needed for production return environment === 'production'; }, execute: async ({ environment, version }) => { await rollback(environment, version); return { status: 'rolled back', environment, version }; }, }, ],});
// Usageasync function deployApp() { const result = await deploymentAgent.run('Deploy version 2.5.0 to production');
// Handle approvals if (result.interruptions?.length > 0) { console.log('â ď¸ Approval required:');
for (const interruption of result.interruptions) { console.log(`\nAction: ${interruption.tool.name}`); console.log(`Arguments:`, interruption.arguments);
// Show approval UI (your implementation) const approved = await promptUser(`Approve ${interruption.tool.name}?`, ['Yes', 'No']);
if (approved) { console.log('â
Approved'); result.state.approve(interruption); } else { console.log('â Rejected'); result.state.reject(interruption); } }
// Resume after handling approvals const finalResult = await deploymentAgent.resume(result.state); console.log('\nđ Final result:', finalResult.content); } else { console.log('\nâ
Completed without approvals'); console.log(result.content); }}Comparison: Function Calling vs Agents SDK
Section titled âComparison: Function Calling vs Agents SDKâgraph TB subgraph FC["Function Calling"] FC1[Define Tool Schema] FC2[Handle Tool Calls] FC3[Implement UI] FC4[Manage State] FC1 --> FC2 --> FC3 --> FC4 end
subgraph SDK["Agents SDK"] SDK1[Define Tool + needsApproval] SDK2[Run Agent] SDK3[Handle Interruptions] SDK1 --> SDK2 --> SDK3 end| Aspect | Function Calling | Agents SDK |
|---|---|---|
| Setup | Manual tool definition | Define with needsApproval |
| Approval Flow | Manual implementation | Built-in with interruptions |
| State Management | Manual | Automatic via result.state |
| Complexity | High (~200+ LOC) | Medium (~50 LOC) |
| Flexibility | Full control | Standardized pattern |
| UI | Fully custom | Need to implement |
| Best For | Custom workflows | Standard approvals |
Best Practices
Section titled âBest Practicesâ1. Use Structured Outputs
Section titled â1. Use Structured Outputsâ# â
Good: Guaranteed schema compliance{ "strict": True, "parameters": { "type": "object", "properties": {...}, "additionalProperties": False # No extra fields }}
# â Bad: Loose schema{ "parameters": { "type": "object", "properties": {...} # No strict mode, no protection }}2. Handle Parallel Tool Calls
Section titled â2. Handle Parallel Tool Callsâ# GPT-4 can make multiple tool calls at onceif response.choices[0].message.tool_calls: for tool_call in response.choices[0].message.tool_calls: # Process each tool call result = execute_tool(tool_call)3. Validate User Input
Section titled â3. Validate User Inputâdef ask_user_in_terminal(args): """Validated terminal input""" while True: try: choice = input(f"Select (1-{len(args['options'])}): ").strip() idx = int(choice) - 1
if 0 <= idx < len(args['options']): return args['options'][idx]['value'] else: print("â Invalid choice. Try again.") except (ValueError, KeyboardInterrupt): print("â Invalid input.")4. Error Handling
Section titled â4. Error Handlingâdef execute_tool(tool_call): """Safe tool execution""" try: function_name = tool_call.function.name arguments = json.loads(tool_call.function.arguments)
# Execute result = TOOL_MAP[function_name](**arguments)
return { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result) }
except json.JSONDecodeError as e: return { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({ "error": f"Invalid JSON: {str(e)}" }) }
except Exception as e: return { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({ "error": f"Execution failed: {str(e)}" }) }5. Tool Choice Control
Section titled â5. Tool Choice Controlâ# Let model decidetool_choice="auto"
# Force tool usetool_choice="required"
# Specific tooltool_choice={"type": "function", "function": {"name": "ask_user_question"}}
# No toolstool_choice="none"Common Pitfalls
Section titled âCommon Pitfallsââ Pitfall 1: Not Handling Parallel Calls
Section titled ââ Pitfall 1: Not Handling Parallel Callsâ# Wrong: Assumes only one tool calltool_call = response.choices[0].message.tool_calls[0] # May crash!
# Correct: Handle multiplefor tool_call in response.choices[0].message.tool_calls: process_tool_call(tool_call)â Pitfall 2: Forgetting to Add Messages
Section titled ââ Pitfall 2: Forgetting to Add Messagesâ# Wrong: Loses contextresponse = openai.chat.completions.create( model="gpt-4o", messages=messages # Missing assistant message and tool result)
# Correct: Maintain full historymessages.append(response.choices[0].message) # Assistant messagemessages.append(tool_result) # Tool resultresponse = openai.chat.completions.create( model="gpt-4o", messages=messages)â Pitfall 3: Invalid JSON Parsing
Section titled ââ Pitfall 3: Invalid JSON Parsingâ# Wrong: No error handlingargs = json.loads(tool_call.function.arguments)
# Correct: Handle errorstry: args = json.loads(tool_call.function.arguments)except json.JSONDecodeError: return create_error_response(tool_call.id, "Invalid JSON")â Pitfall 4: Not Checking finish_reason
Section titled ââ Pitfall 4: Not Checking finish_reasonâ# Wrong: Assumes content existsprint(response.choices[0].message.content) # May be None!
# Correct: Check finish_reasonfinish_reason = response.choices[0].finish_reasonif finish_reason == "tool_calls": handle_tool_calls(response.choices[0].message.tool_calls)elif finish_reason == "stop": print(response.choices[0].message.content)When to Use Each Approach
Section titled âWhen to Use Each Approachâ| Use Case | Recommendation |
|---|---|
| Simple Q&A | Function Calling |
| Approval workflows | Agents SDK |
| Custom validation | Function Calling |
| Standard approvals | Agents SDK |
| Complex UI | Function Calling |
| Quick setup | Agents SDK |
| Multi-provider | Function Calling + LangChain |
Next Steps
Section titled âNext Stepsâ- Need flexibility? â Check Model Agnostic Approach
- Want simplicity? â Review Claude Code Implementation