chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
import { ActionInputAvailability } from "../types/enums";
|
||||
@InputType()
|
||||
export class ActionInput {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
description: string;
|
||||
|
||||
@Field(() => String)
|
||||
jsonSchema: string;
|
||||
|
||||
@Field(() => ActionInputAvailability, { nullable: true })
|
||||
available?: ActionInputAvailability;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class AgentSessionInput {
|
||||
@Field(() => String)
|
||||
agentName: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
threadId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
nodeName?: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class AgentStateInput {
|
||||
@Field(() => String)
|
||||
agentName: string;
|
||||
|
||||
@Field(() => String)
|
||||
state: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
config?: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class GuardrailsRuleInput {
|
||||
@Field(() => [String], { nullable: true })
|
||||
allowList?: string[] = [];
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
denyList?: string[] = [];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class GuardrailsInput {
|
||||
@Field(() => GuardrailsRuleInput, { nullable: false })
|
||||
inputValidationRules: GuardrailsRuleInput;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
import { GuardrailsInput } from "./cloud-guardrails.input";
|
||||
|
||||
@InputType()
|
||||
export class CloudInput {
|
||||
@Field(() => GuardrailsInput, { nullable: true })
|
||||
guardrails?: GuardrailsInput;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class ContextPropertyInput {
|
||||
@Field(() => String)
|
||||
value: string;
|
||||
|
||||
@Field(() => String)
|
||||
description: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class CopilotContextInput {
|
||||
@Field(() => String)
|
||||
description: string;
|
||||
|
||||
@Field(() => String)
|
||||
value: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Field, InputType, Int, createUnionType } from "type-graphql";
|
||||
|
||||
const PrimitiveUnion = createUnionType({
|
||||
name: "Primitive",
|
||||
types: () => [String, Number, Boolean] as const,
|
||||
});
|
||||
|
||||
@InputType()
|
||||
export class CustomPropertyInput {
|
||||
@Field(() => String)
|
||||
key: string;
|
||||
|
||||
@Field(() => PrimitiveUnion)
|
||||
value: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
/**
|
||||
* The extensions input is used to pass additional information to the copilot runtime, specific to a
|
||||
* service adapter or agent framework.
|
||||
*/
|
||||
|
||||
@InputType()
|
||||
export class OpenAIApiAssistantAPIInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
runId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
threadId?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class ExtensionsInput {
|
||||
@Field(() => OpenAIApiAssistantAPIInput, { nullable: true })
|
||||
openaiAssistantAPI?: OpenAIApiAssistantAPIInput;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class ForwardedParametersInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
model?: string;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
maxTokens?: number;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
stop?: string[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
toolChoice?: String;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
toolChoiceFunctionName?: string;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
temperature?: number;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
import { ActionInput } from "./action.input";
|
||||
|
||||
@InputType()
|
||||
export class FrontendInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
toDeprecate_fullContext?: string;
|
||||
|
||||
@Field(() => [ActionInput])
|
||||
actions: ActionInput[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
url?: string;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
import { MessageInput } from "./message.input";
|
||||
import { FrontendInput } from "./frontend.input";
|
||||
import { CloudInput } from "./cloud.input";
|
||||
import { CopilotRequestType } from "../types/enums";
|
||||
import { ForwardedParametersInput } from "./forwarded-parameters.input";
|
||||
import { AgentSessionInput } from "./agent-session.input";
|
||||
import { AgentStateInput } from "./agent-state.input";
|
||||
import { ExtensionsInput } from "./extensions.input";
|
||||
import { MetaEventInput } from "./meta-event.input";
|
||||
import { CopilotContextInput } from "./copilot-context.input";
|
||||
|
||||
@InputType()
|
||||
export class GenerateCopilotResponseMetadataInput {
|
||||
@Field(() => CopilotRequestType, { nullable: true })
|
||||
requestType: CopilotRequestType;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class GenerateCopilotResponseInput {
|
||||
@Field(() => GenerateCopilotResponseMetadataInput, { nullable: false })
|
||||
metadata: GenerateCopilotResponseMetadataInput;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
threadId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
runId?: string;
|
||||
|
||||
@Field(() => [MessageInput])
|
||||
messages: MessageInput[];
|
||||
|
||||
@Field(() => FrontendInput)
|
||||
frontend: FrontendInput;
|
||||
|
||||
@Field(() => CloudInput, { nullable: true })
|
||||
cloud?: CloudInput;
|
||||
|
||||
@Field(() => ForwardedParametersInput, { nullable: true })
|
||||
forwardedParameters?: ForwardedParametersInput;
|
||||
|
||||
@Field(() => AgentSessionInput, { nullable: true })
|
||||
agentSession?: AgentSessionInput;
|
||||
|
||||
@Field(() => AgentStateInput, { nullable: true })
|
||||
agentState?: AgentStateInput;
|
||||
|
||||
@Field(() => [AgentStateInput], { nullable: true })
|
||||
agentStates?: AgentStateInput[];
|
||||
|
||||
@Field(() => ExtensionsInput, { nullable: true })
|
||||
extensions?: ExtensionsInput;
|
||||
|
||||
@Field(() => [MetaEventInput], { nullable: true })
|
||||
metaEvents?: MetaEventInput[];
|
||||
|
||||
@Field(() => [CopilotContextInput], { nullable: true })
|
||||
context?: CopilotContextInput[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class LoadAgentStateInput {
|
||||
@Field(() => String)
|
||||
threadId: string;
|
||||
|
||||
@Field(() => String)
|
||||
agentName: string;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
import { MessageRole } from "../types/enums";
|
||||
import { BaseMessageInput } from "../types/base";
|
||||
|
||||
@InputType()
|
||||
export class TextMessageInput {
|
||||
@Field(() => String)
|
||||
content: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
|
||||
@Field(() => MessageRole)
|
||||
role: MessageRole;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class ActionExecutionMessageInput {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
arguments: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: true,
|
||||
deprecationReason: "This field will be removed in a future version",
|
||||
})
|
||||
scope?: String;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class ResultMessageInput {
|
||||
@Field(() => String)
|
||||
actionExecutionId: string;
|
||||
|
||||
@Field(() => String)
|
||||
actionName: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
|
||||
@Field(() => String)
|
||||
result: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class AgentStateMessageInput {
|
||||
@Field(() => String)
|
||||
threadId: string;
|
||||
|
||||
@Field(() => String)
|
||||
agentName: string;
|
||||
|
||||
@Field(() => MessageRole)
|
||||
role: MessageRole;
|
||||
|
||||
@Field(() => String)
|
||||
state: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
running: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
nodeName: string;
|
||||
|
||||
@Field(() => String)
|
||||
runId: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class ImageMessageInput {
|
||||
@Field(() => String)
|
||||
format: string;
|
||||
|
||||
@Field(() => String)
|
||||
bytes: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
|
||||
@Field(() => MessageRole)
|
||||
role: MessageRole;
|
||||
}
|
||||
|
||||
// GraphQL does not support union types in inputs, so we need to use
|
||||
// optional fields for the different subtypes.
|
||||
@InputType()
|
||||
export class MessageInput extends BaseMessageInput {
|
||||
@Field(() => TextMessageInput, { nullable: true })
|
||||
textMessage?: TextMessageInput;
|
||||
|
||||
@Field(() => ActionExecutionMessageInput, { nullable: true })
|
||||
actionExecutionMessage?: ActionExecutionMessageInput;
|
||||
|
||||
@Field(() => ResultMessageInput, { nullable: true })
|
||||
resultMessage?: ResultMessageInput;
|
||||
|
||||
@Field(() => AgentStateMessageInput, { nullable: true })
|
||||
agentStateMessage?: AgentStateMessageInput;
|
||||
|
||||
@Field(() => ImageMessageInput, { nullable: true })
|
||||
imageMessage?: ImageMessageInput;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
import { MetaEventName } from "../types/meta-events.type";
|
||||
import { MessageInput } from "./message.input";
|
||||
|
||||
@InputType()
|
||||
export class MetaEventInput {
|
||||
@Field(() => MetaEventName)
|
||||
name: MetaEventName;
|
||||
|
||||
@Field(() => String)
|
||||
value?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
response?: string;
|
||||
|
||||
@Field(() => [MessageInput], { nullable: true })
|
||||
messages?: MessageInput[];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,391 @@
|
||||
import * as gql from "../types/converted/index";
|
||||
import { MessageRole } from "../types/enums";
|
||||
import * 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;
|
||||
}
|
||||
|
||||
function normalizeMessageContent(content: agui.Message["content"]): string {
|
||||
if (typeof content === "string" || typeof content === "undefined") {
|
||||
return content || "";
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (part?.type === "text") {
|
||||
return part.text;
|
||||
}
|
||||
if (part?.type === "binary") {
|
||||
return (
|
||||
part.data ||
|
||||
part.url ||
|
||||
part.filename ||
|
||||
`[binary:${part.mimeType}]`
|
||||
);
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
if (content && typeof content === "object") {
|
||||
try {
|
||||
return JSON.stringify(content);
|
||||
} catch (error) {
|
||||
console.warn("Failed to serialize message content", error);
|
||||
}
|
||||
}
|
||||
|
||||
return String(content ?? "");
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------
|
||||
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: normalizeMessageContent(message.content),
|
||||
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 "../types/converted/index";
|
||||
import * as agui from "@copilotkit/shared";
|
||||
import { MessageStatusCode } from "../types/message-status.type";
|
||||
|
||||
// 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 { vi } from "vitest";
|
||||
import * as gql from "../types/converted/index";
|
||||
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 = {
|
||||
id: "img-user-1",
|
||||
role: "user",
|
||||
image: {
|
||||
format: "png",
|
||||
bytes: "userbase64data",
|
||||
},
|
||||
content: "", // required for type
|
||||
} as agui.Message;
|
||||
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,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveMessageId } from "../resolve-message-id";
|
||||
|
||||
describe("resolveMessageId (#2118)", () => {
|
||||
it("preserves a provided non-empty id verbatim", () => {
|
||||
expect(resolveMessageId("msg-123")).toBe("msg-123");
|
||||
});
|
||||
|
||||
it.each(["", null, undefined] as const)(
|
||||
"falls back to a generated id when the event id is %p",
|
||||
(input) => {
|
||||
const id = resolveMessageId(input);
|
||||
// randomId() always produces the "ck-<uuid>" shape; the important
|
||||
// contract for #2118 is that the returned value is a non-empty string,
|
||||
// never null/undefined.
|
||||
expect(id).toMatch(/^ck-[0-9a-f-]{36}$/);
|
||||
},
|
||||
);
|
||||
|
||||
it("generates a fresh id on each fallback call", () => {
|
||||
const a = resolveMessageId(undefined);
|
||||
const b = resolveMessageId(undefined);
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,785 @@
|
||||
import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql";
|
||||
import {
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
Subscription,
|
||||
filter,
|
||||
finalize,
|
||||
firstValueFrom,
|
||||
shareReplay,
|
||||
skipWhile,
|
||||
take,
|
||||
takeWhile,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
import { GenerateCopilotResponseInput } from "../inputs/generate-copilot-response.input";
|
||||
import { CopilotResponse } from "../types/copilot-response.type";
|
||||
import {
|
||||
CopilotKitLangGraphInterruptEvent,
|
||||
LangGraphInterruptEvent,
|
||||
} from "../types/meta-events.type";
|
||||
import { ActionInputAvailability, MessageRole } from "../types/enums";
|
||||
import { Repeater } from "graphql-yoga";
|
||||
import type {
|
||||
CopilotRequestContextProperties,
|
||||
GraphQLContext,
|
||||
} from "../../lib/integrations";
|
||||
import {
|
||||
RuntimeEvent,
|
||||
RuntimeEventTypes,
|
||||
RuntimeMetaEventName,
|
||||
} from "../../service-adapters/events";
|
||||
import {
|
||||
FailedMessageStatus,
|
||||
MessageStatusCode,
|
||||
MessageStatusUnion,
|
||||
SuccessMessageStatus,
|
||||
} from "../types/message-status.type";
|
||||
import {
|
||||
ResponseStatusUnion,
|
||||
SuccessResponseStatus,
|
||||
} from "../types/response-status.type";
|
||||
import { GraphQLJSONObject } from "graphql-scalars";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { GuardrailsResult } from "../types/guardrails-result.type";
|
||||
import { GraphQLError } from "graphql";
|
||||
import {
|
||||
GuardrailsValidationFailureResponse,
|
||||
MessageStreamInterruptedResponse,
|
||||
UnknownErrorResponse,
|
||||
} from "../../utils";
|
||||
import {
|
||||
ActionExecutionMessage,
|
||||
AgentStateMessage,
|
||||
Message,
|
||||
MessageType,
|
||||
ResultMessage,
|
||||
TextMessage,
|
||||
} from "../types/converted";
|
||||
import telemetry from "../../lib/telemetry-client";
|
||||
import { randomId } from "@copilotkit/shared";
|
||||
import { resolveMessageId } from "./resolve-message-id";
|
||||
import { AgentsResponse } from "../types/agents-response.type";
|
||||
import { LangGraphEventTypes } from "../../agents/langgraph/events";
|
||||
import {
|
||||
CopilotKitError,
|
||||
CopilotKitLowLevelError,
|
||||
isStructuredCopilotKitError,
|
||||
} from "@copilotkit/shared";
|
||||
import { CopilotRuntime } from "../../lib";
|
||||
|
||||
const invokeGuardrails = async ({
|
||||
baseUrl,
|
||||
copilotCloudPublicApiKey,
|
||||
data,
|
||||
onResult,
|
||||
onError,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
copilotCloudPublicApiKey: string;
|
||||
data: GenerateCopilotResponseInput;
|
||||
onResult: (result: GuardrailsResult) => void;
|
||||
onError: (err: Error) => void;
|
||||
}) => {
|
||||
if (
|
||||
data.messages.length &&
|
||||
data.messages[data.messages.length - 1].textMessage?.role ===
|
||||
MessageRole.user
|
||||
) {
|
||||
const messages = data.messages
|
||||
.filter(
|
||||
(m) =>
|
||||
m.textMessage !== undefined &&
|
||||
(m.textMessage.role === MessageRole.user ||
|
||||
m.textMessage.role === MessageRole.assistant),
|
||||
)
|
||||
.map((m) => ({
|
||||
role: m.textMessage!.role,
|
||||
content: m.textMessage.content,
|
||||
}));
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const restOfMessages = messages.slice(0, -1);
|
||||
|
||||
const body = {
|
||||
input: lastMessage.content,
|
||||
validTopics: data.cloud.guardrails.inputValidationRules.allowList,
|
||||
invalidTopics: data.cloud.guardrails.inputValidationRules.denyList,
|
||||
messages: restOfMessages,
|
||||
};
|
||||
|
||||
const guardrailsResult = await fetch(`${baseUrl}/guardrails/validate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CopilotCloud-Public-API-Key": copilotCloudPublicApiKey,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (guardrailsResult.ok) {
|
||||
const resultJson: GuardrailsResult = await guardrailsResult.json();
|
||||
onResult(resultJson);
|
||||
} else {
|
||||
onError(await guardrailsResult.json());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Resolver(() => CopilotResponse)
|
||||
export class CopilotResolver {
|
||||
@Query(() => String)
|
||||
async hello() {
|
||||
return "Hello World";
|
||||
}
|
||||
|
||||
@Query(() => AgentsResponse)
|
||||
async availableAgents(@Ctx() ctx: GraphQLContext) {
|
||||
let logger = ctx.logger.child({
|
||||
component: "CopilotResolver.availableAgents",
|
||||
});
|
||||
|
||||
logger.debug("Processing");
|
||||
const agentsWithEndpoints = [];
|
||||
|
||||
logger.debug("Event source created, creating response");
|
||||
|
||||
return {
|
||||
agents: agentsWithEndpoints.map(
|
||||
({ endpoint, ...agentWithoutEndpoint }) => agentWithoutEndpoint,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => CopilotResponse)
|
||||
async generateCopilotResponse(
|
||||
@Ctx() ctx: GraphQLContext,
|
||||
@Arg("data") data: GenerateCopilotResponseInput,
|
||||
@Arg("properties", () => GraphQLJSONObject, { nullable: true })
|
||||
properties?: CopilotRequestContextProperties,
|
||||
) {
|
||||
telemetry.capture("oss.runtime.copilot_request_created", {
|
||||
"cloud.guardrails.enabled": data.cloud?.guardrails !== undefined,
|
||||
requestType: data.metadata.requestType,
|
||||
"cloud.api_key_provided": !!ctx.request.headers.get(
|
||||
"x-copilotcloud-public-api-key",
|
||||
),
|
||||
...(ctx.request.headers.get("x-copilotcloud-public-api-key")
|
||||
? {
|
||||
"cloud.public_api_key": ctx.request.headers.get(
|
||||
"x-copilotcloud-public-api-key",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(ctx._copilotkit.baseUrl
|
||||
? {
|
||||
"cloud.base_url": ctx._copilotkit.baseUrl,
|
||||
}
|
||||
: {
|
||||
"cloud.base_url": "https://api.cloud.copilotkit.ai",
|
||||
}),
|
||||
});
|
||||
|
||||
let logger = ctx.logger.child({
|
||||
component: "CopilotResolver.generateCopilotResponse",
|
||||
});
|
||||
logger.debug({ data }, "Generating Copilot response");
|
||||
|
||||
if (properties) {
|
||||
logger.debug("Properties provided, merging with context properties");
|
||||
ctx.properties = { ...ctx.properties, ...properties };
|
||||
}
|
||||
|
||||
const copilotRuntime = ctx._copilotkit.runtime as unknown as CopilotRuntime;
|
||||
const serviceAdapter = ctx._copilotkit.serviceAdapter;
|
||||
|
||||
let copilotCloudPublicApiKey: string | null = null;
|
||||
let copilotCloudBaseUrl: string;
|
||||
|
||||
// Extract publicApiKey from headers for both cloud and non-cloud requests
|
||||
// This enables onTrace functionality regardless of cloud configuration
|
||||
const publicApiKeyFromHeaders = ctx.request.headers.get(
|
||||
"x-copilotcloud-public-api-key",
|
||||
);
|
||||
if (publicApiKeyFromHeaders) {
|
||||
copilotCloudPublicApiKey = publicApiKeyFromHeaders;
|
||||
}
|
||||
|
||||
if (data.cloud) {
|
||||
logger = logger.child({ cloud: true });
|
||||
logger.debug(
|
||||
"Cloud configuration provided, checking for public API key in headers",
|
||||
);
|
||||
|
||||
if (!copilotCloudPublicApiKey) {
|
||||
logger.error("Public API key not found in headers");
|
||||
|
||||
throw new GraphQLError(
|
||||
"X-CopilotCloud-Public-API-Key header is required",
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.COPILOT_CLOUD_BASE_URL) {
|
||||
copilotCloudBaseUrl = process.env.COPILOT_CLOUD_BASE_URL;
|
||||
} else if (ctx._copilotkit.cloud?.baseUrl) {
|
||||
copilotCloudBaseUrl = ctx._copilotkit.cloud?.baseUrl;
|
||||
} else {
|
||||
copilotCloudBaseUrl = "https://api.cloud.copilotkit.ai";
|
||||
}
|
||||
|
||||
logger = logger.child({ copilotCloudBaseUrl });
|
||||
}
|
||||
|
||||
logger.debug("Setting up subjects");
|
||||
const responseStatus$ = new ReplaySubject<typeof ResponseStatusUnion>();
|
||||
const interruptStreaming$ = new ReplaySubject<{
|
||||
reason: string;
|
||||
messageId?: string;
|
||||
}>();
|
||||
const guardrailsResult$ = new ReplaySubject<GuardrailsResult>();
|
||||
|
||||
let outputMessages: Message[] = [];
|
||||
let resolveOutputMessagesPromise: (messages: Message[]) => void;
|
||||
let rejectOutputMessagesPromise: (err: Error) => void;
|
||||
|
||||
const outputMessagesPromise = new Promise<Message[]>((resolve, reject) => {
|
||||
resolveOutputMessagesPromise = resolve;
|
||||
rejectOutputMessagesPromise = reject;
|
||||
});
|
||||
|
||||
if (copilotCloudPublicApiKey) {
|
||||
ctx.properties["copilotCloudPublicApiKey"] = copilotCloudPublicApiKey;
|
||||
}
|
||||
|
||||
logger.debug("Processing");
|
||||
let runtimeResponse;
|
||||
|
||||
const {
|
||||
eventSource,
|
||||
threadId = randomId(),
|
||||
runId,
|
||||
serverSideActions,
|
||||
actionInputsWithoutAgents,
|
||||
extensions,
|
||||
} = runtimeResponse;
|
||||
|
||||
logger.debug("Event source created, creating response");
|
||||
// run and process the event stream
|
||||
const eventStream = eventSource
|
||||
.processRuntimeEvents({
|
||||
serverSideActions,
|
||||
guardrailsResult$: data.cloud?.guardrails ? guardrailsResult$ : null,
|
||||
actionInputsWithoutAgents: actionInputsWithoutAgents.filter(
|
||||
// TODO-AGENTS: do not exclude ALL server side actions
|
||||
(action) =>
|
||||
!serverSideActions.find(
|
||||
(serverSideAction) => serverSideAction.name == action.name,
|
||||
),
|
||||
),
|
||||
threadId,
|
||||
})
|
||||
.pipe(
|
||||
// shareReplay() ensures that later subscribers will see the whole stream instead of
|
||||
// just the events that were emitted after the subscriber was added.
|
||||
shareReplay(),
|
||||
finalize(() => {
|
||||
logger.debug("Event stream finalized");
|
||||
}),
|
||||
);
|
||||
|
||||
const response = {
|
||||
threadId,
|
||||
runId,
|
||||
status: firstValueFrom(responseStatus$),
|
||||
extensions,
|
||||
metaEvents: new Repeater(async (push, stop) => {
|
||||
let eventStreamSubscription: Subscription;
|
||||
|
||||
eventStreamSubscription = eventStream.subscribe({
|
||||
next: async (event) => {
|
||||
if (event.type != RuntimeEventTypes.MetaEvent) {
|
||||
return;
|
||||
}
|
||||
switch (event.name) {
|
||||
// @ts-ignore
|
||||
case LangGraphEventTypes.OnInterrupt:
|
||||
push(
|
||||
plainToInstance(LangGraphInterruptEvent, {
|
||||
// @ts-ignore
|
||||
type: event.type,
|
||||
// @ts-ignore
|
||||
name: RuntimeMetaEventName.LangGraphInterruptEvent,
|
||||
// @ts-ignore
|
||||
value: event.value,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case RuntimeMetaEventName.LangGraphInterruptEvent:
|
||||
push(
|
||||
plainToInstance(LangGraphInterruptEvent, {
|
||||
type: event.type,
|
||||
name: event.name,
|
||||
value: event.value,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case RuntimeMetaEventName.CopilotKitLangGraphInterruptEvent:
|
||||
push(
|
||||
plainToInstance(CopilotKitLangGraphInterruptEvent, {
|
||||
type: event.type,
|
||||
name: event.name,
|
||||
data: {
|
||||
value: event.data.value,
|
||||
messages: event.data.messages.map((message) => {
|
||||
if (
|
||||
message.type === "TextMessage" ||
|
||||
("content" in message && "role" in message)
|
||||
) {
|
||||
return plainToInstance(TextMessage, {
|
||||
id: message.id,
|
||||
createdAt: new Date(),
|
||||
content: [(message as TextMessage).content],
|
||||
role: (message as TextMessage).role,
|
||||
status: new SuccessMessageStatus(),
|
||||
});
|
||||
}
|
||||
if ("arguments" in message) {
|
||||
return plainToInstance(ActionExecutionMessage, {
|
||||
name: message.name,
|
||||
id: message.id,
|
||||
arguments: [JSON.stringify(message.arguments)],
|
||||
createdAt: new Date(),
|
||||
status: new SuccessMessageStatus(),
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
"Unknown message in metaEvents copilot resolver",
|
||||
);
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
// For structured CopilotKit errors, set proper error response status
|
||||
if (
|
||||
err?.name?.includes("CopilotKit") ||
|
||||
err?.extensions?.visibility
|
||||
) {
|
||||
responseStatus$.next(
|
||||
new UnknownErrorResponse({
|
||||
description: err.message || "Agent error occurred",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
responseStatus$.next(
|
||||
new UnknownErrorResponse({
|
||||
description: `An unknown error has occurred in the event stream`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
eventStreamSubscription?.unsubscribe();
|
||||
stop();
|
||||
},
|
||||
complete: async () => {
|
||||
logger.debug("Meta events stream completed");
|
||||
responseStatus$.next(new SuccessResponseStatus());
|
||||
eventStreamSubscription?.unsubscribe();
|
||||
stop();
|
||||
},
|
||||
});
|
||||
}),
|
||||
messages: new Repeater(async (pushMessage, stopStreamingMessages) => {
|
||||
logger.debug("Messages repeater created");
|
||||
|
||||
if (data.cloud?.guardrails) {
|
||||
logger = logger.child({ guardrails: true });
|
||||
logger.debug("Guardrails is enabled, validating input");
|
||||
|
||||
invokeGuardrails({
|
||||
baseUrl: copilotCloudBaseUrl,
|
||||
copilotCloudPublicApiKey,
|
||||
data,
|
||||
onResult: (result) => {
|
||||
logger.debug(
|
||||
{ status: result.status },
|
||||
"Guardrails validation done",
|
||||
);
|
||||
guardrailsResult$.next(result);
|
||||
|
||||
// Guardrails validation failed
|
||||
if (result.status === "denied") {
|
||||
// send the reason to the client and interrupt streaming
|
||||
responseStatus$.next(
|
||||
new GuardrailsValidationFailureResponse({
|
||||
guardrailsReason: result.reason,
|
||||
}),
|
||||
);
|
||||
interruptStreaming$.next({
|
||||
reason: `Interrupted due to Guardrails validation failure. Reason: ${result.reason}`,
|
||||
});
|
||||
|
||||
// resolve messages promise to the middleware
|
||||
outputMessages = [
|
||||
plainToInstance(TextMessage, {
|
||||
id: randomId(),
|
||||
createdAt: new Date(),
|
||||
content: result.reason,
|
||||
role: MessageRole.assistant,
|
||||
}),
|
||||
];
|
||||
resolveOutputMessagesPromise(outputMessages);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
logger.error({ err }, "Error in guardrails validation");
|
||||
responseStatus$.next(
|
||||
new UnknownErrorResponse({
|
||||
description: `An unknown error has occurred in the guardrails validation`,
|
||||
}),
|
||||
);
|
||||
interruptStreaming$.next({
|
||||
reason: `Interrupted due to unknown error in guardrails validation`,
|
||||
});
|
||||
|
||||
// reject the middleware promise
|
||||
rejectOutputMessagesPromise(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let eventStreamSubscription: Subscription;
|
||||
|
||||
logger.debug("Event stream created, subscribing to event stream");
|
||||
|
||||
eventStreamSubscription = eventStream.subscribe({
|
||||
next: async (event) => {
|
||||
switch (event.type) {
|
||||
case RuntimeEventTypes.MetaEvent:
|
||||
break;
|
||||
////////////////////////////////
|
||||
// TextMessageStart
|
||||
////////////////////////////////
|
||||
case RuntimeEventTypes.TextMessageStart:
|
||||
// create a sub stream that contains the message content
|
||||
const textMessageContentStream = eventStream.pipe(
|
||||
// skip until this message start event
|
||||
skipWhile((e: RuntimeEvent) => e !== event),
|
||||
// take until the message end event
|
||||
takeWhile(
|
||||
(e: RuntimeEvent) =>
|
||||
!(
|
||||
e.type === RuntimeEventTypes.TextMessageEnd &&
|
||||
(e as any).messageId == event.messageId
|
||||
),
|
||||
),
|
||||
// filter out any other message events or message ids
|
||||
filter(
|
||||
(e: RuntimeEvent) =>
|
||||
e.type == RuntimeEventTypes.TextMessageContent &&
|
||||
(e as any).messageId == event.messageId,
|
||||
),
|
||||
);
|
||||
|
||||
// signal when we are done streaming
|
||||
const streamingTextStatus = new Subject<
|
||||
typeof MessageStatusUnion
|
||||
>();
|
||||
|
||||
const messageId = resolveMessageId(event.messageId);
|
||||
// push the new message
|
||||
pushMessage({
|
||||
id: messageId,
|
||||
parentMessageId: event.parentMessageId,
|
||||
status: firstValueFrom(streamingTextStatus),
|
||||
createdAt: new Date(),
|
||||
role: MessageRole.assistant,
|
||||
content: new Repeater(
|
||||
async (pushTextChunk, stopStreamingText) => {
|
||||
logger.debug("Text message content repeater created");
|
||||
|
||||
const textChunks: string[] = [];
|
||||
let textSubscription: Subscription;
|
||||
|
||||
interruptStreaming$
|
||||
.pipe(
|
||||
shareReplay(),
|
||||
take(1),
|
||||
tap(({ reason, messageId }) => {
|
||||
logger.debug(
|
||||
{ reason, messageId },
|
||||
"Text streaming interrupted",
|
||||
);
|
||||
|
||||
streamingTextStatus.next(
|
||||
plainToInstance(FailedMessageStatus, { reason }),
|
||||
);
|
||||
|
||||
responseStatus$.next(
|
||||
new MessageStreamInterruptedResponse({
|
||||
messageId,
|
||||
}),
|
||||
);
|
||||
stopStreamingText();
|
||||
textSubscription?.unsubscribe();
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
logger.debug(
|
||||
"Subscribing to text message content stream",
|
||||
);
|
||||
|
||||
textSubscription = textMessageContentStream.subscribe({
|
||||
next: async (e: RuntimeEvent) => {
|
||||
if (e.type == RuntimeEventTypes.TextMessageContent) {
|
||||
await pushTextChunk(e.content);
|
||||
textChunks.push(e.content);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
logger.error(
|
||||
{ err },
|
||||
"Error in text message content stream",
|
||||
);
|
||||
interruptStreaming$.next({
|
||||
reason: "Error streaming message content",
|
||||
messageId,
|
||||
});
|
||||
stopStreamingText();
|
||||
textSubscription?.unsubscribe();
|
||||
},
|
||||
complete: () => {
|
||||
logger.debug("Text message content stream completed");
|
||||
streamingTextStatus.next(new SuccessMessageStatus());
|
||||
stopStreamingText();
|
||||
textSubscription?.unsubscribe();
|
||||
|
||||
outputMessages.push(
|
||||
plainToInstance(TextMessage, {
|
||||
id: messageId,
|
||||
createdAt: new Date(),
|
||||
content: textChunks.join(""),
|
||||
role: MessageRole.assistant,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
break;
|
||||
////////////////////////////////
|
||||
// ActionExecutionStart
|
||||
////////////////////////////////
|
||||
case RuntimeEventTypes.ActionExecutionStart:
|
||||
logger.debug("Action execution start event received");
|
||||
const actionExecutionArgumentStream = eventStream.pipe(
|
||||
skipWhile((e: RuntimeEvent) => e !== event),
|
||||
// take until the action execution end event
|
||||
takeWhile(
|
||||
(e: RuntimeEvent) =>
|
||||
!(
|
||||
e.type === RuntimeEventTypes.ActionExecutionEnd &&
|
||||
(e as any).actionExecutionId == event.actionExecutionId
|
||||
),
|
||||
),
|
||||
// filter out any other action execution events or action execution ids
|
||||
filter(
|
||||
(e: RuntimeEvent) =>
|
||||
e.type == RuntimeEventTypes.ActionExecutionArgs &&
|
||||
(e as any).actionExecutionId == event.actionExecutionId,
|
||||
),
|
||||
);
|
||||
const streamingArgumentsStatus = new Subject<
|
||||
typeof MessageStatusUnion
|
||||
>();
|
||||
pushMessage({
|
||||
id: event.actionExecutionId,
|
||||
parentMessageId: event.parentMessageId,
|
||||
status: firstValueFrom(streamingArgumentsStatus),
|
||||
createdAt: new Date(),
|
||||
name: event.actionName,
|
||||
arguments: new Repeater(
|
||||
async (pushArgumentsChunk, stopStreamingArguments) => {
|
||||
logger.debug("Action execution argument stream created");
|
||||
|
||||
const argumentChunks: string[] = [];
|
||||
let actionExecutionArgumentSubscription: Subscription;
|
||||
|
||||
actionExecutionArgumentSubscription =
|
||||
actionExecutionArgumentStream.subscribe({
|
||||
next: async (e: RuntimeEvent) => {
|
||||
if (
|
||||
e.type == RuntimeEventTypes.ActionExecutionArgs
|
||||
) {
|
||||
await pushArgumentsChunk(e.args);
|
||||
argumentChunks.push(e.args);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
logger.error(
|
||||
{ err },
|
||||
"Error in action execution argument stream",
|
||||
);
|
||||
streamingArgumentsStatus.next(
|
||||
plainToInstance(FailedMessageStatus, {
|
||||
reason:
|
||||
"An unknown error has occurred in the action execution argument stream",
|
||||
}),
|
||||
);
|
||||
stopStreamingArguments();
|
||||
actionExecutionArgumentSubscription?.unsubscribe();
|
||||
},
|
||||
complete: () => {
|
||||
logger.debug(
|
||||
"Action execution argument stream completed",
|
||||
);
|
||||
streamingArgumentsStatus.next(
|
||||
new SuccessMessageStatus(),
|
||||
);
|
||||
stopStreamingArguments();
|
||||
actionExecutionArgumentSubscription?.unsubscribe();
|
||||
|
||||
outputMessages.push(
|
||||
plainToInstance(ActionExecutionMessage, {
|
||||
id: event.actionExecutionId,
|
||||
createdAt: new Date(),
|
||||
name: event.actionName,
|
||||
arguments: argumentChunks.join(""),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
break;
|
||||
////////////////////////////////
|
||||
// ActionExecutionResult
|
||||
////////////////////////////////
|
||||
case RuntimeEventTypes.ActionExecutionResult:
|
||||
logger.debug(
|
||||
{ result: event.result },
|
||||
"Action execution result event received",
|
||||
);
|
||||
pushMessage({
|
||||
id: "result-" + event.actionExecutionId,
|
||||
status: new SuccessMessageStatus(),
|
||||
createdAt: new Date(),
|
||||
actionExecutionId: event.actionExecutionId,
|
||||
actionName: event.actionName,
|
||||
result: event.result,
|
||||
});
|
||||
|
||||
outputMessages.push(
|
||||
plainToInstance(ResultMessage, {
|
||||
id: "result-" + event.actionExecutionId,
|
||||
createdAt: new Date(),
|
||||
actionExecutionId: event.actionExecutionId,
|
||||
actionName: event.actionName,
|
||||
result: event.result,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
////////////////////////////////
|
||||
// AgentStateMessage
|
||||
////////////////////////////////
|
||||
case RuntimeEventTypes.AgentStateMessage:
|
||||
logger.debug({ event }, "Agent message event received");
|
||||
pushMessage({
|
||||
id: randomId(),
|
||||
status: new SuccessMessageStatus(),
|
||||
threadId: event.threadId,
|
||||
agentName: event.agentName,
|
||||
nodeName: event.nodeName,
|
||||
runId: event.runId,
|
||||
active: event.active,
|
||||
state: event.state,
|
||||
running: event.running,
|
||||
role: MessageRole.assistant,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
outputMessages.push(
|
||||
plainToInstance(AgentStateMessage, {
|
||||
id: randomId(),
|
||||
threadId: event.threadId,
|
||||
agentName: event.agentName,
|
||||
nodeName: event.nodeName,
|
||||
runId: event.runId,
|
||||
active: event.active,
|
||||
state: event.state,
|
||||
running: event.running,
|
||||
role: MessageRole.assistant,
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
// For structured CopilotKit errors, set proper error response status
|
||||
if (
|
||||
err instanceof CopilotKitError ||
|
||||
err instanceof CopilotKitLowLevelError ||
|
||||
(err instanceof Error &&
|
||||
err.name &&
|
||||
err.name.includes("CopilotKit")) ||
|
||||
err?.extensions?.visibility
|
||||
) {
|
||||
responseStatus$.next(
|
||||
new UnknownErrorResponse({
|
||||
description: err.message || "Agent error occurred",
|
||||
// Include original error information for frontend to extract
|
||||
originalError: {
|
||||
code: err.code || err.extensions?.code,
|
||||
statusCode: err.statusCode || err.extensions?.statusCode,
|
||||
severity: err.severity || err.extensions?.severity,
|
||||
visibility: err.visibility || err.extensions?.visibility,
|
||||
originalErrorType:
|
||||
err.originalErrorType ||
|
||||
err.extensions?.originalErrorType,
|
||||
extensions: err.extensions,
|
||||
},
|
||||
}),
|
||||
);
|
||||
eventStreamSubscription?.unsubscribe();
|
||||
rejectOutputMessagesPromise(err);
|
||||
stopStreamingMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
responseStatus$.next(
|
||||
new UnknownErrorResponse({
|
||||
description: `An unknown error has occurred in the event stream`,
|
||||
}),
|
||||
);
|
||||
eventStreamSubscription?.unsubscribe();
|
||||
stopStreamingMessages();
|
||||
|
||||
rejectOutputMessagesPromise(err);
|
||||
},
|
||||
complete: async () => {
|
||||
logger.debug("Event stream completed");
|
||||
if (data.cloud?.guardrails) {
|
||||
logger.debug(
|
||||
"Guardrails is enabled, waiting for guardrails result",
|
||||
);
|
||||
await firstValueFrom(guardrailsResult$);
|
||||
}
|
||||
responseStatus$.next(new SuccessResponseStatus());
|
||||
eventStreamSubscription?.unsubscribe();
|
||||
stopStreamingMessages();
|
||||
|
||||
resolveOutputMessagesPromise(outputMessages);
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { randomId } from "@copilotkit/shared";
|
||||
|
||||
/**
|
||||
* Resolve the id to use for a streamed TextMessageOutput.
|
||||
*
|
||||
* Upstream events occasionally arrive with `messageId` missing, null, or an
|
||||
* empty string. We fall back to a freshly generated id in those cases so the
|
||||
* resulting output never surfaces a null id to the GraphQL client (#2118).
|
||||
*/
|
||||
export function resolveMessageId(
|
||||
eventMessageId: string | null | undefined,
|
||||
): string {
|
||||
return eventMessageId || randomId();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Arg, Resolver } from "type-graphql";
|
||||
import { Ctx } from "type-graphql";
|
||||
import { Query } from "type-graphql";
|
||||
import { LoadAgentStateResponse } from "../types/load-agent-state-response.type";
|
||||
import type { GraphQLContext } from "../../lib/integrations";
|
||||
import { LoadAgentStateInput } from "../inputs/load-agent-state.input";
|
||||
import { CopilotKitAgentDiscoveryError } from "@copilotkit/shared";
|
||||
import { CopilotRuntime } from "../../lib";
|
||||
|
||||
@Resolver(() => LoadAgentStateResponse)
|
||||
export class StateResolver {
|
||||
@Query(() => LoadAgentStateResponse)
|
||||
async loadAgentState(
|
||||
@Ctx() ctx: GraphQLContext,
|
||||
@Arg("data") data: LoadAgentStateInput,
|
||||
) {
|
||||
const agents = [];
|
||||
const hasAgent = agents.some((agent) => agent.name === data.agentName);
|
||||
if (!hasAgent) {
|
||||
throw new CopilotKitAgentDiscoveryError({
|
||||
agentName: data.agentName,
|
||||
availableAgents: agents.map((a) => ({ name: a.name, id: a.name })),
|
||||
});
|
||||
}
|
||||
|
||||
const state = {};
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Field, ObjectType } from "type-graphql";
|
||||
|
||||
@ObjectType()
|
||||
export class Agent {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AgentsResponse {
|
||||
@Field(() => [Agent])
|
||||
agents: Agent[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from "type-graphql";
|
||||
|
||||
@InputType()
|
||||
export class BaseMessageInput {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { randomId } from "@copilotkit/shared";
|
||||
import {
|
||||
ActionExecutionMessageInput,
|
||||
ResultMessageInput,
|
||||
TextMessageInput,
|
||||
AgentStateMessageInput,
|
||||
ImageMessageInput,
|
||||
} from "../../inputs/message.input";
|
||||
import { BaseMessageInput } from "../base";
|
||||
import { BaseMessageOutput } from "../copilot-response.type";
|
||||
import { MessageRole } from "../enums";
|
||||
import { MessageStatus, MessageStatusCode } from "../message-status.type";
|
||||
|
||||
export 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";
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionExecutionMessage
|
||||
extends Message
|
||||
implements Omit<ActionExecutionMessageInput, "arguments" | "scope">
|
||||
{
|
||||
type: MessageType = "ActionExecutionMessage";
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
parentMessageId?: string;
|
||||
}
|
||||
|
||||
export class ResultMessage extends Message implements ResultMessageInput {
|
||||
type: MessageType = "ResultMessage";
|
||||
actionExecutionId: string;
|
||||
actionName: string;
|
||||
result: string;
|
||||
|
||||
static encodeResult(
|
||||
result: any,
|
||||
error?: { code: string; message: string } | string | Error,
|
||||
): string {
|
||||
const errorObj = error
|
||||
? typeof error === "string"
|
||||
? { code: "ERROR", message: error }
|
||||
: error instanceof Error
|
||||
? { code: "ERROR", message: error.message }
|
||||
: error
|
||||
: undefined;
|
||||
|
||||
if (errorObj) {
|
||||
return JSON.stringify({
|
||||
error: errorObj,
|
||||
result: result || "",
|
||||
});
|
||||
}
|
||||
if (result === undefined) {
|
||||
return "";
|
||||
}
|
||||
return typeof result === "string" ? result : JSON.stringify(result);
|
||||
}
|
||||
|
||||
static decodeResult(result: string): {
|
||||
error?: { code: string; message: string };
|
||||
result: string;
|
||||
} {
|
||||
if (!result) {
|
||||
return { result: "" };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(result);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
if ("error" in parsed) {
|
||||
return {
|
||||
error: parsed.error,
|
||||
result: parsed.result || "",
|
||||
};
|
||||
}
|
||||
return { result: JSON.stringify(parsed) };
|
||||
}
|
||||
return { result };
|
||||
} catch (e) {
|
||||
return { result };
|
||||
}
|
||||
}
|
||||
|
||||
hasError(): boolean {
|
||||
try {
|
||||
const { error } = ResultMessage.decodeResult(this.result);
|
||||
return !!error;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getError(): { code: string; message: string } | undefined {
|
||||
try {
|
||||
const { error } = ResultMessage.decodeResult(this.result);
|
||||
return error;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentStateMessage
|
||||
extends Message
|
||||
implements Omit<AgentStateMessageInput, "state">
|
||||
{
|
||||
type: MessageType = "AgentStateMessage";
|
||||
threadId: string;
|
||||
agentName: string;
|
||||
nodeName: string;
|
||||
runId: string;
|
||||
active: boolean;
|
||||
role: MessageRole;
|
||||
state: any;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export class ImageMessage extends Message implements ImageMessageInput {
|
||||
type: MessageType = "ImageMessage";
|
||||
format: string;
|
||||
bytes: string;
|
||||
role: MessageRole;
|
||||
parentMessageId?: string;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Field, InterfaceType, ObjectType } from "type-graphql";
|
||||
import { MessageRole } from "./enums";
|
||||
import { MessageStatusUnion } from "./message-status.type";
|
||||
import { ResponseStatusUnion } from "./response-status.type";
|
||||
import { ExtensionsResponse } from "./extensions-response.type";
|
||||
import { BaseMetaEvent } from "./meta-events.type";
|
||||
|
||||
@InterfaceType({
|
||||
resolveType(value) {
|
||||
if (value.hasOwnProperty("content")) {
|
||||
return TextMessageOutput;
|
||||
} else if (value.hasOwnProperty("name")) {
|
||||
return ActionExecutionMessageOutput;
|
||||
} else if (value.hasOwnProperty("result")) {
|
||||
return ResultMessageOutput;
|
||||
} else if (value.hasOwnProperty("state")) {
|
||||
return AgentStateMessageOutput;
|
||||
} else if (
|
||||
value.hasOwnProperty("format") &&
|
||||
value.hasOwnProperty("bytes")
|
||||
) {
|
||||
return ImageMessageOutput;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
})
|
||||
export abstract class BaseMessageOutput {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
|
||||
@Field(() => MessageStatusUnion)
|
||||
status: typeof MessageStatusUnion;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMessageOutput })
|
||||
export class TextMessageOutput {
|
||||
@Field(() => MessageRole)
|
||||
role: MessageRole;
|
||||
|
||||
@Field(() => [String])
|
||||
content: string[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMessageOutput })
|
||||
export class ActionExecutionMessageOutput {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: true,
|
||||
deprecationReason: "This field will be removed in a future version",
|
||||
})
|
||||
scope?: string;
|
||||
|
||||
@Field(() => [String])
|
||||
arguments: string[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMessageOutput })
|
||||
export class ResultMessageOutput {
|
||||
@Field(() => String)
|
||||
actionExecutionId: string;
|
||||
|
||||
@Field(() => String)
|
||||
actionName: string;
|
||||
|
||||
@Field(() => String)
|
||||
result: string;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMessageOutput })
|
||||
export class AgentStateMessageOutput {
|
||||
@Field(() => String)
|
||||
threadId: string;
|
||||
|
||||
@Field(() => String)
|
||||
agentName: string;
|
||||
|
||||
@Field(() => String)
|
||||
nodeName: string;
|
||||
|
||||
@Field(() => String)
|
||||
runId: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
active: boolean;
|
||||
|
||||
@Field(() => MessageRole)
|
||||
role: MessageRole;
|
||||
|
||||
@Field(() => String)
|
||||
state: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMessageOutput })
|
||||
export class ImageMessageOutput {
|
||||
@Field(() => String)
|
||||
format: string;
|
||||
|
||||
@Field(() => String)
|
||||
bytes: string;
|
||||
|
||||
@Field(() => MessageRole)
|
||||
role: MessageRole;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
parentMessageId?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class CopilotResponse {
|
||||
@Field(() => String)
|
||||
threadId!: string;
|
||||
|
||||
@Field(() => ResponseStatusUnion)
|
||||
status: typeof ResponseStatusUnion;
|
||||
|
||||
@Field({ nullable: true })
|
||||
runId?: string;
|
||||
|
||||
@Field(() => [BaseMessageOutput])
|
||||
messages: (typeof BaseMessageOutput)[];
|
||||
|
||||
@Field(() => ExtensionsResponse, { nullable: true })
|
||||
extensions?: ExtensionsResponse;
|
||||
|
||||
@Field(() => [BaseMetaEvent], { nullable: true })
|
||||
metaEvents?: (typeof BaseMetaEvent)[];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { registerEnumType } from "type-graphql";
|
||||
|
||||
export enum MessageRole {
|
||||
assistant = "assistant",
|
||||
developer = "developer",
|
||||
system = "system",
|
||||
tool = "tool",
|
||||
user = "user",
|
||||
}
|
||||
|
||||
export enum CopilotRequestType {
|
||||
Chat = "Chat",
|
||||
Task = "Task",
|
||||
TextareaCompletion = "TextareaCompletion",
|
||||
TextareaPopover = "TextareaPopover",
|
||||
Suggestion = "Suggestion",
|
||||
}
|
||||
|
||||
export enum ActionInputAvailability {
|
||||
disabled = "disabled",
|
||||
enabled = "enabled",
|
||||
remote = "remote",
|
||||
}
|
||||
|
||||
registerEnumType(MessageRole, {
|
||||
name: "MessageRole",
|
||||
description: "The role of the message",
|
||||
});
|
||||
|
||||
registerEnumType(CopilotRequestType, {
|
||||
name: "CopilotRequestType",
|
||||
description: "The type of Copilot request",
|
||||
});
|
||||
|
||||
registerEnumType(ActionInputAvailability, {
|
||||
name: "ActionInputAvailability",
|
||||
description: "The availability of the frontend action",
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Field, ObjectType } from "type-graphql";
|
||||
|
||||
/**
|
||||
* The extensions response is used to receive additional information from the copilot runtime, specific to a
|
||||
* service adapter or agent framework.
|
||||
*
|
||||
* Next time a request to the runtime is made, the extensions response will be included in the request as input.
|
||||
*/
|
||||
|
||||
@ObjectType()
|
||||
export class OpenAIApiAssistantAPIResponse {
|
||||
@Field(() => String, { nullable: true })
|
||||
runId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
threadId?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class ExtensionsResponse {
|
||||
@Field(() => OpenAIApiAssistantAPIResponse, { nullable: true })
|
||||
openaiAssistantAPI?: OpenAIApiAssistantAPIResponse;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Field, ObjectType, registerEnumType } from "type-graphql";
|
||||
|
||||
export enum GuardrailsResultStatus {
|
||||
ALLOWED = "allowed",
|
||||
DENIED = "denied",
|
||||
}
|
||||
|
||||
registerEnumType(GuardrailsResultStatus, {
|
||||
name: "GuardrailsResultStatus",
|
||||
description: "The status of the guardrails check",
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class GuardrailsResult {
|
||||
@Field(() => GuardrailsResultStatus)
|
||||
status: GuardrailsResultStatus;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
reason?: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Field, ObjectType } from "type-graphql";
|
||||
import { BaseMessageOutput } from "./copilot-response.type";
|
||||
|
||||
@ObjectType()
|
||||
export class LoadAgentStateResponse {
|
||||
@Field(() => String)
|
||||
threadId: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
threadExists: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
state: string;
|
||||
|
||||
@Field(() => String)
|
||||
messages: string;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Field,
|
||||
ObjectType,
|
||||
createUnionType,
|
||||
registerEnumType,
|
||||
} from "type-graphql";
|
||||
|
||||
export enum MessageStatusCode {
|
||||
Pending = "pending",
|
||||
Success = "success",
|
||||
Failed = "failed",
|
||||
}
|
||||
|
||||
registerEnumType(MessageStatusCode, {
|
||||
name: "MessageStatusCode",
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class BaseMessageStatus {
|
||||
@Field(() => MessageStatusCode)
|
||||
code: MessageStatusCode;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PendingMessageStatus extends BaseMessageStatus {
|
||||
code: MessageStatusCode = MessageStatusCode.Pending;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class SuccessMessageStatus extends BaseMessageStatus {
|
||||
code: MessageStatusCode = MessageStatusCode.Success;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class FailedMessageStatus extends BaseMessageStatus {
|
||||
code: MessageStatusCode = MessageStatusCode.Failed;
|
||||
|
||||
@Field(() => String)
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export const MessageStatusUnion = createUnionType({
|
||||
name: "MessageStatus",
|
||||
types: () =>
|
||||
[PendingMessageStatus, SuccessMessageStatus, FailedMessageStatus] as const,
|
||||
});
|
||||
|
||||
export type MessageStatus = typeof MessageStatusUnion;
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
createUnionType,
|
||||
Field,
|
||||
InterfaceType,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from "type-graphql";
|
||||
import {
|
||||
ActionExecutionMessageOutput,
|
||||
AgentStateMessageOutput,
|
||||
BaseMessageOutput,
|
||||
ResultMessageOutput,
|
||||
TextMessageOutput,
|
||||
} from "./copilot-response.type";
|
||||
|
||||
export enum MetaEventName {
|
||||
LangGraphInterruptEvent = "LangGraphInterruptEvent",
|
||||
CopilotKitLangGraphInterruptEvent = "CopilotKitLangGraphInterruptEvent",
|
||||
}
|
||||
|
||||
registerEnumType(MetaEventName, {
|
||||
name: "MetaEventName",
|
||||
description: "Meta event types",
|
||||
});
|
||||
|
||||
@InterfaceType({
|
||||
resolveType(value) {
|
||||
if (value.name === MetaEventName.LangGraphInterruptEvent) {
|
||||
return LangGraphInterruptEvent;
|
||||
} else if (value.name === MetaEventName.CopilotKitLangGraphInterruptEvent) {
|
||||
return CopilotKitLangGraphInterruptEvent;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
})
|
||||
@InterfaceType()
|
||||
export abstract class BaseMetaEvent {
|
||||
@Field(() => String)
|
||||
type: "MetaEvent" = "MetaEvent";
|
||||
|
||||
@Field(() => MetaEventName)
|
||||
name: MetaEventName;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class CopilotKitLangGraphInterruptEventData {
|
||||
@Field(() => String)
|
||||
value: string;
|
||||
|
||||
@Field(() => [BaseMessageOutput])
|
||||
messages: (typeof BaseMessageOutput)[];
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMetaEvent })
|
||||
export class LangGraphInterruptEvent {
|
||||
@Field(() => MetaEventName)
|
||||
name: MetaEventName.LangGraphInterruptEvent =
|
||||
MetaEventName.LangGraphInterruptEvent;
|
||||
|
||||
@Field(() => String)
|
||||
value: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
response?: string;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseMetaEvent })
|
||||
export class CopilotKitLangGraphInterruptEvent {
|
||||
@Field(() => MetaEventName)
|
||||
name: MetaEventName.CopilotKitLangGraphInterruptEvent =
|
||||
MetaEventName.CopilotKitLangGraphInterruptEvent;
|
||||
|
||||
@Field(() => CopilotKitLangGraphInterruptEventData)
|
||||
data: CopilotKitLangGraphInterruptEventData;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
response?: string;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { GraphQLJSON } from "graphql-scalars";
|
||||
import {
|
||||
Field,
|
||||
InterfaceType,
|
||||
ObjectType,
|
||||
createUnionType,
|
||||
registerEnumType,
|
||||
} from "type-graphql";
|
||||
|
||||
export enum ResponseStatusCode {
|
||||
Pending = "pending",
|
||||
Success = "success",
|
||||
Failed = "failed",
|
||||
}
|
||||
|
||||
registerEnumType(ResponseStatusCode, {
|
||||
name: "ResponseStatusCode",
|
||||
});
|
||||
|
||||
@InterfaceType({
|
||||
resolveType(value) {
|
||||
if (value.code === ResponseStatusCode.Success) {
|
||||
return SuccessResponseStatus;
|
||||
} else if (value.code === ResponseStatusCode.Failed) {
|
||||
return FailedResponseStatus;
|
||||
} else if (value.code === ResponseStatusCode.Pending) {
|
||||
return PendingResponseStatus;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
})
|
||||
@ObjectType()
|
||||
abstract class BaseResponseStatus {
|
||||
@Field(() => ResponseStatusCode)
|
||||
code: ResponseStatusCode;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseResponseStatus })
|
||||
export class PendingResponseStatus extends BaseResponseStatus {
|
||||
code: ResponseStatusCode = ResponseStatusCode.Pending;
|
||||
}
|
||||
|
||||
@ObjectType({ implements: BaseResponseStatus })
|
||||
export class SuccessResponseStatus extends BaseResponseStatus {
|
||||
code: ResponseStatusCode = ResponseStatusCode.Success;
|
||||
}
|
||||
|
||||
export enum FailedResponseStatusReason {
|
||||
GUARDRAILS_VALIDATION_FAILED = "GUARDRAILS_VALIDATION_FAILED",
|
||||
MESSAGE_STREAM_INTERRUPTED = "MESSAGE_STREAM_INTERRUPTED",
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
||||
}
|
||||
|
||||
registerEnumType(FailedResponseStatusReason, {
|
||||
name: "FailedResponseStatusReason",
|
||||
});
|
||||
|
||||
@ObjectType({ implements: BaseResponseStatus })
|
||||
export class FailedResponseStatus extends BaseResponseStatus {
|
||||
code: ResponseStatusCode = ResponseStatusCode.Failed;
|
||||
|
||||
@Field(() => FailedResponseStatusReason)
|
||||
reason: FailedResponseStatusReason;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
details?: Record<string, any> = null;
|
||||
}
|
||||
|
||||
export const ResponseStatusUnion = createUnionType({
|
||||
name: "ResponseStatus",
|
||||
types: () =>
|
||||
[
|
||||
PendingResponseStatus,
|
||||
SuccessResponseStatus,
|
||||
FailedResponseStatus,
|
||||
] as const,
|
||||
});
|
||||
Reference in New Issue
Block a user