@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
text/typescript
/**
* <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;
};