CopilotKit

Interactive components

Create approval flows where the agent pauses and waits for human input.


"""LlamaIndex scheduling agent -- interrupt-adapted.This agent powers two demos (gen-ui-interrupt, interrupt-headless) that in theLangGraph showcase rely on the native ``interrupt()`` primitive withcheckpoint/resume. LlamaIndex does NOT have that primitive, so we adapt bydelegating the time-picker interaction to a **frontend tool** that the agentcalls by name (``schedule_meeting``). The frontend registers the tool via``useFrontendTool`` with an async handler; that handler renders the interactivepicker, waits for the user to choose a slot (or cancel), and resolves the toolcall with the result.The backend provides a stub ``schedule_meeting`` tool so the LlamaIndexAGUIChatWorkflow emits the proper AG-UI TOOL_CALL_CHUNK events. Actualexecution happens on the frontend; the stub is never invoked becauseCopilotKit intercepts the tool call before the backend can process the result.See ``src/agents/hitl_in_chat_agent.py`` for the related ``book_call`` patternused by the HITL-in-chat demos in this package."""from __future__ import annotationsimport osfrom llama_index.core.tools import FunctionToolfrom llama_index.llms.openai import OpenAIfrom llama_index.protocols.ag_ui.router import get_ag_ui_workflow_routerfrom agents.hitl_in_chat_agent import FixedAGUIChatWorkflow_openai_kwargs = {}if os.environ.get("OPENAI_BASE_URL"):    _openai_kwargs["api_base"] = os.environ["OPENAI_BASE_URL"]SYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a call "    "or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a "    "short `topic` describing the purpose of the meeting and, if known, an "    "`attendee` describing who the meeting is with.\n\n"    "The `schedule_meeting` tool is implemented on the client: it surfaces a "    "time-picker UI to the user and returns the user's selection. After the "    "tool returns, briefly confirm whether the meeting was scheduled and at "    "what time, or note that the user cancelled. Do NOT ask for approval "    "yourself -- always call the tool and let the picker handle the decision.\n\n"    "Keep responses short and friendly. After you finish executing tools, "    "always send a brief final assistant message summarizing what happened so "    "the message persists.")def _schedule_meeting_stub(topic: str, attendee: str = "") -> str:    """Ask the user to pick a time slot for a meeting.    The picker UI presents fixed candidate slots; the user's choice is    returned to the agent.    """    # Frontend-only tool -- CopilotKit intercepts the call and renders the    # TimePickerCard. This stub satisfies the AGUIChatWorkflow tool registry    # so the proper AG-UI events are emitted.    return ""_schedule_meeting_tool = FunctionTool.from_defaults(    fn=_schedule_meeting_stub,    name="schedule_meeting",    description=(        "Ask the user to pick a time slot for a meeting. Pass a short "        "`topic` and optional `attendee`. The picker UI presents fixed "        "candidate slots; the user's choice is returned to the agent."    ),)async def _workflow_factory():    return FixedAGUIChatWorkflow(        llm=OpenAI(model="gpt-4o-mini", **_openai_kwargs),        frontend_tools=[_schedule_meeting_tool],        backend_tools=[],        system_prompt=SYSTEM_PROMPT,        initial_state={},    )interrupt_router = get_ag_ui_workflow_router(    workflow_factory=_workflow_factory,)

What is this?#

Interactive generative UI creates flows where the agent pauses execution and waits for user input before continuing. This enables approval workflows, confirmation dialogs, and any scenario where human judgment is needed mid-execution.

When should I use this?#

Use interactive generative UI when you need:

  • Approval/rejection flows (e.g. "Run this command?")
  • User decisions that the agent should know about
  • Confirmation dialogs with structured responses
  • Any flow where the agent pauses for human judgment

How it works in code#

This framework implements the same interactive pause shape with a Promise-based frontend tool. The agent calls schedule_meeting, the client renders the picker, and the tool result resolves only after the user chooses a slot or cancels.

page.tsx
  useFrontendTool({    name: "schedule_meeting",    description:      "Ask the user to pick a time slot for a meeting via an in-chat " +      "picker. Blocks until the user chooses a slot or cancels.",    parameters: z.object({      topic: z        .string()        .describe("Short human-readable description of the meeting."),      attendee: z        .string()        .optional()        .describe("Who the meeting is with (optional)."),    }),    // Async handler: returns a Promise that resolves only once the user    // acts on the picker. This is the LlamaIndex shim for LangGraph's    // `interrupt()`/`resolve()` pair.    handler: async (): Promise<string> => {      const result = await new Promise<PickerResult>((resolve) => {        resolverRef.current = resolve;      });      if ("cancelled" in result && result.cancelled) {        return "User cancelled. Meeting NOT scheduled.";      }      if ("chosen_label" in result) {        return `Meeting scheduled for ${result.chosen_label}.`;      }      return "User did not pick a time. Meeting NOT scheduled.";    },    render: ({ args, status }) => {      if (status === "complete") return null;      const topic =        (args as { topic?: string } | undefined)?.topic ?? "a meeting";      const attendee = (args as { attendee?: string } | undefined)?.attendee;      return (        <TimePickerCard          topic={topic}          attendee={attendee}          slots={DEFAULT_SLOTS}          onSubmit={(result) => {            const fn = resolverRef.current;            resolverRef.current = null;            fn?.(result);          }}        />      );    },  });
interrupt_agent.py
SYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a call "    "or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a "    "short `topic` describing the purpose of the meeting and, if known, an "    "`attendee` describing who the meeting is with.\n\n"    "The `schedule_meeting` tool is implemented on the client: it surfaces a "    "time-picker UI to the user and returns the user's selection. After the "    "tool returns, briefly confirm whether the meeting was scheduled and at "    "what time, or note that the user cancelled. Do NOT ask for approval "    "yourself -- always call the tool and let the picker handle the decision.\n\n"    "Keep responses short and friendly. After you finish executing tools, "    "always send a brief final assistant message summarizing what happened so "    "the message persists.")def _schedule_meeting_stub(topic: str, attendee: str = "") -> str:    """Ask the user to pick a time slot for a meeting.    The picker UI presents fixed candidate slots; the user's choice is    returned to the agent.    """    # Frontend-only tool -- CopilotKit intercepts the call and renders the    # TimePickerCard. This stub satisfies the AGUIChatWorkflow tool registry    # so the proper AG-UI events are emitted.    return ""_schedule_meeting_tool = FunctionTool.from_defaults(    fn=_schedule_meeting_stub,    name="schedule_meeting",    description=(        "Ask the user to pick a time slot for a meeting. Pass a short "        "`topic` and optional `attendee`. The picker UI presents fixed "        "candidate slots; the user's choice is returned to the agent."    ),)async def _workflow_factory():    return FixedAGUIChatWorkflow(        llm=OpenAI(model="gpt-4o-mini", **_openai_kwargs),        frontend_tools=[_schedule_meeting_tool],        backend_tools=[],        system_prompt=SYSTEM_PROMPT,        initial_state={},    )interrupt_router = get_ag_ui_workflow_router(    workflow_factory=_workflow_factory,)