@copilotkit/react-core
Version:
<div align="center"> <a href="https://copilotkit.ai" target="_blank"> <img src="https://github.com/copilotkit/copilotkit/raw/main/assets/banner.png" alt="CopilotKit Logo"> </a>
428 lines (397 loc) • 12.9 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="/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,
CopilotMessagesContextParams,
useCopilotContext,
useCopilotMessagesContext,
} from "../context";
import { CoagentState } from "../types/coagent-state";
import { useCopilotChat } from "./use-copilot-chat";
import { Message } from "@copilotkit/runtime-client-gql";
import { useAsyncCallback } from "../components/error-boundary/error-utils";
import { useToast } from "../components/toast/toast-provider";
import { useCopilotRuntimeClient } from "./use-copilot-runtime-client";
import { parseJson } from "@copilotkit/shared";
interface WithInternalStateManagementAndInitial<T> {
/**
* The name of the agent being used.
*/
name: string;
/**
* The initial state of the agent.
*/
initialState: T;
/**
* Config to pass to a LangGraph Agent
*/
configurable?: Record<string, any>;
}
interface WithInternalStateManagement {
/**
* The name of the agent being used.
*/
name: string;
/**
* Optional initialState with default type any
*/
initialState?: any;
/**
* Config to pass to a LangGraph Agent
*/
configurable?: Record<string, any>;
}
interface WithExternalStateManagement<T> {
/**
* The name of the agent being used.
*/
name: string;
/**
* 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;
/**
* Config to pass to a LangGraph Agent
*/
configurable?: Record<string, any>;
}
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 generalContext = useCopilotContext();
const { availableAgents } = generalContext;
const { addToast } = 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);
addToast({ type: "warning", message });
}
}, [availableAgents]);
const messagesContext = useCopilotMessagesContext();
const context = { ...generalContext, ...messagesContext };
const { coagentStates, coagentStatesRef, setCoagentStatesWithRef, threadId, copilotApiConfig } =
context;
const { appendMessage, runChatCompletion } = useCopilotChat();
const runtimeClient = useCopilotRuntimeClient({
url: copilotApiConfig.chatApiEndpoint,
publicApiKey: copilotApiConfig.publicApiKey,
credentials: copilotApiConfig.credentials,
});
// 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,
});
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,
]);
const runAgentCallback = useAsyncCallback(
async (hint?: HintFunction) => {
await runAgent(name, context, appendMessage, runChatCompletion, hint);
},
[name, context, appendMessage, 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 & CopilotMessagesContextParams,
appendMessage: (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 = context.messages.length - 1; i >= 0; i--) {
const message = context.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 appendMessage(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 : {},
configurable: options.configurable ?? {},
running: false,
active: false,
threadId: undefined,
nodeName: undefined,
runId: undefined,
};
}
};