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

367 lines (340 loc) • 10.6 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, useState } from "react"; import { Message } from "@copilotkit/shared"; import { useAgent, useCopilotKit } from "@copilotkitnext/react"; import { type AgentSubscriber } from "@ag-ui/client"; import { useAgentNodeName } from "./use-agent-nodename"; 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: (...args: any[]) => Promise<any>; } 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 { agent } = useAgent({ agentId: options.name }); const { copilotkit } = useCopilotKit(); const nodeName = useAgentNodeName(options.name); const handleStateUpdate = useCallback( (newState: T | ((prevState: T | undefined) => T)) => { if (!agent) return; if (typeof newState === "function") { const updater = newState as (prevState: T | undefined) => T; 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]); const externalStateStr = useMemo( () => (isExternalStateManagement(options) ? JSON.stringify(options.state) : undefined), [isExternalStateManagement(options) ? JSON.stringify(options.state) : undefined], ); // Sync internal state with external state if state management is external useEffect(() => { if ( agent?.state && isExternalStateManagement(options) && JSON.stringify(options.state) !== JSON.stringify(agent.state) ) { handleStateUpdate(options.state); } }, [agent, externalStateStr, handleStateUpdate]); const hasStateValues = useCallback((value?: Record<string, any>) => { return Boolean(value && Object.keys(value).length); }, []); const initialStateRef = useRef<any>( isExternalStateManagement(options) ? options.state : "initialState" in options ? options.initialState : undefined, ); 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) : undefined, ]); useEffect(() => { if (!agent) return; const subscriber: AgentSubscriber = { onStateChanged: (args: any) => { if (isExternalStateManagement(options)) { options.setState(args.state); } }, onRunInitialized: (args: any) => { const runHasState = hasStateValues(args.state); if (runHasState) { handleStateUpdate(args.state); return; } if (hasStateValues(agent.state)) { return; } if (initialStateRef.current !== undefined) { handleStateUpdate(initialStateRef.current); } }, }; const subscription = agent.subscribe(subscriber); return () => { subscription.unsubscribe(); }; }, [agent, handleStateUpdate, hasStateValues]); // Return a consistent shape whether or not the agent is available return useMemo<UseCoagentReturnType<T>>(() => { if (!agent) { const noop = () => {}; const noopAsync = async () => {}; const initialState = // prefer externally provided state if available ("state" in options && (options as any).state) ?? // then initialState if provided ("initialState" in options && (options as any).initialState) ?? ({} as T); return { name: options.name, nodeName, threadId: undefined, running: false, state: initialState as T, 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, // TODO: start and run both have same thing. need to figure out 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 = <T>( options: UseCoagentOptions<T>, ): options is WithExternalStateManagement<T> => { return "state" in options && "setState" in options; };