UNPKG

@copilotkit/react-core

Version:

<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />

1,462 lines (1,446 loc) • 64.1 kB
"use client"; import { B as useInterrupt, G as useAgent, H as useSuggestions, J as useFrontendTool$1, K as useHumanInTheLoop$1, V as useConfigureSuggestions, X as useRenderCustomMessages, a as ThreadsProvider, c as CoAgentStateRendersProvider, d as shouldShowDevConsole, f as useToast, g as useCopilotContext, gt as useCopilotChatConfiguration, h as CopilotContext, i as ThreadsContext, l as useCoAgentStateRenders, lt as defineToolCallRenderer, m as useCopilotMessagesContext, n as defaultCopilotContextCategories, o as useThreads, ot as useRenderToolCall$1, p as CopilotMessagesContext, r as CoAgentStateRenderBridge, s as CoAgentStateRendersContext, t as CopilotKit, u as useAsyncCallback, ut as useCopilotKit } from "./copilotkit-B5RsC6la.mjs"; import React, { Fragment, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { CopilotKitCoreRuntimeConnectionStatus, ToolCallStatus } from "@copilotkit/core"; import { AGUIConnectNotImplementedError, HttpAgent } from "@ag-ui/client"; import { CopilotKitAgentDiscoveryError, CopilotKitApiDiscoveryError, CopilotKitError, CopilotKitErrorCode, CopilotKitRemoteEndpointDiscoveryError, ErrorVisibility, Severity, actionParametersToJsonSchema, getZodParameters, parseJson, randomId, styledConsole } from "@copilotkit/shared"; import { ActionInputAvailability, CopilotRequestType, CopilotRuntimeClient, Message as Message$1, MetaEventName, Role, TextMessage, convertGqlOutputToMessages, convertMessagesToGqlInput, filterAgentStateMessages, gqlToAGUI } from "@copilotkit/runtime-client-gql"; //#region src/utils/suggestions-constants.ts /** * Constants for suggestions retry logic */ const SUGGESTION_RETRY_CONFIG = { MAX_RETRIES: 3, COOLDOWN_MS: 5e3 }; //#endregion //#region src/hooks/use-lazy-tool-renderer.tsx function useLazyToolRenderer() { const renderToolCall = useRenderToolCall$1(); return useCallback((message, messages) => { if (!message?.toolCalls?.length) return null; const toolCall = message.toolCalls[0]; if (!toolCall) return null; const toolMessage = messages?.find((m) => m.role === "tool" && m.toolCallId === toolCall.id); return () => renderToolCall({ toolCall, toolMessage }); }, [renderToolCall]); } //#endregion //#region src/hooks/use-copilot-chat_internal.ts function useCopilotChatInternal({ suggestions, onInProgress, onSubmitMessage, onStopGeneration, onReloadMessages } = {}) { const { copilotkit } = useCopilotKit(); const { threadId, agentSession } = useCopilotContext(); const existingConfig = useCopilotChatConfiguration(); const [agentAvailable, setAgentAvailable] = useState(false); const resolvedAgentId = existingConfig?.agentId ?? "default"; const { agent } = useAgent({ agentId: resolvedAgentId }); const lastConnectedAgentRef = useRef(null); useEffect(() => { let detached = false; const connectAbortController = new AbortController(); if (agent instanceof HttpAgent) agent.abortController = connectAbortController; const connect = async (agent) => { setAgentAvailable(false); try { await copilotkit.connectAgent({ agent }); if (!detached) setAgentAvailable(true); } catch (error) { if (detached) return; if (error instanceof AGUIConnectNotImplementedError) {} else console.error("CopilotChat: connectAgent failed", error); } }; if (agent && agent !== lastConnectedAgentRef.current && copilotkit.runtimeConnectionStatus === CopilotKitCoreRuntimeConnectionStatus.Connected) { lastConnectedAgentRef.current = agent; connect(agent); } return () => { lastConnectedAgentRef.current = null; detached = true; connectAbortController.abort(); agent?.detachActiveRun(); }; }, [ existingConfig?.threadId, agent, copilotkit, copilotkit.runtimeConnectionStatus, resolvedAgentId ]); useEffect(() => { onInProgress?.(Boolean(agent?.isRunning)); }, [agent?.isRunning, onInProgress]); const [interrupt, setInterrupt] = useState(null); useEffect(() => { setInterrupt(copilotkit.interruptElement); const subscription = copilotkit.subscribe({ onInterruptElementChanged: ({ interruptElement }) => { setInterrupt(interruptElement); } }); return () => subscription.unsubscribe(); }, [copilotkit]); const reset = () => { agent?.setMessages([]); agent?.setState(null); }; const latestDelete = useUpdatedRef(useCallback((messageId) => { const filteredMessages = (agent?.messages ?? []).filter((message) => message.id !== messageId); agent?.setMessages(filteredMessages); }, [agent?.setMessages, agent?.messages])); const latestDeleteFunc = useCallback((messageId) => { return latestDelete.current(messageId); }, [latestDelete]); const currentSuggestions = useSuggestions({ agentId: resolvedAgentId }); const reload = useAsyncCallback(async (reloadMessageId) => { if (!agent) return; const messages = agent?.messages ?? []; if (agent.isRunning || messages.length === 0) return; const reloadMessageIndex = messages.findIndex((msg) => msg.id === reloadMessageId); if (reloadMessageIndex === -1) { console.warn(`Message with id ${reloadMessageId} not found`); return; } const reloadMessageRole = messages[reloadMessageIndex].role; if (reloadMessageRole !== "assistant") { console.warn(`Regenerate cannot be performed on ${reloadMessageRole} role`); return; } let historyCutoff = [messages[0]]; if (messages.length > 2 && reloadMessageIndex !== 0) { const lastUserMessageBeforeRegenerate = messages.slice(0, reloadMessageIndex).toReversed().find((msg) => msg.role === "user"); if (!lastUserMessageBeforeRegenerate) historyCutoff = [messages[0]]; else { const indexOfLastUserMessageBeforeRegenerate = messages.findIndex((msg) => msg.id === lastUserMessageBeforeRegenerate.id); historyCutoff = messages.slice(0, indexOfLastUserMessageBeforeRegenerate + 1); } } else if (messages.length > 2 && reloadMessageIndex === 0) historyCutoff = [messages[0], messages[1]]; agent?.setMessages(historyCutoff); if (agent) try { await copilotkit.runAgent({ agent }); } catch (error) { console.error("CopilotChat: runAgent failed during reload", error); } }, [ agent?.messages.length, agent?.isRunning, agent?.setMessages, copilotkit?.runAgent ]); const latestSendMessageFunc = useAsyncCallback(async (message, options) => { if (!agent) return; const followUp = options?.followUp ?? true; if (options?.clearSuggestions) copilotkit.clearSuggestions(resolvedAgentId); if (onSubmitMessage) { const content = typeof message.content === "string" ? message.content : message.content && "text" in message.content ? message.content.text : message.content && "filename" in message.content ? message.content.filename : ""; try { await onSubmitMessage(content); } catch (error) { console.error("Error in onSubmitMessage:", error); } } agent?.addMessage(message); if (followUp) try { await copilotkit.runAgent({ agent }); } catch (error) { console.error("CopilotChat: runAgent failed", error); } }, [ agent, copilotkit, resolvedAgentId, onSubmitMessage ]); const latestAppendFunc = useAsyncCallback(async (message, options) => { return latestSendMessageFunc(gqlToAGUI([message])[0], options); }, [latestSendMessageFunc]); const latestSetMessagesFunc = useCallback((messages) => { if (messages.every((message) => message instanceof Message$1)) return agent?.setMessages?.(gqlToAGUI(messages)); return agent?.setMessages?.(messages); }, [agent?.setMessages, agent]); const latestReload = useUpdatedRef(reload); const latestReloadFunc = useAsyncCallback(async (messageId) => { onReloadMessages?.({ messageId, currentAgentName: agent?.agentId, messages: agent?.messages ?? [] }); return await latestReload.current(messageId); }, [ latestReload, agent, onReloadMessages ]); const latestStopFunc = useCallback(() => { onStopGeneration?.({ currentAgentName: agent?.agentId, messages: agent?.messages ?? [] }); return agent?.abortRun?.(); }, [onStopGeneration, agent]); const latestReset = useUpdatedRef(reset); const latestResetFunc = useCallback(() => { return latestReset.current(); }, [latestReset]); const lazyToolRendered = useLazyToolRenderer(); const renderCustomMessage = useRenderCustomMessages(); const legacyCustomMessageRenderer = useLegacyCoagentRenderer({ copilotkit, agent, agentId: resolvedAgentId, threadId: existingConfig?.threadId ?? threadId }); const allMessages = agent?.messages ?? []; const resolvedMessages = useMemo(() => { let processedMessages = allMessages.map((message) => { if (message.role !== "assistant") return message; const lazyRendered = lazyToolRendered(message, allMessages); if (lazyRendered) { const renderedGenUi = lazyRendered(); if (renderedGenUi) return { ...message, generativeUI: () => renderedGenUi }; } const bridgeRenderer = legacyCustomMessageRenderer || renderCustomMessage ? () => { if (legacyCustomMessageRenderer) return legacyCustomMessageRenderer({ message, position: "before" }); try { return renderCustomMessage?.({ message, position: "before" }) ?? null; } catch (error) { console.warn("[CopilotKit] renderCustomMessages failed, falling back to legacy renderer", error); return null; } } : null; if (bridgeRenderer) return { ...message, generativeUI: bridgeRenderer, generativeUIPosition: "before" }; return message; }); const hasAssistantMessages = processedMessages.some((msg) => msg.role === "assistant"); const canUseCustomRenderer = Boolean(renderCustomMessage && copilotkit?.getAgent?.(resolvedAgentId)); const placeholderRenderer = legacyCustomMessageRenderer ? legacyCustomMessageRenderer : canUseCustomRenderer ? renderCustomMessage : null; const shouldRenderPlaceholder = Boolean(agent?.isRunning) || Boolean(agent?.state && Object.keys(agent.state).length); const effectiveThreadId = threadId ?? agent?.threadId ?? "default"; let latestUserIndex = -1; for (let i = processedMessages.length - 1; i >= 0; i -= 1) if (processedMessages[i].role === "user") { latestUserIndex = i; break; } const latestUserMessageId = latestUserIndex >= 0 ? processedMessages[latestUserIndex].id : void 0; const currentRunId = latestUserMessageId ? copilotkit.getRunIdForMessage(resolvedAgentId, effectiveThreadId, latestUserMessageId) || `pending:${latestUserMessageId}` : void 0; const hasAssistantForCurrentRun = latestUserIndex >= 0 ? processedMessages.slice(latestUserIndex + 1).some((msg) => msg.role === "assistant") : hasAssistantMessages; if (placeholderRenderer && shouldRenderPlaceholder && !hasAssistantForCurrentRun) { const placeholderMessage = { id: currentRunId ? `coagent-state-render-${resolvedAgentId}-${currentRunId}` : `coagent-state-render-${resolvedAgentId}`, role: "assistant", content: "", name: "coagent-state-render", runId: currentRunId }; processedMessages = [...processedMessages, { ...placeholderMessage, generativeUIPosition: "before", generativeUI: () => placeholderRenderer({ message: placeholderMessage, position: "before" }) }]; } return processedMessages; }, [ agent?.messages, lazyToolRendered, allMessages, renderCustomMessage, legacyCustomMessageRenderer, resolvedAgentId, copilotkit, agent?.isRunning, agent?.state ]); const renderedSuggestions = useMemo(() => { if (Array.isArray(suggestions)) return { suggestions: suggestions.map((s) => ({ ...s, isLoading: false })), isLoading: false }; return currentSuggestions; }, [suggestions, currentSuggestions]); return { messages: resolvedMessages, sendMessage: latestSendMessageFunc, appendMessage: latestAppendFunc, setMessages: latestSetMessagesFunc, reloadMessages: latestReloadFunc, stopGeneration: latestStopFunc, reset: latestResetFunc, deleteMessage: latestDeleteFunc, isAvailable: agentAvailable, isLoading: Boolean(agent?.isRunning), suggestions: renderedSuggestions.suggestions, setSuggestions: (suggestions) => copilotkit.addSuggestionsConfig({ suggestions }), generateSuggestions: async () => copilotkit.reloadSuggestions(resolvedAgentId), resetSuggestions: () => copilotkit.clearSuggestions(resolvedAgentId), isLoadingSuggestions: renderedSuggestions.isLoading, interrupt, agent, threadId }; } function useUpdatedRef(value) { const ref = useRef(value); useEffect(() => { ref.current = value; }, [value]); return ref; } function useLegacyCoagentRenderer({ copilotkit, agent, agentId, threadId }) { return useMemo(() => { if (!copilotkit || !agent) return null; return ({ message, position }) => { const effectiveThreadId = threadId ?? agent.threadId ?? "default"; const providedRunId = message.runId; return createElement(CoAgentStateRenderBridge, { message, position, runId: (providedRunId ? providedRunId : copilotkit.getRunIdForMessage(agentId, effectiveThreadId, message.id)) || `pending:${message.id}`, messageIndex: Math.max(agent.messages.findIndex((msg) => msg.id === message.id), 0), messageIndexInRun: 0, numberOfMessagesInRun: 1, agentId, stateSnapshot: message.state }); }; }, [ agent, agentId, copilotkit, threadId ]); } //#endregion //#region src/hooks/use-copilot-chat.ts /** * A lightweight React hook for headless chat interactions. * Perfect for programmatic messaging, background operations, and custom UI implementations. * * **Open Source Friendly** - Works without requiring a `publicApiKey`. */ function useCopilotChat(options = {}) { const { visibleMessages, appendMessage, reloadMessages, stopGeneration, reset, isLoading, isAvailable, runChatCompletion, mcpServers, setMcpServers } = useCopilotChatInternal(options); return { visibleMessages, appendMessage, reloadMessages, stopGeneration, reset, isLoading, isAvailable, runChatCompletion, mcpServers, setMcpServers }; } //#endregion //#region src/hooks/use-copilot-chat-headless_c.ts /** * `useCopilotChatHeadless_c` is for building fully custom UI (headless UI) implementations. * * <Callout title="This is a premium-only feature"> * Sign up for free on [Copilot Cloud](https://cloud.copilotkit.ai) to get your public license key or read more about <a href="/premium/overview">premium features</a>. * * Usage is generous, **free** to get started, and works with **either self-hosted or Copilot Cloud** environments. * </Callout> * * ## Key Features * * - **Fully headless**: Build your own fully custom UI's for your agentic applications. * - **Advanced Suggestions**: Direct access to suggestions array with full control * - **Interrupt Handling**: Support for advanced interrupt functionality * - **MCP Server Support**: Model Context Protocol server configurations * - **Chat Controls**: Complete set of chat management functions * - **Loading States**: Comprehensive loading state management * * * ## Usage * * ### Basic Setup * * ```tsx * import { CopilotKit } from "@copilotkit/react-core"; * import { useCopilotChatHeadless_c } from "@copilotkit/react-core"; * * export function App() { * return ( * <CopilotKit publicApiKey="your-free-public-license-key"> * <YourComponent /> * </CopilotKit> * ); * } * * export function YourComponent() { * const { messages, sendMessage, isLoading } = useCopilotChatHeadless_c(); * * const handleSendMessage = async () => { * await sendMessage({ * id: "123", * role: "user", * content: "Hello World", * }); * }; * * return ( * <div> * {messages.map(msg => <div key={msg.id}>{msg.content}</div>)} * <button onClick={handleSendMessage} disabled={isLoading}> * Send Message * </button> * </div> * ); * } * ``` * * ### Working with Suggestions * * ```tsx * import { useCopilotChatHeadless_c, useCopilotChatSuggestions } from "@copilotkit/react-core"; * * export function SuggestionExample() { * const { * suggestions, * setSuggestions, * generateSuggestions, * isLoadingSuggestions * } = useCopilotChatHeadless_c(); * * // Configure AI suggestion generation * useCopilotChatSuggestions({ * instructions: "Suggest helpful actions based on the current context", * maxSuggestions: 3 * }); * * return ( * <div> * {suggestions.map(suggestion => ( * <button key={suggestion.title}>{suggestion.title}</button> * ))} * <button onClick={generateSuggestions} disabled={isLoadingSuggestions}> * Generate Suggestions * </button> * </div> * ); * } * ``` * * ## Return Values * The following properties are returned from the hook: * * <PropertyReference name="messages" type="Message[]"> * The messages currently in the chat in AG-UI format * </PropertyReference> * * <PropertyReference name="sendMessage" type="(message: Message, options?) => Promise<void>"> * Send a new message to the chat and trigger AI response * </PropertyReference> * * <PropertyReference name="setMessages" type="(messages: Message[] | DeprecatedGqlMessage[]) => void"> * Replace all messages in the chat with new array * </PropertyReference> * * <PropertyReference name="deleteMessage" type="(messageId: string) => void"> * Remove a specific message by ID from the chat * </PropertyReference> * * <PropertyReference name="reloadMessages" type="(messageId: string) => Promise<void>"> * Regenerate the response for a specific message by ID * </PropertyReference> * * <PropertyReference name="stopGeneration" type="() => void"> * Stop the current message generation process * </PropertyReference> * * <PropertyReference name="reset" type="() => void"> * Clear all messages and reset chat state completely * </PropertyReference> * * <PropertyReference name="isLoading" type="boolean"> * Whether the chat is currently generating a response * </PropertyReference> * * <PropertyReference name="runChatCompletion" type="() => Promise<Message[]>"> * Manually trigger chat completion for advanced usage * </PropertyReference> * * <PropertyReference name="mcpServers" type="MCPServerConfig[]"> * Array of Model Context Protocol server configurations * </PropertyReference> * * <PropertyReference name="setMcpServers" type="(servers: MCPServerConfig[]) => void"> * Update MCP server configurations for enhanced context * </PropertyReference> * * <PropertyReference name="suggestions" type="SuggestionItem[]"> * Current suggestions array for reading or manual control * </PropertyReference> * * <PropertyReference name="setSuggestions" type="(suggestions: SuggestionItem[]) => void"> * Manually set suggestions for custom workflows * </PropertyReference> * * <PropertyReference name="generateSuggestions" type="() => Promise<void>"> * Trigger AI-powered suggestion generation using configured settings * </PropertyReference> * * <PropertyReference name="resetSuggestions" type="() => void"> * Clear all current suggestions and reset generation state * </PropertyReference> * * <PropertyReference name="isLoadingSuggestions" type="boolean"> * Whether suggestions are currently being generated * </PropertyReference> * * <PropertyReference name="interrupt" type="string | React.ReactElement | null"> * Interrupt content for human-in-the-loop workflows * </PropertyReference> */ const createNonFunctionalReturn = () => ({ visibleMessages: [], messages: [], sendMessage: async () => {}, appendMessage: async () => {}, setMessages: () => {}, deleteMessage: () => {}, reloadMessages: async () => {}, stopGeneration: () => {}, reset: () => {}, isLoading: false, isAvailable: false, runChatCompletion: async () => [], mcpServers: [], setMcpServers: () => {}, suggestions: [], setSuggestions: () => {}, generateSuggestions: async () => {}, resetSuggestions: () => {}, isLoadingSuggestions: false, interrupt: null }); /** * Enterprise React hook that provides complete chat functionality for fully custom UI implementations. * Includes all advanced features like direct message access, suggestions array, interrupt handling, and MCP support. * * **Requires a publicApiKey** - Sign up for free at https://cloud.copilotkit.ai/ * * @param options - Configuration options for the chat * @returns Complete chat interface with all enterprise features * * @example * ```tsx * const { messages, sendMessage, suggestions, interrupt } = useCopilotChatHeadless_c(); * ``` */ function useCopilotChatHeadless_c(options = {}) { const { copilotApiConfig, setBannerError } = useCopilotContext(); const hasPublicApiKey = Boolean(copilotApiConfig.publicApiKey); const internalResult = useCopilotChatInternal(options); useEffect(() => { if (!hasPublicApiKey) { setBannerError(new CopilotKitError({ message: "You're using useCopilotChatHeadless_c, a premium-only feature, which offers extensive headless chat capabilities. To continue, you'll need to provide a free public license key.", code: CopilotKitErrorCode.MISSING_PUBLIC_API_KEY_ERROR, severity: Severity.WARNING, visibility: ErrorVisibility.BANNER })); styledConsole.logCopilotKitPlatformMessage(); } else setBannerError(null); }, [hasPublicApiKey]); if (hasPublicApiKey) return internalResult; return createNonFunctionalReturn(); } //#endregion //#region src/hooks/use-frontend-tool.ts function useFrontendTool(tool, dependencies) { const { name, description, parameters, render, followUp, available } = tool; const zodParameters = getZodParameters(parameters); const renderRef = useRef(render); useEffect(() => { renderRef.current = render; }, [render, ...dependencies ?? []]); const normalizedRender = useMemo(() => { if (typeof render === "undefined") return; return ((args) => { const currentRender = renderRef.current; if (typeof currentRender === "undefined") return null; if (typeof currentRender === "string") return React.createElement(React.Fragment, null, currentRender); const rendered = currentRender({ ...args, result: typeof args.result === "string" ? parseJson(args.result, args.result) : args.result }); if (typeof rendered === "string") return React.createElement(React.Fragment, null, rendered); return rendered ?? null; }); }, []); const handlerRef = useRef(tool.handler); useEffect(() => { handlerRef.current = tool.handler; }, [tool.handler, ...dependencies ?? []]); useFrontendTool$1({ name, description, parameters: zodParameters, handler: tool.handler ? (args) => handlerRef.current?.(args) : void 0, followUp, render: normalizedRender, available: available === void 0 ? void 0 : available !== "disabled" }); } //#endregion //#region src/hooks/use-render-tool-call.ts function useRenderToolCall(tool, dependencies) { const { copilotkit } = useCopilotKit(); const hasAddedRef = useRef(false); useEffect(() => { const { name, parameters, render } = tool; const zodParameters = getZodParameters(parameters); const renderToolCall = name === "*" ? defineToolCallRenderer({ name: "*", render: ((args) => { return render({ ...args, result: args.result ? parseJson(args.result, args.result) : args.result }); }) }) : defineToolCallRenderer({ name, args: zodParameters, render: ((args) => { return render({ ...args, result: args.result ? parseJson(args.result, args.result) : args.result }); }) }); const existingIndex = copilotkit.renderToolCalls.findIndex((r) => r.name === name); if (existingIndex !== -1) copilotkit.renderToolCalls.splice(existingIndex, 1); copilotkit.renderToolCalls.push(renderToolCall); hasAddedRef.current = true; return () => { if (hasAddedRef.current) { const index = copilotkit.renderToolCalls.findIndex((r) => r.name === name); if (index !== -1) copilotkit.renderToolCalls.splice(index, 1); hasAddedRef.current = false; } }; }, [tool, ...dependencies ?? []]); } //#endregion //#region src/hooks/use-human-in-the-loop.ts function useHumanInTheLoop(tool, dependencies) { const { render, ...toolRest } = tool; const { name, description, parameters, followUp } = toolRest; const zodParameters = getZodParameters(parameters); const renderRef = useRef(null); useEffect(() => { renderRef.current = (args) => { if (typeof render === "string") return React.createElement(React.Fragment, null, render); if (!render) return null; const rendered = render((() => { const mappedArgs = args.args; switch (args.status) { case ToolCallStatus.InProgress: return { args: mappedArgs, respond: args.respond, status: args.status, handler: void 0 }; case ToolCallStatus.Executing: return { args: mappedArgs, respond: args.respond, status: args.status, handler: () => {} }; case ToolCallStatus.Complete: return { args: mappedArgs, respond: args.respond, status: args.status, result: args.result ? parseJson(args.result, args.result) : args.result, handler: void 0 }; default: throw new CopilotKitError({ code: CopilotKitErrorCode.UNKNOWN, message: `Invalid tool call status: ${args.status}` }); } })()); if (typeof rendered === "string") return React.createElement(React.Fragment, null, rendered); return rendered ?? null; }; }, [render, ...dependencies ?? []]); useHumanInTheLoop$1({ name, description, followUp, parameters: zodParameters, render: ((args) => renderRef.current?.(args) ?? null) }); } //#endregion //#region src/hooks/use-copilot-action.ts /** * Example usage of useCopilotAction with complex parameters: * * @example * useCopilotAction({ * name: "myAction", * parameters: [ * { name: "arg1", type: "string", enum: ["option1", "option2", "option3"], required: false }, * { name: "arg2", type: "number" }, * { * name: "arg3", * type: "object", * attributes: [ * { name: "nestedArg1", type: "boolean" }, * { name: "xyz", required: false }, * ], * }, * { name: "arg4", type: "number[]" }, * ], * handler: ({ arg1, arg2, arg3, arg4 }) => { * const x = arg3.nestedArg1; * const z = arg3.xyz; * console.log(arg1, arg2, arg3); * }, * }); * * @example * // Simple action without parameters * useCopilotAction({ * name: "myAction", * handler: () => { * console.log("No parameters provided."); * }, * }); * * @example * // Interactive action with UI rendering and response handling * useCopilotAction({ * name: "handleMeeting", * description: "Handle a meeting by booking or canceling", * parameters: [ * { * name: "meeting", * type: "string", * description: "The meeting to handle", * required: true, * }, * { * name: "date", * type: "string", * description: "The date of the meeting", * required: true, * }, * { * name: "title", * type: "string", * description: "The title of the meeting", * required: true, * }, * ], * renderAndWaitForResponse: ({ args, respond, status }) => { * const { meeting, date, title } = args; * return ( * <MeetingConfirmationDialog * meeting={meeting} * date={date} * title={title} * onConfirm={() => respond('meeting confirmed')} * onCancel={() => respond('meeting canceled')} * /> * ); * }, * }); * * @example * // Catch all action allows you to render actions that are not defined in the frontend * useCopilotAction({ * name: "*", * render: ({ name, args, status, result, handler, respond }) => { * return <div>Rendering action: {name}</div>; * }, * }); */ /** * <img src="https://cdn.copilotkit.ai/docs/copilotkit/images/use-copilot-action/useCopilotAction.gif" width="500" /> * `useCopilotAction` is a React hook that you can use in your application to provide * custom actions that can be called by the AI. Essentially, it allows the Copilot to * execute these actions contextually during a chat, based on the user's interactions * and needs. * * Here's how it works: * * Use `useCopilotAction` to set up actions that the Copilot can call. To provide * more context to the Copilot, you can provide it with a `description` (for example to explain * what the action does, under which conditions it can be called, etc.). * * Then you define the parameters of the action, which can be simple, e.g. primitives like strings or numbers, * or complex, e.g. objects or arrays. * * Finally, you provide a `handler` function that receives the parameters and returns a result. * CopilotKit takes care of automatically inferring the parameter types, so you get type safety * and autocompletion for free. * * To render a custom UI for the action, you can provide a `render()` function. This function * lets you render a custom component or return a string to display. * * ## Usage * * ### Simple Usage * * ```tsx * useCopilotAction({ * name: "sayHello", * description: "Say hello to someone.", * parameters: [ * { * name: "name", * type: "string", * description: "name of the person to say greet", * }, * ], * handler: async ({ name }) => { * alert(`Hello, ${name}!`); * }, * }); * ``` * * ## Generative UI * * This hooks enables you to dynamically generate UI elements and render them in the copilot chat. For more information, check out the [Generative UI](/guides/generative-ui) page. */ function getActionConfig(action) { if (action.name === "*") return { type: "render", action }; if ("renderAndWaitForResponse" in action || "renderAndWait" in action) { let render = action.render; if (!render && "renderAndWaitForResponse" in action) render = action.renderAndWaitForResponse; if (!render && "renderAndWait" in action) render = action.renderAndWait; return { type: "hitl", action: { ...action, render } }; } if ("available" in action) { if (action.available === "enabled" || action.available === "remote") return { type: "frontend", action }; if (action.available === "frontend" || action.available === "disabled") return { type: "render", action }; } if ("handler" in action) return { type: "frontend", action }; throw new Error("Invalid action configuration"); } /** * useCopilotAction is a legacy hook maintained for backwards compatibility. * * To avoid violating React's Rules of Hooks (which prohibit conditional hook calls), * we use a registration pattern: * 1. This hook registers the action configuration with the CopilotContext * 2. A renderer component in CopilotKit actually renders the appropriate hook wrapper * 3. React properly manages hook state since components are rendered, not conditionally called * * This allows action types to change between renders without corrupting React's hook state. */ function useCopilotAction(action, dependencies) { const [initialActionConfig] = useState(getActionConfig(action)); const currentActionConfig = getActionConfig(action); /** * Calling hooks conditionally violates React's Rules of Hooks. This rule exists because * React maintains the call stack for hooks like useEffect or useState, and conditionally * calling a hook would result in inconsistent call stacks between renders. * * Unfortunately, useCopilotAction _has_ to conditionally call a hook based on the * supplied parameters. In order to avoid breaking React's call stack tracking, while * breaking the Rule of Hooks, we use a ref to store the initial action configuration * and throw an error if the _configuration_ changes such that we would call a different hook. */ if (initialActionConfig.type !== currentActionConfig.type) throw new Error("Action configuration changed between renders"); switch (currentActionConfig.type) { case "render": return useRenderToolCall(currentActionConfig.action, dependencies); case "hitl": return useHumanInTheLoop(currentActionConfig.action, dependencies); case "frontend": return useFrontendTool(currentActionConfig.action, dependencies); default: throw new Error("Invalid action configuration"); } } //#endregion //#region src/hooks/use-coagent-state-render.ts /** * The useCoAgentStateRender hook allows you to render UI or text based components on a Agentic Copilot's state in the chat. * This is particularly useful for showing intermediate state or progress during Agentic Copilot operations. * * ## Usage * * ### Simple Usage * * ```tsx * import { useCoAgentStateRender } from "@copilotkit/react-core"; * * type YourAgentState = { * agent_state_property: string; * } * * useCoAgentStateRender<YourAgentState>({ * name: "basic_agent", * nodeName: "optionally_specify_a_specific_node", * render: ({ status, state, nodeName }) => { * return ( * <YourComponent * agentStateProperty={state.agent_state_property} * status={status} * nodeName={nodeName} * /> * ); * }, * }); * ``` * * This allows for you to render UI components or text based on what is happening within the agent. * * ### Example * A great example of this is in our Perplexity Clone where we render the progress of an agent's internet search as it is happening. * You can play around with it below or learn how to build it with its [demo](/coagents/videos/perplexity-clone). * * <Callout type="info"> * This example is hosted on Vercel and may take a few seconds to load. * </Callout> * * <iframe src="https://examples-coagents-ai-researcher-ui.vercel.app/" className="w-full rounded-lg border h-[700px] my-4" /> */ /** * This hook is used to render agent state with custom UI components or text. This is particularly * useful for showing intermediate state or progress during Agentic Copilot operations. * To get started using rendering intermediate state through this hook, checkout the documentation. * * https://docs.copilotkit.ai/langgraph-python/shared-state/predictive-state-updates */ function useCoAgentStateRender(action, dependencies) { const { chatComponentsCache, availableAgents } = useContext(CopilotContext); const { setCoAgentStateRender, removeCoAgentStateRender, coAgentStateRenders } = useCoAgentStateRenders(); const idRef = useRef(randomId()); const { setBannerError, addToast } = useToast(); useEffect(() => { if (availableAgents?.length && !availableAgents.some((a) => a.name === action.name)) { `${action.name}`; setBannerError(new CopilotKitAgentDiscoveryError({ agentName: action.name, availableAgents: availableAgents.map((a) => ({ name: a.name, id: a.id })) })); } }, [availableAgents]); const key = `${action.name}-${action.nodeName || "global"}`; if (dependencies === void 0) { if (coAgentStateRenders[idRef.current]) { coAgentStateRenders[idRef.current].handler = action.handler; if (typeof action.render === "function") { if (chatComponentsCache.current !== null) chatComponentsCache.current.coAgentStateRenders[key] = action.render; } } } useEffect(() => { const currentId = idRef.current; if (Object.entries(coAgentStateRenders).some(([id, otherAction]) => { if (id === currentId) return false; if (otherAction.name !== action.name) return false; const hasNodeName = !!action.nodeName; const hasOtherNodeName = !!otherAction.nodeName; if (!hasNodeName && !hasOtherNodeName) return true; if (hasNodeName !== hasOtherNodeName) return false; return action.nodeName === otherAction.nodeName; })) addToast({ type: "warning", message: action.nodeName ? `Found multiple state renders for agent ${action.name} and node ${action.nodeName}. State renders might get overridden` : `Found multiple state renders for agent ${action.name}. State renders might get overridden`, id: `dup-action-${action.name}` }); }, [coAgentStateRenders]); useEffect(() => { setCoAgentStateRender(idRef.current, action); if (chatComponentsCache.current !== null && action.render !== void 0) chatComponentsCache.current.coAgentStateRenders[key] = action.render; return () => { removeCoAgentStateRender(idRef.current); }; }, [ setCoAgentStateRender, removeCoAgentStateRender, action.name, typeof action.render === "string" ? action.render : void 0, ...dependencies || [] ]); } //#endregion //#region src/hooks/use-make-copilot-document-readable.ts /** * Makes a document readable by Copilot. * @param document The document to make readable. * @param categories The categories to associate with the document. * @param dependencies The dependencies to use for the effect. * @returns The id of the document. */ function useMakeCopilotDocumentReadable(document, categories, dependencies = []) { const { addDocumentContext, removeDocumentContext } = useCopilotContext(); const idRef = useRef(void 0); useEffect(() => { const id = addDocumentContext(document, categories); idRef.current = id; return () => { removeDocumentContext(id); }; }, [ addDocumentContext, removeDocumentContext, ...dependencies ]); return idRef.current; } //#endregion //#region src/hooks/use-copilot-readable.ts /** * `useCopilotReadable` is a React hook that provides app-state and other information * to the Copilot. Optionally, the hook can also handle hierarchical state within your * application, passing these parent-child relationships to the Copilot. * * ## Usage * * ### Simple Usage * * In its most basic usage, useCopilotReadable accepts a single string argument * representing any piece of app state, making it available for the Copilot to use * as context when responding to user input. * * ```tsx * import { useCopilotReadable } from "@copilotkit/react-core"; * * export function MyComponent() { * const [employees, setEmployees] = useState([]); * * useCopilotReadable({ * description: "The list of employees", * value: employees, * }); * } * ``` * * ### Nested Components * * Optionally, you can maintain the hierarchical structure of information by passing * `parentId`. This allows you to use `useCopilotReadable` in nested components: * * ```tsx /employeeContextId/1 {17,23} * import { useCopilotReadable } from "@copilotkit/react-core"; * * function Employee(props: EmployeeProps) { * const { employeeName, workProfile, metadata } = props; * * // propagate any information to copilot * const employeeContextId = useCopilotReadable({ * description: "Employee name", * value: employeeName * }); * * // Pass a parentID to maintain a hierarchical structure. * // Especially useful with child React components, list elements, etc. * useCopilotReadable({ * description: "Work profile", * value: workProfile.description(), * parentId: employeeContextId * }); * * useCopilotReadable({ * description: "Employee metadata", * value: metadata.description(), * parentId: employeeContextId * }); * * return ( * // Render as usual... * ); * } * ``` */ /** * Adds the given information to the Copilot context to make it readable by Copilot. */ function useCopilotReadable({ description, value, convert, available }, dependencies) { const { copilotkit } = useCopilotKit(); const ctxIdRef = useRef(void 0); useEffect(() => { if (!copilotkit) return; const found = Object.entries(copilotkit.context).find(([id, ctxItem]) => { return JSON.stringify({ description, value }) == JSON.stringify(ctxItem); }); if (found) { ctxIdRef.current = found[0]; if (available === "disabled") copilotkit.removeContext(ctxIdRef.current); return; } if (!found && available === "disabled") return; ctxIdRef.current = copilotkit.addContext({ description, value: (convert ?? JSON.stringify)(value) }); return () => { if (!ctxIdRef.current) return; copilotkit.removeContext(ctxIdRef.current); }; }, [ description, value, convert ]); return ctxIdRef.current; } //#endregion //#region src/hooks/use-agent-nodename.ts function useAgentNodeName(agentName) { const { agent } = useAgent({ agentId: agentName }); const nodeNameRef = useRef("start"); useEffect(() => { if (!agent) return; const subscription = agent.subscribe({ onStepStartedEvent: ({ event }) => { nodeNameRef.current = event.stepName; }, onRunStartedEvent: () => { nodeNameRef.current = "start"; }, onRunFinishedEvent: () => { nodeNameRef.current = "end"; }, onRunErrorEvent: () => { nodeNameRef.current = "end"; } }); return () => { subscription.unsubscribe(); }; }, [agent]); return nodeNameRef.current; } //#endregion //#region src/hooks/use-coagent.ts /** * <Callout type="info"> * Usage of this hook assumes some additional setup in your application, for more information * on that see the CoAgents <span className="text-blue-500">[getting started guide](/coagents/quickstart/langgraph)</span>. * </Callout> * <Frame className="my-12"> * <img * src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/SharedStateCoAgents.gif" * alt="CoAgents demonstration" * className="w-auto" * /> * </Frame> * * This hook is used to integrate an agent into your application. With its use, you can * render and update the state of an agent, allowing for a dynamic and interactive experience. * We call these shared state experiences agentic copilots, or CoAgents for short. * * ## Usage * * ### Simple Usage * * ```tsx * import { useCoAgent } from "@copilotkit/react-core"; * * type AgentState = { * count: number; * } * * const agent = useCoAgent<AgentState>({ * name: "my-agent", * initialState: { * count: 0, * }, * }); * * ``` * * `useCoAgent` returns an object with the following properties: * * ```tsx * const { * name, // The name of the agent currently being used. * nodeName, // The name of the current LangGraph node. * state, // The current state of the agent. * setState, // A function to update the state of the agent. * running, // A boolean indicating if the agent is currently running. * start, // A function to start the agent. * stop, // A function to stop the agent. * run, // A function to re-run the agent. Takes a HintFunction to inform the agent why it is being re-run. * } = agent; * ``` * * Finally we can leverage these properties to create reactive experiences with the agent! * * ```tsx * const { state, setState } = useCoAgent<AgentState>({ * name: "my-agent", * initialState: { * count: 0, * }, * }); * * return ( * <div> * <p>Count: {state.count}</p> * <button onClick={() => setState({ count: state.count + 1 })}>Increment</button> * </div> * ); * ``` * * This reactivity is bidirectional, meaning that changes to the state from the agent will be reflected in the UI and vice versa. * * ## Parameters * <PropertyReference name="options" type="UseCoagentOptions<T>" required> * The options to use when creating the coagent. * <PropertyReference name="name" type="string" required> * The name of the agent to use. * </PropertyReference> * <PropertyReference name="initialState" type="T | any"> * The initial state of the agent. * </PropertyReference> * <PropertyReference name="state" type="T | any"> * State to manage externally if you are using this hook with external state management. * </PropertyReference> * <PropertyReference name="setState" type="(newState: T | ((prevState: T | undefined) => T)) => void"> * A function to update the state of the agent if you are using this hook with external state management. * </PropertyReference> * </PropertyReference> */ /** * This hook is used to integrate an agent into your application. With its use, you can * render and update the state of the agent, allowing for a dynamic and interactive experience. * We call these shared state experiences "agentic copilots". To get started using agentic copilots, which * we refer to as CoAgents, checkout the documentation at https://docs.copilotkit.ai/langgraph-python/quickstart. */ function useCoAgent(options) { const { agent } = useAgent({ agentId: options.name }); const { copilotkit } = useCopilotKit(); const nodeName = useAgentNodeName(options.name); const handleStateUpdate = useCallback((newState) => { if (!agent) return; if (typeof newState === "function") { const updater = newState; agent.setState(updater(agent.state)); } else agent.setState({ ...agent.state, ...newState }); }, [agent?.state, agent?.setState]); useEffect(() => { if (!options.config && !options.configurable) return; let config = options.config ?? {}; if (options.configurable) config = { ...config, configurable: { ...options.configurable, ...config.configurable } }; copilotkit.setProperties(config); }, [options.config, options.configurable]); useEffect(() => { if (agent?.state && isExternalStateManagement(options) && JSON.stringify(options.state) !== JSON.stringify(agent.state)) handleStateUpdate(options.state); }, [ agent, useMemo(() => isExternalStateManagement(options) ? JSON.stringify(options.state) : void 0, [isExternalStateManagement(options) ? JSON.stringify(options.state) : void 0]), handleStateUpdate ]); const hasStateValues = useCallback((value) => { return Boolean(value && Object.keys(value).length); }, []); const initialStateRef = useRef(isExternalStateManagement(options) ? options.state : "initialState" in options ? options.initialState : void 0); useEffect(() => { if (isExternalStateManagement(options)) initialStateRef.current = options.state; else if ("initialState" in options) initialStateRef.current = options.initialState; }, [isExternalStateManagement(options) ? JSON.stringify(options.state) : "initialState" in options ? JSON.stringify(options.initialState) : void 0]); useEffect(() => { if (!agent) return; const subscription = agent.subscribe({ onStateChanged: (args) => { if (isExternalStateManagement(options)) options.setState(args.state); }, onRunInitialized: (args) => { if (hasStateValues(args.state)) { handleStateUpdate(args.state); return; } if (hasStateValues(agent.state)) return; if (initialStateRef.current !== void 0) handleStateUpdate(initialStateRef.current); } }); return () => { subscription.unsubscribe(); }; }, [ agent, handleStateUpdate, hasStateValues ]); return useMemo(() => { if (!agent) { const noop = () => {}; const noopAsync = async () => {}; const initialState = ("state" in options && options.state) ?? ("initialState" in options && options.initialState) ?? {}; return { name: options.name, nodeName, threadId: void 0, running: false, state: initialState, setState: noop, start: noop, stop: noop, run: noopAsync }; } return { name: agent?.agentId ?? options.name, nodeName, threadId: agent.threadId, running: agent.isRunning, state: agent.state, setState: handleStateUpdate, start: agent.runAgent, stop: agent.abortRun, run: agent.runAgent }; }, [ agent?.state, agent?.runAgent, agent?.abortRun, agent?.runAgent, agent?.threadId, agent?.isRunning, agent?.agentId, handleStateUpdate, options.name ]); } const isExternalStateManagement = (options) => { return "state" in options && "setState" in options; }; //#endregion //#region src/hooks/use-copilot-runtime-client.ts const useCopilotRuntimeClient = (options) => { const { setBannerError } = useToast(); const { showDevConsole, onError, ...runtimeOptions } = options; const lastStructuredErrorRef = useRef(null); const traceUIError = async (error, originalError) => { try { await onError({ type: "error", timestamp: Date.now(), context: { source: "ui", request: { operation: "runtimeClient", url: runtimeOptions.url, startTime: Date.now() }, technical: { environment: "browser", userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0, stackTrace: originalError instanceof Error ? originalError.stack : void 0 } }, error }); } catch (error) { console.error("Error in onError handler:", error); } }; return useMemo(() => { return new CopilotRuntimeClient({ ...runtimeOptions, handleGQLErrors: (error) => { if (error.graphQLErrors?.length) { const graphQLErrors = error.graphQLErrors; const routeError = (gqlError) => { if (gqlError.extensions?.visibility === ErrorVisibility.SILENT) { console.error("CopilotKit Silent Error:", gqlError.message); return; } const now = Date.now(); const errorMessage = gqlError.message; if (lastStructuredErrorRef.current && lastStructuredErrorRef.current.message === errorMessage && now - lastStructuredErrorRef.current.timestamp < 150) return; lastStructuredErrorRef.current = { message: errorMessage, timestamp: now }; const ckError = createStructuredError(gqlError); if (ckError) { setBannerError(ckError); traceUIError(ckError, gqlError); } else { const fallbackError = new CopilotKitError({ message: gqlError.message, code: CopilotKitErrorCode.UNKNOWN }); setBannerError(fallbackError); traceUIError(fallbackError, gqlError); } }; graphQLErrors.forEach(routeError); } else { const fallbackError = new CopilotKitError({ message: error?.message || String(error), code: CopilotKitErrorCode.UNKNOWN }); setBannerError(fallbackError); traceUIError(fallbackError, error); } }, handleGQLWarning: (message) => { console.warn(message); setBannerError(new CopilotKitError({ message, code: CopilotKitErrorCode.UNKNOWN })); } }); }, [ runtimeOptions, setBannerError, onError ]); }; function createStructuredError(gqlError) { const extensions = gqlError.extensions; const originalError = extensions?.originalError; const message = originalError?.messag