Tutorial 5: Human-in-the-Loop
Learn how to implement approval workflows for sensitive operations.
What You’ll Learn
Section titled “What You’ll Learn”- Implement tool approval with
OnToolAsync() - Check for pending approvals
- Approve or reject tool calls
- Build safe AI agents
Why HITL?
Section titled “Why HITL?”Some operations should require human approval:
- Financial transactions - Refunds, purchases over threshold
- Data modifications - Delete records, update profiles
- External actions - Send emails, make API calls
- Sensitive queries - Access personal data
Prerequisites
Section titled “Prerequisites”Complete Tutorial 3: Tools and understand tool registration.
Step 1: Create HITL Pack
Section titled “Step 1: Create HITL Pack”Create hitl.pack.json:
{ "id": "hitl-demo", "name": "HITL Demo", "version": "1.0.0", "template_engine": { "version": "v1", "syntax": "{{variable}}" }, "prompts": { "refund_agent": { "id": "refund-agent", "name": "Refund Agent", "version": "1.0.0", "system_template": "You are a customer support agent that processes refunds. For any refund request, use the process_refund tool.", "tools": ["process_refund"] } }, "tools": { "process_refund": { "name": "process_refund", "description": "Process a customer refund", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "Order ID to refund" }, "amount": { "type": "number", "description": "Refund amount in dollars" }, "reason": { "type": "string", "description": "Reason for refund" } }, "required": ["order_id", "amount", "reason"] } } }}Step 2: Register Async Tool Handler
Section titled “Step 2: Register Async Tool Handler”Use OnToolAsync() for approval workflows:
package main
import ( "bufio" "context" "fmt" "log" "os" "strings"
"github.com/AltairaLabs/PromptKit/sdk" "github.com/AltairaLabs/PromptKit/sdk/tools")
func main() { conv, err := sdk.Open("./hitl.pack.json", "refund_agent") if err != nil { log.Fatal(err) } defer conv.Close()
// Register tool with approval check conv.OnToolAsync( "process_refund", // Check function - determines if approval needed func(args map[string]any) tools.PendingResult { amount, _ := args["amount"].(float64)
// High-value refunds need approval if amount > 100 { return tools.PendingResult{ Reason: "high_value_refund", Message: fmt.Sprintf("Refund of $%.2f requires approval", amount), } }
// Auto-approve small refunds return tools.PendingResult{} }, // Execute function - runs after approval func(args map[string]any) (any, error) { orderID, _ := args["order_id"].(string) amount, _ := args["amount"].(float64) reason, _ := args["reason"].(string)
return map[string]any{ "status": "completed", "order_id": orderID, "refund_id": "RF-" + orderID, "amount": amount, "reason": reason, }, nil }, )
ctx := context.Background() reader := bufio.NewReader(os.Stdin)
// Start conversation resp, _ := conv.Send(ctx, "Refund $150 for order #12345, damaged product")
// Check for pending tools pending := resp.PendingTools() if len(pending) > 0 { for _, p := range pending { fmt.Printf("\n⚠️ Approval Required\n") fmt.Printf("Tool: %s\n", p.Name) fmt.Printf("Reason: %s\n", p.Reason) fmt.Printf("Message: %s\n", p.Message) fmt.Printf("Args: %v\n", p.Arguments) fmt.Print("\nApprove? (yes/no): ")
input, _ := reader.ReadString('\n') input = strings.TrimSpace(strings.ToLower(input))
if input == "yes" || input == "y" { result, _ := conv.ResolveTool(p.ID) fmt.Printf("✅ Approved: %v\n", result.Result) } else { result, _ := conv.RejectTool(p.ID, "Not authorized") fmt.Printf("❌ Rejected: %s\n", result.RejectionReason) } } }
fmt.Println("\nResponse:", resp.Text())}Understanding OnToolAsync
Section titled “Understanding OnToolAsync”Check Function
Section titled “Check Function”Returns PendingResult to indicate if approval is needed:
func(args map[string]any) tools.PendingResult { // Check conditions if needsApproval(args) { return tools.PendingResult{ Reason: "approval_code", // Machine-readable reason Message: "Human-readable msg", // Show to approver } }
// Empty result = auto-approve return tools.PendingResult{}}Execute Function
Section titled “Execute Function”Called after approval (or immediately if auto-approved):
func(args map[string]any) (any, error) { // Perform the actual operation return result, nil}Handling Pending Tools
Section titled “Handling Pending Tools”Check for Pending
Section titled “Check for Pending”resp, _ := conv.Send(ctx, message)
pending := resp.PendingTools()if len(pending) > 0 { // Handle approvals}Approve a Tool
Section titled “Approve a Tool”result, err := conv.ResolveTool(pendingID)if err != nil { log.Printf("Resolve failed: %v", err)}fmt.Printf("Result: %v\n", result.Result)Reject a Tool
Section titled “Reject a Tool”result, err := conv.RejectTool(pendingID, "Reason for rejection")if err != nil { log.Printf("Reject failed: %v", err)}Multiple Approval Conditions
Section titled “Multiple Approval Conditions”Combine multiple checks:
conv.OnToolAsync( "delete_record", func(args map[string]any) tools.PendingResult { recordType := args["type"].(string) count, _ := args["count"].(float64)
// Check 1: Sensitive record types if recordType == "user" || recordType == "payment" { return tools.PendingResult{ Reason: "sensitive_data", Message: fmt.Sprintf("Deleting %s records requires approval", recordType), } }
// Check 2: Bulk operations if count > 10 { return tools.PendingResult{ Reason: "bulk_operation", Message: fmt.Sprintf("Bulk delete of %d records requires approval", int(count)), } }
return tools.PendingResult{} }, func(args map[string]any) (any, error) { // Perform deletion return map[string]any{"deleted": true}, nil },)Approval UI Patterns
Section titled “Approval UI Patterns”Console Approval
Section titled “Console Approval”fmt.Printf("Approve %s? [y/N]: ", pending.Name)input, _ := reader.ReadString('\n')if strings.ToLower(strings.TrimSpace(input)) == "y" { conv.ResolveTool(pending.ID)}Web API Approval
Section titled “Web API Approval”// Store pending for later approvalpendingStore[pending.ID] = pending
// API endpoint handles approvalhttp.HandleFunc("/approve", func(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") conv.ResolveTool(id)})What You’ve Learned
Section titled “What You’ve Learned”✅ Register async handlers with OnToolAsync()
✅ Check for pending approvals
✅ Approve with ResolveTool()
✅ Reject with RejectTool()
✅ Build safe approval workflows
Next Steps
Section titled “Next Steps”- Tutorial 6: Media - Working with images
- How-To: HITL Workflows - Advanced patterns
Complete Example
Section titled “Complete Example”See the full example at sdk/examples/hitl/.