UNPKG

@copilotkit/runtime

Version:

<div align="center"> <a href="https://copilotkit.ai" target="_blank"> <img src="https://github.com/copilotkit/copilotkit/raw/main/assets/banner.png" alt="CopilotKit Logo"> </a>

323 lines (293 loc) 11.7 kB
import { catchError, mergeMap, ReplaySubject, scan } from "rxjs"; import { CustomEventNames, LangGraphEvent, LangGraphEventTypes } from "./events"; import { RuntimeEvent, RuntimeEventTypes, RuntimeMetaEventName, } from "../../service-adapters/events"; import { randomId } from "@copilotkit/shared"; interface LangGraphEventWithState { event: LangGraphEvent | null; isMessageStart: boolean; isMessageEnd: boolean; isToolCallStart: boolean; isToolCallEnd: boolean; isToolCall: boolean; lastMessageId: string | null; lastToolCallId: string | null; lastToolCallName: string | null; currentContent: string | null; processedToolCallIds: Set<string>; } export class RemoteLangGraphEventSource { public eventStream$ = new ReplaySubject<LangGraphEvent>(); private shouldEmitToolCall( shouldEmitToolCalls: string | string[] | boolean, toolCallName: string, ) { if (typeof shouldEmitToolCalls === "boolean") { return shouldEmitToolCalls; } if (Array.isArray(shouldEmitToolCalls)) { return shouldEmitToolCalls.includes(toolCallName); } return shouldEmitToolCalls === toolCallName; } private getCurrentContent(event: LangGraphEvent) { // @ts-expect-error -- LangGraph Platform implementation stores data outside of kwargs const content = event.data?.chunk?.kwargs?.content ?? event.data?.chunk?.content; if (!content) { const toolCallChunks = this.getCurrentToolCallChunks(event) ?? []; for (const chunk of toolCallChunks) { if (chunk.args) { return chunk.args; } } } if (typeof content === "string") { return content; } else if (Array.isArray(content) && content.length > 0) { return content[0].text; } return null; } private getCurrentMessageId(event: LangGraphEvent) { // @ts-expect-error -- LangGraph Platform implementation stores data outside of kwargs return event.data?.chunk?.kwargs?.id ?? event.data?.chunk?.id; } private getCurrentToolCallChunks(event: LangGraphEvent) { // @ts-expect-error -- LangGraph Platform implementation stores data outside of kwargs return event.data?.chunk?.kwargs?.tool_call_chunks ?? event.data?.chunk?.tool_call_chunks; } private getResponseMetadata(event: LangGraphEvent) { // @ts-expect-error -- LangGraph Platform implementation stores data outside of kwargs return event.data?.chunk?.kwargs?.response_metadata ?? event.data?.chunk?.response_metadata; } processLangGraphEvents() { let lastEventWithState: LangGraphEventWithState | null = null; return this.eventStream$.pipe( scan( (acc, event) => { if (event.event === LangGraphEventTypes.OnChatModelStream) { const prevMessageId = acc.lastMessageId; acc.currentContent = this.getCurrentContent(event); acc.lastMessageId = this.getCurrentMessageId(event) ?? acc.lastMessageId; const toolCallChunks = this.getCurrentToolCallChunks(event) ?? []; const responseMetadata = this.getResponseMetadata(event); // Check if a given event is a tool call const toolCallCheck = toolCallChunks && toolCallChunks.length > 0; let isToolCallEnd = responseMetadata?.finish_reason === "tool_calls"; acc.isToolCallStart = toolCallChunks.some((chunk: any) => chunk.name && chunk.id); acc.isMessageStart = prevMessageId !== acc.lastMessageId && !acc.isToolCallStart; let previousRoundHadToolCall = acc.isToolCall; acc.isToolCall = toolCallCheck; // Previous "acc.isToolCall" was set but now it won't pass the check, it means the tool call just ended. if (previousRoundHadToolCall && !toolCallCheck) { isToolCallEnd = true; } acc.isToolCallEnd = isToolCallEnd; acc.isMessageEnd = responseMetadata?.finish_reason === "stop"; ({ name: acc.lastToolCallName, id: acc.lastToolCallId } = toolCallChunks.find( (chunk: any) => chunk.name && chunk.id, ) ?? { name: acc.lastToolCallName, id: acc.lastToolCallId }); } acc.event = event; lastEventWithState = acc; // Capture the state return acc; }, { event: null, isMessageStart: false, isMessageEnd: false, isToolCallStart: false, isToolCallEnd: false, isToolCall: false, lastMessageId: null, lastToolCallId: null, lastToolCallName: null, currentContent: null, processedToolCallIds: new Set<string>(), } as LangGraphEventWithState, ), mergeMap((acc): RuntimeEvent[] => { const events: RuntimeEvent[] = []; let shouldEmitMessages = true; let shouldEmitToolCalls: string | string[] | boolean = true; if (acc.event.event == LangGraphEventTypes.OnChatModelStream) { if ("copilotkit:emit-tool-calls" in (acc.event.metadata || {})) { shouldEmitToolCalls = acc.event.metadata["copilotkit:emit-tool-calls"]; } if ("copilotkit:emit-messages" in (acc.event.metadata || {})) { shouldEmitMessages = acc.event.metadata["copilotkit:emit-messages"]; } } if (acc.event.event === LangGraphEventTypes.OnInterrupt) { events.push({ type: RuntimeEventTypes.MetaEvent, name: RuntimeMetaEventName.LangGraphInterruptEvent, value: acc.event.value, }); } if (acc.event.event === LangGraphEventTypes.OnCopilotKitInterrupt) { events.push({ type: RuntimeEventTypes.MetaEvent, name: RuntimeMetaEventName.CopilotKitLangGraphInterruptEvent, data: acc.event.data, }); } const responseMetadata = this.getResponseMetadata(acc.event); // Tool call ended: emit ActionExecutionEnd if ( acc.isToolCallEnd && this.shouldEmitToolCall(shouldEmitToolCalls, acc.lastToolCallName) && acc.lastToolCallId && !acc.processedToolCallIds.has(acc.lastToolCallId) ) { acc.processedToolCallIds.add(acc.lastToolCallId); events.push({ type: RuntimeEventTypes.ActionExecutionEnd, actionExecutionId: acc.lastToolCallId, }); } // Message ended: emit TextMessageEnd else if (responseMetadata?.finish_reason === "stop" && shouldEmitMessages) { events.push({ type: RuntimeEventTypes.TextMessageEnd, messageId: acc.lastMessageId, }); } switch (acc.event!.event) { // // Custom events // case LangGraphEventTypes.OnCustomEvent: // // Manually emit a message // if (acc.event.name === CustomEventNames.CopilotKitManuallyEmitMessage) { events.push({ type: RuntimeEventTypes.TextMessageStart, messageId: acc.event.data.message_id, }); events.push({ type: RuntimeEventTypes.TextMessageContent, messageId: acc.event.data.message_id, content: acc.event.data.message, }); events.push({ type: RuntimeEventTypes.TextMessageEnd, messageId: acc.event.data.message_id, }); } // // Manually emit a tool call // else if (acc.event.name === CustomEventNames.CopilotKitManuallyEmitToolCall) { events.push({ type: RuntimeEventTypes.ActionExecutionStart, actionExecutionId: acc.event.data.id, actionName: acc.event.data.name, parentMessageId: acc.event.data.id, }); events.push({ type: RuntimeEventTypes.ActionExecutionArgs, actionExecutionId: acc.event.data.id, args: JSON.stringify(acc.event.data.args), }); events.push({ type: RuntimeEventTypes.ActionExecutionEnd, actionExecutionId: acc.event.data.id, }); } break; case LangGraphEventTypes.OnCopilotKitStateSync: events.push({ type: RuntimeEventTypes.AgentStateMessage, threadId: acc.event.thread_id, role: acc.event.role, agentName: acc.event.agent_name, nodeName: acc.event.node_name, runId: acc.event.run_id, active: acc.event.active, state: JSON.stringify(acc.event.state), running: acc.event.running, }); break; case LangGraphEventTypes.OnChatModelStream: if ( acc.isToolCallStart && this.shouldEmitToolCall(shouldEmitToolCalls, acc.lastToolCallName) ) { events.push({ type: RuntimeEventTypes.ActionExecutionStart, actionExecutionId: acc.lastToolCallId, actionName: acc.lastToolCallName, parentMessageId: acc.lastMessageId, }); } // Message started: emit TextMessageStart else if (acc.isMessageStart && shouldEmitMessages) { acc.processedToolCallIds.clear(); events.push({ type: RuntimeEventTypes.TextMessageStart, messageId: acc.lastMessageId, }); } // Tool call args: emit ActionExecutionArgs if ( acc.isToolCall && acc.currentContent && this.shouldEmitToolCall(shouldEmitToolCalls, acc.lastToolCallName) ) { events.push({ type: RuntimeEventTypes.ActionExecutionArgs, actionExecutionId: acc.lastToolCallId, args: acc.currentContent, }); } // Message content: emit TextMessageContent else if (!acc.isToolCall && acc.currentContent && shouldEmitMessages) { events.push({ type: RuntimeEventTypes.TextMessageContent, messageId: acc.lastMessageId, content: acc.currentContent, }); } break; } return events; }), catchError((error) => { console.error(error); const events: RuntimeEvent[] = []; if (lastEventWithState?.lastMessageId && !lastEventWithState.isToolCall) { events.push({ type: RuntimeEventTypes.TextMessageEnd, messageId: lastEventWithState.lastMessageId, }); } if (lastEventWithState?.lastToolCallId) { events.push({ type: RuntimeEventTypes.ActionExecutionEnd, actionExecutionId: lastEventWithState.lastToolCallId, }); } const messageId = randomId(); events.push({ type: RuntimeEventTypes.TextMessageStart, messageId: messageId, }); events.push({ type: RuntimeEventTypes.TextMessageContent, messageId: messageId, content: "❌ An error occurred. Please try again.", }); events.push({ type: RuntimeEventTypes.TextMessageEnd, messageId: messageId, }); return events; }), ); } }