• Uncategorised

Real world web application to implement human-in-loop(interrupt) in langgraph4j

Excellent question! This is where the real-world architecture matters. Let me show you the correct pattern:

WRONG Approach (Don’t Do This):

// BAD: Storing workflow in session/global variable
HttpSession session = request.getSession();
session.setAttribute("workflow", app);  // ❌ NO!

CORRECT Approach:

The key insight: The workflow object is just the definition (stateless). The checkpoint stores the state (in database/memory).

[1 tool called]

Perfect! Here’s the real-world pattern:

🌐 Web Application Architecture:

CORRECT Pattern:

┌─────────────────────────────────────────────────────┐
│ HTTP Request 1: Submit                              │
│ POST /api/requests                                  │
├─────────────────────────────────────────────────────┤
│ 1. Generate request_id = "REQ-123"                 │
│ 2. Create workflow (fresh instance)                │
│ 3. Execute: workflow.stream(data, config)          │
│ 4. Checkpoint auto-saved                           │
│ 5. Return: { request_id, status, message }         │
└─────────────────────────────────────────────────────┘
         ↓
    (User closes browser, time passes...)
         ↓
┌─────────────────────────────────────────────────────┐
│ HTTP Request 2: Check Status                        │
│ GET /api/requests/REQ-123                           │
├─────────────────────────────────────────────────────┤
│ 1. Create workflow (NEW instance!)                 │
│ 2. Load checkpoint: workflow.getState(config)      │
│ 3. Show status to user                             │
│ 4. Return: { status, actions: ["approve"] }        │
└─────────────────────────────────────────────────────┘
         ↓
    (User reviews and approves...)
         ↓
┌─────────────────────────────────────────────────────┐
│ HTTP Request 3: Approve                             │
│ POST /api/requests/REQ-123/approve                  │
├─────────────────────────────────────────────────────┤
│ 1. Create workflow (NEW instance again!)           │
│ 2. Load checkpoint                                  │
│ 3. Update: workflow.updateState(...)               │
│ 4. Resume: workflow.stream(null, config)           │
│ 5. Return: { status: "completed" }                 │
└─────────────────────────────────────────────────────┘

🔑 Key Points:

1. Store Only thread_id

// In your database/session
{
  "request_id": "REQ-123",  // ← Store this
  "user_id": "user_456",
  "created_at": "2024-01-15"
}

// DON'T store workflow object!

2. Recreate Workflow on Each Request

public Map<String, Object> handleRequest(String requestId) {
    // Fresh workflow instance each time - it's cheap!
    CompiledGraph<AgentState> workflow = buildWorkflow();

    RunnableConfig config = RunnableConfig.builder()
        .threadId(requestId)  // Links to checkpoint
        .build();

    // Load state from checkpoint
    StateSnapshot<AgentState> snapshot = workflow.getState(config);
    // ...
}

3. Return State to UI

// Get current state
AgentState state = snapshot.state();

// Return to UI
return Map.of(
    "status", state.data().get("status"),
    "amount", state.data().get("amount"),
    "needs_approval", true,
    "actions", List.of("approve", "reject")
);

4. UI Shows Approval Form

// Frontend receives:
{
  "request_id": "REQ-123",
  "status": "pending_approval",
  "amount": 5000,
  "actions": ["approve", "reject"]
}

// UI renders:
// [ Approve ] [ Reject ] buttons

5. User Approves → Resume

// User clicks "Approve"
// Frontend sends: POST /api/requests/REQ-123/approve

// Backend:
workflow.updateState(config, approvalData, "review");
workflow.stream(null, config);  // Resume!

💾 Production: Use Database Checkpoint

// Replace MemorySaver with database persistence
CompileConfig config = CompileConfig.builder()
    .checkpointSaver(new PostgresCheckpointSaver(dataSource))
    // or .checkpointSaver(new MongoCheckpointSaver(mongoClient))
    .build();

🎯 Why This Works:

