--- name: hitl-patterns description: Human-in-the-Loop pattern library for CopilotKit. Use when implementing approval workflows, user input collection, option selection, or any human interaction patterns. Includes React components and hooks. allowed-tools: Read, Write, Grep --- # HITL (Human-in-the-Loop) Patterns Comprehensive patterns for human-agent interaction using CopilotKit. ## Core Hook: useHumanInTheLoop ```tsx import { useHumanInTheLoop } from "@copilotkit/react-core"; useHumanInTheLoop({ name: "hookName", // Unique identifier description: "...", // LLM uses this to decide when to call parameters: [ // Input schema { name: "param", type: "string", description: "..." } ], render: ({ args, respond }) => ( // React component for user interaction respond(result)} /> ) }); ``` ## Pattern 1: Approval Workflow Request user approval before executing sensitive actions. ### Hook Definition ```tsx useHumanInTheLoop({ name: "approveAction", description: "Request user approval before executing sensitive or irreversible actions", parameters: [ { name: "action", type: "string", description: "Description of the action to approve" }, { name: "risk_level", type: "string", description: "Risk assessment: low, medium, high" }, { name: "details", type: "object", description: "Additional action details" } ], render: ({ args, respond }) => ( respond({ approved: true, timestamp: new Date() })} onReject={(reason) => respond({ approved: false, reason })} /> ) }); ``` ### Component Implementation ```tsx interface ApprovalDialogProps { action: string; risk: 'low' | 'medium' | 'high'; details?: Record; onApprove: () => void; onReject: (reason?: string) => void; } function ApprovalDialog({ action, risk, details, onApprove, onReject }: ApprovalDialogProps) { const [reason, setReason] = useState(''); const riskColors = { low: 'bg-green-100 border-green-500', medium: 'bg-yellow-100 border-yellow-500', high: 'bg-red-100 border-red-500' }; return (

Approval Required

{action}

{details && (
          {JSON.stringify(details, null, 2)}
        
)}
setReason(e.target.value)} className="mt-2 w-full p-2 border rounded" />
); } ``` ## Pattern 2: User Input Collection Collect structured data from the user. ### Hook Definition ```tsx useHumanInTheLoop({ name: "collectUserInput", description: "Collect structured information from the user via a form", parameters: [ { name: "fields", type: "array", description: "Form fields to display", items: { type: "object", properties: { name: { type: "string" }, label: { type: "string" }, type: { type: "string", enum: ["text", "email", "number", "textarea", "select"] }, required: { type: "boolean" }, options: { type: "array", items: { type: "string" } } } } }, { name: "title", type: "string", description: "Form title" } ], render: ({ args, respond }) => ( respond({ success: true, data })} onCancel={() => respond({ success: false, cancelled: true })} /> ) }); ``` ### Component Implementation ```tsx interface Field { name: string; label: string; type: 'text' | 'email' | 'number' | 'textarea' | 'select'; required?: boolean; options?: string[]; placeholder?: string; } interface DynamicFormProps { title: string; fields: Field[]; onSubmit: (data: Record) => void; onCancel: () => void; } function DynamicForm({ title, fields, onSubmit, onCancel }: DynamicFormProps) { const [formData, setFormData] = useState>({}); const [errors, setErrors] = useState>({}); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Validate required fields const newErrors: Record = {}; fields.forEach(field => { if (field.required && !formData[field.name]) { newErrors[field.name] = `${field.label} is required`; } }); if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } onSubmit(formData); }; const renderField = (field: Field) => { const commonProps = { id: field.name, name: field.name, value: formData[field.name] || '', onChange: (e: React.ChangeEvent) => setFormData({ ...formData, [field.name]: e.target.value }), className: `w-full p-2 border rounded ${errors[field.name] ? 'border-red-500' : ''}`, placeholder: field.placeholder }; switch (field.type) { case 'textarea': return