Files
gh-jezweb-claude-skills-ski…/templates/text-agents/agent-human-approval.ts
2025-11-30 08:25:09 +08:00

229 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Human-in-the-Loop (HITL) Pattern
*
* Demonstrates:
* - Requiring human approval for tools
* - Handling interruptions
* - Approving/rejecting tool calls
* - Serializing and resuming state
*/
import { z } from 'zod';
import {
Agent,
Runner,
tool,
ToolApprovalItem,
} from '@openai/agents';
import * as readline from 'readline';
// ========================================
// Tools Requiring Approval
// ========================================
const sendEmailTool = tool({
name: 'send_email',
description: 'Send an email to a recipient',
parameters: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
execute: async ({ to, subject, body }) => {
console.log('\n📧 Email sent!');
return `Email sent to ${to} with subject "${subject}"`;
},
requiresApproval: true, // Require human approval
});
const processRefundTool = tool({
name: 'process_refund',
description: 'Process a refund for a customer',
parameters: z.object({
customerId: z.string(),
amount: z.number(),
reason: z.string(),
}),
execute: async ({ customerId, amount, reason }) => {
console.log('\n💰 Refund processed!');
return `Refunded $${amount} to customer ${customerId}. Reason: ${reason}`;
},
requiresApproval: true,
});
const deleteAccountTool = tool({
name: 'delete_account',
description: 'Permanently delete a user account',
parameters: z.object({
userId: z.string(),
confirmation: z.string(),
}),
execute: async ({ userId }) => {
console.log('\n🗑 Account deleted!');
return `Account ${userId} has been permanently deleted`;
},
requiresApproval: true,
});
// ========================================
// Agent with Approval-Required Tools
// ========================================
const customerServiceAgent = new Agent({
name: 'Customer Service Agent',
instructions: 'You help customers with their requests. Use tools when necessary but they will require human approval.',
tools: [sendEmailTool, processRefundTool, deleteAccountTool],
});
// ========================================
// Helper: Prompt User for Approval
// ========================================
async function promptUserForApproval(
toolName: string,
args: Record<string, any>
): Promise<boolean> {
console.log('\n' + '='.repeat(60));
console.log('⚠️ APPROVAL REQUIRED');
console.log('='.repeat(60));
console.log('Tool:', toolName);
console.log('Arguments:', JSON.stringify(args, null, 2));
console.log('='.repeat(60));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise(resolve => {
rl.question('Approve this action? (y/n): ', answer => {
rl.close();
resolve(answer.toLowerCase() === 'y');
});
});
}
// ========================================
// Run Agent with Human-in-the-Loop
// ========================================
async function runWithApproval(input: string) {
console.log('\n🤖 Running agent with human approval...\n');
console.log('User:', input);
const runner = new Runner(customerServiceAgent);
let result = await runner.run(input);
// Handle interruptions (approval requests)
while (result.interruption) {
if (result.interruption.type === 'tool_approval') {
const approvalItem = result.interruption as ToolApprovalItem;
console.log(`\n🛑 Agent wants to call: ${approvalItem.toolCall.name}`);
// Ask user for approval
const approved = await promptUserForApproval(
approvalItem.toolCall.name,
approvalItem.toolCall.arguments
);
if (approved) {
console.log('\n✅ Approved - resuming agent...');
result = await result.state.approve(approvalItem);
} else {
console.log('\n❌ Rejected - agent will find alternative...');
result = await result.state.reject(approvalItem, {
reason: 'User rejected the action',
});
}
} else {
// Handle other interruption types if needed
console.log('Unexpected interruption type:', result.interruption.type);
break;
}
}
console.log('\n✅ Agent finished');
console.log('Final output:', result.output);
return result;
}
// ========================================
// Example: State Serialization
// ========================================
async function exampleStateSerialization(input: string) {
console.log('\n🔄 Example: Serializing and Resuming State\n');
const runner = new Runner(customerServiceAgent);
let result = await runner.run(input);
if (result.interruption?.type === 'tool_approval') {
const approvalItem = result.interruption as ToolApprovalItem;
console.log('\n💾 Saving state for later...');
// Serialize state (e.g., save to database)
const serializedState = JSON.stringify(result.state);
console.log('State saved (length:', serializedState.length, 'chars)');
// Simulate delay (user goes away and comes back later)
console.log('\n⏳ User away...\n');
await new Promise(resolve => setTimeout(resolve, 2000));
console.log('👤 User returned!\n');
// Deserialize state
// Note: In real implementation, you'd use RunState.fromString()
// const restoredState = RunState.fromString(customerServiceAgent, serializedState);
// For demo, we'll just approve from current state
const approved = await promptUserForApproval(
approvalItem.toolCall.name,
approvalItem.toolCall.arguments
);
if (approved) {
result = await result.state.approve(approvalItem);
} else {
result = await result.state.reject(approvalItem);
}
}
console.log('\n✅ Final output:', result.output);
}
// ========================================
// Example Usage
// ========================================
async function main() {
const examples = [
'Send an email to customer@example.com saying their order has shipped',
'Process a $50 refund for customer ABC123 due to defective product',
'Delete account user-456 permanently',
];
// Interactive mode (uncomment to run)
// for (const input of examples) {
// await runWithApproval(input);
// console.log('\n' + '='.repeat(80) + '\n');
// }
// State serialization example
// await exampleStateSerialization(examples[1]);
console.log('\n💡 Uncomment the code above to run interactive approval demos\n');
}
main();
export {
customerServiceAgent,
sendEmailTool,
processRefundTool,
deleteAccountTool,
runWithApproval,
};