ComponentWhat It StoresLifetime
WorkflowDefinition/logicRecreated each request
CheckpointState/dataPersistent (database)
thread_idLink to checkpointStored in DB/session

🚀 Run the Example:

java ai.RealWorldWorkflowPattern

This shows 3 separate HTTP requests simulating a real web application! Each request creates a fresh workflow but loads the same checkpoint state. Perfect for production! 🎉

package ai;

import org.bsc.langgraph4j.*;
import org.bsc.langgraph4j.checkpoint.MemorySaver;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.StateSnapshot;

import java.util.*;
import java.util.concurrent.CompletableFuture;

import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;

/**
 * Real-World Pattern: How to Use LangGraph in Web Applications
 * 
 * Key Points:
 * - Don't store workflow in session/global variable
 * - Workflow is stateless, can be recreated
 * - Only store thread_id (request ID)
 * - Checkpoint stores the state (in database)
 * - Recreate workflow on each HTTP request
 */
public class RealWorldWorkflowPattern {

    // ================================================================
    // WORKFLOW DEFINITION (Stateless - can be recreated anytime)
    // ================================================================
    
    private static Map<String, Object> reviewRequest(AgentState state) {
        System.out.println("[REVIEW] Request under review");
        Map<String, Object> updates = new HashMap<>();
        updates.put("status", "pending_approval");
        updates.put("current_stage", "review");
        return updates;
    }

    private static Map<String, Object> approveRequest(AgentState state) {
        System.out.println("[APPROVE] Request approved");
        Map<String, Object> updates = new HashMap<>();
        updates.put("status", "approved");
        updates.put("current_stage", "approved");
        return updates;
    }

    private static Map<String, Object> executeRequest(AgentState state) {
        System.out.println("[EXECUTE] Executing request");
        Map<String, Object> updates = new HashMap<>();
        updates.put("status", "completed");
        updates.put("current_stage", "completed");
        return updates;
    }

    /**
     * Build workflow - This can be called multiple times
     * It's just the definition, not the state!
     */
    public static CompiledGraph<AgentState> buildWorkflow() throws Exception {
        StateGraph<AgentState> workflow = new StateGraph<>(AgentState::new);
        
        workflow.addNode("review", state -> CompletableFuture.completedFuture(reviewRequest(state)));
        workflow.addNode("approve", state -> CompletableFuture.completedFuture(approveRequest(state)));
        workflow.addNode("execute", state -> CompletableFuture.completedFuture(executeRequest(state)));
        
        workflow.addEdge(START, "review");
        workflow.addEdge("review", "approve");
        workflow.addEdge("approve", "execute");
        workflow.addEdge("execute", END);
        
        // Use MemorySaver (in production, use persistent storage!)
        CompileConfig config = CompileConfig.builder()
            .checkpointSaver(new MemorySaver())  // TODO: Replace with DB in production
            .interruptAfter("review")
            .build();
        
        return workflow.compile(config);
    }

    // ================================================================
    // SIMULATED WEB APPLICATION PATTERN
    // ================================================================
    
