Back to Copilotkit

Interrupts

showcase/shell-docs/src/content/docs/integrations/deepagents/human-in-the-loop/interrupt-flow.mdx

1.57.014.1 KB
Original Source

<IframeSwitcher id="human-in-the-loop-example" exampleUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/human_in_the_loop?sidebar=false&chatDefaultOpen=false" codeUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/human_in_the_loop?view=code&sidebar=false&codeLayout=tabs" exampleLabel="Demo" codeLabel="Code" height="700px" />

<Callout type="info"> This example demonstrates interrupt-based human-in-the-loop (HITL) in the [CopilotKit Feature Viewer](https://feature-viewer.copilotkit.ai/langgraph/feature/human_in_the_loop). </Callout>

What is this?

LangGraph's interrupt flow provides an intuitive way to implement Human-in-the-loop workflows.

This guide will show you how to both use interrupt and how to integrate it with CopilotKit.

When should I use this?

Human-in-the-loop is a powerful way to implement complex workflows that are production ready. By having a human in the loop, you can ensure that the agent is always making the right decisions and ultimately is being steered in the right direction.

Interrupt-based flows are a very intuitive way to implement HITL. Instead of having a node await user input before or after its execution, nodes can be interrupted in the middle of their execution to allow for user input. The trade-off is that the agent is not aware of the interaction, however CopilotKit's SDKs provide helpers to alleviate this.

Implementation

<Steps> <Step> ### Run and connect your agent <RunAndConnect /> </Step> <Step> ### Install the CopilotKit SDK <InstallSDKSnippet /> </Step> <Step> ### Set up your agent state We're going to have the agent ask us to name it, so we'll need a state property to store the name.

<Tabs groupId="agent_language" items={['Python', 'TypeScript']} persist> <Tab value="Python"> ```python title="agent.py" # ... from copilotkit import CopilotKitState # extends MessagesState # ...

    # This is the state of the agent.
    # It inherits from the CopilotKitState properties from CopilotKit.
    class AgentState(CopilotKitState):
        agent_name: str
    ```
</Tab>
<Tab value="TypeScript">
    ```ts title="agent.ts"
    import { createMiddleware } from "langchain";
    import { copilotkitMiddleware } from "@copilotkit/sdk-js/langgraph"; // [!code highlight]
    import { z } from "zod";

    // Define the agent's custom state as a middleware.
    export const agentNameMiddleware = createMiddleware({
        name: "AgentState",
        stateSchema: z.object({
            agentName: z.string().optional(),
        }),
    });

    // createDeepAgent({ middleware: [agentNameMiddleware, copilotkitMiddleware], ... })
    ```
</Tab>
</Tabs> </Step> <Step> ### Call `interrupt` in your Deep Agents agent Now we can call `interrupt` in our Deep Agents agent.
<Callout type="info">
    Your agent will not be aware of the `interrupt` interaction by default in LangGraph.

    If you want this behavior, see the [section on it below](#make-your-agent-aware-of-interruptions).
</Callout>

<Tabs groupId="agent_language" items={['Python', 'TypeScript']} persist>
    <Tab value="Python">
        ```python title="agent.py"
        from typing import Any
        from copilotkit import CopilotKitState
        from langchain.agents.middleware import AgentMiddleware # [!code highlight]
        from langgraph.runtime import Runtime
        from langgraph.types import interrupt # [!code highlight]

        # add the agent state definition from the previous step
        class AgentState(CopilotKitState):
            agent_name: str

        class AgentNameMiddleware(AgentMiddleware[AgentState, Any]):
            state_schema = AgentState

            def before_model(self, state: AgentState, runtime: Runtime[Any]) -> dict[str, Any] | None:
                if not state.get("agent_name"):
                    # Interrupt and wait for the user to respond with a name
                    name = interrupt("Before we start, what would you like to call me?") # [!code highlight]
                    return {"agent_name": name}
                return None
        ```
    </Tab>
    <Tab value="TypeScript">
        In Deep Agents TypeScript, wire the interrupt into a middleware hook rather than a custom node. The hook below runs before the model call and asks the user for a name the first time.

        ```ts title="agent.ts"
        import { createMiddleware } from "langchain";
        import { interrupt } from "@langchain/langgraph"; // [!code highlight]
        import { z } from "zod";

        export const agentNameMiddleware = createMiddleware({
            name: "AgentState",
            stateSchema: z.object({
                agentName: z.string().optional(),
            }),
            beforeModel: async (state) => {
                if (!state.agentName) {
                    // Interrupt and wait for the user to respond with a name
                    const name = await interrupt("Before we start, what would you like to call me?"); // [!code highlight]
                    return { agentName: name };
                }
            },
        });
        ```
    </Tab>
</Tabs>
</Step> <Step> ### Handle the interrupt in your frontend At this point, your Deep Agents agent's `interrupt` will be called. However, we currently have no handling for rendering or responding to the interrupt in the frontend.
To do this, we'll use the `useInterrupt` hook, give it a component to render, and then call `resolve` with the user's response.

```tsx title="app/page.tsx"
import { useInterrupt } from "@copilotkit/react-core/v2"; // [!code highlight]
// ...

const YourMainContent = () => {
// ...
// [!code highlight:15]
// styles omitted for brevity
useInterrupt({
    render: ({ event, resolve }) => (
        <div>
            <p>{event.value}</p>
            <form onSubmit={(e) => {
                e.preventDefault();
                resolve((e.target as HTMLFormElement).response.value);
            }}>
                <input type="text" name="response" placeholder="Enter your response" />
                <button type="submit">Submit</button>
            </form>
        </div>
    )
});
// ...

return <div></div>
}
```
</Step> <Step> ### Give it a try! Try talking to your agent, you'll see that it now pauses execution and waits for you to respond! <video src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/interrupt-flow.mp4" className="rounded-lg shadow-xl" loop playsInline controls autoPlay muted /> </Step> </Steps>

Advanced usage

Condition UI executions

When rendering multiple interrupt events in the agent, there could be conflicts between multiple useInterrupt hooks calls in the UI. For this reason, the hook can take an enabled argument which will apply it conditionally:

<Steps> <Step> ### Define multiple interrupts First, let's define two different interrupts. We will include a "type" property to differentiate them. <Tabs groupId="agent_language" items={['Python', 'TypeScript']} persist> <Tab value="Python"> ```python title="agent.py" from typing import Any from langchain.agents.middleware import AgentMiddleware from langgraph.runtime import Runtime from langgraph.types import interrupt # [!code highlight]
            # ... your full state definition

            class ApprovalAndNameMiddleware(AgentMiddleware[AgentState, Any]):
                state_schema = AgentState

                def before_model(self, state: AgentState, runtime: Runtime[Any]) -> dict[str, Any] | None:
                    approval = interrupt({ "type": "approval", "content": "please approve" }) # [!code highlight]
                    updates: dict[str, Any] = {"approval": approval}

                    if not state.get("agent_name"):
                        # Interrupt and wait for the user to respond with a name
                        updates["agent_name"] = interrupt({ "type": "ask", "content": "Before we start, what would you like to call me?" }) # [!code highlight]

                    return updates
            ```
        </Tab>
        <Tab value="TypeScript">
            ```ts title="agent.ts"
            import { createMiddleware } from "langchain";
            import { interrupt } from "@langchain/langgraph"; // [!code highlight]
            import { z } from "zod";

            export const approvalAndNameMiddleware = createMiddleware({
                name: "AgentState",
                stateSchema: z.object({
                    agentName: z.string().optional(),
                    approval: z.unknown().optional(),
                }),
                beforeModel: async (state) => {
                    const approval = await interrupt({ type: "approval", content: "please approve" }); // [!code highlight]
                    const updates: Record<string, unknown> = { approval };

                    if (!state.agentName) {
                        updates.agentName = await interrupt({ type: "ask", content: "Before we start, what would you like to call me?" }); // [!code highlight]
                    }

                    return updates;
                },
            });
            ```
        </Tab>
    </Tabs>
</Step>
<Step>
    ### Add multiple frontend handlers
    With the differentiator in mind, we will add a handler that takes care of any "ask" and any "approve" types.
    With two `useInterrupt` hooks in our page, we can leverage the `enabled` property to enable each in the right time:

    ```tsx title="app/page.tsx"
    import { useInterrupt } from "@copilotkit/react-core/v2"; // [!code highlight]
    // ...

    const ApproveComponent = ({ content, onAnswer }: { content: string; onAnswer: (approved: boolean) => void }) => (
        // styles omitted for brevity
        <div>
            <h1>Do you approve?</h1>
            <button onClick={() => onAnswer(true)}>Approve</button>
            <button onClick={() => onAnswer(false)}>Reject</button>
        </div>
    )

    const AskComponent = ({ question, onAnswer }: { question: string; onAnswer: (answer: string) => void }) => (
    // styles omitted for brevity
        <div>
            <p>{question}</p>
            <form onSubmit={(e) => {
                e.preventDefault();
                onAnswer((e.target as HTMLFormElement).response.value);
            }}>
                <input type="text" name="response" placeholder="Enter your response" />
                <button type="submit">Submit</button>
            </form>
        </div>
    )

    const YourMainContent = () => {
        // ...
        // [!code highlight:13]
        useInterrupt({
            enabled: ({ eventValue }) => eventValue.type === 'ask',
            render: ({ event, resolve }) => (
                <AskComponent question={event.value.content} onAnswer={answer => resolve(answer)} />
            )
        });

        useInterrupt({
            enabled: ({ eventValue }) => eventValue.type === 'approval',
            render: ({ event, resolve }) => (
                <ApproveComponent content={event.value.content} onAnswer={answer => resolve(answer)} />
            )
        });

        // ...
    }
```
</Step>
</Steps>

Preprocessing of an interrupt and programmatically handling an interrupt value

When opting for custom chat UI, some cases may require pre-processing of the incoming values of interrupt event or even resolving it entirely without showing a UI for it. This can be achieved using the handler property, which is not required to return a React component.

The return value of the handler will be passed to the render method as the result argument.

tsx
// We will assume an interrupt event in the following shape
type Department = 'finance' | 'engineering' | 'admin'
interface AuthorizationInterruptEvent {
    type: 'auth',
    accessDepartment: Department,
}

import { useInterrupt } from "@copilotkit/react-core/v2";

const YourMainContent = () => {
    const [userEmail, setUserEmail] = useState({ email: '[email protected]' })
    function getUserByEmail(email: string): { id: string; department: Department } {
        // ... an implementation of user fetching
    }

    // ...
    // styles omitted for brevity
    // [!code highlight:28]
    useInterrupt({
        handler: async ({ result, event, resolve }) => {
            const { department } = await getUserByEmail(userEmail)
            if (event.value.accessDepartment === department || department === 'admin') {
                // Following the resolution of the event, we will not proceed to the render method
                resolve({ code: 'AUTH_BY_DEPARTMENT' })
                return;
            }

            return { department, userId }
        },
        render: ({ result, event, resolve }) => (
            <div>
                <h1>Request for {event.value.type}</h1>
                <p>Members from {result.department} department cannot access this information</p>
                <p>You can request access from an administrator to continue.</p>
                <button
                    onClick={() => resolve({ code: 'REQUEST_AUTH', data: { department: result.department, userId: result.userId } })}
                >
                    Request Access
                </button>
                <button
                    onClick={() => resolve({ code: 'CANCEL' })}
                >
                    Cancel
                </button>
            </div>
        )
    });
    // ...

    return <div></div>
}