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;" />

478 lines (438 loc) • 14.5 kB
/** * <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> */ import { useCallback, useEffect, useMemo, useRef } from "react"; import { CopilotContextParams, useCopilotContext } from "../context"; import { CoagentState } from "../types/coagent-state"; import { useCopilotChat } from "./use-copilot-chat_internal"; import { Message } from "@copilotkit/shared"; import { useAsyncCallback } from "../components/error-boundary/error-utils"; import { useToast } from "../components/toast/toast-provider"; import { useCopilotRuntimeClient } from "./use-copilot-runtime-client"; import { parseJson, CopilotKitAgentDiscoveryError } from "@copilotkit/shared"; import { useMessagesTap } from "../components/copilot-provider/copilot-messages"; import { Message as GqlMessage } from "@copilotkit/runtime-client-gql"; interface UseCoagentOptionsBase { /** * The name of the agent being used. */ name: string; /** * @deprecated - use "config.configurable" * Config to pass to a LangGraph Agent */ configurable?: Record<string, any>; /** * Config to pass to a LangGraph Agent */ config?: { configurable?: Record<string, any>; [key: string]: any; }; } interface WithInternalStateManagementAndInitial<T> extends UseCoagentOptionsBase { /** * The initial state of the agent. */ initialState: T; } interface WithInternalStateManagement extends UseCoagentOptionsBase { /** * Optional initialState with default type any */ initialState?: any; } interface WithExternalStateManagement<T> extends UseCoagentOptionsBase { /** * The current state of the agent. */ state: T; /** * A function to update the state of the agent. */ setState: (newState: T | ((prevState: T | undefined) => T)) => void; } type UseCoagentOptions<T> = | WithInternalStateManagementAndInitial<T> | WithInternalStateManagement | WithExternalStateManagement<T>; export interface UseCoagentReturnType<T> { /** * The name of the agent being used. */ name: string; /** * The name of the current LangGraph node. */ nodeName?: string; /** * The ID of the thread the agent is running in. */ threadId?: string; /** * A boolean indicating if the agent is currently running. */ running: boolean; /** * The current state of the agent. */ state: T; /** * A function to update the state of the agent. */ setState: (newState: T | ((prevState: T | undefined) => T)) => void; /** * A function to start the agent. */ start: () => void; /** * A function to stop the agent. */ stop: () => void; /** * A function to re-run the agent. The hint function can be used to provide a hint to the agent * about why it is being re-run again. */ run: (hint?: HintFunction) => Promise<void>; } export interface HintFunctionParams { /** * The previous state of the agent. */ previousState: any; /** * The current state of the agent. */ currentState: any; } export type HintFunction = (params: HintFunctionParams) => Message | undefined; /** * 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/coagents/quickstart/langgraph. */ export function useCoAgent<T = any>(options: UseCoagentOptions<T>): UseCoagentReturnType<T> { const context = useCopilotContext(); const { availableAgents, onError } = context; const { setBannerError } = useToast(); const lastLoadedThreadId = useRef<string>(); const lastLoadedState = useRef<any>(); const { name } = options; useEffect(() => { if (availableAgents?.length && !availableAgents.some((a) => a.name === name)) { const message = `(useCoAgent): Agent "${name}" not found. Make sure the agent exists and is properly configured.`; console.warn(message); // Route to banner instead of toast for consistency const agentError = new CopilotKitAgentDiscoveryError({ agentName: name, availableAgents: availableAgents.map((a) => ({ name: a.name, id: a.id })), }); setBannerError(agentError); } }, [availableAgents]); const { getMessagesFromTap } = useMessagesTap(); const { coagentStates, coagentStatesRef, setCoagentStatesWithRef, threadId, copilotApiConfig } = context; const { sendMessage, runChatCompletion } = useCopilotChat(); const headers = { ...(copilotApiConfig.headers || {}), }; const runtimeClient = useCopilotRuntimeClient({ url: copilotApiConfig.chatApiEndpoint, publicApiKey: copilotApiConfig.publicApiKey, headers, credentials: copilotApiConfig.credentials, showDevConsole: context.showDevConsole, onError, }); // if we manage state internally, we need to provide a function to set the state const setState = useCallback( (newState: T | ((prevState: T | undefined) => T)) => { // coagentStatesRef.current || {} let coagentState: CoagentState = getCoagentState({ coagentStates, name, options }); const updatedState = typeof newState === "function" ? (newState as Function)(coagentState.state) : newState; setCoagentStatesWithRef({ ...coagentStatesRef.current, [name]: { ...coagentState, state: updatedState, }, }); }, [coagentStates, name], ); useEffect(() => { const fetchAgentState = async () => { if (!threadId || threadId === lastLoadedThreadId.current) return; const result = await runtimeClient.loadAgentState({ threadId, agentName: name, }); // Runtime client handles errors automatically via handleGQLErrors if (result.error) { return; // Don't process data on error } const newState = result.data?.loadAgentState?.state; if (newState === lastLoadedState.current) return; if (result.data?.loadAgentState?.threadExists && newState && newState != "{}") { lastLoadedState.current = newState; lastLoadedThreadId.current = threadId; const fetchedState = parseJson(newState, {}); isExternalStateManagement(options) ? options.setState(fetchedState) : setState(fetchedState); } }; void fetchAgentState(); }, [threadId]); // Sync internal state with external state if state management is external useEffect(() => { if (isExternalStateManagement(options)) { setState(options.state); } else if (coagentStates[name] === undefined) { setState(options.initialState === undefined ? {} : options.initialState); } }, [ isExternalStateManagement(options) ? JSON.stringify(options.state) : undefined, // reset initialstate on reset coagentStates[name] === undefined, ]); // Sync config when runtime configuration changes useEffect(() => { const newConfig = options.config ? options.config : options.configurable ? { configurable: options.configurable } : undefined; if (newConfig === undefined) return; setCoagentStatesWithRef((prev) => { const existing = prev[name] ?? { name, state: isInternalStateManagementWithInitial(options) ? options.initialState : {}, config: {}, running: false, active: false, threadId: undefined, nodeName: undefined, runId: undefined, }; if (JSON.stringify(existing.config) === JSON.stringify(newConfig)) { return prev; } return { ...prev, [name]: { ...existing, config: newConfig, }, }; }); }, [JSON.stringify(options.config), JSON.stringify(options.configurable)]); const runAgentCallback = useAsyncCallback( async (hint?: HintFunction) => { await runAgent(name, context, getMessagesFromTap(), sendMessage, runChatCompletion, hint); }, [name, context, sendMessage, runChatCompletion], ); // Return the state and setState function return useMemo(() => { const coagentState = getCoagentState({ coagentStates, name, options }); return { name, nodeName: coagentState.nodeName, threadId: coagentState.threadId, running: coagentState.running, state: coagentState.state, setState: isExternalStateManagement(options) ? options.setState : setState, start: () => startAgent(name, context), stop: () => stopAgent(name, context), run: runAgentCallback, }; }, [name, coagentStates, options, setState, runAgentCallback]); } export function startAgent(name: string, context: CopilotContextParams) { const { setAgentSession } = context; setAgentSession({ agentName: name, }); } export function stopAgent(name: string, context: CopilotContextParams) { const { agentSession, setAgentSession } = context; if (agentSession && agentSession.agentName === name) { setAgentSession(null); context.setCoagentStates((prevAgentStates) => { return { ...prevAgentStates, [name]: { ...prevAgentStates[name], running: false, active: false, threadId: undefined, nodeName: undefined, runId: undefined, }, }; }); } else { console.warn(`No agent session found for ${name}`); } } export async function runAgent( name: string, context: CopilotContextParams, messages: GqlMessage[], sendMessage: (message: Message) => Promise<void>, runChatCompletion: () => Promise<Message[]>, hint?: HintFunction, ) { const { agentSession, setAgentSession } = context; if (!agentSession || agentSession.agentName !== name) { setAgentSession({ agentName: name, }); } let previousState: any = null; for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (message.isAgentStateMessage() && message.agentName === name) { previousState = message.state; } } let state = context.coagentStatesRef.current?.[name]?.state || {}; if (hint) { const hintMessage = hint({ previousState, currentState: state }); if (hintMessage) { await sendMessage(hintMessage); } else { await runChatCompletion(); } } else { await runChatCompletion(); } } const isExternalStateManagement = <T>( options: UseCoagentOptions<T>, ): options is WithExternalStateManagement<T> => { return "state" in options && "setState" in options; }; const isInternalStateManagementWithInitial = <T>( options: UseCoagentOptions<T>, ): options is WithInternalStateManagementAndInitial<T> => { return "initialState" in options; }; const getCoagentState = <T>({ coagentStates, name, options, }: { coagentStates: Record<string, CoagentState>; name: string; options: UseCoagentOptions<T>; }) => { if (coagentStates[name]) { return coagentStates[name]; } else { return { name, state: isInternalStateManagementWithInitial<T>(options) ? options.initialState : {}, config: options.config ? options.config : options.configurable ? { configurable: options.configurable } : {}, running: false, active: false, threadId: undefined, nodeName: undefined, runId: undefined, }; } };