    /**
     * REQUEST 1: User submits a new approval request
     * 
     * HTTP POST /api/requests
     * Body: { "amount": 5000, "description": "..." }
     */
    public static Map<String, Object> handleSubmitRequest(Map<String, Object> requestData) {
        try {
            System.out.println("\n" + "=".repeat(70));
            System.out.println("HTTP REQUEST 1: POST /api/requests");
            System.out.println("=".repeat(70));
            
            // 1. Generate unique request ID
            String requestId = "REQ-" + System.currentTimeMillis();
            System.out.println("Generated Request ID: " + requestId);
            
            // 2. Store basic info in database (metadata)
            // database.save(requestId, requestData);
            System.out.println("Saved to database: " + requestId);
            
            // 3. Create workflow (fresh instance - no session storage!)
            CompiledGraph<AgentState> workflow = buildWorkflow();
            
            // 4. Create config with thread_id
            RunnableConfig config = RunnableConfig.builder()
                .threadId(requestId)  // This is what links to checkpoint
                .build();
            
            // 5. Start workflow execution
            System.out.println("\nStarting workflow...");
            workflow.stream(requestData, config).forEach(output -> {
                System.out.println("  → " + output.node());
            });
            
            // 6. Get current state from checkpoint
            StateSnapshot<AgentState> snapshot = workflow.getState(config);
            AgentState state = snapshot.state();
            
            // 7. Build response for UI
            Map<String, Object> response = new HashMap<>();
            response.put("request_id", requestId);
            response.put("status", state.data().get("status"));
            response.put("current_stage", state.data().get("current_stage"));
            response.put("message", "Request submitted. Awaiting approval.");
            response.put("amount", state.data().get("amount"));
            
            System.out.println("\nResponse to UI: " + response);
            return response;
            
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * REQUEST 2: Get current status of a request
     * 
     * HTTP GET /api/requests/{requestId}
     */
    public static Map<String, Object> handleGetRequestStatus(String requestId) {
        try {
            System.out.println("\n" + "=".repeat(70));
            System.out.println("HTTP REQUEST 2: GET /api/requests/" + requestId);
            System.out.println("=".repeat(70));
            
            // 1. Recreate workflow (it's just the definition!)
            CompiledGraph<AgentState> workflow = buildWorkflow();
            
            // 2. Create config with same thread_id
            RunnableConfig config = RunnableConfig.builder()
                .threadId(requestId)
                .build();
            
            // 3. Get state from checkpoint
            StateSnapshot<AgentState> snapshot = workflow.getState(config);
            AgentState state = snapshot.state();
            
            // 4. Build response for UI
            Map<String, Object> response = new HashMap<>();
            response.put("request_id", requestId);
            response.put("status", state.data().get("status"));
            response.put("current_stage", state.data().get("current_stage"));
            response.put("amount", state.data().get("amount"));
            response.put("needs_approval", "pending_approval".equals(state.data().get("status")));
            
            // What can user do?
            String currentStage = (String) state.data().get("current_stage");
            if ("review".equals(currentStage)) {
                response.put("actions", List.of("approve", "reject"));
                response.put("message", "Please review and approve/reject this request");
            }
            
            System.out.println("Response to UI: " + response);
            return response;
            
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * REQUEST 3: User approves the request
     * 
     * HTTP POST /api/requests/{requestId}/approve
     * Body: { "approved": true, "comments": "..." }
     */
    public static Map<String, Object> handleApproveRequest(String requestId, Map<String, Object> approvalData) {
        try {
            System.out.println("\n" + "=".repeat(70));
            System.out.println("HTTP REQUEST 3: POST /api/requests/" + requestId + "/approve");
            System.out.println("=".repeat(70));
            
            // 1. Recreate workflow
            CompiledGraph<AgentState> workflow = buildWorkflow();
            
            // 2. Create config with same thread_id
            RunnableConfig config = RunnableConfig.builder()
                .threadId(requestId)
                .build();
            
            // 3. Get current state to know which stage we're at
            StateSnapshot<AgentState> snapshot = workflow.getState(config);
            String currentStage = (String) snapshot.state().data().get("current_stage");
            
            System.out.println("Current stage: " + currentStage);
            System.out.println("Applying approval: " + approvalData);
            
            // 4. Update checkpoint with approval
            Map<String, Object> updates = new HashMap<>();
            updates.put("approved", approvalData.get("approved"));
            updates.put("approver", approvalData.get("approver"));
            updates.put("comments", approvalData.get("comments"));
            updates.put("approved_at", System.currentTimeMillis());
            
            workflow.updateState(config, updates, currentStage);
            
            // 5. Resume workflow
            System.out.println("\nResuming workflow...");
            workflow.stream(null, config).forEach(output -> {
                System.out.println("  → " + output.node());
            });
            
            // 6. Get updated state
            StateSnapshot<AgentState> updatedSnapshot = workflow.getState(config);
            AgentState updatedState = updatedSnapshot.state();
            
            // 7. Build response
            Map<String, Object> response = new HashMap<>();
            response.put("request_id", requestId);
            response.put("status", updatedState.data().get("status"));
            response.put("current_stage", updatedState.data().get("current_stage"));
            response.put("message", "Request approved and processed successfully");
            
            System.out.println("\nResponse to UI: " + response);
            return response;
            
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // ================================================================
    // MAIN: Simulates the complete flow
    // ================================================================
    
    public static void main(String[] args) {
        System.out.println("╔════════════════════════════════════════════════════════════╗");
        System.out.println("║        Real-World Web Application Pattern                 ║");
        System.out.println("╚════════════════════════════════════════════════════════════╝");
        
        try {
            // ================================================================
            // SCENARIO: User interacts with web application
            // ================================================================
            
            // Request 1: User submits request via web form
            Map<String, Object> requestData = new HashMap<>();
            requestData.put("amount", 5000);
            requestData.put("description", "New equipment");
            
            Map<String, Object> submitResponse = handleSubmitRequest(requestData);
            String requestId = (String) submitResponse.get("request_id");
            
            System.out.println("\n💡 UI shows: Request submitted! ID = " + requestId);
            
            // Simulate time passing (user closes browser, goes for coffee...)
            Thread.sleep(2000);
            
            // Request 2: User comes back, checks status
            Map<String, Object> statusResponse = handleGetRequestStatus(requestId);
            
            System.out.println("\n💡 UI shows: Status = " + statusResponse.get("status"));
            System.out.println("💡 UI shows approval form with buttons: Approve / Reject");
            
            // Simulate time passing
            Thread.sleep(2000);
            
            // Request 3: User approves
            Map<String, Object> approvalData = new HashMap<>();
            approvalData.put("approved", true);
            approvalData.put("approver", "Manager John");
            approvalData.put("comments", "Looks good!");
            
            Map<String, Object> approveResponse = handleApproveRequest(requestId, approvalData);
            
            System.out.println("\n💡 UI shows: " + approveResponse.get("message"));
            System.out.println("💡 Final status: " + approveResponse.get("status"));
            
            // ================================================================
            // KEY POINTS SUMMARY
            // ================================================================
            
            System.out.println("\n\n" + "=".repeat(70));
            System.out.println("KEY ARCHITECTURAL PATTERNS");
            System.out.println("=".repeat(70));
            
            System.out.println("\n✅ DO:");
            System.out.println("  1. Store only thread_id (request ID) in session/database");
            System.out.println("  2. Recreate workflow object on each HTTP request");
            System.out.println("  3. Use thread_id to load checkpoint state");
            System.out.println("  4. Return current state to UI for display");
            System.out.println("  5. Use checkpoint for persistence (survives restarts)");
            
            System.out.println("\n❌ DON'T:");
            System.out.println("  1. Store workflow object in session");
            System.out.println("  2. Store workflow in global/static variable");
            System.out.println("  3. Try to keep workflow in memory");
            System.out.println("  4. Worry about recreating workflow (it's cheap!)");
            
            System.out.println("\n📊 DATA FLOW:");
            System.out.println("  Request 1 (Submit):");
            System.out.println("    UI → Server → Create Workflow → Execute → Save Checkpoint");
            System.out.println("    → Return (request_id, status) → UI");
            
            System.out.println("\n  Request 2 (Check Status):");
            System.out.println("    UI → Server → Create Workflow → Load Checkpoint");
            System.out.println("    → Return (status, actions) → UI");
            
            System.out.println("\n  Request 3 (Approve):");
            System.out.println("    UI → Server → Create Workflow → Load Checkpoint");
            System.out.println("    → Update Checkpoint → Resume → Save Checkpoint");
            System.out.println("    → Return (new status) → UI");
            
            System.out.println("\n🔑 THE MAGIC:");
            System.out.println("  • Workflow = Definition (stateless, recreatable)");
            System.out.println("  • Checkpoint = State (persistent, survives restarts)");
            System.out.println("  • thread_id = Link between requests");
            System.out.println("  • Each HTTP request is independent!");
            
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

You may also like...