diff --git a/CopilotKit/.changeset/twenty-dogs-trade.md b/CopilotKit/.changeset/twenty-dogs-trade.md new file mode 100644 index 0000000000..8b2fe93e4e --- /dev/null +++ b/CopilotKit/.changeset/twenty-dogs-trade.md @@ -0,0 +1,7 @@ +--- +"@copilotkit/react-core": patch +"@copilotkit/runtime-client-gql": patch +"@copilotkit/runtime": patch +--- + +- fix: add warning when using agents that are not available on agent related hooks diff --git a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx index 220e89533d..7823f537f9 100644 --- a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx +++ b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx @@ -14,7 +14,7 @@ * ``` */ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CopilotContext, CopilotApiConfig, @@ -42,6 +42,7 @@ import { ToastProvider } from "../toast/toast-provider"; import { useCopilotRuntimeClient } from "../../hooks/use-copilot-runtime-client"; import { shouldShowDevConsole } from "../../utils"; import { CopilotErrorBoundary } from "../error-boundary/error-boundary"; +import { Agent } from "@copilotkit/runtime-client-gql"; export function CopilotKit({ children, ...props }: CopilotKitProps) { const showDevConsole = props.showDevConsole === undefined ? "auto" : props.showDevConsole; @@ -277,6 +278,7 @@ export function CopilotKitInternal({ children, ...props }: CopilotKitProps) { }); }; + const [availableAgents, setAvailableAgents] = useState([]); const [coagentStates, setCoagentStates] = useState>({}); const coagentStatesRef = useRef>({}); const setCoagentStatesWithRef = useCallback( @@ -294,6 +296,16 @@ export function CopilotKitInternal({ children, ...props }: CopilotKitProps) { [], ); + useEffect(() => { + const fetchData = async () => { + const result = await runtimeClient.availableAgents(); + if (result.data?.availableAgents) { + setAvailableAgents(result.data.availableAgents.agents); + } + }; + void fetchData(); + }, []); + let initialAgentSession: AgentSession | null = null; if (props.agent) { initialAgentSession = { @@ -349,6 +361,7 @@ export function CopilotKitInternal({ children, ...props }: CopilotKitProps) { runId, setRunId, chatAbortControllerRef, + availableAgents, authConfig: props.authConfig, authStates, setAuthStates, diff --git a/CopilotKit/packages/react-core/src/components/toast/toast-provider.tsx b/CopilotKit/packages/react-core/src/components/toast/toast-provider.tsx index 09abbb3e1b..d1e749d685 100644 --- a/CopilotKit/packages/react-core/src/components/toast/toast-provider.tsx +++ b/CopilotKit/packages/react-core/src/components/toast/toast-provider.tsx @@ -141,7 +141,7 @@ function Toast({ style={{ backgroundColor: bgColors[type], color: "white", - padding: "0.5rem 1rem", + padding: "0.5rem 1.5rem", borderRadius: "0.25rem", boxShadow: "0 2px 4px rgba(0,0,0,0.1)", position: "relative", diff --git a/CopilotKit/packages/react-core/src/context/copilot-context.tsx b/CopilotKit/packages/react-core/src/context/copilot-context.tsx index 43e8557950..950fafe505 100644 --- a/CopilotKit/packages/react-core/src/context/copilot-context.tsx +++ b/CopilotKit/packages/react-core/src/context/copilot-context.tsx @@ -11,6 +11,7 @@ import { CopilotChatSuggestionConfiguration } from "../types/chat-suggestion-con import { CoAgentStateRender, CoAgentStateRenderProps } from "../types/coagent-action"; import { CoagentState } from "../types/coagent-state"; import { CopilotRuntimeClient, ForwardedParametersInput } from "@copilotkit/runtime-client-gql"; +import { Agent } from "@copilotkit/runtime-client-gql"; /** * Interface for the configuration of the Copilot API. @@ -176,6 +177,7 @@ export interface CopilotContextParams { * The forwarded parameters to use for the task. */ forwardedParameters?: Pick; + availableAgents: Agent[]; /** * The auth states for the CopilotKit. @@ -251,6 +253,7 @@ const emptyCopilotContext: CopilotContextParams = { runId: null, setRunId: () => {}, chatAbortControllerRef: { current: null }, + availableAgents: [], }; export const CopilotContext = React.createContext(emptyCopilotContext); diff --git a/CopilotKit/packages/react-core/src/hooks/use-coagent-state-render.ts b/CopilotKit/packages/react-core/src/hooks/use-coagent-state-render.ts index c1e172d82f..f84b69e126 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-coagent-state-render.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-coagent-state-render.ts @@ -50,6 +50,7 @@ import { useRef, useContext, useEffect } from "react"; import { CopilotContext } from "../context/copilot-context"; import { randomId } from "@copilotkit/shared"; import { CoAgentStateRender } from "../types/coagent-action"; +import { useToast } from "../components/toast/toast-provider"; /** * This hook is used to render agent state with custom UI components or text. This is particularly @@ -71,8 +72,18 @@ export function useCoAgentStateRender( removeCoAgentStateRender, coAgentStateRenders, chatComponentsCache, + availableAgents, } = useContext(CopilotContext); const idRef = useRef(randomId()); + const { addToast } = useToast(); + + useEffect(() => { + if (availableAgents?.length && !availableAgents.some((a) => a.name === action.name)) { + const message = `(useCoAgentStateRender): Agent "${action.name}" not found. Make sure the agent exists and is properly configured.`; + console.warn(message); + addToast({ type: "warning", message }); + } + }, [availableAgents]); const key = `${action.name}-${action.nodeName || "global"}`; diff --git a/CopilotKit/packages/react-core/src/hooks/use-coagent.ts b/CopilotKit/packages/react-core/src/hooks/use-coagent.ts index de187d5594..22912ea394 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-coagent.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-coagent.ts @@ -100,6 +100,7 @@ import { useCopilotChat } from "./use-copilot-chat"; import { Message } from "@copilotkit/runtime-client-gql"; import { flushSync } from "react-dom"; import { useAsyncCallback } from "../components/error-boundary/error-utils"; +import { useToast } from "../components/toast/toast-provider"; interface WithInternalStateManagementAndInitial { /** @@ -203,6 +204,9 @@ export type HintFunction = (params: HintFunctionParams) => Message | undefined; * we refer to as CoAgents, checkout the documentation at https://docs.copilotkit.ai/coagents/quickstart. */ export function useCoAgent(options: UseCoagentOptions): UseCoagentReturnType { + const generalContext = useCopilotContext(); + const { availableAgents } = generalContext; + const { addToast } = useToast(); const isExternalStateManagement = ( options: UseCoagentOptions, ): options is WithExternalStateManagement => { @@ -210,6 +214,13 @@ export function useCoAgent(options: UseCoagentOptions): UseCoagentRe }; 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 isInternalStateManagementWithInitial = ( options: UseCoagentOptions, @@ -217,7 +228,6 @@ export function useCoAgent(options: UseCoagentOptions): UseCoagentRe return "initialState" in options; }; - const generalContext = useCopilotContext(); const messagesContext = useCopilotMessagesContext(); const context = { ...generalContext, ...messagesContext }; const { coagentStates, coagentStatesRef, setCoagentStatesWithRef } = context; diff --git a/CopilotKit/packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts b/CopilotKit/packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts index bba9b18806..d149c5ae45 100644 --- a/CopilotKit/packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts +++ b/CopilotKit/packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts @@ -2,12 +2,31 @@ import { Client, cacheExchange, fetchExchange } from "@urql/core"; import * as packageJson from "../../package.json"; import { + AvailableAgentsQuery, GenerateCopilotResponseMutation, GenerateCopilotResponseMutationVariables, } from "../graphql/@generated/graphql"; import { generateCopilotResponseMutation } from "../graphql/definitions/mutations"; +import { getAvailableAgentsQuery } from "../graphql/definitions/queries"; import { OperationResultSource, OperationResult } from "urql"; +const createFetchFn = + (signal?: AbortSignal) => + async (...args: Parameters) => { + const result = await fetch(args[0], { ...(args[1] ?? {}), signal }); + if (result.status !== 200) { + switch (result.status) { + case 404: + throw new Error( + "Runtime URL seems to be invalid - got 404 response. Please check the runtimeUrl passed to CopilotKit", + ); + default: + throw new Error("Could not fetch copilot response"); + } + } + return result; + }; + export interface CopilotRuntimeClientOptions { url: string; publicApiKey?: string; @@ -55,20 +74,7 @@ export class CopilotRuntimeClient { properties?: GenerateCopilotResponseMutationVariables["properties"]; signal?: AbortSignal; }) { - const fetchFn = async (...args: Parameters) => { - const result = await fetch(args[0], { ...(args[1] ?? {}), signal }); - if (result.status !== 200) { - switch (result.status) { - case 404: - throw new Error( - "Runtime URL seems to be invalid - got 404 response. Please check the runtimeUrl passed to CopilotKit", - ); - default: - throw new Error("Could not fetch copilot response"); - } - } - return result; - }; + const fetchFn = createFetchFn(signal); const result = this.client.mutation< GenerateCopilotResponseMutation, GenerateCopilotResponseMutationVariables @@ -97,4 +103,9 @@ export class CopilotRuntimeClient { }, }); } + + availableAgents() { + const fetchFn = createFetchFn(); + return this.client.query(getAvailableAgentsQuery, {}, { fetch: fetchFn }); + } } diff --git a/CopilotKit/packages/runtime-client-gql/src/graphql/definitions/queries.ts b/CopilotKit/packages/runtime-client-gql/src/graphql/definitions/queries.ts new file mode 100644 index 0000000000..8a07305240 --- /dev/null +++ b/CopilotKit/packages/runtime-client-gql/src/graphql/definitions/queries.ts @@ -0,0 +1,13 @@ +import { graphql } from "../@generated/gql"; + +export const getAvailableAgentsQuery = graphql(/** GraphQL **/ ` + query availableAgents { + availableAgents { + agents { + name + id + description + } + } + } +`); diff --git a/CopilotKit/packages/runtime/src/graphql/resolvers/copilot.resolver.ts b/CopilotKit/packages/runtime/src/graphql/resolvers/copilot.resolver.ts index 5c1b1d9b55..d5c1c774e0 100644 --- a/CopilotKit/packages/runtime/src/graphql/resolvers/copilot.resolver.ts +++ b/CopilotKit/packages/runtime/src/graphql/resolvers/copilot.resolver.ts @@ -42,6 +42,8 @@ import { } from "../types/converted"; import telemetry from "../../lib/telemetry-client"; import { randomId } from "@copilotkit/shared"; +import { EndpointType, LangGraphPlatformAgent } from "../../lib/runtime/remote-actions"; +import { AgentsResponse } from "../types/agents-response.type"; const invokeGuardrails = async ({ baseUrl, @@ -106,6 +108,20 @@ export class CopilotResolver { return "Hello World"; } + @Query(() => AgentsResponse) + async availableAgents(@Ctx() ctx: GraphQLContext) { + let logger = ctx.logger.child({ component: "CopilotResolver.availableAgents" }); + + logger.debug("Processing"); + const agents = await ctx._copilotkit.runtime.discoverAgentsFromEndpoints(ctx); + + logger.debug("Event source created, creating response"); + + return { + agents, + }; + } + @Mutation(() => CopilotResponse) async generateCopilotResponse( @Ctx() ctx: GraphQLContext, diff --git a/CopilotKit/packages/runtime/src/graphql/types/agents-response.type.ts b/CopilotKit/packages/runtime/src/graphql/types/agents-response.type.ts new file mode 100644 index 0000000000..39b7def48f --- /dev/null +++ b/CopilotKit/packages/runtime/src/graphql/types/agents-response.type.ts @@ -0,0 +1,22 @@ +import { Field, InterfaceType, ObjectType } from "type-graphql"; +import { MessageRole } from "./enums"; +import { MessageStatusUnion } from "./message-status.type"; +import { ResponseStatusUnion } from "./response-status.type"; + +@ObjectType() +export class Agent { + @Field(() => String) + id: string; + + @Field(() => String) + name: string; + + @Field(() => String) + description?: string; +} + +@ObjectType() +export class AgentsResponse { + @Field(() => [Agent]) + agents: Agent[]; +} diff --git a/CopilotKit/packages/runtime/src/lib/runtime/copilot-runtime.ts b/CopilotKit/packages/runtime/src/lib/runtime/copilot-runtime.ts index 58156b6e26..d64c18d145 100644 --- a/CopilotKit/packages/runtime/src/lib/runtime/copilot-runtime.ts +++ b/CopilotKit/packages/runtime/src/lib/runtime/copilot-runtime.ts @@ -34,6 +34,8 @@ import { AgentSessionInput } from "../../graphql/inputs/agent-session.input"; import { from } from "rxjs"; import { AgentStateInput } from "../../graphql/inputs/agent-state.input"; import { ActionInputAvailability } from "../../graphql/types/enums"; +import { createHeaders } from "./remote-action-constructors"; +import { Agent } from "../../graphql/types/agents-response.type"; interface CopilotRuntimeRequest { serviceAdapter: CopilotServiceAdapter; @@ -253,6 +255,54 @@ export class CopilotRuntime { } } + async discoverAgentsFromEndpoints(graphqlContext: GraphQLContext): Promise { + const headers = createHeaders(null, graphqlContext); + const agents = this.remoteEndpointDefinitions.reduce( + async (acc: Promise, endpoint) => { + const agents = await acc; + if (endpoint.type === EndpointType.LangGraphPlatform) { + const response = await fetch( + `${(endpoint as LangGraphPlatformEndpoint).deploymentUrl}/assistants/search`, + { + method: "POST", + headers, + }, + ); + + const data: Array<{ assistant_id: string; graph_id: string }> = await response.json(); + const endpointAgents = (data ?? []).map((entry) => ({ + name: entry.graph_id, + id: entry.assistant_id, + })); + return [...agents, ...endpointAgents]; + } + + interface InfoResponse { + agents?: Array<{ + name: string; + description: string; + }>; + } + + const response = await fetch(`${(endpoint as CopilotKitEndpoint).url}/info`, { + method: "POST", + headers, + body: JSON.stringify({ properties: graphqlContext.properties }), + }); + const data: InfoResponse = await response.json(); + const endpointAgents = (data?.agents ?? []).map((agent) => ({ + name: agent.name, + description: agent.description, + id: randomId(), // Required by Agent type + })); + return [...agents, ...endpointAgents]; + }, + Promise.resolve([]), + ); + + return agents; + } + private async processAgentRequest( request: CopilotRuntimeRequest, ): Promise { diff --git a/docs/content/docs/reference/classes/CopilotRuntime.mdx b/docs/content/docs/reference/classes/CopilotRuntime.mdx index b8ae7b8401..cfb09af70d 100644 --- a/docs/content/docs/reference/classes/CopilotRuntime.mdx +++ b/docs/content/docs/reference/classes/CopilotRuntime.mdx @@ -72,3 +72,12 @@ An array of LangServer URLs. + + + + + + + + +