204 lines
7.9 KiB
TypeScript
204 lines
7.9 KiB
TypeScript
import {
|
|
ChatRequest,
|
|
FunctionCall,
|
|
JSONValue,
|
|
Message,
|
|
ToolCall,
|
|
} from './types';
|
|
|
|
export async function processChatStream({
|
|
getStreamedResponse,
|
|
experimental_onFunctionCall,
|
|
experimental_onToolCall,
|
|
updateChatRequest,
|
|
getCurrentMessages,
|
|
}: {
|
|
getStreamedResponse: () => Promise<
|
|
Message | { messages: Message[]; data: JSONValue[] }
|
|
>;
|
|
experimental_onFunctionCall?: (
|
|
chatMessages: Message[],
|
|
functionCall: FunctionCall,
|
|
) => Promise<void | ChatRequest>;
|
|
experimental_onToolCall?: (
|
|
chatMessages: Message[],
|
|
toolCalls: ToolCall[],
|
|
) => Promise<void | ChatRequest>;
|
|
updateChatRequest: (chatRequest: ChatRequest) => void;
|
|
getCurrentMessages: () => Message[];
|
|
}) {
|
|
while (true) {
|
|
// TODO-STREAMDATA: This should be { const { messages: streamedResponseMessages, data } =
|
|
// await getStreamedResponse(} once Stream Data is not experimental
|
|
const messagesAndDataOrJustMessage = await getStreamedResponse();
|
|
|
|
// Using experimental stream data
|
|
if ('messages' in messagesAndDataOrJustMessage) {
|
|
let hasFollowingResponse = false;
|
|
|
|
for (const message of messagesAndDataOrJustMessage.messages) {
|
|
// See if the message has a complete function call or tool call
|
|
if (
|
|
(message.function_call === undefined ||
|
|
typeof message.function_call === 'string') &&
|
|
(message.tool_calls === undefined ||
|
|
typeof message.tool_calls === 'string')
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
hasFollowingResponse = true;
|
|
// Try to handle function call
|
|
if (experimental_onFunctionCall) {
|
|
const functionCall = message.function_call;
|
|
// Make sure functionCall is an object
|
|
// If not, we got tool calls instead of function calls
|
|
if (typeof functionCall !== 'object') {
|
|
console.warn(
|
|
'experimental_onFunctionCall should not be defined when using tools',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// User handles the function call in their own functionCallHandler.
|
|
// The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler.
|
|
// If the "arguments" JSON is malformed due to model error the user will have to handle that themselves.
|
|
|
|
const functionCallResponse: ChatRequest | void =
|
|
await experimental_onFunctionCall(
|
|
getCurrentMessages(),
|
|
functionCall,
|
|
);
|
|
|
|
// If the user does not return anything as a result of the function call, the loop will break.
|
|
if (functionCallResponse === undefined) {
|
|
hasFollowingResponse = false;
|
|
break;
|
|
}
|
|
|
|
// A function call response was returned.
|
|
// The updated chat with function call response will be sent to the API in the next iteration of the loop.
|
|
updateChatRequest(functionCallResponse);
|
|
}
|
|
// Try to handle tool call
|
|
if (experimental_onToolCall) {
|
|
const toolCalls = message.tool_calls;
|
|
// Make sure toolCalls is an array of objects
|
|
// If not, we got function calls instead of tool calls
|
|
if (
|
|
!Array.isArray(toolCalls) ||
|
|
toolCalls.some(toolCall => typeof toolCall !== 'object')
|
|
) {
|
|
console.warn(
|
|
'experimental_onToolCall should not be defined when using tools',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// User handles the function call in their own functionCallHandler.
|
|
// The "arguments" key of the function call object will still be a string which will have to be parsed in the function handler.
|
|
// If the "arguments" JSON is malformed due to model error the user will have to handle that themselves.
|
|
const toolCallResponse: ChatRequest | void =
|
|
await experimental_onToolCall(getCurrentMessages(), toolCalls);
|
|
|
|
// If the user does not return anything as a result of the function call, the loop will break.
|
|
if (toolCallResponse === undefined) {
|
|
hasFollowingResponse = false;
|
|
break;
|
|
}
|
|
|
|
// A function call response was returned.
|
|
// The updated chat with function call response will be sent to the API in the next iteration of the loop.
|
|
updateChatRequest(toolCallResponse);
|
|
}
|
|
}
|
|
if (!hasFollowingResponse) {
|
|
break;
|
|
}
|
|
} else {
|
|
const streamedResponseMessage = messagesAndDataOrJustMessage;
|
|
|
|
// TODO-STREAMDATA: Remove this once Stream Data is not experimental
|
|
if (
|
|
(streamedResponseMessage.function_call === undefined ||
|
|
typeof streamedResponseMessage.function_call === 'string') &&
|
|
(streamedResponseMessage.tool_calls === undefined ||
|
|
typeof streamedResponseMessage.tool_calls === 'string')
|
|
) {
|
|
break;
|
|
}
|
|
|
|
// If we get here and are expecting a function call, the message should have one, if not warn and continue
|
|
if (experimental_onFunctionCall) {
|
|
const functionCall = streamedResponseMessage.function_call;
|
|
if (!(typeof functionCall === 'object')) {
|
|
console.warn(
|
|
'experimental_onFunctionCall should not be defined when using tools',
|
|
);
|
|
continue;
|
|
}
|
|
const functionCallResponse: ChatRequest | void =
|
|
await experimental_onFunctionCall(getCurrentMessages(), functionCall);
|
|
|
|
// If the user does not return anything as a result of the function call, the loop will break.
|
|
if (functionCallResponse === undefined) break;
|
|
// A function call response was returned.
|
|
// The updated chat with function call response will be sent to the API in the next iteration of the loop.
|
|
fixFunctionCallArguments(functionCallResponse);
|
|
updateChatRequest(functionCallResponse);
|
|
}
|
|
// If we get here and are expecting a tool call, the message should have one, if not warn and continue
|
|
if (experimental_onToolCall) {
|
|
const toolCalls = streamedResponseMessage.tool_calls;
|
|
if (!(typeof toolCalls === 'object')) {
|
|
console.warn(
|
|
'experimental_onToolCall should not be defined when using functions',
|
|
);
|
|
continue;
|
|
}
|
|
const toolCallResponse: ChatRequest | void =
|
|
await experimental_onToolCall(getCurrentMessages(), toolCalls);
|
|
|
|
// If the user does not return anything as a result of the function call, the loop will break.
|
|
if (toolCallResponse === undefined) break;
|
|
// A function call response was returned.
|
|
// The updated chat with function call response will be sent to the API in the next iteration of the loop.
|
|
fixFunctionCallArguments(toolCallResponse);
|
|
updateChatRequest(toolCallResponse);
|
|
}
|
|
|
|
// Make sure function call arguments are sent back to the API as a string
|
|
function fixFunctionCallArguments(response: ChatRequest) {
|
|
for (const message of response.messages) {
|
|
if (message.tool_calls !== undefined) {
|
|
for (const toolCall of message.tool_calls) {
|
|
if (typeof toolCall === 'object') {
|
|
if (
|
|
toolCall.function.arguments &&
|
|
typeof toolCall.function.arguments !== 'string'
|
|
) {
|
|
toolCall.function.arguments = JSON.stringify(
|
|
toolCall.function.arguments,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (message.function_call !== undefined) {
|
|
if (typeof message.function_call === 'object') {
|
|
if (
|
|
message.function_call.arguments &&
|
|
typeof message.function_call.arguments !== 'string'
|
|
) {
|
|
message.function_call.arguments = JSON.stringify(
|
|
message.function_call.arguments,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|