@convo-lang/convo-lang
Version:
The language of AI
355 lines (350 loc) • 14.8 kB
JavaScript
import { uuid, zodTypeToJsonScheme } from "@iyio/common";
import { parseJson5 } from "@iyio/json5";
import { ConvoError } from "./ConvoError.js";
import { appendFlatConvoMessageSuffix, convoAnyModelName, convoTags, createFunctionCallConvoCompletionMessage, createTextConvoCompletionMessage, getConvoCompletionServiceModelsAsync, getLastConvoMessageWithRole, insertSystemMessageIntoFlatConvo, isConvoModelAliasMatch, parseConvoJsonMessage } from "./convo-lib.js";
import { convoTypeToJsonScheme } from "./convo-zod.js";
export const convertConvoInput = (flat, inputType, converters) => {
for (const converter of converters) {
if (converter.supportedInputTypes.includes(inputType)) {
return {
success: true,
converter,
result: converter.convertConvoToInput(flat, inputType),
};
}
}
return {
success: false
};
};
export const convertConvoOutput = (output, outputType, input, inputType, converters, flat) => {
for (const converter of converters) {
if (converter.supportedOutputTypes.includes(outputType)) {
return {
success: true,
converter,
result: converter.convertOutputToConvo(output, outputType, input, inputType, flat),
};
}
}
return {
success: false
};
};
export const requireConvertConvoInput = (flat, inputType, converters) => {
const r = convertConvoInput(flat, inputType, converters);
if (!r.success) {
throw new Error(`No convo converter found for input type - ${inputType}`);
}
return r.result;
};
export const requireConvertConvoOutput = (output, outputType, input, inputType, converters, flat) => {
const r = convertConvoOutput(output, outputType, input, inputType, converters, flat);
if (!r.success) {
throw new Error(`No convo converter found for output type - ${outputType}`);
}
return r.result;
};
export const completeConvoUsingCompletionServiceAsync = async (flat, service, converters, ctx = {}) => {
if (!service) {
return [];
}
const input = requireConvertConvoInput(flat, service.inputType, converters);
const r = await service.completeConvoAsync(input, flat, ctx);
await ctx.afterComplete?.(service, r, input, flat);
return requireConvertConvoOutput(r, service.outputType, input, service.inputType, converters, flat);
};
export const getConvoCompletionServiceAsync = async (flat, services, updateTargetModel = false, cache) => {
const serviceModels = await getConvoCompletionServicesForModelAsync(flat.responseModel ?? convoAnyModelName, services, cache);
for (const s of serviceModels) {
if (s.service?.canComplete(s.model?.name ?? flat.responseModel ?? convoAnyModelName, flat)) {
if (updateTargetModel && s.model) {
flat.responseModel = s.model.name;
flat.model = s.model;
}
return s;
}
}
return undefined;
};
export const getConvoCompletionServicesForModelAsync = async (model, services, cache) => {
const cached = cache?.[model];
if (cached) {
return cached;
}
const matches = [];
for (const s of services) {
const models = await getConvoCompletionServiceModelsAsync(s);
if (!models.length) {
matches.push({ priority: Number.MIN_SAFE_INTEGER, service: { service: s } });
continue;
}
let hasMatch = false;
for (const m of models) {
if (m.name === model) {
matches.push({ priority: m.priority ?? 0, service: { service: s, model: m } });
hasMatch = true;
}
if (m.aliases) {
for (const a of m.aliases) {
if (isConvoModelAliasMatch(model, a)) {
matches.push({ priority: a.priority ?? 0, service: { service: s, model: m } });
hasMatch = true;
}
}
}
}
if (!hasMatch) {
const m = models.find(m => m.isServiceDefault);
if (m) {
matches.push({ priority: Number.MIN_SAFE_INTEGER, service: { service: s, model: m } });
}
}
}
matches.sort((a, b) => b.priority - a.priority);
const service = matches.map(m => m.service);
if (cache) {
cache[model] = service;
}
return service;
};
export const applyConvoModelConfigurationToInputAsync = async (model, flat, convo) => {
const lastMsg = getLastConvoMessageWithRole(flat.messages, 'user');
const jsonMode = lastMsg?.responseFormat === 'json';
let hasFunctions = false;
let fnSystem;
// first role requirement
const roleRequireAry = model.requiredFirstMessageRole ? (model.requiredFirstMessageRoleList ?? ['user', 'assistant']) : undefined;
let firstRequiredRoleChecked = roleRequireAry === undefined;
for (let i = 0; i < flat.messages.length; i++) {
const msg = flat.messages[i];
if (!msg) {
continue;
}
if (jsonMode &&
(model.jsonModeDisableFunctions || model.jsonModeImplementAsFunction) &&
msg?.fn &&
!msg.called) {
flat.messages.splice(i, 1);
i--;
continue;
}
if (msg.responseFormat === 'json' && msg === lastMsg) {
applyJsonModeToMessage(msg, model, flat);
}
if (msg.fn && !msg.called) {
hasFunctions = true;
}
if (!model.supportsFunctionCalling) {
if (msg.fn && !msg.called) {
if (!fnSystem) {
fnSystem = [];
}
fnSystem.push(`<function>\nName: ${msg.fn.name}\nDescription: ${msg.fn.description ?? ''}\nParameters JSON Scheme: ${JSON.stringify((msg._fnParams ?? (msg.fnParams ? (zodTypeToJsonScheme(msg.fnParams) ?? {}) : {})))}\n</function>\n`);
flat.messages.splice(i, 1);
i--;
}
else if (msg.called) {
const updated = { ...msg };
flat.messages[i] = updated;
delete updated.called;
updated.role = 'assistant';
const fnCall = {
functionName: msg.called.name,
parameters: msg.calledParams ?? {}
};
updated.content = JSON.stringify(fnCall, null, 4);
const resultMsg = {
role: 'user',
content: (`The return value of calling ${msg.called.name} is:\`\`\` json\n${msg.calledReturn === undefined ? 'undefined' : JSON.stringify(msg.calledReturn, null, 4)}\n\`\`\``)
};
flat.messages.splice(i + 1, 0, resultMsg);
}
}
if (!firstRequiredRoleChecked && roleRequireAry?.includes(msg.role)) {
firstRequiredRoleChecked = true;
if (msg.role !== model.requiredFirstMessageRole) {
const msg = {
role: model.requiredFirstMessageRole ?? 'user',
content: model.requiredFirstMessageRoleContent ?? 'You can start the conversation',
tags: { [convoTags.hidden]: '' }
};
if (convo.isUserMessage(msg)) {
msg.isUser = true;
}
else if (convo.isAssistantMessage(msg)) {
msg.isAssistant = true;
}
else if (convo.isSystemMessage(msg)) {
msg.isSystem = true;
}
flat.messages.splice(i, 0, msg);
i++;
continue;
}
}
}
if (fnSystem) {
fnSystem.unshift('## Function Calling\nYou can call functions when responding to the user if any of the ' +
'functions relate to the user\'s message.\n\n<callable-functions>\n');
fnSystem.push(`</callable-functions>
To call a function respond with a JSON object with 2 properties, "functionName" and "parameters".
The value of parameters property should conform to the function parameter JSON scheme.
For example to call a function named "openFolder" with a user asks to open the folder named "My Documents"
you would respond with the following JSON object.
<call-function>
{
"functionName":"openFolder",
"parameters":{
"folderName":"My Documents"
}
}
</call-function>
`);
insertSystemMessageIntoFlatConvo(fnSystem.join(''), flat);
}
if (!jsonMode && model.enableRespondWithTextFunction && flat.messages.some(m => m.fn)) {
await convo.flattenSourceAsync({
appendTo: flat,
passExe: true,
cacheName: 'responseWithTextFunction',
convo: model.respondWithTextFunctionSource ?? /*convo*/ `
# You can call this function if no other functions match the user's message
> respondWithText(
# Message to response with
text: string
)
`
});
}
if (jsonMode && model.jsonModeImplementAsFunction) {
const isAry = lastMsg?.responseFormatWrapArray;
const convoType = `${isAry ? 'array(' : ''}${lastMsg?.responseFormatTypeName ?? 'any'}${isAry ? ')' : ''}`;
await convo.flattenSourceAsync({
appendTo: flat,
passExe: true,
cacheName: 'respondWithJSONFunction_' + convoType,
convo: model.respondWithJSONFunctionSource?.replace('__TYPE__', convoType) ?? /*convo*/ `
# You can call this function to return JSON values to the user
> respondWithJSON(
# JSON object. Do not serialize the value.
value: ${convoType}
)
`
});
}
return { lastMsg, jsonMode, hasFunctions };
};
export const applyConvoModelConfigurationToOutput = (model, flat, output, { lastMsg, jsonMode, hasFunctions, }) => {
for (let i = 0; i < output.length; i++) {
const msg = output[i];
if (!msg) {
continue;
}
if (hasFunctions && !model.supportsFunctionCalling && msg.content && msg.content.includes('"functionName"')) {
try {
let content = msg.content;
if (content.includes('<call')) {
content = content.replace(/<\/?call-?function\/?>/g, '');
}
const call = parseConvoJsonMessage(content);
if (call.functionName && call.parameters) {
output[i] = createFunctionCallConvoCompletionMessage({
flat,
callFn: call.functionName,
callParams: call.parameters,
toolId: uuid(),
model: model.name,
inputTokens: msg?.inputTokens,
outputTokens: msg?.outputTokens,
tokenPrice: msg.tokenPrice,
});
}
}
catch { }
}
if (model.enableRespondWithTextFunction && msg.callFn === 'respondWithText') {
output[i] = createTextConvoCompletionMessage({
flat,
role: msg.role ?? 'assistant',
content: msg.callParams?.text,
model: msg.model ?? convoAnyModelName,
inputTokens: msg.inputTokens,
outputTokens: msg.outputTokens,
tokenPrice: msg.tokenPrice,
});
}
if (model.jsonModeImplementAsFunction && jsonMode) {
if (msg.callFn === 'respondWithJSON') {
let paramValue = msg.callParams?.value ?? null;
if (lastMsg?.responseFormatTypeName &&
lastMsg?.responseFormatTypeName !== 'string' &&
(typeof paramValue === 'string')) {
paramValue = parseJson5(paramValue);
}
output[i] = createTextConvoCompletionMessage({
flat,
role: msg.role ?? 'assistant',
content: JSON.stringify(paramValue),
model: msg.model ?? convoAnyModelName,
inputTokens: msg.inputTokens,
outputTokens: msg.outputTokens,
tokenPrice: msg.tokenPrice,
defaults: {
format: 'json',
formatTypeName: lastMsg?.responseFormatTypeName,
formatIsArray: lastMsg?.responseFormatIsArray,
}
});
}
else {
output.splice(i, 1);
i--;
}
}
}
};
const applyJsonModeToMessage = (msg, model, flat) => {
if (msg.responseFormat !== 'json' || model.jsonModeInstructions) {
return;
}
if (model.jsonModeInstructions) {
appendFlatConvoMessageSuffix(msg, model.jsonModeInstructions);
}
else if (model.jsonModeImplementAsFunction) {
appendFlatConvoMessageSuffix(msg, 'Call the respondWithJSON function');
}
else if (msg.responseFormatTypeName) {
const type = flat.exe.getVar(msg.responseFormatTypeName);
let scheme = convoTypeToJsonScheme(type);
if (!scheme) {
throw new ConvoError('invalid-message-response-scheme', {}, `${msg.responseFormatTypeName} does not point to a convo type object `);
}
if (msg.responseFormatIsArray) {
scheme = {
type: 'object',
required: ['values'],
properties: {
values: msg.responseFormatWrapArray ? {
type: 'array',
items: scheme
} : scheme
}
};
}
appendFlatConvoMessageSuffix(msg, `<response-format>\nReturn a well formatted JSON ${msg.responseFormatIsArray ? 'array' : 'object'} that conforms to the following JSON Schema for this message:\n${JSON.stringify(scheme)}</response-format>`);
}
else {
appendFlatConvoMessageSuffix(msg, `<response-format>Return a well formatted JSON ${msg.responseFormatIsArray ? 'array' : 'object'} for this message.</response-format>`);
}
if (model.jsonModeInstructWrapInCodeBlock) {
appendFlatConvoMessageSuffix(msg, 'Wrap the generated JSON in a markdown json code fence and do not include any pre or post-amble.');
}
if (model.jsonModeInstructionsPrefix) {
msg.suffix = `${model.jsonModeInstructionsPrefix}\n\n${msg.suffix}`;
}
if (model.jsonModeInstructionsSuffix) {
msg.suffix = `${msg.suffix}\n\n${model.jsonModeInstructionsSuffix}`;
}
};
//# sourceMappingURL=convo-completion-lib.js.map