@copilotkit/runtime 0.0.0-0.0.0-max-changeset-10101010101010-20260109191632
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +7 -0
- package/CHANGELOG.md +2905 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/__snapshots__/schema/schema.graphql +371 -0
- package/dist/index.d.ts +1495 -0
- package/dist/index.js +5644 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5601 -0
- package/dist/index.mjs.map +1 -0
- package/dist/langgraph.d.ts +284 -0
- package/dist/langgraph.js +211 -0
- package/dist/langgraph.js.map +1 -0
- package/dist/langgraph.mjs +206 -0
- package/dist/langgraph.mjs.map +1 -0
- package/dist/v2/index.d.ts +2 -0
- package/dist/v2/index.js +22 -0
- package/dist/v2/index.js.map +1 -0
- package/dist/v2/index.mjs +5 -0
- package/dist/v2/index.mjs.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +143 -0
- package/scripts/generate-gql-schema.ts +13 -0
- package/src/agents/langgraph/event-source.ts +329 -0
- package/src/agents/langgraph/events.ts +377 -0
- package/src/graphql/inputs/action.input.ts +16 -0
- package/src/graphql/inputs/agent-session.input.ts +13 -0
- package/src/graphql/inputs/agent-state.input.ts +13 -0
- package/src/graphql/inputs/cloud-guardrails.input.ts +16 -0
- package/src/graphql/inputs/cloud.input.ts +8 -0
- package/src/graphql/inputs/context-property.input.ts +10 -0
- package/src/graphql/inputs/copilot-context.input.ts +10 -0
- package/src/graphql/inputs/custom-property.input.ts +15 -0
- package/src/graphql/inputs/extensions.input.ts +21 -0
- package/src/graphql/inputs/forwarded-parameters.input.ts +22 -0
- package/src/graphql/inputs/frontend.input.ts +14 -0
- package/src/graphql/inputs/generate-copilot-response.input.ts +59 -0
- package/src/graphql/inputs/load-agent-state.input.ts +10 -0
- package/src/graphql/inputs/message.input.ts +110 -0
- package/src/graphql/inputs/meta-event.input.ts +18 -0
- package/src/graphql/message-conversion/agui-to-gql.test.ts +1263 -0
- package/src/graphql/message-conversion/agui-to-gql.ts +333 -0
- package/src/graphql/message-conversion/gql-to-agui.test.ts +1580 -0
- package/src/graphql/message-conversion/gql-to-agui.ts +278 -0
- package/src/graphql/message-conversion/index.ts +2 -0
- package/src/graphql/message-conversion/roundtrip-conversion.test.ts +526 -0
- package/src/graphql/resolvers/copilot.resolver.ts +708 -0
- package/src/graphql/resolvers/state.resolver.ts +27 -0
- package/src/graphql/types/agents-response.type.ts +19 -0
- package/src/graphql/types/base/index.ts +10 -0
- package/src/graphql/types/converted/index.ts +176 -0
- package/src/graphql/types/copilot-response.type.ts +138 -0
- package/src/graphql/types/enums.ts +38 -0
- package/src/graphql/types/extensions-response.type.ts +23 -0
- package/src/graphql/types/guardrails-result.type.ts +20 -0
- package/src/graphql/types/load-agent-state-response.type.ts +17 -0
- package/src/graphql/types/message-status.type.ts +42 -0
- package/src/graphql/types/meta-events.type.ts +71 -0
- package/src/graphql/types/response-status.type.ts +66 -0
- package/src/index.ts +4 -0
- package/src/langgraph.ts +1 -0
- package/src/lib/cloud/index.ts +4 -0
- package/src/lib/error-messages.ts +200 -0
- package/src/lib/index.ts +52 -0
- package/src/lib/integrations/index.ts +6 -0
- package/src/lib/integrations/nest/index.ts +14 -0
- package/src/lib/integrations/nextjs/app-router.ts +38 -0
- package/src/lib/integrations/nextjs/pages-router.ts +39 -0
- package/src/lib/integrations/node-express/index.ts +14 -0
- package/src/lib/integrations/node-http/index.ts +143 -0
- package/src/lib/integrations/node-http/request-handler.ts +111 -0
- package/src/lib/integrations/shared.ts +161 -0
- package/src/lib/logger.ts +28 -0
- package/src/lib/observability.ts +160 -0
- package/src/lib/runtime/__tests__/copilot-runtime-error.test.ts +169 -0
- package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +464 -0
- package/src/lib/runtime/agent-integrations/langgraph/agent.ts +209 -0
- package/src/lib/runtime/agent-integrations/langgraph/consts.ts +34 -0
- package/src/lib/runtime/agent-integrations/langgraph/index.ts +2 -0
- package/src/lib/runtime/copilot-runtime.ts +710 -0
- package/src/lib/runtime/mcp-tools-utils.ts +254 -0
- package/src/lib/runtime/retry-utils.ts +96 -0
- package/src/lib/runtime/telemetry-agent-runner.ts +139 -0
- package/src/lib/runtime/types.ts +49 -0
- package/src/lib/runtime/utils.ts +87 -0
- package/src/lib/streaming.ts +202 -0
- package/src/lib/telemetry-client.ts +64 -0
- package/src/service-adapters/anthropic/anthropic-adapter.ts +452 -0
- package/src/service-adapters/anthropic/utils.ts +152 -0
- package/src/service-adapters/bedrock/bedrock-adapter.ts +73 -0
- package/src/service-adapters/conversion.ts +67 -0
- package/src/service-adapters/empty/empty-adapter.ts +38 -0
- package/src/service-adapters/events.ts +294 -0
- package/src/service-adapters/experimental/ollama/ollama-adapter.ts +84 -0
- package/src/service-adapters/google/google-genai-adapter.test.ts +104 -0
- package/src/service-adapters/google/google-genai-adapter.ts +88 -0
- package/src/service-adapters/groq/groq-adapter.ts +203 -0
- package/src/service-adapters/index.ts +18 -0
- package/src/service-adapters/langchain/langchain-adapter.ts +111 -0
- package/src/service-adapters/langchain/langserve.ts +88 -0
- package/src/service-adapters/langchain/types.ts +14 -0
- package/src/service-adapters/langchain/utils.ts +313 -0
- package/src/service-adapters/openai/openai-adapter.ts +283 -0
- package/src/service-adapters/openai/openai-assistant-adapter.ts +344 -0
- package/src/service-adapters/openai/utils.ts +199 -0
- package/src/service-adapters/service-adapter.ts +41 -0
- package/src/service-adapters/shared/error-utils.ts +61 -0
- package/src/service-adapters/shared/index.ts +1 -0
- package/src/service-adapters/unify/unify-adapter.ts +151 -0
- package/src/utils/failed-response-status-reasons.ts +70 -0
- package/src/utils/index.ts +1 -0
- package/src/v2/index.ts +3 -0
- package/tests/global.d.ts +13 -0
- package/tests/service-adapters/anthropic/allowlist-approach.test.ts +226 -0
- package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +389 -0
- package/tests/service-adapters/openai/allowlist-approach.test.ts +238 -0
- package/tests/service-adapters/openai/openai-adapter.test.ts +301 -0
- package/tests/setup.jest.ts +21 -0
- package/tests/tsconfig.json +10 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +20 -0
- package/typedoc.json +4 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionExecutionMessage,
|
|
3
|
+
Message,
|
|
4
|
+
ResultMessage,
|
|
5
|
+
TextMessage,
|
|
6
|
+
} from "../../graphql/types/converted";
|
|
7
|
+
import {
|
|
8
|
+
AIMessage,
|
|
9
|
+
AIMessageChunk,
|
|
10
|
+
BaseMessage,
|
|
11
|
+
BaseMessageChunk,
|
|
12
|
+
HumanMessage,
|
|
13
|
+
SystemMessage,
|
|
14
|
+
ToolMessage,
|
|
15
|
+
} from "@langchain/core/messages";
|
|
16
|
+
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { ActionInput } from "../../graphql/inputs/action.input";
|
|
19
|
+
import { LangChainReturnType } from "./types";
|
|
20
|
+
import { RuntimeEventSubject } from "../events";
|
|
21
|
+
import { randomId, convertJsonSchemaToZodSchema } from "@copilotkit/shared";
|
|
22
|
+
|
|
23
|
+
export function convertMessageToLangChainMessage(message: Message): BaseMessage {
|
|
24
|
+
if (message.isTextMessage()) {
|
|
25
|
+
if (message.role == "user") {
|
|
26
|
+
return new HumanMessage(message.content);
|
|
27
|
+
} else if (message.role == "assistant") {
|
|
28
|
+
return new AIMessage(message.content);
|
|
29
|
+
} else if (message.role === "system") {
|
|
30
|
+
return new SystemMessage(message.content);
|
|
31
|
+
}
|
|
32
|
+
} else if (message.isActionExecutionMessage()) {
|
|
33
|
+
return new AIMessage({
|
|
34
|
+
content: "",
|
|
35
|
+
tool_calls: [
|
|
36
|
+
{
|
|
37
|
+
id: message.id,
|
|
38
|
+
args: message.arguments,
|
|
39
|
+
name: message.name,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
} else if (message.isResultMessage()) {
|
|
44
|
+
return new ToolMessage({
|
|
45
|
+
content: message.result,
|
|
46
|
+
tool_call_id: message.actionExecutionId,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function convertActionInputToLangChainTool(actionInput: ActionInput): any {
|
|
52
|
+
return new DynamicStructuredTool({
|
|
53
|
+
...actionInput,
|
|
54
|
+
name: actionInput.name,
|
|
55
|
+
description: actionInput.description,
|
|
56
|
+
schema: convertJsonSchemaToZodSchema(
|
|
57
|
+
JSON.parse(actionInput.jsonSchema),
|
|
58
|
+
true,
|
|
59
|
+
) as z.ZodObject<any>,
|
|
60
|
+
func: async () => {
|
|
61
|
+
return "";
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface StreamLangChainResponseParams {
|
|
67
|
+
result: LangChainReturnType;
|
|
68
|
+
eventStream$: RuntimeEventSubject;
|
|
69
|
+
actionExecution?: {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
returnDirect?: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getConstructorName(object: any): string {
|
|
77
|
+
if (object && typeof object === "object" && object.constructor && object.constructor.name) {
|
|
78
|
+
return object.constructor.name;
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isAIMessage(message: any): message is AIMessage {
|
|
84
|
+
return Object.prototype.toString.call(message) === "[object AIMessage]";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isAIMessageChunk(message: any): message is AIMessageChunk {
|
|
88
|
+
return Object.prototype.toString.call(message) === "[object AIMessageChunk]";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isBaseMessageChunk(message: any): message is BaseMessageChunk {
|
|
92
|
+
return Object.prototype.toString.call(message) === "[object BaseMessageChunk]";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function maybeSendActionExecutionResultIsMessage(
|
|
96
|
+
eventStream$: RuntimeEventSubject,
|
|
97
|
+
actionExecution?: { id: string; name: string },
|
|
98
|
+
) {
|
|
99
|
+
// language models need a result after the function call
|
|
100
|
+
// we simply let them know that we are sending a message
|
|
101
|
+
if (actionExecution) {
|
|
102
|
+
eventStream$.sendActionExecutionResult({
|
|
103
|
+
actionExecutionId: actionExecution.id,
|
|
104
|
+
actionName: actionExecution.name,
|
|
105
|
+
result: "Sending a message",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function streamLangChainResponse({
|
|
111
|
+
result,
|
|
112
|
+
eventStream$,
|
|
113
|
+
actionExecution,
|
|
114
|
+
}: StreamLangChainResponseParams) {
|
|
115
|
+
// We support several types of return values from LangChain functions:
|
|
116
|
+
|
|
117
|
+
// 1. string
|
|
118
|
+
|
|
119
|
+
if (typeof result === "string") {
|
|
120
|
+
if (!actionExecution || actionExecution?.returnDirect) {
|
|
121
|
+
// Just send one chunk with the string as the content.
|
|
122
|
+
eventStream$.sendActionExecutionResult({
|
|
123
|
+
actionExecutionId: actionExecution.id,
|
|
124
|
+
actionName: actionExecution.name,
|
|
125
|
+
result: result,
|
|
126
|
+
});
|
|
127
|
+
eventStream$.sendTextMessage(randomId(), result);
|
|
128
|
+
} else {
|
|
129
|
+
// Send as a result
|
|
130
|
+
eventStream$.sendActionExecutionResult({
|
|
131
|
+
actionExecutionId: actionExecution.id,
|
|
132
|
+
actionName: actionExecution.name,
|
|
133
|
+
result: result,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 2. AIMessage
|
|
139
|
+
// Send the content and function call of the AIMessage as the content of the chunk.
|
|
140
|
+
else if (isAIMessage(result)) {
|
|
141
|
+
maybeSendActionExecutionResultIsMessage(eventStream$, actionExecution);
|
|
142
|
+
|
|
143
|
+
if (result.content) {
|
|
144
|
+
eventStream$.sendTextMessage(randomId(), result.content as string);
|
|
145
|
+
}
|
|
146
|
+
for (const toolCall of result.tool_calls) {
|
|
147
|
+
eventStream$.sendActionExecution({
|
|
148
|
+
actionExecutionId: toolCall.id || randomId(),
|
|
149
|
+
actionName: toolCall.name,
|
|
150
|
+
args: JSON.stringify(toolCall.args),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 3. BaseMessageChunk
|
|
156
|
+
// Send the content and function call of the AIMessage as the content of the chunk.
|
|
157
|
+
else if (isBaseMessageChunk(result)) {
|
|
158
|
+
maybeSendActionExecutionResultIsMessage(eventStream$, actionExecution);
|
|
159
|
+
|
|
160
|
+
if (result.lc_kwargs?.content) {
|
|
161
|
+
eventStream$.sendTextMessage(randomId(), result.content as string);
|
|
162
|
+
}
|
|
163
|
+
if (result.lc_kwargs?.tool_calls) {
|
|
164
|
+
for (const toolCall of result.lc_kwargs?.tool_calls) {
|
|
165
|
+
eventStream$.sendActionExecution({
|
|
166
|
+
actionExecutionId: toolCall.id || randomId(),
|
|
167
|
+
actionName: toolCall.name,
|
|
168
|
+
args: JSON.stringify(toolCall.args),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. IterableReadableStream
|
|
175
|
+
// Stream the result of the LangChain function.
|
|
176
|
+
else if (result && "getReader" in result) {
|
|
177
|
+
maybeSendActionExecutionResultIsMessage(eventStream$, actionExecution);
|
|
178
|
+
|
|
179
|
+
let reader = result.getReader();
|
|
180
|
+
|
|
181
|
+
let mode: "function" | "message" | null = null;
|
|
182
|
+
let currentMessageId: string;
|
|
183
|
+
|
|
184
|
+
const toolCallDetails = {
|
|
185
|
+
name: null,
|
|
186
|
+
id: null,
|
|
187
|
+
index: null,
|
|
188
|
+
prevIndex: null,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
while (true) {
|
|
192
|
+
try {
|
|
193
|
+
const { done, value } = await reader.read();
|
|
194
|
+
|
|
195
|
+
let toolCallName: string | undefined = undefined;
|
|
196
|
+
let toolCallId: string | undefined = undefined;
|
|
197
|
+
let toolCallArgs: string | undefined = undefined;
|
|
198
|
+
let hasToolCall: boolean = false;
|
|
199
|
+
let content = "";
|
|
200
|
+
if (value && value.content) {
|
|
201
|
+
content = Array.isArray(value.content)
|
|
202
|
+
? (((value.content[0] as any)?.text ?? "") as string)
|
|
203
|
+
: value.content;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (isAIMessageChunk(value)) {
|
|
207
|
+
let chunk = value.tool_call_chunks?.[0];
|
|
208
|
+
toolCallArgs = chunk?.args;
|
|
209
|
+
hasToolCall = chunk != undefined;
|
|
210
|
+
if (chunk?.name) toolCallDetails.name = chunk.name;
|
|
211
|
+
// track different index on the same tool cool
|
|
212
|
+
if (chunk?.index != null) {
|
|
213
|
+
toolCallDetails.index = chunk.index; // 1
|
|
214
|
+
if (toolCallDetails.prevIndex == null) toolCallDetails.prevIndex = chunk.index;
|
|
215
|
+
}
|
|
216
|
+
// Differentiate when calling the same tool but with different index
|
|
217
|
+
if (chunk?.id)
|
|
218
|
+
toolCallDetails.id = chunk.index != null ? `${chunk.id}-idx-${chunk.index}` : chunk.id;
|
|
219
|
+
|
|
220
|
+
// Assign to internal variables that the entire script here knows how to work with
|
|
221
|
+
toolCallName = toolCallDetails.name;
|
|
222
|
+
toolCallId = toolCallDetails.id;
|
|
223
|
+
} else if (isBaseMessageChunk(value)) {
|
|
224
|
+
let chunk = value.additional_kwargs?.tool_calls?.[0];
|
|
225
|
+
toolCallName = chunk?.function?.name;
|
|
226
|
+
toolCallId = chunk?.id;
|
|
227
|
+
toolCallArgs = chunk?.function?.arguments;
|
|
228
|
+
hasToolCall = chunk?.function != undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// When switching from message to function or vice versa,
|
|
232
|
+
// send the respective end event.
|
|
233
|
+
// If toolCallName is defined, it means a new tool call starts.
|
|
234
|
+
if (mode === "message" && (toolCallId || done)) {
|
|
235
|
+
mode = null;
|
|
236
|
+
eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
|
|
237
|
+
} else if (mode === "function" && (!hasToolCall || done)) {
|
|
238
|
+
mode = null;
|
|
239
|
+
eventStream$.sendActionExecutionEnd({ actionExecutionId: toolCallId });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (done) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// If we send a new message type, send the appropriate start event.
|
|
247
|
+
if (mode === null) {
|
|
248
|
+
if (hasToolCall && toolCallId && toolCallName) {
|
|
249
|
+
mode = "function";
|
|
250
|
+
eventStream$.sendActionExecutionStart({
|
|
251
|
+
actionExecutionId: toolCallId,
|
|
252
|
+
actionName: toolCallName,
|
|
253
|
+
parentMessageId: value.lc_kwargs?.id,
|
|
254
|
+
});
|
|
255
|
+
} else if (content) {
|
|
256
|
+
mode = "message";
|
|
257
|
+
currentMessageId = value.lc_kwargs?.id || randomId();
|
|
258
|
+
eventStream$.sendTextMessageStart({ messageId: currentMessageId });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// send the content events
|
|
263
|
+
if (mode === "message" && content) {
|
|
264
|
+
eventStream$.sendTextMessageContent({
|
|
265
|
+
messageId: currentMessageId,
|
|
266
|
+
content,
|
|
267
|
+
});
|
|
268
|
+
} else if (mode === "function" && toolCallArgs) {
|
|
269
|
+
// For calls of the same tool with different index, we seal last tool call and register a new one
|
|
270
|
+
if (toolCallDetails.index !== toolCallDetails.prevIndex) {
|
|
271
|
+
eventStream$.sendActionExecutionEnd({ actionExecutionId: toolCallId });
|
|
272
|
+
eventStream$.sendActionExecutionStart({
|
|
273
|
+
actionExecutionId: toolCallId,
|
|
274
|
+
actionName: toolCallName,
|
|
275
|
+
parentMessageId: value.lc_kwargs?.id,
|
|
276
|
+
});
|
|
277
|
+
toolCallDetails.prevIndex = toolCallDetails.index;
|
|
278
|
+
}
|
|
279
|
+
eventStream$.sendActionExecutionArgs({
|
|
280
|
+
actionExecutionId: toolCallId,
|
|
281
|
+
args: toolCallArgs,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error("Error reading from stream", error);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else if (actionExecution) {
|
|
290
|
+
eventStream$.sendActionExecutionResult({
|
|
291
|
+
actionExecutionId: actionExecution.id,
|
|
292
|
+
actionName: actionExecution.name,
|
|
293
|
+
result: encodeResult(result),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// unsupported type
|
|
298
|
+
else {
|
|
299
|
+
throw new Error("Invalid return type from LangChain function.");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
eventStream$.complete();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function encodeResult(result: any): string {
|
|
306
|
+
if (result === undefined) {
|
|
307
|
+
return "";
|
|
308
|
+
} else if (typeof result === "string") {
|
|
309
|
+
return result;
|
|
310
|
+
} else {
|
|
311
|
+
return JSON.stringify(result);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copilot Runtime adapter for OpenAI.
|
|
3
|
+
*
|
|
4
|
+
* ## Example
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/runtime";
|
|
8
|
+
* import OpenAI from "openai";
|
|
9
|
+
*
|
|
10
|
+
* const copilotKit = new CopilotRuntime();
|
|
11
|
+
*
|
|
12
|
+
* const openai = new OpenAI({
|
|
13
|
+
* organization: "<your-organization-id>", // optional
|
|
14
|
+
* apiKey: "<your-api-key>",
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* return new OpenAIAdapter({ openai });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* ## Example with Azure OpenAI
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/runtime";
|
|
24
|
+
* import OpenAI from "openai";
|
|
25
|
+
*
|
|
26
|
+
* // The name of your Azure OpenAI Instance.
|
|
27
|
+
* // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
|
|
28
|
+
* const instance = "<your instance name>";
|
|
29
|
+
*
|
|
30
|
+
* // Corresponds to your Model deployment within your OpenAI resource, e.g. my-gpt35-16k-deployment
|
|
31
|
+
* // Navigate to the Azure OpenAI Studio to deploy a model.
|
|
32
|
+
* const model = "<your model>";
|
|
33
|
+
*
|
|
34
|
+
* const apiKey = process.env["AZURE_OPENAI_API_KEY"];
|
|
35
|
+
* if (!apiKey) {
|
|
36
|
+
* throw new Error("The AZURE_OPENAI_API_KEY environment variable is missing or empty.");
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* const copilotKit = new CopilotRuntime();
|
|
40
|
+
*
|
|
41
|
+
* const openai = new OpenAI({
|
|
42
|
+
* apiKey,
|
|
43
|
+
* baseURL: `https://${instance}.openai.azure.com/openai/deployments/${model}`,
|
|
44
|
+
* defaultQuery: { "api-version": "2024-04-01-preview" },
|
|
45
|
+
* defaultHeaders: { "api-key": apiKey },
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* return new OpenAIAdapter({ openai });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
import type OpenAI from "openai";
|
|
52
|
+
import {
|
|
53
|
+
CopilotServiceAdapter,
|
|
54
|
+
CopilotRuntimeChatCompletionRequest,
|
|
55
|
+
CopilotRuntimeChatCompletionResponse,
|
|
56
|
+
} from "../service-adapter";
|
|
57
|
+
import {
|
|
58
|
+
convertActionInputToOpenAITool,
|
|
59
|
+
convertMessageToOpenAIMessage,
|
|
60
|
+
limitMessagesToTokenCount,
|
|
61
|
+
} from "./utils";
|
|
62
|
+
import { randomUUID } from "@copilotkit/shared";
|
|
63
|
+
import { convertServiceAdapterError } from "../shared";
|
|
64
|
+
|
|
65
|
+
const DEFAULT_MODEL = "gpt-4o";
|
|
66
|
+
|
|
67
|
+
export interface OpenAIAdapterParams {
|
|
68
|
+
/**
|
|
69
|
+
* An optional OpenAI instance to use. If not provided, a new instance will be
|
|
70
|
+
* created.
|
|
71
|
+
*/
|
|
72
|
+
openai?: OpenAI;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The model to use.
|
|
76
|
+
*/
|
|
77
|
+
model?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether to disable parallel tool calls.
|
|
81
|
+
* You can disable parallel tool calls to force the model to execute tool calls sequentially.
|
|
82
|
+
* This is useful if you want to execute tool calls in a specific order so that the state changes
|
|
83
|
+
* introduced by one tool call are visible to the next tool call. (i.e. new actions or readables)
|
|
84
|
+
*
|
|
85
|
+
* @default false
|
|
86
|
+
*/
|
|
87
|
+
disableParallelToolCalls?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether to keep the role in system messages as "System".
|
|
91
|
+
* By default, it is converted to "developer", which is used by newer OpenAI models
|
|
92
|
+
*
|
|
93
|
+
* @default false
|
|
94
|
+
*/
|
|
95
|
+
keepSystemRole?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class OpenAIAdapter implements CopilotServiceAdapter {
|
|
99
|
+
public model: string = DEFAULT_MODEL;
|
|
100
|
+
public provider = "openai";
|
|
101
|
+
|
|
102
|
+
private disableParallelToolCalls: boolean = false;
|
|
103
|
+
private _openai: OpenAI;
|
|
104
|
+
private keepSystemRole: boolean = false;
|
|
105
|
+
|
|
106
|
+
public get openai(): OpenAI {
|
|
107
|
+
return this._openai;
|
|
108
|
+
}
|
|
109
|
+
public get name() {
|
|
110
|
+
return "OpenAIAdapter";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
constructor(params?: OpenAIAdapterParams) {
|
|
114
|
+
if (params?.openai) {
|
|
115
|
+
this._openai = params.openai;
|
|
116
|
+
}
|
|
117
|
+
// If no instance provided, we'll lazy-load in ensureOpenAI()
|
|
118
|
+
|
|
119
|
+
if (params?.model) {
|
|
120
|
+
this.model = params.model;
|
|
121
|
+
}
|
|
122
|
+
this.disableParallelToolCalls = params?.disableParallelToolCalls || false;
|
|
123
|
+
this.keepSystemRole = params?.keepSystemRole ?? false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private ensureOpenAI(): OpenAI {
|
|
127
|
+
if (!this._openai) {
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
129
|
+
const OpenAI = require("openai").default;
|
|
130
|
+
this._openai = new OpenAI();
|
|
131
|
+
}
|
|
132
|
+
return this._openai;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async process(
|
|
136
|
+
request: CopilotRuntimeChatCompletionRequest,
|
|
137
|
+
): Promise<CopilotRuntimeChatCompletionResponse> {
|
|
138
|
+
const {
|
|
139
|
+
threadId: threadIdFromRequest,
|
|
140
|
+
model = this.model,
|
|
141
|
+
messages,
|
|
142
|
+
actions,
|
|
143
|
+
eventSource,
|
|
144
|
+
forwardedParameters,
|
|
145
|
+
} = request;
|
|
146
|
+
const tools = actions.map(convertActionInputToOpenAITool);
|
|
147
|
+
const threadId = threadIdFromRequest ?? randomUUID();
|
|
148
|
+
|
|
149
|
+
// ALLOWLIST APPROACH: Only include tool_result messages that correspond to valid tool_calls
|
|
150
|
+
// Step 1: Extract valid tool_call IDs
|
|
151
|
+
const validToolUseIds = new Set<string>();
|
|
152
|
+
|
|
153
|
+
for (const message of messages) {
|
|
154
|
+
if (message.isActionExecutionMessage()) {
|
|
155
|
+
validToolUseIds.add(message.id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Step 2: Filter messages, keeping only those with valid tool_call IDs
|
|
160
|
+
const filteredMessages = messages.filter((message) => {
|
|
161
|
+
if (message.isResultMessage()) {
|
|
162
|
+
// Skip if there's no corresponding tool_call
|
|
163
|
+
if (!validToolUseIds.has(message.actionExecutionId)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove this ID from valid IDs so we don't process duplicates
|
|
168
|
+
validToolUseIds.delete(message.actionExecutionId);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Keep all non-tool-result messages
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
let openaiMessages = filteredMessages.map((m) =>
|
|
177
|
+
convertMessageToOpenAIMessage(m, { keepSystemRole: this.keepSystemRole }),
|
|
178
|
+
);
|
|
179
|
+
openaiMessages = limitMessagesToTokenCount(openaiMessages, tools, model);
|
|
180
|
+
|
|
181
|
+
let toolChoice: any = forwardedParameters?.toolChoice;
|
|
182
|
+
if (forwardedParameters?.toolChoice === "function") {
|
|
183
|
+
toolChoice = {
|
|
184
|
+
type: "function",
|
|
185
|
+
function: { name: forwardedParameters.toolChoiceFunctionName },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const openai = this.ensureOpenAI();
|
|
191
|
+
const stream = openai.beta.chat.completions.stream({
|
|
192
|
+
model: model,
|
|
193
|
+
stream: true,
|
|
194
|
+
messages: openaiMessages,
|
|
195
|
+
...(tools.length > 0 && { tools }),
|
|
196
|
+
...(forwardedParameters?.maxTokens && {
|
|
197
|
+
max_completion_tokens: forwardedParameters.maxTokens,
|
|
198
|
+
}),
|
|
199
|
+
...(forwardedParameters?.stop && { stop: forwardedParameters.stop }),
|
|
200
|
+
...(toolChoice && { tool_choice: toolChoice }),
|
|
201
|
+
...(this.disableParallelToolCalls && { parallel_tool_calls: false }),
|
|
202
|
+
...(forwardedParameters?.temperature && { temperature: forwardedParameters.temperature }),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
eventSource.stream(async (eventStream$) => {
|
|
206
|
+
let mode: "function" | "message" | null = null;
|
|
207
|
+
let currentMessageId: string;
|
|
208
|
+
let currentToolCallId: string;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
for await (const chunk of stream) {
|
|
212
|
+
if (chunk.choices.length === 0) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const toolCall = chunk.choices[0].delta.tool_calls?.[0];
|
|
217
|
+
const content = chunk.choices[0].delta.content;
|
|
218
|
+
|
|
219
|
+
// When switching from message to function or vice versa,
|
|
220
|
+
// send the respective end event.
|
|
221
|
+
// If toolCall?.id is defined, it means a new tool call starts.
|
|
222
|
+
if (mode === "message" && toolCall?.id) {
|
|
223
|
+
mode = null;
|
|
224
|
+
eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
|
|
225
|
+
} else if (mode === "function" && (toolCall === undefined || toolCall?.id)) {
|
|
226
|
+
mode = null;
|
|
227
|
+
eventStream$.sendActionExecutionEnd({ actionExecutionId: currentToolCallId });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// If we send a new message type, send the appropriate start event.
|
|
231
|
+
if (mode === null) {
|
|
232
|
+
if (toolCall?.id) {
|
|
233
|
+
mode = "function";
|
|
234
|
+
currentToolCallId = toolCall!.id;
|
|
235
|
+
eventStream$.sendActionExecutionStart({
|
|
236
|
+
actionExecutionId: currentToolCallId,
|
|
237
|
+
parentMessageId: chunk.id,
|
|
238
|
+
actionName: toolCall!.function!.name,
|
|
239
|
+
});
|
|
240
|
+
} else if (content) {
|
|
241
|
+
mode = "message";
|
|
242
|
+
currentMessageId = chunk.id;
|
|
243
|
+
eventStream$.sendTextMessageStart({ messageId: currentMessageId });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// send the content events
|
|
248
|
+
if (mode === "message" && content) {
|
|
249
|
+
eventStream$.sendTextMessageContent({
|
|
250
|
+
messageId: currentMessageId,
|
|
251
|
+
content: content,
|
|
252
|
+
});
|
|
253
|
+
} else if (mode === "function" && toolCall?.function?.arguments) {
|
|
254
|
+
eventStream$.sendActionExecutionArgs({
|
|
255
|
+
actionExecutionId: currentToolCallId,
|
|
256
|
+
args: toolCall.function.arguments,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// send the end events
|
|
262
|
+
if (mode === "message") {
|
|
263
|
+
eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
|
|
264
|
+
} else if (mode === "function") {
|
|
265
|
+
eventStream$.sendActionExecutionEnd({ actionExecutionId: currentToolCallId });
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error("[OpenAI] Error during API call:", error);
|
|
269
|
+
throw convertServiceAdapterError(error, "OpenAI");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
eventStream$.complete();
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error("[OpenAI] Error during API call:", error);
|
|
276
|
+
throw convertServiceAdapterError(error, "OpenAI");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
threadId,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|