chore: import upstream snapshot with attribution
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
# CopilotKit - Runtime Client GQL
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
|
||||
|
||||
<br>
|
||||
<div align="center" style="display:flex;justify-content:center;gap:16px;height:20px;margin: 0;">
|
||||
<a href="https://www.npmjs.com/package/@copilotkit/react-core" target="_blank">
|
||||
<img src="https://img.shields.io/npm/v/%40copilotkit%2Fruntime-client-gql?logo=npm&logoColor=%23FFFFFF&label=Version&color=%236963ff" alt="NPM">
|
||||
</a>
|
||||
<a href="https://github.com/copilotkit/copilotkit/blob/main/LICENSE" target="_blank">
|
||||
<img src="https://img.shields.io/github/license/copilotkit/copilotkit?color=%236963ff&label=License" alt="MIT">
|
||||
</a>
|
||||
<a href="https://discord.gg/6dffbvGU3D" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1122926057641742418?logo=discord&logoColor=%23FFFFFF&label=Discord&color=%236963ff" alt="Discord">
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
<div align="center">
|
||||
<a href="https://www.producthunt.com/posts/copilotkit" target="_blank">
|
||||
<img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=428778&theme=light&period=daily">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## ✨ Why CopilotKit?
|
||||
|
||||
- Minutes to integrate - Get started quickly with our CLI
|
||||
- Framework agnostic - Works with React, Next.js, AGUI and more
|
||||
- Production-ready UI - Use customizable components or build with headless UI
|
||||
- Built-in security - Prompt injection protection
|
||||
- Open source - Full transparency and community-driven
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/6cb425f8-ffcb-49d2-9bbb-87cab5995b78" alt="class-support-ecosystem" style="border-radius: 12px; border: 2px solid #d6d4fa;">
|
||||
|
||||
## 🧑💻 Real life use cases
|
||||
|
||||
<span>Deploy deeply-integrated AI assistants & agents that work alongside your users inside your applications.</span>
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/3b810240-e9f8-43ae-acec-31a58095e223" alt="headless-ui" style="border-radius: 12px; border: 2px solid #d6d4fa;">
|
||||
|
||||
## 🏆 Featured Examples
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.copilotkit.ai/examples/form-filling-copilot">
|
||||
<img src="https://github.com/user-attachments/assets/874da84a-67ff-47fa-a6b4-cbc3c65eb704" width="300" style="border-radius: 16px;" />
|
||||
</a>
|
||||
<a href="https://www.copilotkit.ai/examples/state-machine-copilot">
|
||||
<img src="https://github.com/user-attachments/assets/0b5e45b3-2704-4678-82dc-2f3e1c58e2dd" width="300" style="border-radius: 16px;" />
|
||||
</a>
|
||||
<a href="https://www.copilotkit.ai/examples/chat-with-your-data">
|
||||
<img src="https://github.com/user-attachments/assets/0fed66be-a4c2-4093-8eab-75c0b27a62f6" width="300" style="border-radius: 16px;" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Documentation
|
||||
|
||||
To get started with CopilotKit, please check out the [documentation](https://docs.copilotkit.ai).
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
import path from "node:path";
|
||||
|
||||
const schema = path.resolve(
|
||||
__dirname,
|
||||
"../runtime/__snapshots__/schema/schema.graphql",
|
||||
);
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema,
|
||||
documents: ["src/graphql/definitions/**/*.{ts,tsx}"],
|
||||
generates: {
|
||||
"./src/graphql/@generated/": {
|
||||
preset: "client",
|
||||
config: {
|
||||
useTypeImports: true,
|
||||
withHooks: false,
|
||||
},
|
||||
plugins: [],
|
||||
},
|
||||
},
|
||||
hooks: {},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@copilotkit/runtime-client-gql",
|
||||
"version": "1.62.3",
|
||||
"private": false,
|
||||
"keywords": [
|
||||
"ai",
|
||||
"assistant",
|
||||
"automation",
|
||||
"copilot",
|
||||
"copilotkit",
|
||||
"javascript",
|
||||
"nextjs",
|
||||
"nodejs",
|
||||
"react",
|
||||
"textarea"
|
||||
],
|
||||
"homepage": "https://github.com/CopilotKit/CopilotKit",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CopilotKit/CopilotKit.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.cts",
|
||||
"unpkg": "./dist/index.umd.js",
|
||||
"jsdelivr": "./dist/index.umd.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm run graphql-codegen && tsdown",
|
||||
"compat-check": "es-check es2022 --module 'dist/**/!(*.umd).{mjs,cjs,js}' && es-check es2018 'dist/**/*.umd.js'",
|
||||
"dev": "concurrently \"pnpm run graphql-codegen:watch\" \"tsdown --watch\"",
|
||||
"test": "pnpm run graphql-codegen && vitest run",
|
||||
"test:watch": "pnpm run graphql-codegen && vitest",
|
||||
"check-types": "tsc --noEmit",
|
||||
"graphql-codegen": "graphql-codegen -c codegen.ts",
|
||||
"graphql-codegen:watch": "graphql-codegen -c codegen.ts --watch",
|
||||
"link:global": "pnpm link --global",
|
||||
"unlink:global": "pnpm unlink --global",
|
||||
"publint": "publint .",
|
||||
"attw": "attw --pack . --profile node16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@copilotkit/shared": "workspace:*",
|
||||
"@urql/core": "^5.0.3",
|
||||
"untruncate-json": "^0.0.1",
|
||||
"urql": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/client-preset": "^4.2.6",
|
||||
"@graphql-codegen/introspection": "^4.0.3",
|
||||
"@graphql-codegen/typescript": "^4.0.7",
|
||||
"@graphql-codegen/typescript-operations": "^4.2.1",
|
||||
"@graphql-codegen/typescript-urql": "^4.0.0",
|
||||
"@graphql-codegen/urql-introspection": "^3.0.0",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@types/node": "^20.12.12",
|
||||
"concurrently": "^8.2.2",
|
||||
"esbuild": "^0.23.0",
|
||||
"graphql": "^16.8.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsdown": "^0.20.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@copilotkit/runtime-client-gql",
|
||||
"implicitDependencies": ["@copilotkit/runtime"]
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { Client, cacheExchange, fetchExchange } from "@urql/core";
|
||||
import * as packageJson from "../../package.json";
|
||||
import {
|
||||
AvailableAgentsQuery,
|
||||
GenerateCopilotResponseMutation,
|
||||
GenerateCopilotResponseMutationVariables,
|
||||
LoadAgentStateQuery,
|
||||
} from "../graphql/@generated/graphql";
|
||||
import { generateCopilotResponseMutation } from "../graphql/definitions/mutations";
|
||||
import {
|
||||
getAvailableAgentsQuery,
|
||||
loadAgentStateQuery,
|
||||
} from "../graphql/definitions/queries";
|
||||
import { OperationResultSource, OperationResult } from "urql";
|
||||
import {
|
||||
ResolvedCopilotKitError,
|
||||
CopilotKitLowLevelError,
|
||||
CopilotKitError,
|
||||
CopilotKitVersionMismatchError,
|
||||
getPossibleVersionMismatch,
|
||||
} from "@copilotkit/shared";
|
||||
|
||||
const createFetchFn =
|
||||
(signal?: AbortSignal, handleGQLWarning?: (warning: string) => void) =>
|
||||
async (...args: Parameters<typeof fetch>) => {
|
||||
// @ts-expect-error -- since this is our own header, TS will not recognize
|
||||
const publicApiKey = args[1]?.headers?.["x-copilotcloud-public-api-key"];
|
||||
try {
|
||||
const result = await fetch(args[0], { ...args[1], signal });
|
||||
|
||||
// No mismatch checking if cloud is being used
|
||||
const mismatch = publicApiKey
|
||||
? null
|
||||
: await getPossibleVersionMismatch({
|
||||
runtimeVersion: result.headers.get("X-CopilotKit-Runtime-Version")!,
|
||||
runtimeClientGqlVersion: packageJson.version,
|
||||
});
|
||||
if (result.status !== 200) {
|
||||
if (result.status >= 400 && result.status <= 500) {
|
||||
if (mismatch) {
|
||||
throw new CopilotKitVersionMismatchError(mismatch);
|
||||
}
|
||||
|
||||
throw new ResolvedCopilotKitError({ status: result.status });
|
||||
}
|
||||
}
|
||||
|
||||
if (mismatch && handleGQLWarning) {
|
||||
handleGQLWarning(mismatch.message);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Let abort error pass through. It will be suppressed later
|
||||
if (
|
||||
(error as Error).message.includes("BodyStreamBuffer was aborted") ||
|
||||
(error as Error).message.includes("signal is aborted without reason")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof CopilotKitError) {
|
||||
throw error;
|
||||
}
|
||||
throw new CopilotKitLowLevelError({
|
||||
error: error as Error,
|
||||
url: args[0] as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export interface CopilotRuntimeClientOptions {
|
||||
url: string;
|
||||
publicApiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
credentials?: RequestCredentials;
|
||||
handleGQLErrors?: (error: Error) => void;
|
||||
handleGQLWarning?: (warning: string) => void;
|
||||
}
|
||||
|
||||
export class CopilotRuntimeClient {
|
||||
client: Client;
|
||||
public handleGQLErrors?: (error: Error) => void;
|
||||
public handleGQLWarning?: (warning: string) => void;
|
||||
|
||||
constructor(options: CopilotRuntimeClientOptions) {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
this.handleGQLErrors = options.handleGQLErrors;
|
||||
this.handleGQLWarning = options.handleGQLWarning;
|
||||
|
||||
if (options.headers) {
|
||||
Object.assign(headers, options.headers);
|
||||
}
|
||||
|
||||
if (options.publicApiKey) {
|
||||
headers["x-copilotcloud-public-api-key"] = options.publicApiKey;
|
||||
}
|
||||
|
||||
this.client = new Client({
|
||||
url: options.url,
|
||||
exchanges: [cacheExchange, fetchExchange],
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
...headers,
|
||||
"X-CopilotKit-Runtime-Client-GQL-Version": packageJson.version,
|
||||
},
|
||||
...(options.credentials ? { credentials: options.credentials } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
generateCopilotResponse({
|
||||
data,
|
||||
properties,
|
||||
signal,
|
||||
}: {
|
||||
data: GenerateCopilotResponseMutationVariables["data"];
|
||||
properties?: GenerateCopilotResponseMutationVariables["properties"];
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const fetchFn = createFetchFn(signal, this.handleGQLWarning);
|
||||
const result = this.client.mutation<
|
||||
GenerateCopilotResponseMutation,
|
||||
GenerateCopilotResponseMutationVariables
|
||||
>(
|
||||
generateCopilotResponseMutation,
|
||||
{ data, properties },
|
||||
{ fetch: fetchFn },
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public asStream<S, T>(
|
||||
source: OperationResultSource<OperationResult<S, { data: T }>>,
|
||||
) {
|
||||
const handleGQLErrors = this.handleGQLErrors;
|
||||
return new ReadableStream<S>({
|
||||
start(controller) {
|
||||
source.subscribe(({ data, hasNext, error }) => {
|
||||
if (error) {
|
||||
if (
|
||||
error.message.includes("BodyStreamBuffer was aborted") ||
|
||||
error.message.includes("signal is aborted without reason")
|
||||
) {
|
||||
// close the stream if there is no next item
|
||||
if (!hasNext) controller.close();
|
||||
|
||||
//suppress this specific error
|
||||
console.warn("Abort error suppressed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle structured errors specially - check if it's a CopilotKitError with visibility
|
||||
if ((error as any).extensions?.visibility) {
|
||||
// Create a synthetic GraphQL error with the structured error info
|
||||
const syntheticError = {
|
||||
...error,
|
||||
graphQLErrors: [
|
||||
{
|
||||
message: error.message,
|
||||
extensions: (error as any).extensions,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (handleGQLErrors) {
|
||||
handleGQLErrors(syntheticError);
|
||||
}
|
||||
return; // Don't close the stream for structured errors, let the error handler decide
|
||||
}
|
||||
|
||||
controller.error(error);
|
||||
if (handleGQLErrors) {
|
||||
handleGQLErrors(error);
|
||||
}
|
||||
} else {
|
||||
controller.enqueue(data);
|
||||
if (!hasNext) {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
availableAgents() {
|
||||
const fetchFn = createFetchFn();
|
||||
return this.client.query<AvailableAgentsQuery>(
|
||||
getAvailableAgentsQuery,
|
||||
{},
|
||||
{ fetch: fetchFn },
|
||||
);
|
||||
}
|
||||
|
||||
loadAgentState(data: { threadId: string; agentName: string }) {
|
||||
const fetchFn = createFetchFn();
|
||||
const result = this.client.query<LoadAgentStateQuery>(
|
||||
loadAgentStateQuery,
|
||||
{ data },
|
||||
{ fetch: fetchFn },
|
||||
);
|
||||
|
||||
// Add error handling for GraphQL errors - similar to generateCopilotResponse
|
||||
result
|
||||
.toPromise()
|
||||
.then(({ error }) => {
|
||||
if (error && this.handleGQLErrors) {
|
||||
this.handleGQLErrors(error);
|
||||
}
|
||||
})
|
||||
.catch(() => {}); // Suppress promise rejection warnings
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static removeGraphQLTypename(data: any) {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item) => CopilotRuntimeClient.removeGraphQLTypename(item));
|
||||
} else if (typeof data === "object" && data !== null) {
|
||||
delete data.__typename;
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (typeof data[key] === "object" && data[key] !== null) {
|
||||
CopilotRuntimeClient.removeGraphQLTypename(data[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { convertGqlOutputToMessages } from "../conversion";
|
||||
|
||||
describe("getPartialArguments non-object guard", () => {
|
||||
function makeActionOutput(argsFragments: string[]) {
|
||||
return [
|
||||
{
|
||||
__typename: "ActionExecutionMessageOutput" as const,
|
||||
id: "msg-1",
|
||||
name: "myAction",
|
||||
arguments: argsFragments,
|
||||
parentMessageId: undefined,
|
||||
status: { code: "Pending" },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
it("passes through valid object arguments", () => {
|
||||
const messages = convertGqlOutputToMessages(
|
||||
makeActionOutput(['{"key":"val"}']) as any,
|
||||
);
|
||||
expect((messages[0] as any).arguments).toEqual({ key: "val" });
|
||||
});
|
||||
|
||||
it("replaces a string argument with an empty object", () => {
|
||||
const messages = convertGqlOutputToMessages(
|
||||
makeActionOutput(['""']) as any,
|
||||
);
|
||||
expect((messages[0] as any).arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("replaces an array argument with an empty object", () => {
|
||||
const messages = convertGqlOutputToMessages(
|
||||
makeActionOutput(["[1,2]"]) as any,
|
||||
);
|
||||
expect((messages[0] as any).arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("replaces null with an empty object", () => {
|
||||
const messages = convertGqlOutputToMessages(
|
||||
makeActionOutput(["null"]) as any,
|
||||
);
|
||||
expect((messages[0] as any).arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("replaces a number with an empty object", () => {
|
||||
const messages = convertGqlOutputToMessages(
|
||||
makeActionOutput(["99"]) as any,
|
||||
);
|
||||
expect((messages[0] as any).arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("returns empty object for empty arguments array", () => {
|
||||
const messages = convertGqlOutputToMessages(makeActionOutput([]) as any);
|
||||
expect((messages[0] as any).arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("returns empty object for unparseable JSON", () => {
|
||||
const messages = convertGqlOutputToMessages(
|
||||
makeActionOutput(["{broken"]) as any,
|
||||
);
|
||||
expect((messages[0] as any).arguments).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
GenerateCopilotResponseMutation,
|
||||
MessageInput,
|
||||
MessageStatusCode,
|
||||
} from "../graphql/@generated/graphql";
|
||||
import {
|
||||
ActionExecutionMessage,
|
||||
AgentStateMessage,
|
||||
Message,
|
||||
ResultMessage,
|
||||
TextMessage,
|
||||
ImageMessage,
|
||||
} from "./types";
|
||||
|
||||
import untruncateJson from "untruncate-json";
|
||||
import { parseJson } from "@copilotkit/shared";
|
||||
|
||||
export function filterAgentStateMessages(messages: Message[]): Message[] {
|
||||
return messages.filter((message) => !message.isAgentStateMessage());
|
||||
}
|
||||
|
||||
export function convertMessagesToGqlInput(messages: Message[]): MessageInput[] {
|
||||
return messages.map((message) => {
|
||||
if (message.isTextMessage()) {
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
textMessage: {
|
||||
content: message.content,
|
||||
role: message.role as any,
|
||||
parentMessageId: message.parentMessageId,
|
||||
},
|
||||
};
|
||||
} else if (message.isActionExecutionMessage()) {
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
actionExecutionMessage: {
|
||||
name: message.name,
|
||||
arguments: JSON.stringify(message.arguments),
|
||||
parentMessageId: message.parentMessageId,
|
||||
},
|
||||
};
|
||||
} else if (message.isResultMessage()) {
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
resultMessage: {
|
||||
result: message.result,
|
||||
actionExecutionId: message.actionExecutionId,
|
||||
actionName: message.actionName,
|
||||
},
|
||||
};
|
||||
} else if (message.isAgentStateMessage()) {
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
agentStateMessage: {
|
||||
threadId: message.threadId,
|
||||
role: message.role,
|
||||
agentName: message.agentName,
|
||||
nodeName: message.nodeName,
|
||||
runId: message.runId,
|
||||
active: message.active,
|
||||
running: message.running,
|
||||
state: JSON.stringify(message.state),
|
||||
},
|
||||
};
|
||||
} else if (message.isImageMessage()) {
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
imageMessage: {
|
||||
format: message.format,
|
||||
bytes: message.bytes,
|
||||
role: message.role as any,
|
||||
parentMessageId: message.parentMessageId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error("Unknown message type");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function filterAdjacentAgentStateMessages(
|
||||
messages: GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"],
|
||||
): GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"] {
|
||||
const filteredMessages: GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"] =
|
||||
[];
|
||||
|
||||
messages.forEach((message, i) => {
|
||||
// keep all other message types
|
||||
if (message.__typename !== "AgentStateMessageOutput") {
|
||||
filteredMessages.push(message);
|
||||
} else {
|
||||
const prevAgentStateMessageIndex = filteredMessages.findIndex(
|
||||
// TODO: also check runId
|
||||
(m) =>
|
||||
m.__typename === "AgentStateMessageOutput" &&
|
||||
m.agentName === message.agentName,
|
||||
);
|
||||
if (prevAgentStateMessageIndex === -1) {
|
||||
filteredMessages.push(message);
|
||||
} else {
|
||||
filteredMessages[prevAgentStateMessageIndex] = message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return filteredMessages;
|
||||
}
|
||||
|
||||
export function convertGqlOutputToMessages(
|
||||
messages: GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"],
|
||||
): Message[] {
|
||||
return messages.map((message) => {
|
||||
if (message.__typename === "TextMessageOutput") {
|
||||
return new TextMessage({
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
content: message.content.join(""),
|
||||
parentMessageId: message.parentMessageId,
|
||||
createdAt: new Date(),
|
||||
status: message.status || { code: MessageStatusCode.Pending },
|
||||
});
|
||||
} else if (message.__typename === "ActionExecutionMessageOutput") {
|
||||
return new ActionExecutionMessage({
|
||||
id: message.id,
|
||||
name: message.name,
|
||||
arguments: getPartialArguments(message.arguments),
|
||||
parentMessageId: message.parentMessageId,
|
||||
createdAt: new Date(),
|
||||
status: message.status || { code: MessageStatusCode.Pending },
|
||||
});
|
||||
} else if (message.__typename === "ResultMessageOutput") {
|
||||
return new ResultMessage({
|
||||
id: message.id,
|
||||
result: message.result,
|
||||
actionExecutionId: message.actionExecutionId,
|
||||
actionName: message.actionName,
|
||||
createdAt: new Date(),
|
||||
status: message.status || { code: MessageStatusCode.Pending },
|
||||
});
|
||||
} else if (message.__typename === "AgentStateMessageOutput") {
|
||||
return new AgentStateMessage({
|
||||
id: message.id,
|
||||
threadId: message.threadId,
|
||||
role: message.role,
|
||||
agentName: message.agentName,
|
||||
nodeName: message.nodeName,
|
||||
runId: message.runId,
|
||||
active: message.active,
|
||||
running: message.running,
|
||||
state: parseJson(message.state, {}),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
} else if (message.__typename === "ImageMessageOutput") {
|
||||
return new ImageMessage({
|
||||
id: message.id,
|
||||
format: message.format,
|
||||
bytes: message.bytes,
|
||||
role: message.role,
|
||||
parentMessageId: message.parentMessageId,
|
||||
createdAt: new Date(),
|
||||
status: message.status || { code: MessageStatusCode.Pending },
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("Unknown message type");
|
||||
});
|
||||
}
|
||||
|
||||
export function loadMessagesFromJsonRepresentation(json: any[]): Message[] {
|
||||
const result: Message[] = [];
|
||||
for (const item of json) {
|
||||
if ("content" in item) {
|
||||
result.push(
|
||||
new TextMessage({
|
||||
id: item.id,
|
||||
role: item.role,
|
||||
content: item.content,
|
||||
parentMessageId: item.parentMessageId,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
status: item.status || { code: MessageStatusCode.Success },
|
||||
}),
|
||||
);
|
||||
} else if ("arguments" in item) {
|
||||
result.push(
|
||||
new ActionExecutionMessage({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
parentMessageId: item.parentMessageId,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
status: item.status || { code: MessageStatusCode.Success },
|
||||
}),
|
||||
);
|
||||
} else if ("result" in item) {
|
||||
result.push(
|
||||
new ResultMessage({
|
||||
id: item.id,
|
||||
result: item.result,
|
||||
actionExecutionId: item.actionExecutionId,
|
||||
actionName: item.actionName,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
status: item.status || { code: MessageStatusCode.Success },
|
||||
}),
|
||||
);
|
||||
} else if ("state" in item) {
|
||||
result.push(
|
||||
new AgentStateMessage({
|
||||
id: item.id,
|
||||
threadId: item.threadId,
|
||||
role: item.role,
|
||||
agentName: item.agentName,
|
||||
nodeName: item.nodeName,
|
||||
runId: item.runId,
|
||||
active: item.active,
|
||||
running: item.running,
|
||||
state: item.state,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
}),
|
||||
);
|
||||
} else if ("format" in item && "bytes" in item) {
|
||||
result.push(
|
||||
new ImageMessage({
|
||||
id: item.id,
|
||||
format: item.format,
|
||||
bytes: item.bytes,
|
||||
role: item.role,
|
||||
parentMessageId: item.parentMessageId,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
status: item.status || { code: MessageStatusCode.Success },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getPartialArguments(args: string[]) {
|
||||
try {
|
||||
if (!args.length) return {};
|
||||
|
||||
const parsed = JSON.parse(untruncateJson(args.join("")));
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
Array.isArray(parsed)
|
||||
) {
|
||||
console.warn(
|
||||
`[CopilotKit] Tool arguments parsed to non-object (${typeof parsed}), falling back to empty object`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
// Incomplete JSON is expected during streaming — no warning needed
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from "./CopilotRuntimeClient";
|
||||
export {
|
||||
convertMessagesToGqlInput,
|
||||
convertGqlOutputToMessages,
|
||||
filterAdjacentAgentStateMessages,
|
||||
filterAgentStateMessages,
|
||||
loadMessagesFromJsonRepresentation,
|
||||
} from "./conversion";
|
||||
export * from "./types";
|
||||
export type { GraphQLError } from "graphql";
|
||||
@@ -0,0 +1,191 @@
|
||||
import { randomId } from "@copilotkit/shared";
|
||||
import {
|
||||
ActionExecutionMessageInput,
|
||||
MessageRole,
|
||||
MessageStatus,
|
||||
ResultMessageInput,
|
||||
TextMessageInput,
|
||||
BaseMessageOutput,
|
||||
AgentStateMessageInput,
|
||||
MessageStatusCode,
|
||||
LangGraphInterruptEvent as GqlLangGraphInterruptEvent,
|
||||
MetaEventName,
|
||||
CopilotKitLangGraphInterruptEvent as GqlCopilotKitLangGraphInterruptEvent,
|
||||
ImageMessageInput,
|
||||
} from "../graphql/@generated/graphql";
|
||||
import { parseJson } from "@copilotkit/shared";
|
||||
|
||||
type MessageType =
|
||||
| "TextMessage"
|
||||
| "ActionExecutionMessage"
|
||||
| "ResultMessage"
|
||||
| "AgentStateMessage"
|
||||
| "ImageMessage";
|
||||
|
||||
export class Message {
|
||||
type: MessageType;
|
||||
id: BaseMessageOutput["id"];
|
||||
createdAt: BaseMessageOutput["createdAt"];
|
||||
status: MessageStatus;
|
||||
|
||||
constructor(props: any) {
|
||||
props.id ??= randomId();
|
||||
props.status ??= { code: MessageStatusCode.Success };
|
||||
props.createdAt ??= new Date();
|
||||
Object.assign(this, props);
|
||||
}
|
||||
|
||||
isTextMessage(): this is TextMessage {
|
||||
return this.type === "TextMessage";
|
||||
}
|
||||
|
||||
isActionExecutionMessage(): this is ActionExecutionMessage {
|
||||
return this.type === "ActionExecutionMessage";
|
||||
}
|
||||
|
||||
isResultMessage(): this is ResultMessage {
|
||||
return this.type === "ResultMessage";
|
||||
}
|
||||
|
||||
isAgentStateMessage(): this is AgentStateMessage {
|
||||
return this.type === "AgentStateMessage";
|
||||
}
|
||||
|
||||
isImageMessage(): this is ImageMessage {
|
||||
return this.type === "ImageMessage";
|
||||
}
|
||||
}
|
||||
|
||||
// alias Role to MessageRole
|
||||
export const Role = MessageRole;
|
||||
|
||||
// when constructing any message, the base fields are optional
|
||||
type MessageConstructorOptions = Partial<Message>;
|
||||
|
||||
type TextMessageConstructorOptions = MessageConstructorOptions &
|
||||
TextMessageInput;
|
||||
|
||||
export class TextMessage
|
||||
extends Message
|
||||
implements TextMessageConstructorOptions
|
||||
{
|
||||
content: TextMessageInput["content"];
|
||||
parentMessageId: TextMessageInput["parentMessageId"];
|
||||
role: TextMessageInput["role"];
|
||||
type = "TextMessage" as const;
|
||||
|
||||
constructor(props: TextMessageConstructorOptions) {
|
||||
super(props);
|
||||
this.type = "TextMessage";
|
||||
}
|
||||
}
|
||||
|
||||
type ActionExecutionMessageConstructorOptions = MessageConstructorOptions &
|
||||
Omit<ActionExecutionMessageInput, "arguments"> & {
|
||||
arguments: Record<string, any>;
|
||||
};
|
||||
|
||||
export class ActionExecutionMessage
|
||||
extends Message
|
||||
implements Omit<ActionExecutionMessageInput, "arguments" | "scope">
|
||||
{
|
||||
name: ActionExecutionMessageInput["name"];
|
||||
arguments: Record<string, any>;
|
||||
parentMessageId: ActionExecutionMessageInput["parentMessageId"];
|
||||
constructor(props: ActionExecutionMessageConstructorOptions) {
|
||||
super(props);
|
||||
this.type = "ActionExecutionMessage";
|
||||
}
|
||||
}
|
||||
|
||||
type ResultMessageConstructorOptions = MessageConstructorOptions &
|
||||
ResultMessageInput;
|
||||
|
||||
export class ResultMessage
|
||||
extends Message
|
||||
implements ResultMessageConstructorOptions
|
||||
{
|
||||
actionExecutionId: ResultMessageInput["actionExecutionId"];
|
||||
actionName: ResultMessageInput["actionName"];
|
||||
result: ResultMessageInput["result"];
|
||||
|
||||
constructor(props: ResultMessageConstructorOptions) {
|
||||
super(props);
|
||||
this.type = "ResultMessage";
|
||||
}
|
||||
|
||||
static decodeResult(result: string): any {
|
||||
return parseJson(result, result);
|
||||
}
|
||||
|
||||
static encodeResult(result: any): string {
|
||||
if (result === undefined) {
|
||||
return "";
|
||||
} else if (typeof result === "string") {
|
||||
return result;
|
||||
} else {
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentStateMessage
|
||||
extends Message
|
||||
implements Omit<AgentStateMessageInput, "state">
|
||||
{
|
||||
agentName: AgentStateMessageInput["agentName"];
|
||||
state: any;
|
||||
running: AgentStateMessageInput["running"];
|
||||
threadId: AgentStateMessageInput["threadId"];
|
||||
role: AgentStateMessageInput["role"];
|
||||
nodeName: AgentStateMessageInput["nodeName"];
|
||||
runId: AgentStateMessageInput["runId"];
|
||||
active: AgentStateMessageInput["active"];
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.type = "AgentStateMessage";
|
||||
}
|
||||
}
|
||||
|
||||
type ImageMessageConstructorOptions = MessageConstructorOptions &
|
||||
ImageMessageInput;
|
||||
|
||||
export class ImageMessage
|
||||
extends Message
|
||||
implements ImageMessageConstructorOptions
|
||||
{
|
||||
format: ImageMessageInput["format"];
|
||||
bytes: ImageMessageInput["bytes"];
|
||||
role: ImageMessageInput["role"];
|
||||
parentMessageId: ImageMessageInput["parentMessageId"];
|
||||
|
||||
constructor(props: ImageMessageConstructorOptions) {
|
||||
super(props);
|
||||
this.type = "ImageMessage";
|
||||
}
|
||||
}
|
||||
|
||||
export function langGraphInterruptEvent(
|
||||
eventProps: Omit<LangGraphInterruptEvent, "name" | "type" | "__typename">,
|
||||
): LangGraphInterruptEvent {
|
||||
return {
|
||||
...eventProps,
|
||||
name: MetaEventName.LangGraphInterruptEvent,
|
||||
type: "MetaEvent",
|
||||
};
|
||||
}
|
||||
|
||||
export type LangGraphInterruptEvent<TValue extends any = any> =
|
||||
GqlLangGraphInterruptEvent & {
|
||||
value: TValue;
|
||||
};
|
||||
|
||||
type CopilotKitLangGraphInterruptEvent<TValue extends any = any> =
|
||||
GqlCopilotKitLangGraphInterruptEvent & {
|
||||
data: GqlCopilotKitLangGraphInterruptEvent["data"] & { value: TValue };
|
||||
};
|
||||
|
||||
export type MetaEvent =
|
||||
| LangGraphInterruptEvent
|
||||
| CopilotKitLangGraphInterruptEvent;
|
||||
@@ -0,0 +1,133 @@
|
||||
import { graphql } from "../@generated/gql";
|
||||
|
||||
export const generateCopilotResponseMutation = graphql(/** GraphQL **/ `
|
||||
mutation generateCopilotResponse(
|
||||
$data: GenerateCopilotResponseInput!
|
||||
$properties: JSONObject
|
||||
) {
|
||||
generateCopilotResponse(data: $data, properties: $properties) {
|
||||
threadId
|
||||
runId
|
||||
extensions {
|
||||
openaiAssistantAPI {
|
||||
runId
|
||||
threadId
|
||||
}
|
||||
}
|
||||
... on CopilotResponse @defer {
|
||||
status {
|
||||
... on BaseResponseStatus {
|
||||
code
|
||||
}
|
||||
... on FailedResponseStatus {
|
||||
reason
|
||||
details
|
||||
}
|
||||
}
|
||||
}
|
||||
messages @stream {
|
||||
__typename
|
||||
... on BaseMessageOutput {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
... on BaseMessageOutput @defer {
|
||||
status {
|
||||
... on SuccessMessageStatus {
|
||||
code
|
||||
}
|
||||
... on FailedMessageStatus {
|
||||
code
|
||||
reason
|
||||
}
|
||||
... on PendingMessageStatus {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
... on TextMessageOutput {
|
||||
content @stream
|
||||
role
|
||||
parentMessageId
|
||||
}
|
||||
... on ImageMessageOutput {
|
||||
format
|
||||
bytes
|
||||
role
|
||||
parentMessageId
|
||||
}
|
||||
... on ActionExecutionMessageOutput {
|
||||
name
|
||||
arguments @stream
|
||||
parentMessageId
|
||||
}
|
||||
... on ResultMessageOutput {
|
||||
result
|
||||
actionExecutionId
|
||||
actionName
|
||||
}
|
||||
... on AgentStateMessageOutput {
|
||||
threadId
|
||||
state
|
||||
running
|
||||
agentName
|
||||
nodeName
|
||||
runId
|
||||
active
|
||||
role
|
||||
}
|
||||
}
|
||||
metaEvents @stream {
|
||||
... on LangGraphInterruptEvent {
|
||||
type
|
||||
name
|
||||
value
|
||||
}
|
||||
|
||||
... on CopilotKitLangGraphInterruptEvent {
|
||||
type
|
||||
name
|
||||
data {
|
||||
messages {
|
||||
__typename
|
||||
... on BaseMessageOutput {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
... on BaseMessageOutput @defer {
|
||||
status {
|
||||
... on SuccessMessageStatus {
|
||||
code
|
||||
}
|
||||
... on FailedMessageStatus {
|
||||
code
|
||||
reason
|
||||
}
|
||||
... on PendingMessageStatus {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
... on TextMessageOutput {
|
||||
content
|
||||
role
|
||||
parentMessageId
|
||||
}
|
||||
... on ActionExecutionMessageOutput {
|
||||
name
|
||||
arguments
|
||||
parentMessageId
|
||||
}
|
||||
... on ResultMessageOutput {
|
||||
result
|
||||
actionExecutionId
|
||||
actionName
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { graphql } from "../@generated/gql";
|
||||
|
||||
export const getAvailableAgentsQuery = graphql(/** GraphQL **/ `
|
||||
query availableAgents {
|
||||
availableAgents {
|
||||
agents {
|
||||
name
|
||||
id
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const loadAgentStateQuery = graphql(/** GraphQL **/ `
|
||||
query loadAgentState($data: LoadAgentStateInput!) {
|
||||
loadAgentState(data: $data) {
|
||||
threadId
|
||||
threadExists
|
||||
state
|
||||
messages
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./client";
|
||||
export * from "./graphql/@generated/graphql";
|
||||
export * from "./message-conversion";
|
||||
export type { LangGraphInterruptEvent } from "./client";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,355 @@
|
||||
import * as gql from "../client";
|
||||
import type { MessageRole } from "../graphql/@generated/graphql";
|
||||
import type * as agui from "@copilotkit/shared"; // named agui for clarity, but this only includes agui message types
|
||||
|
||||
// Helper function to extract agent name from message
|
||||
function extractAgentName(message: agui.Message): string {
|
||||
if (message.role !== "assistant") {
|
||||
throw new Error(
|
||||
`Cannot extract agent name from message with role ${message.role}`,
|
||||
);
|
||||
}
|
||||
|
||||
return message.agentName || "unknown";
|
||||
}
|
||||
|
||||
// Type guard for agent state message
|
||||
function isAgentStateMessage(message: agui.Message): boolean {
|
||||
return (
|
||||
message.role === "assistant" && "agentName" in message && "state" in message
|
||||
);
|
||||
}
|
||||
|
||||
// Type guard for messages with image property
|
||||
function hasImageProperty(
|
||||
message: agui.Message,
|
||||
): message is agui.Message & { image: agui.ImageData } {
|
||||
const canContainImage =
|
||||
message.role === "assistant" || message.role === "user";
|
||||
if (!canContainImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const image: { format?: string; bytes?: string } | undefined =
|
||||
"image" in message ? message.image : undefined;
|
||||
if (image === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isMalformed = image.format === undefined || image.bytes === undefined;
|
||||
if (isMalformed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------
|
||||
AGUI Message -> GQL Message
|
||||
----------------------------
|
||||
*/
|
||||
export function aguiToGQL(
|
||||
messages: agui.Message[] | agui.Message,
|
||||
actions?: Record<string, any>,
|
||||
coAgentStateRenders?: Record<string, any>,
|
||||
): gql.Message[] {
|
||||
const gqlMessages: gql.Message[] = [];
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
|
||||
// Track tool call names by their IDs for use in result messages
|
||||
const toolCallNames: Record<string, string> = {};
|
||||
|
||||
for (const message of messages) {
|
||||
// Agent state message support
|
||||
if (isAgentStateMessage(message)) {
|
||||
const agentName = extractAgentName(message);
|
||||
const state = "state" in message && message.state ? message.state : {};
|
||||
gqlMessages.push(
|
||||
new gql.AgentStateMessage({
|
||||
id: message.id,
|
||||
agentName,
|
||||
state,
|
||||
role: gql.Role.Assistant,
|
||||
}),
|
||||
);
|
||||
// Optionally preserve render function
|
||||
if (
|
||||
"generativeUI" in message &&
|
||||
message.generativeUI &&
|
||||
coAgentStateRenders
|
||||
) {
|
||||
coAgentStateRenders[agentName] = {
|
||||
name: agentName,
|
||||
render: message.generativeUI,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasImageProperty(message)) {
|
||||
gqlMessages.push(aguiMessageWithImageToGQLMessage(message));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Action execution message support
|
||||
if (message.role === "assistant" && message.toolCalls) {
|
||||
gqlMessages.push(aguiTextMessageToGQLMessage(message));
|
||||
for (const toolCall of message.toolCalls) {
|
||||
// Track the tool call name by its ID
|
||||
toolCallNames[toolCall.id] = toolCall.function.name;
|
||||
|
||||
const actionExecMsg = aguiToolCallToGQLActionExecution(
|
||||
toolCall,
|
||||
message.id,
|
||||
);
|
||||
// Preserve render function in actions context
|
||||
if ("generativeUI" in message && message.generativeUI && actions) {
|
||||
const actionName = toolCall.function.name;
|
||||
// Check for specific action first, then wild card action
|
||||
const specificAction = Object.values(actions).find(
|
||||
(action: any) => action.name === actionName,
|
||||
);
|
||||
const wildcardAction = Object.values(actions).find(
|
||||
(action: any) => action.name === "*",
|
||||
);
|
||||
|
||||
// Assign render function to the matching action (specific takes priority)
|
||||
if (specificAction) {
|
||||
specificAction.render = message.generativeUI;
|
||||
} else if (wildcardAction) {
|
||||
wildcardAction.render = message.generativeUI;
|
||||
}
|
||||
}
|
||||
gqlMessages.push(actionExecMsg);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Reasoning messages are ephemeral display-only content with no GQL equivalent — skip them
|
||||
if (message.role === "reasoning") {
|
||||
continue;
|
||||
}
|
||||
// Regular text messages
|
||||
if (
|
||||
message.role === "developer" ||
|
||||
message.role === "system" ||
|
||||
message.role === "assistant" ||
|
||||
message.role === "user"
|
||||
) {
|
||||
gqlMessages.push(aguiTextMessageToGQLMessage(message));
|
||||
continue;
|
||||
}
|
||||
// Tool result message
|
||||
if (message.role === "tool") {
|
||||
gqlMessages.push(
|
||||
aguiToolMessageToGQLResultMessage(message, toolCallNames),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw new Error(
|
||||
`Unknown message role: "${(message as any).role}" in message with id: ${(message as any).id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return gqlMessages;
|
||||
}
|
||||
|
||||
export function aguiTextMessageToGQLMessage(
|
||||
message: agui.Message,
|
||||
): gql.TextMessage {
|
||||
if (
|
||||
message.role !== "developer" &&
|
||||
message.role !== "system" &&
|
||||
message.role !== "assistant" &&
|
||||
message.role !== "user"
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot convert message with role ${message.role} to TextMessage`,
|
||||
);
|
||||
}
|
||||
|
||||
let roleValue: MessageRole;
|
||||
|
||||
if (message.role === "developer") {
|
||||
roleValue = gql.Role.Developer;
|
||||
} else if (message.role === "system") {
|
||||
roleValue = gql.Role.System;
|
||||
} else if (message.role === "assistant") {
|
||||
roleValue = gql.Role.Assistant;
|
||||
} else {
|
||||
roleValue = gql.Role.User;
|
||||
}
|
||||
|
||||
return new gql.TextMessage({
|
||||
id: message.id,
|
||||
content: (message.content as any) || "",
|
||||
role: roleValue,
|
||||
});
|
||||
}
|
||||
|
||||
export function aguiToolCallToGQLActionExecution(
|
||||
toolCall: agui.ToolCall,
|
||||
parentMessageId: string,
|
||||
): gql.ActionExecutionMessage {
|
||||
if (toolCall.type !== "function") {
|
||||
throw new Error(`Unsupported tool call type: ${toolCall.type}`);
|
||||
}
|
||||
|
||||
// Handle arguments - they should be a JSON string in AGUI format,
|
||||
// but we need to convert them to an object for GQL format
|
||||
let argumentsObj: any;
|
||||
|
||||
if (typeof toolCall.function.arguments === "string") {
|
||||
// Expected case: arguments is a JSON string
|
||||
try {
|
||||
argumentsObj = JSON.parse(toolCall.function.arguments);
|
||||
} catch {
|
||||
console.warn(
|
||||
`[CopilotKit] Failed to parse tool arguments, falling back to empty object`,
|
||||
);
|
||||
// Provide fallback empty object to prevent application crash
|
||||
argumentsObj = {};
|
||||
}
|
||||
} else if (
|
||||
typeof toolCall.function.arguments === "object" &&
|
||||
toolCall.function.arguments !== null
|
||||
) {
|
||||
// Backward compatibility: arguments is already an object
|
||||
argumentsObj = toolCall.function.arguments;
|
||||
} else {
|
||||
// Fallback for undefined, null, or other types
|
||||
console.warn(
|
||||
`[CopilotKit] Tool arguments parsed to non-object (${typeof toolCall.function.arguments}), falling back to empty object`,
|
||||
);
|
||||
argumentsObj = {};
|
||||
}
|
||||
|
||||
// Guard against successfully parsed non-object values (e.g. JSON.parse('""') → "")
|
||||
if (
|
||||
typeof argumentsObj !== "object" ||
|
||||
argumentsObj === null ||
|
||||
Array.isArray(argumentsObj)
|
||||
) {
|
||||
console.warn(
|
||||
`[CopilotKit] Tool arguments parsed to non-object (${typeof argumentsObj}), falling back to empty object`,
|
||||
);
|
||||
argumentsObj = {};
|
||||
}
|
||||
|
||||
// Always include name and arguments
|
||||
return new gql.ActionExecutionMessage({
|
||||
id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
arguments: argumentsObj,
|
||||
parentMessageId: parentMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
export function aguiToolMessageToGQLResultMessage(
|
||||
message: agui.Message,
|
||||
toolCallNames: Record<string, string>,
|
||||
): gql.ResultMessage {
|
||||
if (message.role !== "tool") {
|
||||
throw new Error(
|
||||
`Cannot convert message with role ${message.role} to ResultMessage`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!message.toolCallId) {
|
||||
throw new Error("Tool message must have a toolCallId");
|
||||
}
|
||||
|
||||
const actionName = toolCallNames[message.toolCallId] || "unknown";
|
||||
|
||||
// Handle result content - it could be a string or an object that needs serialization
|
||||
let resultContent: string;
|
||||
const messageContent = message.content || "";
|
||||
|
||||
if (typeof messageContent === "string") {
|
||||
// Expected case: content is already a string
|
||||
resultContent = messageContent;
|
||||
} else if (typeof messageContent === "object" && messageContent !== null) {
|
||||
// Handle case where content is an object that needs to be serialized
|
||||
try {
|
||||
resultContent = JSON.stringify(messageContent);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to stringify tool result for ${actionName}:`, error);
|
||||
resultContent = String(messageContent);
|
||||
}
|
||||
} else {
|
||||
// Handle other types (number, boolean, etc.)
|
||||
resultContent = String(messageContent);
|
||||
}
|
||||
|
||||
return new gql.ResultMessage({
|
||||
id: message.id,
|
||||
result: resultContent,
|
||||
actionExecutionId: message.toolCallId,
|
||||
actionName: message.toolName || actionName,
|
||||
});
|
||||
}
|
||||
|
||||
// New function to handle AGUI messages with render functions
|
||||
export function aguiMessageWithRenderToGQL(
|
||||
message: agui.Message,
|
||||
actions?: Record<string, any>,
|
||||
coAgentStateRenders?: Record<string, any>,
|
||||
): gql.Message[] {
|
||||
// Handle the special case: assistant messages with render function but no tool calls
|
||||
if (
|
||||
message.role === "assistant" &&
|
||||
"generativeUI" in message &&
|
||||
message.generativeUI &&
|
||||
!message.toolCalls
|
||||
) {
|
||||
const gqlMessages: gql.Message[] = [];
|
||||
gqlMessages.push(
|
||||
new gql.AgentStateMessage({
|
||||
id: message.id,
|
||||
agentName: "unknown",
|
||||
state: {},
|
||||
role: gql.Role.Assistant,
|
||||
}),
|
||||
);
|
||||
if (coAgentStateRenders) {
|
||||
coAgentStateRenders.unknown = {
|
||||
name: "unknown",
|
||||
render: message.generativeUI,
|
||||
};
|
||||
}
|
||||
return gqlMessages;
|
||||
}
|
||||
|
||||
// For all other cases, delegate to aguiToGQL
|
||||
return aguiToGQL([message], actions, coAgentStateRenders);
|
||||
}
|
||||
|
||||
export function aguiMessageWithImageToGQLMessage(
|
||||
message: agui.Message,
|
||||
): gql.ImageMessage {
|
||||
if (!hasImageProperty(message)) {
|
||||
throw new Error(
|
||||
`Cannot convert message to ImageMessage: missing format or bytes`,
|
||||
);
|
||||
}
|
||||
|
||||
let roleValue: MessageRole;
|
||||
if (message.role === "assistant") {
|
||||
roleValue = gql.Role.Assistant;
|
||||
} else {
|
||||
roleValue = gql.Role.User;
|
||||
}
|
||||
|
||||
if (message.role !== "assistant" && message.role !== "user") {
|
||||
throw new Error(
|
||||
`Cannot convert message with role ${message.role} to ImageMessage`,
|
||||
);
|
||||
}
|
||||
|
||||
return new gql.ImageMessage({
|
||||
id: message.id,
|
||||
format: message.image.format,
|
||||
bytes: message.image.bytes,
|
||||
role: roleValue,
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,297 @@
|
||||
import * as gql from "../client";
|
||||
import * as agui from "@copilotkit/shared";
|
||||
import { MessageStatusCode } from "../graphql/@generated/graphql";
|
||||
|
||||
// Define valid image formats based on the supported formats in the codebase
|
||||
const VALID_IMAGE_FORMATS = ["jpeg", "png", "webp", "gif"] as const;
|
||||
type ValidImageFormat = (typeof VALID_IMAGE_FORMATS)[number];
|
||||
|
||||
// Validation function for image format
|
||||
function validateImageFormat(format: string): format is ValidImageFormat {
|
||||
return VALID_IMAGE_FORMATS.includes(format as ValidImageFormat);
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------
|
||||
GQL Message -> AGUI Message
|
||||
----------------------------
|
||||
*/
|
||||
export function gqlToAGUI(
|
||||
messages: gql.Message[] | gql.Message,
|
||||
actions?: Record<string, any>,
|
||||
coAgentStateRenders?: Record<string, any>,
|
||||
): agui.Message[] {
|
||||
let aguiMessages: agui.Message[] = [];
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
|
||||
// Create a map of action execution ID to result for completed actions
|
||||
const actionResults = new Map<string, string>();
|
||||
for (const message of messages) {
|
||||
if (message.isResultMessage()) {
|
||||
actionResults.set(message.actionExecutionId, message.result);
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.isTextMessage()) {
|
||||
aguiMessages.push(gqlTextMessageToAGUIMessage(message));
|
||||
} else if (message.isResultMessage()) {
|
||||
aguiMessages.push(gqlResultMessageToAGUIMessage(message));
|
||||
} else if (message.isActionExecutionMessage()) {
|
||||
aguiMessages.push(
|
||||
gqlActionExecutionMessageToAGUIMessage(message, actions, actionResults),
|
||||
);
|
||||
} else if (message.isAgentStateMessage()) {
|
||||
aguiMessages.push(
|
||||
gqlAgentStateMessageToAGUIMessage(message, coAgentStateRenders),
|
||||
);
|
||||
} else if (message.isImageMessage()) {
|
||||
aguiMessages.push(gqlImageMessageToAGUIMessage(message));
|
||||
} else {
|
||||
throw new Error("Unknown message type");
|
||||
}
|
||||
}
|
||||
|
||||
return aguiMessages;
|
||||
}
|
||||
|
||||
export function gqlActionExecutionMessageToAGUIMessage(
|
||||
message: gql.ActionExecutionMessage,
|
||||
actions?: Record<string, any>,
|
||||
actionResults?: Map<string, string>,
|
||||
): agui.Message {
|
||||
// Check if we have actions and if there's a specific action or wild card action
|
||||
const hasSpecificAction =
|
||||
actions &&
|
||||
Object.values(actions).some((action: any) => action.name === message.name);
|
||||
const hasWildcardAction =
|
||||
actions &&
|
||||
Object.values(actions).some((action: any) => action.name === "*");
|
||||
|
||||
if (!actions || (!hasSpecificAction && !hasWildcardAction)) {
|
||||
return {
|
||||
id: message.id,
|
||||
role: "assistant",
|
||||
toolCalls: [actionExecutionMessageToAGUIMessage(message)],
|
||||
name: message.name,
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific action first, then fall back to wild card action
|
||||
const action =
|
||||
Object.values(actions).find(
|
||||
(action: any) => action.name === message.name,
|
||||
) || Object.values(actions).find((action: any) => action.name === "*");
|
||||
|
||||
// Create render function wrapper that provides proper props
|
||||
const createRenderWrapper = (originalRender: any) => {
|
||||
if (!originalRender) return undefined;
|
||||
|
||||
return (props?: any) => {
|
||||
// Determine the correct status based on the same logic as RenderActionExecutionMessage
|
||||
let actionResult: any = actionResults?.get(message.id);
|
||||
let status: "inProgress" | "executing" | "complete" = "inProgress";
|
||||
|
||||
if (actionResult !== undefined) {
|
||||
status = "complete";
|
||||
} else if (message.status?.code !== MessageStatusCode.Pending) {
|
||||
status = "executing";
|
||||
}
|
||||
|
||||
// if props.result is a string, parse it as JSON but don't throw an error if it's not valid JSON
|
||||
if (typeof props?.result === "string") {
|
||||
try {
|
||||
props.result = JSON.parse(props.result);
|
||||
} catch (e) {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
|
||||
// if actionResult is a string, parse it as JSON but don't throw an error if it's not valid JSON
|
||||
if (typeof actionResult === "string") {
|
||||
try {
|
||||
actionResult = JSON.parse(actionResult);
|
||||
} catch (e) {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
|
||||
// Base props that all actions receive
|
||||
const baseProps = {
|
||||
status: props?.status || status,
|
||||
args: message.arguments || {},
|
||||
result: props?.result || actionResult || undefined,
|
||||
messageId: message.id,
|
||||
};
|
||||
|
||||
// Add properties based on action type
|
||||
if (action.name === "*") {
|
||||
// Wildcard actions get the tool name; ensure it cannot be overridden by incoming props
|
||||
return originalRender({
|
||||
...baseProps,
|
||||
...props,
|
||||
name: message.name,
|
||||
});
|
||||
} else {
|
||||
// Regular actions get respond (defaulting to a no-op if not provided)
|
||||
const respond = props?.respond ?? (() => {});
|
||||
return originalRender({
|
||||
...baseProps,
|
||||
...props,
|
||||
respond,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
toolCalls: [actionExecutionMessageToAGUIMessage(message)],
|
||||
generativeUI: createRenderWrapper(action.render),
|
||||
name: message.name,
|
||||
} as agui.AIMessage;
|
||||
}
|
||||
|
||||
function gqlAgentStateMessageToAGUIMessage(
|
||||
message: gql.AgentStateMessage,
|
||||
coAgentStateRenders?: Record<string, any>,
|
||||
): agui.Message {
|
||||
if (
|
||||
coAgentStateRenders &&
|
||||
Object.values(coAgentStateRenders).some(
|
||||
(render: any) => render.name === message.agentName,
|
||||
)
|
||||
) {
|
||||
const render = Object.values(coAgentStateRenders).find(
|
||||
(render: any) => render.name === message.agentName,
|
||||
);
|
||||
|
||||
// Create render function wrapper that provides proper props
|
||||
const createRenderWrapper = (originalRender: any) => {
|
||||
if (!originalRender) return undefined;
|
||||
|
||||
return (props?: any) => {
|
||||
// Determine the correct status based on the same logic as RenderActionExecutionMessage
|
||||
const state = message.state;
|
||||
|
||||
// Provide the full props structure that the render function expects
|
||||
const renderProps = {
|
||||
state: state,
|
||||
};
|
||||
|
||||
return originalRender(renderProps);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
role: "assistant",
|
||||
generativeUI: createRenderWrapper(render.render),
|
||||
agentName: message.agentName,
|
||||
state: message.state,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
role: "assistant",
|
||||
agentName: message.agentName,
|
||||
state: message.state,
|
||||
};
|
||||
}
|
||||
|
||||
function actionExecutionMessageToAGUIMessage(
|
||||
actionExecutionMessage: gql.ActionExecutionMessage,
|
||||
): agui.ToolCall {
|
||||
return {
|
||||
id: actionExecutionMessage.id,
|
||||
function: {
|
||||
name: actionExecutionMessage.name,
|
||||
arguments: JSON.stringify(actionExecutionMessage.arguments),
|
||||
},
|
||||
type: "function",
|
||||
};
|
||||
}
|
||||
|
||||
export function gqlTextMessageToAGUIMessage(
|
||||
message: gql.TextMessage,
|
||||
): agui.Message {
|
||||
switch (message.role) {
|
||||
case gql.Role.Developer:
|
||||
return {
|
||||
id: message.id,
|
||||
role: "developer",
|
||||
content: message.content,
|
||||
};
|
||||
case gql.Role.System:
|
||||
return {
|
||||
id: message.id,
|
||||
role: "system",
|
||||
content: message.content,
|
||||
};
|
||||
case gql.Role.Assistant:
|
||||
return {
|
||||
id: message.id,
|
||||
role: "assistant",
|
||||
content: message.content,
|
||||
};
|
||||
case gql.Role.User:
|
||||
return {
|
||||
id: message.id,
|
||||
role: "user",
|
||||
content: message.content,
|
||||
};
|
||||
default:
|
||||
throw new Error("Unknown message role");
|
||||
}
|
||||
}
|
||||
|
||||
export function gqlResultMessageToAGUIMessage(
|
||||
message: gql.ResultMessage,
|
||||
): agui.Message {
|
||||
return {
|
||||
id: message.id,
|
||||
role: "tool",
|
||||
content: message.result,
|
||||
toolCallId: message.actionExecutionId,
|
||||
toolName: message.actionName,
|
||||
};
|
||||
}
|
||||
|
||||
export function gqlImageMessageToAGUIMessage(
|
||||
message: gql.ImageMessage,
|
||||
): agui.Message {
|
||||
// Validate image format
|
||||
if (!validateImageFormat(message.format)) {
|
||||
throw new Error(
|
||||
`Invalid image format: ${message.format}. Supported formats are: ${VALID_IMAGE_FORMATS.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that bytes is a non-empty string
|
||||
if (
|
||||
!message.bytes ||
|
||||
typeof message.bytes !== "string" ||
|
||||
message.bytes.trim() === ""
|
||||
) {
|
||||
throw new Error("Image bytes must be a non-empty string");
|
||||
}
|
||||
|
||||
// Determine the role based on the message role
|
||||
const role = message.role === gql.Role.Assistant ? "assistant" : "user";
|
||||
|
||||
// Create the image message with proper typing
|
||||
const imageMessage: agui.Message = {
|
||||
id: message.id,
|
||||
role,
|
||||
content: "",
|
||||
image: {
|
||||
format: message.format,
|
||||
bytes: message.bytes,
|
||||
},
|
||||
};
|
||||
|
||||
return imageMessage;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./agui-to-gql";
|
||||
export * from "./gql-to-agui";
|
||||
@@ -0,0 +1,561 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import * as gql from "../client";
|
||||
import * as agui from "@copilotkit/shared";
|
||||
import { aguiToGQL } from "./agui-to-gql";
|
||||
import { gqlToAGUI } from "./gql-to-agui";
|
||||
|
||||
// Helper to strip functions for deep equality
|
||||
function stripFunctions(obj: any): any {
|
||||
if (typeof obj === "function") return undefined;
|
||||
if (Array.isArray(obj)) return obj.map(stripFunctions);
|
||||
if (obj && typeof obj === "object") {
|
||||
const out: any = {};
|
||||
for (const k in obj) {
|
||||
if (typeof obj[k] !== "function") {
|
||||
out[k] = stripFunctions(obj[k]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
describe("roundtrip message conversion", () => {
|
||||
test("text message AGUI -> GQL -> AGUI", () => {
|
||||
const aguiMsg: agui.Message = {
|
||||
id: "user-1",
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
};
|
||||
const gqlMsgs = aguiToGQL(aguiMsg);
|
||||
const aguiMsgs2 = gqlToAGUI(gqlMsgs);
|
||||
expect(stripFunctions(aguiMsgs2[0])).toEqual(stripFunctions(aguiMsg));
|
||||
});
|
||||
|
||||
test("text message GQL -> AGUI -> GQL", () => {
|
||||
const gqlMsg = new gql.TextMessage({
|
||||
id: "assistant-1",
|
||||
content: "Hi!",
|
||||
role: gql.Role.Assistant,
|
||||
});
|
||||
const aguiMsgs = gqlToAGUI(gqlMsg);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs);
|
||||
// Should be equivalent in content, id, and role
|
||||
expect(gqlMsgs2[0].id).toBe(gqlMsg.id);
|
||||
expect((gqlMsgs2[0] as any).content).toBe(gqlMsg.content);
|
||||
expect((gqlMsgs2[0] as any).role).toBe(gqlMsg.role);
|
||||
});
|
||||
|
||||
test("tool message AGUI -> GQL -> AGUI", () => {
|
||||
const aguiMsg: agui.Message = {
|
||||
id: "tool-1",
|
||||
role: "tool",
|
||||
content: "Tool result",
|
||||
toolCallId: "tool-call-1",
|
||||
toolName: "testAction",
|
||||
};
|
||||
const gqlMsgs = aguiToGQL(aguiMsg);
|
||||
const aguiMsgs2 = gqlToAGUI(gqlMsgs);
|
||||
expect(stripFunctions(aguiMsgs2[0])).toEqual(stripFunctions(aguiMsg));
|
||||
});
|
||||
|
||||
test("tool message GQL -> AGUI -> GQL", () => {
|
||||
const gqlMsg = new gql.ResultMessage({
|
||||
id: "tool-1",
|
||||
result: "Tool result",
|
||||
actionExecutionId: "tool-call-1",
|
||||
actionName: "testAction",
|
||||
});
|
||||
const aguiMsgs = gqlToAGUI(gqlMsg);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs);
|
||||
expect(gqlMsgs2[0].id).toBe(gqlMsg.id);
|
||||
expect((gqlMsgs2[0] as any).result).toBe(gqlMsg.result);
|
||||
expect((gqlMsgs2[0] as any).actionExecutionId).toBe(
|
||||
gqlMsg.actionExecutionId,
|
||||
);
|
||||
});
|
||||
|
||||
test("action execution AGUI -> GQL -> AGUI", () => {
|
||||
const aguiMsg: agui.Message = {
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
content: "Running action",
|
||||
toolCalls: [
|
||||
{
|
||||
id: "tool-call-1",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "doSomething",
|
||||
arguments: JSON.stringify({ foo: "bar" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const gqlMsgs = aguiToGQL(aguiMsg);
|
||||
const aguiMsgs2 = gqlToAGUI(gqlMsgs);
|
||||
// Should have an assistant message and an action execution message
|
||||
expect(aguiMsgs2[0].role).toBe("assistant");
|
||||
expect(aguiMsgs2[1].role).toBe("assistant");
|
||||
// Only check toolCalls if present
|
||||
if ("toolCalls" in aguiMsgs2[1]) {
|
||||
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
|
||||
"doSomething",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("action execution GQL -> AGUI -> GQL", () => {
|
||||
const actionExecMsg = new gql.ActionExecutionMessage({
|
||||
id: "tool-call-1",
|
||||
name: "doSomething",
|
||||
arguments: { foo: "bar" },
|
||||
parentMessageId: "assistant-1",
|
||||
});
|
||||
const aguiMsgs = gqlToAGUI([actionExecMsg]);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs);
|
||||
// The ActionExecutionMessage is at index 1, not index 0
|
||||
expect(gqlMsgs2[1].id).toBe("tool-call-1");
|
||||
// The name should be extracted from the toolCall function name
|
||||
expect((gqlMsgs2[1] as any).name).toBe("doSomething");
|
||||
expect((gqlMsgs2[1] as any).arguments).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
test("agent state GQL -> AGUI -> GQL", () => {
|
||||
const agentStateMsg = new gql.AgentStateMessage({
|
||||
id: "agent-state-1",
|
||||
agentName: "testAgent",
|
||||
state: { status: "running" },
|
||||
role: gql.Role.Assistant,
|
||||
});
|
||||
const aguiMsgs = gqlToAGUI([agentStateMsg]);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs);
|
||||
expect(gqlMsgs2[0].id).toBe("agent-state-1");
|
||||
// The agentName should be preserved in the roundtrip
|
||||
expect((gqlMsgs2[0] as any).agentName).toBe("testAgent");
|
||||
});
|
||||
|
||||
test("action execution with render function roundtrip", () => {
|
||||
const mockRender = vi.fn();
|
||||
const aguiMsg: agui.Message = {
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
content: "Running action",
|
||||
toolCalls: [
|
||||
{
|
||||
id: "tool-call-1",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "doSomething",
|
||||
arguments: JSON.stringify({ foo: "bar" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
generativeUI: mockRender,
|
||||
};
|
||||
const actions: Record<string, any> = {
|
||||
doSomething: { name: "doSomething" },
|
||||
};
|
||||
const gqlMsgs = aguiToGQL(aguiMsg, actions);
|
||||
const aguiMsgs2 = gqlToAGUI(gqlMsgs, actions);
|
||||
// The render function should be preserved in actions context
|
||||
expect(typeof actions.doSomething.render).toBe("function");
|
||||
// The roundtripped message should have the same tool call
|
||||
if ("toolCalls" in aguiMsgs2[1]) {
|
||||
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
|
||||
"doSomething",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("image message GQL -> AGUI -> GQL", () => {
|
||||
const gqlMsg = new gql.ImageMessage({
|
||||
id: "img-1",
|
||||
format: "jpeg",
|
||||
bytes: "somebase64string",
|
||||
role: gql.Role.User,
|
||||
});
|
||||
const aguiMsgs = gqlToAGUI(gqlMsg);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs);
|
||||
expect(gqlMsgs2[0].id).toBe(gqlMsg.id);
|
||||
expect((gqlMsgs2[0] as any).format).toBe(gqlMsg.format);
|
||||
expect((gqlMsgs2[0] as any).bytes).toBe(gqlMsg.bytes);
|
||||
expect((gqlMsgs2[0] as any).role).toBe(gqlMsg.role);
|
||||
});
|
||||
|
||||
test("image message AGUI -> GQL -> AGUI (assistant and user)", () => {
|
||||
// Assistant image message
|
||||
const aguiAssistantImageMsg: agui.Message = {
|
||||
id: "img-assistant-1",
|
||||
role: "assistant",
|
||||
image: {
|
||||
format: "jpeg",
|
||||
bytes: "assistantbase64data",
|
||||
},
|
||||
content: "", // required for type
|
||||
};
|
||||
const gqlAssistantMsgs = aguiToGQL(aguiAssistantImageMsg);
|
||||
const aguiAssistantMsgs2 = gqlToAGUI(gqlAssistantMsgs);
|
||||
expect(aguiAssistantMsgs2[0].id).toBe(aguiAssistantImageMsg.id);
|
||||
expect(aguiAssistantMsgs2[0].role).toBe("assistant");
|
||||
expect((aguiAssistantMsgs2[0] as any).image.format).toBe("jpeg");
|
||||
expect((aguiAssistantMsgs2[0] as any).image.bytes).toBe(
|
||||
"assistantbase64data",
|
||||
);
|
||||
|
||||
// User image message
|
||||
const aguiUserImageMsg: agui.Message = {
|
||||
id: "img-user-1",
|
||||
role: "user",
|
||||
image: {
|
||||
format: "png",
|
||||
bytes: "userbase64data",
|
||||
},
|
||||
content: "", // required for type
|
||||
};
|
||||
const gqlUserMsgs = aguiToGQL(aguiUserImageMsg);
|
||||
const aguiUserMsgs2 = gqlToAGUI(gqlUserMsgs);
|
||||
expect(aguiUserMsgs2[0].id).toBe(aguiUserImageMsg.id);
|
||||
expect(aguiUserMsgs2[0].role).toBe("user");
|
||||
expect((aguiUserMsgs2[0] as any).image.format).toBe("png");
|
||||
expect((aguiUserMsgs2[0] as any).image.bytes).toBe("userbase64data");
|
||||
});
|
||||
|
||||
test("wild card action roundtrip conversion", () => {
|
||||
const mockRender = vi.fn(
|
||||
(props) => `Wildcard rendered: ${props.args.test}`,
|
||||
);
|
||||
const aguiMsg: agui.Message = {
|
||||
id: "assistant-wildcard-1",
|
||||
role: "assistant",
|
||||
content: "Running wild card action",
|
||||
toolCalls: [
|
||||
{
|
||||
id: "tool-call-wildcard-1",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "unknownAction",
|
||||
arguments: JSON.stringify({ test: "wildcard-value" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
generativeUI: mockRender,
|
||||
};
|
||||
|
||||
const actions: Record<string, any> = {
|
||||
"*": { name: "*" },
|
||||
};
|
||||
|
||||
// AGUI -> GQL -> AGUI roundtrip
|
||||
const gqlMsgs = aguiToGQL(aguiMsg, actions);
|
||||
const aguiMsgs2 = gqlToAGUI(gqlMsgs, actions);
|
||||
|
||||
// Verify the wild card action preserved the render function
|
||||
expect(typeof actions["*"].render).toBe("function");
|
||||
expect(actions["*"].render).toBe(mockRender);
|
||||
|
||||
// Verify the roundtripped message structure
|
||||
expect(aguiMsgs2).toHaveLength(2);
|
||||
expect(aguiMsgs2[0].role).toBe("assistant");
|
||||
expect(aguiMsgs2[1].role).toBe("assistant");
|
||||
|
||||
// Check that the tool call is preserved
|
||||
if ("toolCalls" in aguiMsgs2[1]) {
|
||||
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
|
||||
"unknownAction",
|
||||
);
|
||||
expect((aguiMsgs2[1] as any).toolCalls[0].function.arguments).toBe(
|
||||
'{"test":"wildcard-value"}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("wild card action with specific action priority roundtrip", () => {
|
||||
const mockRender = vi.fn(
|
||||
(props) => `Specific action rendered: ${props.args.test}`,
|
||||
);
|
||||
const aguiMsg: agui.Message = {
|
||||
id: "assistant-priority-1",
|
||||
role: "assistant",
|
||||
content: "Running specific action",
|
||||
toolCalls: [
|
||||
{
|
||||
id: "tool-call-priority-1",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "specificAction",
|
||||
arguments: JSON.stringify({ test: "specific-value" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
generativeUI: mockRender,
|
||||
};
|
||||
|
||||
const actions: Record<string, any> = {
|
||||
specificAction: { name: "specificAction" },
|
||||
"*": { name: "*" },
|
||||
};
|
||||
|
||||
// AGUI -> GQL -> AGUI roundtrip
|
||||
const gqlMsgs = aguiToGQL(aguiMsg, actions);
|
||||
const aguiMsgs2 = gqlToAGUI(gqlMsgs, actions);
|
||||
|
||||
// Verify the specific action preserved the render function (not wild card)
|
||||
expect(typeof actions.specificAction.render).toBe("function");
|
||||
expect(actions.specificAction.render).toBe(mockRender);
|
||||
expect(actions["*"].render).toBeUndefined();
|
||||
|
||||
// Verify the roundtripped message structure
|
||||
expect(aguiMsgs2).toHaveLength(2);
|
||||
expect(aguiMsgs2[0].role).toBe("assistant");
|
||||
expect(aguiMsgs2[1].role).toBe("assistant");
|
||||
|
||||
// Check that the tool call is preserved
|
||||
if ("toolCalls" in aguiMsgs2[1]) {
|
||||
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
|
||||
"specificAction",
|
||||
);
|
||||
expect((aguiMsgs2[1] as any).toolCalls[0].function.arguments).toBe(
|
||||
'{"test":"specific-value"}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("wild card action GQL -> AGUI -> GQL roundtrip", () => {
|
||||
const actionExecMsg = new gql.ActionExecutionMessage({
|
||||
id: "wildcard-action-1",
|
||||
name: "unknownAction",
|
||||
arguments: { test: "wildcard-gql-value" },
|
||||
parentMessageId: "assistant-1",
|
||||
});
|
||||
|
||||
const actions: Record<string, any> = {
|
||||
"*": {
|
||||
name: "*",
|
||||
render: vi.fn((props) => `GQL wildcard rendered: ${props.args.test}`),
|
||||
},
|
||||
};
|
||||
|
||||
// GQL -> AGUI -> GQL roundtrip
|
||||
const aguiMsgs = gqlToAGUI([actionExecMsg], actions);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
|
||||
|
||||
// When converting ActionExecutionMessage to AGUI and back, we get:
|
||||
// 1. A TextMessage (assistant message with toolCalls)
|
||||
// 2. An ActionExecutionMessage (the tool call itself)
|
||||
expect(gqlMsgs2).toHaveLength(2);
|
||||
expect(gqlMsgs2[0].id).toBe("wildcard-action-1");
|
||||
expect((gqlMsgs2[0] as any).role).toBe(gql.Role.Assistant);
|
||||
expect(gqlMsgs2[1].id).toBe("wildcard-action-1");
|
||||
expect((gqlMsgs2[1] as any).name).toBe("unknownAction");
|
||||
expect((gqlMsgs2[1] as any).arguments).toEqual({
|
||||
test: "wildcard-gql-value",
|
||||
});
|
||||
});
|
||||
|
||||
test("roundtrip conversion with result parsing edge cases", () => {
|
||||
// Test with a tool result that contains a JSON string
|
||||
const toolResultMsg: agui.Message = {
|
||||
id: "tool-result-json",
|
||||
role: "tool",
|
||||
content: '{"status": "success", "data": {"value": 42}}',
|
||||
toolCallId: "tool-call-json",
|
||||
toolName: "jsonAction",
|
||||
};
|
||||
|
||||
// Convert AGUI -> GQL -> AGUI
|
||||
const gqlMsgs = aguiToGQL(toolResultMsg);
|
||||
const aguiMsgs = gqlToAGUI(gqlMsgs);
|
||||
|
||||
expect(gqlMsgs).toHaveLength(1);
|
||||
expect(gqlMsgs[0]).toBeInstanceOf(gql.ResultMessage);
|
||||
expect((gqlMsgs[0] as any).result).toBe(
|
||||
'{"status": "success", "data": {"value": 42}}',
|
||||
);
|
||||
|
||||
expect(aguiMsgs).toHaveLength(1);
|
||||
expect(aguiMsgs[0].role).toBe("tool");
|
||||
expect(aguiMsgs[0].content).toBe(
|
||||
'{"status": "success", "data": {"value": 42}}',
|
||||
);
|
||||
});
|
||||
|
||||
test("roundtrip conversion with object content in tool results", () => {
|
||||
// Test with a tool result that has object content (edge case)
|
||||
const toolResultMsg: agui.Message = {
|
||||
id: "tool-result-object",
|
||||
role: "tool",
|
||||
content: { status: "success", data: { value: 42 } } as any,
|
||||
toolCallId: "tool-call-object",
|
||||
toolName: "objectAction",
|
||||
};
|
||||
|
||||
// Convert AGUI -> GQL -> AGUI
|
||||
const gqlMsgs = aguiToGQL(toolResultMsg);
|
||||
const aguiMsgs = gqlToAGUI(gqlMsgs);
|
||||
|
||||
expect(gqlMsgs).toHaveLength(1);
|
||||
expect(gqlMsgs[0]).toBeInstanceOf(gql.ResultMessage);
|
||||
expect((gqlMsgs[0] as any).result).toBe(
|
||||
'{"status":"success","data":{"value":42}}',
|
||||
);
|
||||
|
||||
expect(aguiMsgs).toHaveLength(1);
|
||||
expect(aguiMsgs[0].role).toBe("tool");
|
||||
expect(aguiMsgs[0].content).toBe(
|
||||
'{"status":"success","data":{"value":42}}',
|
||||
);
|
||||
});
|
||||
|
||||
test("roundtrip conversion with action execution and result parsing", () => {
|
||||
const mockRender = vi.fn(
|
||||
(props) => `Rendered: ${JSON.stringify(props.result)}`,
|
||||
);
|
||||
|
||||
// Create action execution message
|
||||
const actionExecMsg = new gql.ActionExecutionMessage({
|
||||
id: "action-with-result",
|
||||
name: "testAction",
|
||||
arguments: { input: "test-value" },
|
||||
parentMessageId: "parent-result",
|
||||
});
|
||||
|
||||
// Create result message
|
||||
const resultMsg = new gql.ResultMessage({
|
||||
id: "result-with-json",
|
||||
result: '{"output": "processed", "count": 5}',
|
||||
actionExecutionId: "action-with-result",
|
||||
actionName: "testAction",
|
||||
});
|
||||
|
||||
const actions = {
|
||||
testAction: {
|
||||
name: "testAction",
|
||||
render: mockRender,
|
||||
},
|
||||
};
|
||||
|
||||
// Convert GQL -> AGUI
|
||||
const aguiMsgs = gqlToAGUI([actionExecMsg, resultMsg], actions);
|
||||
|
||||
// The action execution should have a generativeUI function that parses string results
|
||||
expect(aguiMsgs).toHaveLength(2);
|
||||
expect(aguiMsgs[0].role).toBe("assistant");
|
||||
expect("generativeUI" in aguiMsgs[0]).toBe(true);
|
||||
expect(aguiMsgs[1].role).toBe("tool");
|
||||
expect(aguiMsgs[1].content).toBe('{"output": "processed", "count": 5}');
|
||||
|
||||
// Test that the render function receives parsed results
|
||||
if ("generativeUI" in aguiMsgs[0] && aguiMsgs[0].generativeUI) {
|
||||
aguiMsgs[0].generativeUI({ result: '{"parsed": true}' });
|
||||
expect(mockRender).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
result: { parsed: true }, // Should be parsed from string
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert back AGUI -> GQL
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
|
||||
|
||||
// Should have 3 messages: TextMessage, ActionExecutionMessage, ResultMessage
|
||||
expect(gqlMsgs2).toHaveLength(3);
|
||||
expect(gqlMsgs2[0]).toBeInstanceOf(gql.TextMessage);
|
||||
expect(gqlMsgs2[1]).toBeInstanceOf(gql.ActionExecutionMessage);
|
||||
expect(gqlMsgs2[2]).toBeInstanceOf(gql.ResultMessage);
|
||||
|
||||
// Check that arguments roundtripped correctly
|
||||
expect((gqlMsgs2[1] as any).arguments).toEqual({ input: "test-value" });
|
||||
expect((gqlMsgs2[2] as any).result).toBe(
|
||||
'{"output": "processed", "count": 5}',
|
||||
);
|
||||
});
|
||||
|
||||
test("roundtrip conversion verifies correct property distribution for regular actions", () => {
|
||||
const mockRender = vi.fn(
|
||||
(props) => `Regular action: ${JSON.stringify(props.args)}`,
|
||||
);
|
||||
|
||||
const actionExecMsg = new gql.ActionExecutionMessage({
|
||||
id: "regular-action-test",
|
||||
name: "regularAction",
|
||||
arguments: { test: "regular-value" },
|
||||
parentMessageId: "parent-regular",
|
||||
});
|
||||
|
||||
const actions = {
|
||||
regularAction: {
|
||||
name: "regularAction",
|
||||
render: mockRender,
|
||||
},
|
||||
};
|
||||
|
||||
// GQL -> AGUI -> GQL roundtrip
|
||||
const aguiMsgs = gqlToAGUI([actionExecMsg], actions);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
|
||||
|
||||
// Verify the roundtrip preserved the action
|
||||
expect(gqlMsgs2).toHaveLength(2);
|
||||
expect(gqlMsgs2[1]).toBeInstanceOf(gql.ActionExecutionMessage);
|
||||
expect((gqlMsgs2[1] as any).name).toBe("regularAction");
|
||||
|
||||
// Test that regular actions do NOT receive the name property in render props
|
||||
if ("generativeUI" in aguiMsgs[0] && aguiMsgs[0].generativeUI) {
|
||||
aguiMsgs[0].generativeUI();
|
||||
expect(mockRender).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
args: { test: "regular-value" },
|
||||
// name property should NOT be present for regular actions
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify name property is NOT present
|
||||
const callArgs = mockRender.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("name");
|
||||
}
|
||||
});
|
||||
|
||||
test("roundtrip conversion verifies correct property distribution for wildcard actions", () => {
|
||||
const mockRender = vi.fn(
|
||||
(props) =>
|
||||
`Wildcard action: ${props.name} with ${JSON.stringify(props.args)}`,
|
||||
);
|
||||
|
||||
const actionExecMsg = new gql.ActionExecutionMessage({
|
||||
id: "wildcard-action-test",
|
||||
name: "unknownAction",
|
||||
arguments: { test: "wildcard-value" },
|
||||
parentMessageId: "parent-wildcard",
|
||||
});
|
||||
|
||||
const actions = {
|
||||
"*": {
|
||||
name: "*",
|
||||
render: mockRender,
|
||||
},
|
||||
};
|
||||
|
||||
// GQL -> AGUI -> GQL roundtrip
|
||||
const aguiMsgs = gqlToAGUI([actionExecMsg], actions);
|
||||
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
|
||||
|
||||
// Verify the roundtrip preserved the action
|
||||
expect(gqlMsgs2).toHaveLength(2);
|
||||
expect(gqlMsgs2[1]).toBeInstanceOf(gql.ActionExecutionMessage);
|
||||
expect((gqlMsgs2[1] as any).name).toBe("unknownAction");
|
||||
|
||||
// Test that wildcard actions DO receive the name property in render props
|
||||
if ("generativeUI" in aguiMsgs[0] && aguiMsgs[0].generativeUI) {
|
||||
aguiMsgs[0].generativeUI();
|
||||
expect(mockRender).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
args: { test: "wildcard-value" },
|
||||
name: "unknownAction", // name property SHOULD be present for wildcard actions
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify name property IS present
|
||||
const callArgs = mockRender.mock.calls[0][0];
|
||||
expect(callArgs).toHaveProperty("name", "unknownAction");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"strictPropertyInitialization": false,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/__tests__/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
target: "es2022",
|
||||
outDir: "dist",
|
||||
unbundle: true,
|
||||
external: ["react", "@graphql-typed-document-node/core"],
|
||||
exports: true,
|
||||
},
|
||||
{
|
||||
entry: ["src/index.ts"],
|
||||
format: ["umd"],
|
||||
globalName: "CopilotKitRuntimeClientGQL",
|
||||
sourcemap: true,
|
||||
target: "es2018",
|
||||
outDir: "dist",
|
||||
external: [
|
||||
"react",
|
||||
"@copilotkit/shared",
|
||||
"urql",
|
||||
"@urql/core",
|
||||
"graphql",
|
||||
"@graphql-typed-document-node/core",
|
||||
"untruncate-json",
|
||||
],
|
||||
outputOptions(options) {
|
||||
options.codeSplitting = false;
|
||||
options.entryFileNames = "[name].umd.js";
|
||||
options.globals = {
|
||||
react: "React",
|
||||
"@copilotkit/shared": "CopilotKitShared",
|
||||
urql: "Urql",
|
||||
"@urql/core": "UrqlCore",
|
||||
graphql: "GraphQL",
|
||||
"@graphql-typed-document-node/core": "GraphQLTypedDocumentNode",
|
||||
"untruncate-json": "untruncateJson",
|
||||
};
|
||||
return options;
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["../../typedoc.base.json"],
|
||||
"entryPoints": ["src/index.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
globals: true,
|
||||
include: ["src/**/*.{test,spec}.ts"],
|
||||
reporters: [["default", { summary: false }]],
|
||||
silent: true,
|
||||
retry: 2,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user