First upload version 0.0.1

This commit is contained in:
Neyra
2026-02-05 15:27:49 +08:00
commit 8e9b7201ed
4182 changed files with 593136 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
import { ChatHistoryItem, ChatModelFunctions, ChatWrapperGenerateContextStateOptions, ChatWrapperGeneratedContextState, ChatWrapperSettings, Tokenizer } from "../../types.js";
import { LlamaText } from "../../utils/LlamaText.js";
import { ChatWrapper } from "../../ChatWrapper.js";
import { ChatHistoryFunctionCallMessageTemplate } from "./utils/chatHistoryFunctionCallMessageTemplate.js";
import { TemplateChatWrapperSegmentsOptions } from "./utils/templateSegmentOptionsToChatWrapperSettings.js";
export type JinjaTemplateChatWrapperOptions = {
template: string;
/**
* Defaults to `"assistant"`.
*/
modelRoleName?: string;
/**
* Defaults to `"user"`.
*/
userRoleName?: string;
/**
* Defaults to `"system"`.
*/
systemRoleName?: string;
/**
* Some Jinja templates may not support system messages, and in such cases,
* it'll be detected and system messages can be converted to user messages.
*
* You can specify the format of the converted user message.
* - **"auto"**: Convert system messages to user messages only if the template does not support system messages.
* - **`true`**: Always convert system messages to user messages.
* - **`false`**: Never convert system messages to user messages.
* May throw an error if some system messages don't appear in the template.
* - **`{use: "ifNeeded", format: "..."}`**: Convert system messages to user messages only if the template does not support system
* messages with the specified format.
* - **`{use: "always", format: "..."}`**: Always convert system messages to user messages with the specified format.
*
* Defaults to `"auto"`.
*/
convertUnsupportedSystemMessagesToUserMessages?: "auto" | boolean | JinjaTemplateChatWrapperOptionsConvertMessageFormat;
/**
* Template format for how functions can be called by the model and how their results are fed to the model after function calls.
*
* - **`"auto"`**: Extract the function call message template from the Jinja template.
* Fallback to the default template if not found.
* - **`"noJinja"`**: Use the default template.
* - **Custom template**: Use the specified {@link ChatHistoryFunctionCallMessageTemplate template}.
* See {@link ChatHistoryFunctionCallMessageTemplate `ChatHistoryFunctionCallMessageTemplate`} for more details.
*
* Defaults to `"auto"`.
*/
functionCallMessageTemplate?: "auto" | "noJinja" | ChatHistoryFunctionCallMessageTemplate;
/**
* Whether to join adjacent messages of the same type.
* Some Jinja templates may throw an error if this is not set to `true`.
*
* Defaults to `true`.
*/
joinAdjacentMessagesOfTheSameType?: boolean;
/**
* Whether to trim leading whitespace in responses.
*
* Defaults to `true`.
*/
trimLeadingWhitespaceInResponses?: boolean;
/**
* Additional parameters to use for rendering the Jinja template.
*/
additionalRenderParameters?: Record<string, any>;
/**
* Format of the segments generated by the model (like thought segments)
*/
segments?: TemplateChatWrapperSegmentsOptions;
/**
* Pass a model's tokenizer to attempt to detect common tokens used for chat formatting from it.
*
* Currently only used for detecting support for `<think>` tags for thought segments.
*/
tokenizer?: Tokenizer;
};
export type JinjaTemplateChatWrapperOptionsConvertMessageFormat = {
use?: "always" | "ifNeeded";
format: `${string}{{message}}${string}`;
};
/**
* A chat wrapper based on a Jinja template.
* Useful for using the original model's Jinja template as-is without any additional conversion work to chat with a model.
*
* If you want to create a new chat wrapper from scratch, using this chat wrapper is not recommended, and instead you better inherit
* from the `ChatWrapper` class and implement a custom chat wrapper of your own in TypeScript.
*
* For a simpler way to create a chat wrapper, see the `TemplateChatWrapper` class.
* @example
* <span v-pre>
*
* ```ts
* import {JinjaTemplateChatWrapper} from "node-llama-cpp";
*
* const chatWrapper = new JinjaTemplateChatWrapper({
* template: "<Jinja template here>",
* // functionCallMessageTemplate: { // optional
* // call: "[[call: {{functionName}}({{functionParams}})]]",
* // result: " [[result: {{functionCallResult}}]]"
* // },
* // segments: {
* // thoughtTemplate: "<think>{{content}}</think>",
* // reopenThoughtAfterFunctionCalls: true
* // }
* });
* ```
*
* </span>
*/
export declare class JinjaTemplateChatWrapper extends ChatWrapper {
readonly wrapperName = "JinjaTemplate";
readonly settings: ChatWrapperSettings;
readonly template: string;
readonly modelRoleName: string;
readonly userRoleName: string;
readonly systemRoleName: string;
readonly convertUnsupportedSystemMessagesToUserMessages?: JinjaTemplateChatWrapperOptionsConvertMessageFormat;
readonly joinAdjacentMessagesOfTheSameType: boolean;
readonly trimLeadingWhitespaceInResponses: boolean;
readonly additionalRenderParameters?: Record<string, any>;
/**
* @param options
*/
constructor(options: JinjaTemplateChatWrapperOptions);
/**
* Whether the function call syntax settings were extracted from the given Jinja template.
*
* The function call syntax settings can be accessed using the `.settings.functions` property.
*/
get usingJinjaFunctionCallTemplate(): boolean;
generateContextState({ chatHistory, availableFunctions, documentFunctionParams }: ChatWrapperGenerateContextStateOptions): ChatWrapperGeneratedContextState & {
transformedSystemMessagesToUserMessages: boolean;
};
addAvailableFunctionsSystemMessageToHistory(history: readonly ChatHistoryItem[], availableFunctions?: ChatModelFunctions, options?: {
documentParams?: boolean;
}): readonly ChatHistoryItem[];
generateFunctionCall(name: string, params: any): LlamaText;
generateFunctionCallResult(functionName: string, functionParams: any, result: any): LlamaText;
}

View File

@@ -0,0 +1,626 @@
import { Template } from "@huggingface/jinja";
import { splitText } from "lifecycle-utils";
import { SpecialToken, LlamaText, SpecialTokensText } from "../../utils/LlamaText.js";
import { ChatWrapper } from "../../ChatWrapper.js";
import { fromChatHistoryToIntermediateOpenAiMessages, fromIntermediateToCompleteOpenAiMessages } from "../../utils/OpenAIFormat.js";
import { removeUndefinedFields } from "../../utils/removeNullFields.js";
import { jsonDumps } from "../utils/jsonDumps.js";
import { tryMatrix } from "../../utils/optionsMatrix.js";
import { parseFunctionCallMessageTemplate } from "./utils/chatHistoryFunctionCallMessageTemplate.js";
import { templateSegmentOptionsToChatWrapperSettings } from "./utils/templateSegmentOptionsToChatWrapperSettings.js";
import { UniqueIdGenerator } from "./utils/UniqueIdGenerator.js";
import { extractFunctionCallSettingsFromJinjaTemplate } from "./utils/extractFunctionCallSettingsFromJinjaTemplate.js";
import { squashChatHistoryItems } from "./utils/squashChatHistoryItems.js";
import { extractSegmentSettingsFromTokenizerAndChatTemplate } from "./utils/extractSegmentSettingsFromTokenizerAndChatTemplate.js";
const defaultConvertUnsupportedSystemMessagesToUserMessagesFormat = {
format: "### System message\n\n{{message}}\n\n----"
};
/**
* A chat wrapper based on a Jinja template.
* Useful for using the original model's Jinja template as-is without any additional conversion work to chat with a model.
*
* If you want to create a new chat wrapper from scratch, using this chat wrapper is not recommended, and instead you better inherit
* from the `ChatWrapper` class and implement a custom chat wrapper of your own in TypeScript.
*
* For a simpler way to create a chat wrapper, see the `TemplateChatWrapper` class.
* @example
* <span v-pre>
*
* ```ts
* import {JinjaTemplateChatWrapper} from "node-llama-cpp";
*
* const chatWrapper = new JinjaTemplateChatWrapper({
* template: "<Jinja template here>",
* // functionCallMessageTemplate: { // optional
* // call: "[[call: {{functionName}}({{functionParams}})]]",
* // result: " [[result: {{functionCallResult}}]]"
* // },
* // segments: {
* // thoughtTemplate: "<think>{{content}}</think>",
* // reopenThoughtAfterFunctionCalls: true
* // }
* });
* ```
*
* </span>
*/
export class JinjaTemplateChatWrapper extends ChatWrapper {
wrapperName = "JinjaTemplate";
settings;
template;
modelRoleName;
userRoleName;
systemRoleName;
convertUnsupportedSystemMessagesToUserMessages;
joinAdjacentMessagesOfTheSameType;
trimLeadingWhitespaceInResponses;
additionalRenderParameters;
/** @internal */ _jinjaTemplate;
/** @internal */ _usingJinjaFunctionCallTemplate = false;
/** @internal */ _stringifyFunctionParams = false;
/** @internal */ _stringifyFunctionResult = false;
/** @internal */ _combineJinjaModelMessageAndToolCalls = true;
/** @internal */ _endJinjaMessagesWithUserMessage = false;
/**
* @param options
*/
constructor(options) {
super();
const { template, modelRoleName = "assistant", userRoleName = "user", systemRoleName = "system", convertUnsupportedSystemMessagesToUserMessages = defaultConvertUnsupportedSystemMessagesToUserMessagesFormat, functionCallMessageTemplate = "auto", joinAdjacentMessagesOfTheSameType = true, trimLeadingWhitespaceInResponses = true, additionalRenderParameters, segments, tokenizer, _requireFunctionCallSettingsExtraction = false } = options;
if (template == null)
throw new Error("template cannot be null");
this.template = template;
this.modelRoleName = modelRoleName;
this.userRoleName = userRoleName;
this.systemRoleName = systemRoleName;
this.convertUnsupportedSystemMessagesToUserMessages =
resolveConvertUnsupportedSystemMessagesToUserMessagesOption(convertUnsupportedSystemMessagesToUserMessages);
this.joinAdjacentMessagesOfTheSameType = joinAdjacentMessagesOfTheSameType;
this.trimLeadingWhitespaceInResponses = trimLeadingWhitespaceInResponses;
this.additionalRenderParameters = additionalRenderParameters;
if (this.convertUnsupportedSystemMessagesToUserMessages != null && !this.convertUnsupportedSystemMessagesToUserMessages.format.includes("{{message}}"))
throw new Error('convertUnsupportedSystemMessagesToUserMessages format must include "{{message}}"');
this._jinjaTemplate = new Template(this.template);
this.settings = {
...ChatWrapper.defaultSettings,
segments: templateSegmentOptionsToChatWrapperSettings(segments)
};
const { supportsSystemMessages, needsToEndJinjaMessagesWithUserMessage } = this._runSanityTest();
this.settings = {
...this.settings,
supportsSystemMessages,
segments: {
...this.settings.segments,
...extractSegmentSettingsFromTokenizerAndChatTemplate(this.template, tokenizer)
}
};
if (needsToEndJinjaMessagesWithUserMessage)
this._endJinjaMessagesWithUserMessage = true;
let functionCallSettings = parseFunctionCallMessageTemplate((functionCallMessageTemplate === "auto" || functionCallMessageTemplate === "noJinja")
? undefined
: functionCallMessageTemplate);
if (functionCallSettings == null && functionCallMessageTemplate !== "noJinja") {
try {
const idsGenerator = new UniqueIdGenerator(this.template + this.modelRoleName + this.userRoleName + this.systemRoleName +
(this.convertUnsupportedSystemMessagesToUserMessages?.format ?? ""));
const extractedSettings = extractFunctionCallSettingsFromJinjaTemplate({
idsGenerator,
renderTemplate: ({ chatHistory, functions, additionalParams, stringifyFunctionParams, stringifyFunctionResults, combineModelMessageAndToolCalls, squashModelTextResponses = true }) => {
const render = (convertSystemMessagesToUserMessagesFormat, wipeFunctionCallIds) => {
const { messages: intermediateMessages, tools } = fromChatHistoryToIntermediateOpenAiMessages({
chatHistory: this._transformChatHistory(chatHistory, {
convertSystemMessagesToUserMessagesFormat,
joinAdjacentMessagesOfTheSameType: !squashModelTextResponses
? false
: undefined
}).transformedHistory,
chatWrapperSettings: this.settings,
useRawValues: false,
functions,
stringifyFunctionParams,
stringifyFunctionResults,
combineModelMessageAndToolCalls,
squashModelTextResponses
});
const messages = fromIntermediateToCompleteOpenAiMessages(intermediateMessages)
.map((item) => {
if (!wipeFunctionCallIds)
return item;
if (item.role === "assistant" && item["tool_calls"] != null && item["tool_calls"].length > 0) {
for (const toolCall of item["tool_calls"]) {
if (wipeFunctionCallIds === "align")
toolCall.id = "fc_1_0001";
else
delete toolCall.id;
}
}
else if (item.role === "tool") {
if (wipeFunctionCallIds === "align")
item["tool_call_id"] = "fc_1_0001";
else
delete item["tool_call_id"];
}
return item;
});
const lastJinjaItem = messages.at(-1);
let eraseRenderedJinjaFromId;
if (this._endJinjaMessagesWithUserMessage && lastJinjaItem?.role === this.modelRoleName &&
typeof lastJinjaItem.content === "string" &&
lastJinjaItem.content.length > 0 &&
(lastJinjaItem["tool_calls"] == null ||
lastJinjaItem["tool_calls"]?.length === 0)) {
eraseRenderedJinjaFromId = lastJinjaItem.content;
messages.push({
role: this.userRoleName,
content: idsGenerator.generateId()
});
}
let res = this._jinjaTemplate.render({
...(this.additionalRenderParameters == null
? {}
: structuredClone(this.additionalRenderParameters)),
...additionalParams,
messages,
...removeUndefinedFields({ tools })
});
if (eraseRenderedJinjaFromId != null) {
const eraseIndex = res.lastIndexOf(eraseRenderedJinjaFromId);
if (eraseIndex >= 0)
res = res.slice(0, eraseIndex + eraseRenderedJinjaFromId.length);
}
// attempt to remove the ID pattern from the output
if (wipeFunctionCallIds === "align")
res = res
.replaceAll(/,\s*"(tool_call_id|call_id|id)":\s*"fc_1_0001"/g, "")
.replaceAll(/"(tool_call_id|call_id|id)":\s*"fc_1_0001"\s*,/g, "");
return res;
};
return tryMatrix({
convertSystemMessagesToUserMessagesFormat: getConvertUnsupportedSystemMessagesToUserMessagesTryOptions(this.convertUnsupportedSystemMessagesToUserMessages),
wipeFunctionCallIds: [true, "align", false]
}, ({ convertSystemMessagesToUserMessagesFormat, wipeFunctionCallIds }) => {
return render(convertSystemMessagesToUserMessagesFormat, wipeFunctionCallIds);
});
}
});
functionCallSettings = extractedSettings.settings;
if (functionCallSettings != null) {
this._usingJinjaFunctionCallTemplate = true;
this._stringifyFunctionParams = extractedSettings.stringifyParams;
this._stringifyFunctionResult = extractedSettings.stringifyResult;
}
}
catch (err) {
// do nothing
}
if (functionCallSettings == null && _requireFunctionCallSettingsExtraction)
throw new Error("failed to extract function call settings from the Jinja template");
}
this.settings = {
...this.settings,
functions: functionCallSettings ?? ChatWrapper.defaultSettings.functions
};
}
/**
* Whether the function call syntax settings were extracted from the given Jinja template.
*
* The function call syntax settings can be accessed using the `.settings.functions` property.
*/
get usingJinjaFunctionCallTemplate() {
return this._usingJinjaFunctionCallTemplate;
}
generateContextState({ chatHistory, availableFunctions, documentFunctionParams }) {
const { contextText, stopGenerationTriggers, ignoreStartText, functionCall, transformedSystemMessagesToUserMessages } = this._generateContextState({
chatHistory, availableFunctions, documentFunctionParams,
endJinjaMessagesWithUserMessage: this._endJinjaMessagesWithUserMessage
});
return { contextText, stopGenerationTriggers, ignoreStartText, functionCall, transformedSystemMessagesToUserMessages };
}
addAvailableFunctionsSystemMessageToHistory(history, availableFunctions, options = {}) {
if (this._usingJinjaFunctionCallTemplate)
return history;
return super.addAvailableFunctionsSystemMessageToHistory(history, availableFunctions, options);
}
generateFunctionCall(name, params) {
if (!this._stringifyFunctionParams)
return super.generateFunctionCall(name, params);
const emptyCallParamsPlaceholder = this.settings.functions.call.emptyCallParamsPlaceholder;
return LlamaText([
this.settings.functions.call.prefix,
name,
this.settings.functions.call.paramsPrefix,
(params === undefined
? (emptyCallParamsPlaceholder === undefined || emptyCallParamsPlaceholder === "")
? ""
: JSON.stringify(jsonDumps(emptyCallParamsPlaceholder))
: JSON.stringify(jsonDumps(params))),
this.settings.functions.call.suffix
]);
}
generateFunctionCallResult(functionName, functionParams, result) {
const resolveParameters = (text) => {
return LlamaText(text)
.mapValues((value) => {
if (typeof value !== "string")
return value;
const funcParamsText = functionParams === undefined
? ""
: jsonDumps(functionParams);
return value
.replaceAll("{{functionName}}", functionName)
.replaceAll("{{functionParams}}", (this._stringifyFunctionParams && funcParamsText !== "")
? JSON.stringify(funcParamsText)
: funcParamsText);
});
};
const resultText = result === undefined
? "void"
: jsonDumps(result);
return LlamaText([
resolveParameters(this.settings.functions.result.prefix),
((this._stringifyFunctionResult && result !== undefined)
? JSON.stringify(resultText)
: resultText),
resolveParameters(this.settings.functions.result.suffix)
]);
}
/** @internal */
_generateContextState({ chatHistory, availableFunctions, documentFunctionParams, endJinjaMessagesWithUserMessage }) {
return tryMatrix({
convertSystemMessagesToUserMessagesFormat: getConvertUnsupportedSystemMessagesToUserMessagesTryOptions(this.convertUnsupportedSystemMessagesToUserMessages),
endJinjaMessagesWithUserMessage: endJinjaMessagesWithUserMessage == null
? [false, true]
: [endJinjaMessagesWithUserMessage],
useMessagesWithEmbeddedTools: this._usingJinjaFunctionCallTemplate
? [undefined, true]
: [undefined]
}, ({ useMessagesWithEmbeddedTools, endJinjaMessagesWithUserMessage, convertSystemMessagesToUserMessagesFormat }) => {
return this._generateContextText(chatHistory, {
convertSystemMessagesToUserMessagesFormat, availableFunctions, documentFunctionParams,
endJinjaMessagesWithUserMessage,
useMessagesWithEmbeddedTools
});
});
}
/** @internal */
_transformChatHistory(history, { convertSystemMessagesToUserMessagesFormat, availableFunctions, documentFunctionParams = true, joinAdjacentMessagesOfTheSameType = this.joinAdjacentMessagesOfTheSameType }) {
const historyWithFunctions = this.addAvailableFunctionsSystemMessageToHistory(history, availableFunctions, {
documentParams: documentFunctionParams
});
let transformedSystemMessagesToUserMessages = false;
const transformedHistory = convertSystemMessagesToUserMessagesFormat == null
? historyWithFunctions
: historyWithFunctions.map((item) => {
if (item.type === "system") {
transformedSystemMessagesToUserMessages = true;
return {
type: "user",
text: LlamaText.joinValues(LlamaText.fromJSON(item.text), convertSystemMessagesToUserMessagesFormat.split("{{message}}")).toString()
};
}
return item;
});
return {
transformedHistory: joinAdjacentMessagesOfTheSameType
? squashChatHistoryItems(transformedHistory)
: transformedHistory,
transformedSystemMessagesToUserMessages
};
}
/** @internal */
_generateContextText(history, { convertSystemMessagesToUserMessagesFormat, availableFunctions, documentFunctionParams = true, endJinjaMessagesWithUserMessage, useMessagesWithEmbeddedTools = false }) {
const { transformedSystemMessagesToUserMessages, transformedHistory } = this._transformChatHistory(history, { convertSystemMessagesToUserMessagesFormat, availableFunctions, documentFunctionParams });
const generateMessagesWithEmbeddedTools = (chatHistory) => ({
messages: chatHistory.map((item) => {
if (item.type === "system")
return {
role: "system",
content: LlamaText.fromJSON(item.text)
};
else if (item.type === "user")
return {
role: "user",
content: LlamaText(item.text)
};
else if (item.type === "model")
return {
role: "assistant",
content: this.generateModelResponseText(item.response)
};
void item;
return { role: "user", content: LlamaText("") };
}),
tools: undefined
});
const generateMessagesWithTools = (chatHistory) => (fromChatHistoryToIntermediateOpenAiMessages({
chatHistory,
chatWrapperSettings: this.settings,
useRawValues: false,
functions: (availableFunctions != null && !documentFunctionParams)
? Object.fromEntries(Object.entries(availableFunctions)
.map(([funcName, { description, ...func }]) => [funcName, func]))
: availableFunctions,
stringifyFunctionParams: this._stringifyFunctionParams,
stringifyFunctionResults: this._stringifyFunctionResult,
combineModelMessageAndToolCalls: this._combineJinjaModelMessageAndToolCalls
}));
const lastItemIsModelMessage = transformedHistory.at(-1)?.type === "model";
const { messages: intermediateMessages, tools } = this._usingJinjaFunctionCallTemplate
? useMessagesWithEmbeddedTools
? {
messages: generateMessagesWithEmbeddedTools(transformedHistory).messages,
tools: generateMessagesWithTools(transformedHistory).tools
}
: generateMessagesWithTools(transformedHistory)
: generateMessagesWithEmbeddedTools(transformedHistory);
const idsGenerator = new UniqueIdGenerator(this.template + this.modelRoleName + this.userRoleName + this.systemRoleName +
(convertSystemMessagesToUserMessagesFormat ?? "") +
intermediateMessages.map(({ content }) => (content?.toString() ?? "")).join("\n\n"));
const jinjaItems = [];
const jinjaRoleMap = {
system: this.systemRoleName,
user: this.userRoleName,
assistant: this.modelRoleName,
tool: "tool"
};
const idToContent = new Map();
const modelMessageIds = new Set();
const messageIds = new Set();
for (const intermediateMessage of intermediateMessages) {
if (intermediateMessage.content == null) {
jinjaItems.push({
...intermediateMessage,
role: jinjaRoleMap[intermediateMessage.role] ?? intermediateMessage.role
});
continue;
}
const id = idsGenerator.generateId(intermediateMessage.role === "tool");
messageIds.add(id);
idToContent.set(id, LlamaText(intermediateMessage.content));
jinjaItems.push({
...intermediateMessage,
role: jinjaRoleMap[intermediateMessage.role] ?? intermediateMessage.role,
content: id
});
if (intermediateMessage.role === "assistant" || intermediateMessage.role === "tool")
modelMessageIds.add(id);
}
const bosTokenId = idsGenerator.generateId();
const eosTokenId = idsGenerator.generateId();
const eotTokenId = idsGenerator.generateId();
idToContent.set(bosTokenId, new SpecialToken("BOS"));
idToContent.set(eosTokenId, new SpecialToken("EOS"));
idToContent.set(eotTokenId, new SpecialToken("EOT"));
const lastJinjaItem = jinjaItems.at(-1);
let eraseRenderedJinjaFromId;
if (endJinjaMessagesWithUserMessage && lastJinjaItem?.role === this.modelRoleName &&
typeof lastJinjaItem.content === "string" &&
lastJinjaItem.content.length > 0 &&
(lastJinjaItem["tool_calls"] == null ||
lastJinjaItem["tool_calls"]?.length === 0)) {
eraseRenderedJinjaFromId = lastJinjaItem.content;
jinjaItems.push({
role: this.userRoleName,
content: idsGenerator.generateId()
});
}
const renderJinjaText = () => {
let res = tryMatrix({
options: [{}, { "add_generation_prompt": true }]
}, ({ options }) => (this._jinjaTemplate.render({
...(this.additionalRenderParameters == null
? {}
: structuredClone(this.additionalRenderParameters)),
messages: jinjaItems,
...removeUndefinedFields({ tools }),
"bos_token": bosTokenId,
"eos_token": eosTokenId,
"eot_token": eotTokenId,
...options
})));
if (eraseRenderedJinjaFromId != null) {
const eraseIndex = res.lastIndexOf(eraseRenderedJinjaFromId);
if (eraseIndex >= 0)
res = res.slice(0, eraseIndex + eraseRenderedJinjaFromId.length);
}
return res;
};
const validateThatAllMessageIdsAreUsed = (parts) => {
const messageIdsLeft = new Set(messageIds);
for (const part of parts) {
if (typeof part === "string")
continue;
messageIdsLeft.delete(part.separator);
}
if (messageIdsLeft.size !== 0)
throw new Error("Some input messages are not present in the generated Jinja template output");
};
const renderJinjaAndSplitIntoParts = () => {
const splitJinjaParts = splitText(renderJinjaText(), [...idToContent.keys()]);
if (lastItemIsModelMessage) {
let lastModelResponseIndex = -1;
for (let i = splitJinjaParts.length - 1; i >= 0; i--) {
const part = splitJinjaParts[i];
if (part == null || typeof part === "string")
continue;
if (modelMessageIds.has(part.separator)) {
lastModelResponseIndex = i;
break;
}
else if (messageIds.has(part.separator)) {
validateThatAllMessageIdsAreUsed(splitJinjaParts);
throw new Error("Last message was expected to be a model message, but it was not");
}
}
if (lastModelResponseIndex < 0) {
validateThatAllMessageIdsAreUsed(splitJinjaParts);
throw new Error("A model message was expected to be the last message, but it was not found");
}
return {
splitJinjaParts: splitJinjaParts.slice(0, lastModelResponseIndex + 1),
stopGenerationJinjaParts: splitJinjaParts.slice(lastModelResponseIndex + 1)
};
}
return {
splitJinjaParts,
stopGenerationJinjaParts: []
};
};
const { splitJinjaParts, stopGenerationJinjaParts } = renderJinjaAndSplitIntoParts();
const messageIdsLeftToProcess = new Set(messageIds);
const contextText = LlamaText(splitJinjaParts.map((part) => {
if (typeof part === "string")
return new SpecialTokensText(part); // things that are not message content can be tokenized with special tokens
const message = idToContent.get(part.separator);
if (message == null)
throw new Error(`Message with id "${part.separator}" not found`);
messageIdsLeftToProcess.delete(part.separator);
return message;
}));
if (messageIdsLeftToProcess.size !== 0)
throw new Error("Some input messages are not present in the generated Jinja template output");
return {
contextText,
ignoreStartText: !this.trimLeadingWhitespaceInResponses
? []
: [
// ignore up to 4 leading spaces
...Array(4).fill(0)
.map((_, index) => LlamaText(" ".repeat(index + 1))),
LlamaText("\t"),
LlamaText("\t\t"),
LlamaText("\t "),
LlamaText(" \t")
],
stopGenerationTriggers: [
LlamaText(new SpecialToken("EOS")),
...(stopGenerationJinjaParts.length === 0
? []
: [
LlamaText(stopGenerationJinjaParts.map((part) => {
if (typeof part === "string")
return new SpecialTokensText(part);
const message = idToContent.get(part.separator);
if (message == null)
throw new Error(`Message with id "${part.separator}" not found`);
return message;
}))
])
],
transformedSystemMessagesToUserMessages,
endJinjaMessagesWithUserMessage
};
}
/**
* Validate that this Jinja template can be rendered
* @internal
*/
_runSanityTest(needsToEndJinjaMessagesWithUserMessage = false) {
try {
let supportsSystemMessages = true;
for (const chatHistory of chatHistoriesForSanityTest) {
const { transformedSystemMessagesToUserMessages, endJinjaMessagesWithUserMessage: endedJinjaMessagesWithUserMessage } = this._generateContextState({
chatHistory,
endJinjaMessagesWithUserMessage: needsToEndJinjaMessagesWithUserMessage
? true
: undefined
});
if (transformedSystemMessagesToUserMessages)
supportsSystemMessages = false;
if (!needsToEndJinjaMessagesWithUserMessage && endedJinjaMessagesWithUserMessage) {
if (chatHistory !== chatHistoriesForSanityTest[0])
// validate tha this doesn't break the template
return this._runSanityTest(true);
else
needsToEndJinjaMessagesWithUserMessage = true;
}
}
return { supportsSystemMessages, needsToEndJinjaMessagesWithUserMessage };
}
catch (err) {
throw new Error("The provided Jinja template failed the sanity test: " + String(err) + ". Inspect the Jinja template to find out what went wrong");
}
}
}
function resolveConvertUnsupportedSystemMessagesToUserMessagesOption(convertUnsupportedSystemMessagesToUserMessages) {
if (convertUnsupportedSystemMessagesToUserMessages === false)
return undefined;
if (convertUnsupportedSystemMessagesToUserMessages === true)
return {
...defaultConvertUnsupportedSystemMessagesToUserMessagesFormat,
use: "always"
};
if (convertUnsupportedSystemMessagesToUserMessages === "auto")
return {
...defaultConvertUnsupportedSystemMessagesToUserMessagesFormat,
use: "ifNeeded"
};
if (typeof convertUnsupportedSystemMessagesToUserMessages === "object")
return {
...convertUnsupportedSystemMessagesToUserMessages,
use: convertUnsupportedSystemMessagesToUserMessages.use ?? "ifNeeded"
};
return { ...defaultConvertUnsupportedSystemMessagesToUserMessagesFormat, use: "ifNeeded" };
}
function getConvertUnsupportedSystemMessagesToUserMessagesTryOptions(convertUnsupportedSystemMessagesToUserMessages) {
if (convertUnsupportedSystemMessagesToUserMessages == null)
return [undefined];
else if (convertUnsupportedSystemMessagesToUserMessages.use === "always")
return [convertUnsupportedSystemMessagesToUserMessages.format];
return [undefined, convertUnsupportedSystemMessagesToUserMessages.format];
}
const chatHistoriesForSanityTest = [
[{
type: "system",
text: "System message ~!@#$%^&*()\n*"
}, {
type: "user",
text: "Message 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"
}, {
type: "model",
response: [""]
}],
[{
type: "system",
text: "System message ~!@#$%^&*()\n*"
}, {
type: "user",
text: "Message 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"
}, {
type: "model",
response: ["Result 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"]
}],
[{
type: "system",
text: "System message ~!@#$%^&*()\n*"
}, {
type: "user",
text: "Message 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"
}, {
type: "model",
response: ["Result 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"]
}, {
type: "user",
text: "Message2 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"
}, {
type: "model",
response: [""]
}],
[{
type: "system",
text: "System message ~!@#$%^&*()\n*"
}, {
type: "user",
text: "Message 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"
}, {
type: "model",
response: ["Result 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"]
}, {
type: "user",
text: "Message2 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"
}, {
type: "model",
response: ["Result2 1234567890!@#$%^&*()_+-=[]{}|\\:;\"',./<>?`~"]
}]
];
//# sourceMappingURL=JinjaTemplateChatWrapper.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,76 @@
import { ChatWrapperGenerateContextStateOptions, ChatWrapperGeneratedContextState, ChatWrapperSettings } from "../../types.js";
import { ChatWrapper } from "../../ChatWrapper.js";
import { ChatHistoryFunctionCallMessageTemplate } from "./utils/chatHistoryFunctionCallMessageTemplate.js";
import { TemplateChatWrapperSegmentsOptions } from "./utils/templateSegmentOptionsToChatWrapperSettings.js";
export type TemplateChatWrapperOptions = {
template: `${"" | `${string}{{systemPrompt}}`}${string}{{history}}${string}{{completion}}${string}`;
historyTemplate: {
system: `${string}{{message}}${string}`;
user: `${string}{{message}}${string}`;
model: `${string}{{message}}${string}`;
};
functionCallMessageTemplate?: ChatHistoryFunctionCallMessageTemplate;
/**
* Whether to join adjacent messages of the same type.
*
* Defaults to `true`.
*/
joinAdjacentMessagesOfTheSameType?: boolean;
/**
* Format of the segments generated by the model (like thought segments)
*/
segments?: TemplateChatWrapperSegmentsOptions;
};
/**
* A chat wrapper based on a simple template.
* @example
* <span v-pre>
*
* ```ts
* import {TemplateChatWrapper} from "node-llama-cpp";
*
* const chatWrapper = new TemplateChatWrapper({
* template: "{{systemPrompt}}\n{{history}}model: {{completion}}\nuser: ",
* historyTemplate: {
* system: "system: {{message}}\n",
* user: "user: {{message}}\n",
* model: "model: {{message}}\n"
* },
* // functionCallMessageTemplate: { // optional
* // call: "[[call: {{functionName}}({{functionParams}})]]",
* // result: " [[result: {{functionCallResult}}]]"
* // },
* // segments: {
* // thoughtTemplate: "<think>{{content}}</think>",
* // reopenThoughtAfterFunctionCalls: true
* // }
* });
* ```
*
* </span>
*
* **<span v-pre>`{{systemPrompt}}`</span>** is optional and is replaced with the first system message
* (when is does, that system message is not included in the history).
*
* **<span v-pre>`{{history}}`</span>** is replaced with the chat history.
* Each message in the chat history is converted using the template passed to `historyTemplate` for the message role,
* and all messages are joined together.
*
* **<span v-pre>`{{completion}}`</span>** is where the model's response is generated.
* The text that comes after <span v-pre>`{{completion}}`</span> is used to determine when the model has finished generating the response,
* and thus is mandatory.
*
* **`functionCallMessageTemplate`** is used to specify the format in which functions can be called by the model and
* how their results are fed to the model after the function call.
*
* **`segments`** is used to specify the format of the segments generated by the model (like thought segments).
*/
export declare class TemplateChatWrapper extends ChatWrapper {
readonly wrapperName = "Template";
readonly settings: ChatWrapperSettings;
readonly template: TemplateChatWrapperOptions["template"];
readonly historyTemplate: Readonly<TemplateChatWrapperOptions["historyTemplate"]>;
readonly joinAdjacentMessagesOfTheSameType: boolean;
constructor({ template, historyTemplate, functionCallMessageTemplate, joinAdjacentMessagesOfTheSameType, segments }: TemplateChatWrapperOptions);
generateContextState({ chatHistory, availableFunctions, documentFunctionParams }: ChatWrapperGenerateContextStateOptions): ChatWrapperGeneratedContextState;
}

View File

@@ -0,0 +1,212 @@
import { SpecialToken, LlamaText, SpecialTokensText } from "../../utils/LlamaText.js";
import { ChatWrapper } from "../../ChatWrapper.js";
import { parseTextTemplate } from "../../utils/parseTextTemplate.js";
import { parseFunctionCallMessageTemplate } from "./utils/chatHistoryFunctionCallMessageTemplate.js";
import { templateSegmentOptionsToChatWrapperSettings } from "./utils/templateSegmentOptionsToChatWrapperSettings.js";
/**
* A chat wrapper based on a simple template.
* @example
* <span v-pre>
*
* ```ts
* import {TemplateChatWrapper} from "node-llama-cpp";
*
* const chatWrapper = new TemplateChatWrapper({
* template: "{{systemPrompt}}\n{{history}}model: {{completion}}\nuser: ",
* historyTemplate: {
* system: "system: {{message}}\n",
* user: "user: {{message}}\n",
* model: "model: {{message}}\n"
* },
* // functionCallMessageTemplate: { // optional
* // call: "[[call: {{functionName}}({{functionParams}})]]",
* // result: " [[result: {{functionCallResult}}]]"
* // },
* // segments: {
* // thoughtTemplate: "<think>{{content}}</think>",
* // reopenThoughtAfterFunctionCalls: true
* // }
* });
* ```
*
* </span>
*
* **<span v-pre>`{{systemPrompt}}`</span>** is optional and is replaced with the first system message
* (when is does, that system message is not included in the history).
*
* **<span v-pre>`{{history}}`</span>** is replaced with the chat history.
* Each message in the chat history is converted using the template passed to `historyTemplate` for the message role,
* and all messages are joined together.
*
* **<span v-pre>`{{completion}}`</span>** is where the model's response is generated.
* The text that comes after <span v-pre>`{{completion}}`</span> is used to determine when the model has finished generating the response,
* and thus is mandatory.
*
* **`functionCallMessageTemplate`** is used to specify the format in which functions can be called by the model and
* how their results are fed to the model after the function call.
*
* **`segments`** is used to specify the format of the segments generated by the model (like thought segments).
*/
export class TemplateChatWrapper extends ChatWrapper {
wrapperName = "Template";
settings;
template;
historyTemplate;
joinAdjacentMessagesOfTheSameType;
/** @internal */ _parsedChatTemplate;
/** @internal */ _parsedChatHistoryTemplate;
constructor({ template, historyTemplate, functionCallMessageTemplate, joinAdjacentMessagesOfTheSameType = true, segments }) {
super();
if (template == null || historyTemplate == null)
throw new Error("Template chat wrapper settings must have a template and historyTemplate.");
if (historyTemplate.system == null || historyTemplate.user == null || historyTemplate.model == null)
throw new Error("Template chat wrapper historyTemplate must have system, user, and model templates.");
this.template = template;
this.historyTemplate = historyTemplate;
this.joinAdjacentMessagesOfTheSameType = joinAdjacentMessagesOfTheSameType;
this._parsedChatTemplate = parseChatTemplate(template);
this._parsedChatHistoryTemplate = {
system: parseChatHistoryTemplate(historyTemplate.system),
user: parseChatHistoryTemplate(historyTemplate.user),
model: parseChatHistoryTemplate(historyTemplate.model)
};
this.settings = {
...ChatWrapper.defaultSettings,
functions: parseFunctionCallMessageTemplate(functionCallMessageTemplate) ?? ChatWrapper.defaultSettings.functions,
segments: templateSegmentOptionsToChatWrapperSettings(segments)
};
}
generateContextState({ chatHistory, availableFunctions, documentFunctionParams }) {
const historyWithFunctions = this.addAvailableFunctionsSystemMessageToHistory(chatHistory, availableFunctions, {
documentParams: documentFunctionParams
});
const resultItems = [];
const systemTexts = [];
const userTexts = [];
const modelTexts = [];
let currentAggregateFocus = null;
function flush() {
if (systemTexts.length > 0 || userTexts.length > 0 || modelTexts.length > 0)
resultItems.push({
system: LlamaText.joinValues("\n\n", systemTexts),
user: LlamaText.joinValues("\n\n", userTexts),
model: LlamaText.joinValues("\n\n", modelTexts)
});
systemTexts.length = 0;
userTexts.length = 0;
modelTexts.length = 0;
}
for (const item of historyWithFunctions) {
if (item.type === "system") {
if (!this.joinAdjacentMessagesOfTheSameType || currentAggregateFocus !== "system")
flush();
currentAggregateFocus = "system";
systemTexts.push(LlamaText.fromJSON(item.text));
}
else if (item.type === "user") {
if (!this.joinAdjacentMessagesOfTheSameType || (currentAggregateFocus !== "system" && currentAggregateFocus !== "user"))
flush();
currentAggregateFocus = "user";
userTexts.push(LlamaText(item.text));
}
else if (item.type === "model") {
if (!this.joinAdjacentMessagesOfTheSameType)
flush();
currentAggregateFocus = "model";
modelTexts.push(this.generateModelResponseText(item.response));
}
else
void item;
}
flush();
const getHistoryItem = (role, text, prefix) => {
const { messagePrefix, messageSuffix } = this._parsedChatHistoryTemplate[role];
return LlamaText([
new SpecialTokensText((prefix ?? "") + messagePrefix),
text,
new SpecialTokensText(messageSuffix)
]);
};
const contextText = LlamaText(resultItems.map(({ system, user, model }, index) => {
const isFirstItem = index === 0;
const isLastItem = index === resultItems.length - 1;
const res = LlamaText([
isFirstItem
? system.values.length === 0
? new SpecialTokensText((this._parsedChatTemplate.systemPromptPrefix ?? "") + this._parsedChatTemplate.historyPrefix)
: this._parsedChatTemplate.systemPromptPrefix != null
? LlamaText([
new SpecialTokensText(this._parsedChatTemplate.systemPromptPrefix),
system,
new SpecialTokensText(this._parsedChatTemplate.historyPrefix)
])
: getHistoryItem("system", system, this._parsedChatTemplate.historyPrefix)
: system.values.length === 0
? LlamaText([])
: getHistoryItem("system", system),
user.values.length === 0
? LlamaText([])
: getHistoryItem("user", user),
model.values.length === 0
? LlamaText([])
: !isLastItem
? getHistoryItem("model", model)
: LlamaText([
new SpecialTokensText(this._parsedChatTemplate.completionPrefix),
model
])
]);
return LlamaText(res.values.reduce((res, value) => {
if (value instanceof SpecialTokensText) {
const lastItem = res[res.length - 1];
if (lastItem == null || !(lastItem instanceof SpecialTokensText))
return res.concat([value]);
return res.slice(0, -1).concat([
new SpecialTokensText(lastItem.value + value.value)
]);
}
return res.concat([value]);
}, []));
}));
return {
contextText,
stopGenerationTriggers: [
LlamaText(new SpecialToken("EOS")),
LlamaText(this._parsedChatTemplate.completionSuffix),
LlamaText(new SpecialTokensText(this._parsedChatTemplate.completionSuffix))
]
};
}
}
function parseChatTemplate(template) {
const parsedTemplate = parseTextTemplate(template, [{
text: "{{systemPrompt}}",
key: "systemPrompt",
optional: true
}, {
text: "{{history}}",
key: "history"
}, {
text: "{{completion}}",
key: "completion"
}]);
if (parsedTemplate.completion.suffix.length == 0)
throw new Error('Chat template must have text after "{{completion}}"');
return {
systemPromptPrefix: parsedTemplate.systemPrompt?.prefix ?? null,
historyPrefix: parsedTemplate.history.prefix,
completionPrefix: parsedTemplate.completion.prefix,
completionSuffix: parsedTemplate.completion.suffix
};
}
function parseChatHistoryTemplate(template) {
const parsedTemplate = parseTextTemplate(template, [{
text: "{{message}}",
key: "message"
}]);
return {
messagePrefix: parsedTemplate.message.prefix,
messageSuffix: parsedTemplate.message.suffix
};
}
//# sourceMappingURL=TemplateChatWrapper.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
export declare class UniqueIdGenerator {
readonly antiText: string;
private readonly _ids;
constructor(antiText: string);
generateId(numbersOnly?: boolean): string;
removeId(id: string): void;
}

View File

@@ -0,0 +1,30 @@
export class UniqueIdGenerator {
antiText;
_ids = new Set();
constructor(antiText) {
this.antiText = antiText;
}
generateId(numbersOnly = false) {
let id;
do {
if (numbersOnly) {
do {
id = (Math.random()
.toString(10)
.slice(2)
.slice(0, String(Number.MAX_SAFE_INTEGER).length - 1));
} while (id.startsWith("0"));
}
else
id = "W" + (Math.random()
.toString(36)
.slice(2)) + "W";
} while (this._ids.has(id) || this.antiText.includes(id));
this._ids.add(id);
return id;
}
removeId(id) {
this._ids.delete(id);
}
}
//# sourceMappingURL=UniqueIdGenerator.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"UniqueIdGenerator.js","sourceRoot":"","sources":["../../../../src/chatWrappers/generic/utils/UniqueIdGenerator.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,iBAAiB;IACV,QAAQ,CAAS;IAChB,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,YAAmB,QAAgB;QAC/B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAEM,UAAU,CAAC,cAAuB,KAAK;QAC1C,IAAI,EAAU,CAAC;QAEf,GAAG,CAAC;YACA,IAAI,WAAW,EAAE,CAAC;gBACd,GAAG,CAAC;oBACA,EAAE,GAAG,CACD,IAAI,CAAC,MAAM,EAAE;yBACR,QAAQ,CAAC,EAAE,CAAC;yBACZ,KAAK,CAAC,CAAC,CAAC;yBACR,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAC5D,CAAC;gBACN,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YACjC,CAAC;;gBACG,EAAE,GAAG,GAAG,GAAG,CACP,IAAI,CAAC,MAAM,EAAE;qBACR,QAAQ,CAAC,EAAE,CAAC;qBACZ,KAAK,CAAC,CAAC,CAAC,CAChB,GAAG,GAAG,CAAC;QAChB,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;QAE1D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAElB,OAAO,EAAE,CAAC;IACd,CAAC;IAEM,QAAQ,CAAC,EAAU;QACtB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzB,CAAC;CACJ"}

View File

@@ -0,0 +1,24 @@
import { ChatWrapperSettings } from "../../../types.js";
export declare function parseFunctionCallMessageTemplate(template?: ChatHistoryFunctionCallMessageTemplate): ChatWrapperSettings["functions"] | null;
/**
* Template format for how functions can be called by the model and how their results are fed to the model after function calls.
*
* Consists of an object with two properties:
* 1. **`call`**: The function call template.
* 2. **`result`**: The function call result template.
*
* For example:
* ```ts
* const template: ChatHistoryFunctionCallMessageTemplate = {
* call: "[[call: {{functionName}}({{functionParams}})]]",
* result: " [[result: {{functionCallResult}}]]"
* };
* ```
*
* It's mandatory for the call template to have text before <span v-pre>`{{functionName}}`</span> in order for the chat wrapper know when
* to activate the function calling grammar.
*/
export type ChatHistoryFunctionCallMessageTemplate = {
call: `${string}{{functionName}}${string}{{functionParams}}${string}`;
result: `${string}{{functionCallResult}}${string}`;
};

View File

@@ -0,0 +1,45 @@
import { parseTextTemplate } from "../../../utils/parseTextTemplate.js";
export function parseFunctionCallMessageTemplate(template) {
if (template == null)
return null;
const { call: functionCallTemplate, result: functionCallResultTemplate } = template;
if (functionCallTemplate == null || functionCallResultTemplate == null)
throw new Error("Both function call and function call result templates are required");
const parsedFunctionCallTemplate = parseTextTemplate(functionCallTemplate, [{
text: "{{functionName}}",
key: "functionName"
}, {
text: "{{functionParams}}",
key: "functionParams"
}]);
const parsedFunctionCallResultTemplate = parseTextTemplate(functionCallResultTemplate, [{
text: "{{functionCallResult}}",
key: "functionCallResult"
}]);
const callPrefix = parsedFunctionCallTemplate.functionName.prefix;
const callParamsPrefix = parsedFunctionCallTemplate.functionParams.prefix;
const callSuffix = parsedFunctionCallTemplate.functionParams.suffix;
const resultPrefix = parsedFunctionCallResultTemplate.functionCallResult.prefix;
const resultSuffix = parsedFunctionCallResultTemplate.functionCallResult.suffix;
if (callPrefix.length === 0)
throw new Error("Function call template must have text before \"{{functionName}}\"");
if (callSuffix.length === 0)
throw new Error("Function call template must have text after \"{{functionParams}}\"");
if (resultPrefix.length === 0)
throw new Error("Function call result template must have text before \"{{functionCallResult}}\"");
if (resultSuffix.length === 0)
throw new Error("Function call result template must have text after \"{{functionCallResult}}\"");
return {
call: {
optionalPrefixSpace: true,
prefix: callPrefix,
paramsPrefix: callParamsPrefix,
suffix: callSuffix
},
result: {
prefix: resultPrefix,
suffix: resultSuffix
}
};
}
//# sourceMappingURL=chatHistoryFunctionCallMessageTemplate.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"chatHistoryFunctionCallMessageTemplate.js","sourceRoot":"","sources":["../../../../src/chatWrappers/generic/utils/chatHistoryFunctionCallMessageTemplate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAC,MAAM,qCAAqC,CAAC;AAGtE,MAAM,UAAU,gCAAgC,CAC5C,QAAiD;IAEjD,IAAI,QAAQ,IAAI,IAAI;QAChB,OAAO,IAAI,CAAC;IAEhB,MAAM,EACF,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,0BAA0B,EACrC,GAAG,QAAQ,CAAC;IAEb,IAAI,oBAAoB,IAAI,IAAI,IAAI,0BAA0B,IAAI,IAAI;QAClE,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;IAE1F,MAAM,0BAA0B,GAAG,iBAAiB,CAAC,oBAAoB,EAAE,CAAC;YACxE,IAAI,EAAE,kBAAkB;YACxB,GAAG,EAAE,cAAc;SACtB,EAAE;YACC,IAAI,EAAE,oBAAoB;YAC1B,GAAG,EAAE,gBAAgB;SACxB,CAAC,CAAC,CAAC;IACJ,MAAM,gCAAgC,GAAG,iBAAiB,CAAC,0BAA0B,EAAE,CAAC;YACpF,IAAI,EAAE,wBAAwB;YAC9B,GAAG,EAAE,oBAAoB;SAC5B,CAAC,CAAC,CAAC;IAEJ,MAAM,UAAU,GAAG,0BAA0B,CAAC,YAAY,CAAC,MAAM,CAAC;IAClE,MAAM,gBAAgB,GAAG,0BAA0B,CAAC,cAAc,CAAC,MAAM,CAAC;IAC1E,MAAM,UAAU,GAAG,0BAA0B,CAAC,cAAc,CAAC,MAAM,CAAC;IAEpE,MAAM,YAAY,GAAG,gCAAgC,CAAC,kBAAkB,CAAC,MAAM,CAAC;IAChF,MAAM,YAAY,GAAG,gCAAgC,CAAC,kBAAkB,CAAC,MAAM,CAAC;IAEhF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IAEzF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;IAE1F,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,gFAAgF,CAAC,CAAC;IAEtG,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,+EAA+E,CAAC,CAAC;IAErG,OAAO;QACH,IAAI,EAAE;YACF,mBAAmB,EAAE,IAAI;YACzB,MAAM,EAAE,UAAU;YAClB,YAAY,EAAE,gBAAgB;YAC9B,MAAM,EAAE,UAAU;SACrB;QACD,MAAM,EAAE;YACJ,MAAM,EAAE,YAAY;YACpB,MAAM,EAAE,YAAY;SACvB;KACJ,CAAC;AACN,CAAC"}

View File

@@ -0,0 +1,19 @@
import { ChatHistoryItem, ChatModelFunctions, ChatWrapperSettings } from "../../../types.js";
import { UniqueIdGenerator } from "./UniqueIdGenerator.js";
export declare function extractFunctionCallSettingsFromJinjaTemplate({ idsGenerator, renderTemplate }: {
idsGenerator: UniqueIdGenerator;
renderTemplate({}: {
chatHistory: ChatHistoryItem[];
functions: ChatModelFunctions;
additionalParams: Record<string, unknown>;
stringifyFunctionParams: boolean;
stringifyFunctionResults: boolean;
combineModelMessageAndToolCalls: boolean;
squashModelTextResponses?: boolean;
}): string;
}): {
settings: ChatWrapperSettings["functions"] | null;
stringifyParams: boolean;
stringifyResult: boolean;
combineModelMessageAndToolCalls: boolean;
};

View File

@@ -0,0 +1,521 @@
import { splitText } from "lifecycle-utils";
import { LlamaText, SpecialToken, SpecialTokensText } from "../../../utils/LlamaText.js";
import { getFirstValidResult } from "./getFirstValidResult.js";
export function extractFunctionCallSettingsFromJinjaTemplate({ idsGenerator, renderTemplate }) {
const idToStaticContent = new Map();
const bosTokenId = idsGenerator.generateId();
const eosTokenId = idsGenerator.generateId();
const eotTokenId = idsGenerator.generateId();
idToStaticContent.set(bosTokenId, new SpecialToken("BOS"));
idToStaticContent.set(eosTokenId, new SpecialToken("EOS"));
idToStaticContent.set(eotTokenId, new SpecialToken("EOT"));
const contentIds = new Set();
const addContentId = (id) => {
contentIds.add(id);
return id;
};
const systemMessage = addContentId(idsGenerator.generateId());
const userMessage1 = addContentId(idsGenerator.generateId());
const modelMessage1 = addContentId(idsGenerator.generateId());
const func1name = addContentId(idsGenerator.generateId());
const func1description = addContentId(idsGenerator.generateId());
const func1params = addContentId(idsGenerator.generateId(true));
const func1result = addContentId(idsGenerator.generateId(true));
const func2name = addContentId(idsGenerator.generateId());
const func2description = addContentId(idsGenerator.generateId());
const func2params = addContentId(idsGenerator.generateId(true));
const func2result = addContentId(idsGenerator.generateId(true));
const modelMessage2 = addContentId(idsGenerator.generateId());
const func1StringifyParam = addContentId(idsGenerator.generateId());
const func1StringifyResult = addContentId(idsGenerator.generateId());
const functions1 = {
[func1name]: {
description: func1description,
params: {
type: "number"
}
}
};
const functions2 = {
...functions1,
[func2name]: {
description: func2description,
params: {
type: "number"
}
}
};
const baseChatHistory = [{
type: "system",
text: systemMessage
}, {
type: "user",
text: userMessage1
}];
const chatHistory1Call = [...baseChatHistory, {
type: "model",
response: [
modelMessage1,
{
type: "functionCall",
name: func1name,
// convert to number since this will go through JSON.stringify,
// and we want to avoid escaping characters in the rendered output
params: Number(func1params),
result: Number(func1result),
startsNewChunk: true
},
modelMessage2
]
}];
const chatHistoryOnlyCall = [...baseChatHistory, {
type: "model",
response: [
{
type: "functionCall",
name: func1name,
// convert to number since this will go through JSON.stringify,
// and we want to avoid escaping characters in the rendered output
params: Number(func1params),
result: Number(func1result),
startsNewChunk: true
},
modelMessage2
]
}];
const chatHistory2Calls = [...baseChatHistory, {
type: "model",
response: [
modelMessage1,
{
type: "functionCall",
name: func1name,
// convert to number since this will go through JSON.stringify,
// and we want to avoid escaping characters in the rendered output
params: Number(func1params),
result: Number(func1result),
startsNewChunk: true
},
{
type: "functionCall",
name: func2name,
params: Number(func2params),
result: Number(func2result),
startsNewChunk: false
},
modelMessage2
]
}];
const chatHistory2CallsNewChunk = [...baseChatHistory, {
type: "model",
response: [
modelMessage1,
{
type: "functionCall",
name: func1name,
// convert to number since this will go through JSON.stringify,
// and we want to avoid escaping characters in the rendered output
params: Number(func1params),
result: Number(func1result),
startsNewChunk: true
},
{
type: "functionCall",
name: func2name,
params: Number(func2params),
result: Number(func2result),
startsNewChunk: true
},
modelMessage2
]
}];
const additionalParams = {
"bos_token": bosTokenId,
"eos_token": eosTokenId,
"eot_token": eotTokenId
};
let combineModelMessageAndToolCalls = true;
let stringifyParams = true;
let stringifyResult = true;
try {
const paramsObjectTest = renderTemplate({
chatHistory: [...baseChatHistory, {
type: "model",
response: [
modelMessage1,
{
type: "functionCall",
name: func1name,
params: { [func1StringifyParam]: "test" },
result: func1StringifyResult,
startsNewChunk: true
},
modelMessage2
]
}],
functions: functions1,
additionalParams,
stringifyFunctionParams: false,
stringifyFunctionResults: false,
combineModelMessageAndToolCalls
});
stringifyParams = (!paramsObjectTest.includes(`"${func1StringifyParam}":`) &&
!paramsObjectTest.includes(`'${func1StringifyParam}':`));
}
catch (err) {
// do nothing
}
try {
const resultObjectTest = renderTemplate({
chatHistory: [...baseChatHistory, {
type: "model",
response: [
modelMessage1,
{
type: "functionCall",
name: func1name,
params: func1StringifyParam,
result: { [func1StringifyResult]: "test" },
startsNewChunk: true
},
modelMessage2
]
}],
functions: functions1,
additionalParams,
stringifyFunctionParams: false,
stringifyFunctionResults: false,
combineModelMessageAndToolCalls
});
stringifyResult = (!resultObjectTest.includes(`"${func1StringifyResult}":`) &&
!resultObjectTest.includes(`'${func1StringifyResult}':`));
}
catch (err) {
// do nothing
}
combineModelMessageAndToolCalls = renderTemplate({
chatHistory: chatHistory1Call,
functions: functions1,
additionalParams,
stringifyFunctionParams: true,
stringifyFunctionResults: true,
combineModelMessageAndToolCalls
}).includes(modelMessage1);
let textBetween2TextualModelResponses = LlamaText();
if (!combineModelMessageAndToolCalls) {
try {
const betweenModelTextualResponsesTest = renderTemplate({
chatHistory: [...baseChatHistory, {
type: "model",
response: [modelMessage1]
}, {
type: "model",
response: [modelMessage2]
}],
functions: {},
additionalParams,
stringifyFunctionParams: false,
stringifyFunctionResults: false,
combineModelMessageAndToolCalls,
squashModelTextResponses: false
});
const textDiff = getTextBetweenIds(betweenModelTextualResponsesTest, modelMessage1, modelMessage2).text ?? "";
textBetween2TextualModelResponses = reviveSeparatorText(textDiff, idToStaticContent, contentIds);
}
catch (err) {
// do nothing
}
}
let usedNewChunkFor2Calls = false;
const rendered1Call = renderTemplate({
chatHistory: chatHistory1Call,
functions: functions1,
additionalParams,
stringifyFunctionParams: stringifyParams,
stringifyFunctionResults: stringifyResult,
combineModelMessageAndToolCalls
});
const renderedOnlyCall = getFirstValidResult([
() => renderTemplate({
chatHistory: chatHistoryOnlyCall,
functions: functions1,
additionalParams,
stringifyFunctionParams: stringifyParams,
stringifyFunctionResults: stringifyResult,
combineModelMessageAndToolCalls
}),
() => undefined
]);
const rendered2Calls = getFirstValidResult([
() => renderTemplate({
chatHistory: chatHistory2Calls,
functions: functions2,
additionalParams,
stringifyFunctionParams: stringifyParams,
stringifyFunctionResults: stringifyResult,
combineModelMessageAndToolCalls
}),
() => {
usedNewChunkFor2Calls = true;
return renderTemplate({
chatHistory: chatHistory2CallsNewChunk,
functions: functions2,
additionalParams,
stringifyFunctionParams: stringifyParams,
stringifyFunctionResults: stringifyResult,
combineModelMessageAndToolCalls
});
}
]);
const modelMessage1ToFunc1Name = getTextBetweenIds(rendered2Calls, modelMessage1, func1name);
const func1NameToFunc1Params = getTextBetweenIds(rendered2Calls, func1name, func1params, modelMessage1ToFunc1Name.endIndex);
const func1ResultIndex = rendered2Calls.indexOf(func1result, func1NameToFunc1Params.endIndex);
const func2NameIndex = rendered2Calls.indexOf(func2name, modelMessage1ToFunc1Name.endIndex);
if (modelMessage1ToFunc1Name.text == null ||
func1NameToFunc1Params.text == null ||
func1ResultIndex < 0 ||
func2NameIndex < 0)
return { settings: null, stringifyParams, stringifyResult, combineModelMessageAndToolCalls };
const supportsParallelCalls = func1ResultIndex > func2NameIndex;
if (!supportsParallelCalls || usedNewChunkFor2Calls) {
const prefix = getTextBetweenIds(rendered1Call, modelMessage1, func1name);
const paramsPrefix = getTextBetweenIds(rendered1Call, func1name, func1params, prefix.endIndex);
const resultPrefix = getTextBetweenIds(rendered1Call, func1params, func1result, paramsPrefix.endIndex);
const resultSuffix = getTextBetweenIds(rendered1Call, func1result, modelMessage2, resultPrefix.endIndex);
if (prefix.text == null || prefix.text === "" || paramsPrefix.text == null || resultPrefix.text == null || resultSuffix.text == null)
return { settings: null, stringifyParams, stringifyResult, combineModelMessageAndToolCalls };
return {
stringifyParams,
stringifyResult,
combineModelMessageAndToolCalls,
settings: {
call: {
optionalPrefixSpace: true,
prefix: removeCommonRevivedPrefix(reviveSeparatorText(prefix.text, idToStaticContent, contentIds), !combineModelMessageAndToolCalls
? textBetween2TextualModelResponses
: LlamaText()),
paramsPrefix: reviveSeparatorText(paramsPrefix.text, idToStaticContent, contentIds),
suffix: "",
emptyCallParamsPlaceholder: {}
},
result: {
prefix: reviveSeparatorText(resultPrefix.text, new Map([
...idToStaticContent.entries(),
[func1name, LlamaText("{{functionName}}")],
[func1params, LlamaText("{{functionParams}}")]
]), contentIds),
suffix: reviveSeparatorText(resultSuffix.text, new Map([
...idToStaticContent.entries(),
[func1name, LlamaText("{{functionName}}")],
[func1params, LlamaText("{{functionParams}}")]
]), contentIds)
}
}
};
}
const func1ParamsToFunc2Name = getTextBetweenIds(rendered2Calls, func1params, func2name, func1NameToFunc1Params.endIndex);
const func2ParamsToFunc1Result = getTextBetweenIds(rendered2Calls, func2params, func1result, func1ParamsToFunc2Name.endIndex);
const func1ResultToFunc2Result = getTextBetweenIds(rendered2Calls, func1result, func2result, func2ParamsToFunc1Result.endIndex);
const func2ResultToModelMessage2 = getTextBetweenIds(rendered2Calls, func2result, modelMessage2, func1ResultToFunc2Result.endIndex);
if (func1ParamsToFunc2Name.text == null || func2ParamsToFunc1Result.text == null || func1ResultToFunc2Result.text == null ||
func2ResultToModelMessage2.text == null)
return { settings: null, stringifyParams, stringifyResult, combineModelMessageAndToolCalls };
const callPrefixLength = findCommonEndLength(modelMessage1ToFunc1Name.text, func1ParamsToFunc2Name.text);
const callPrefixText = func1ParamsToFunc2Name.text.slice(-callPrefixLength);
const parallelismCallPrefix = modelMessage1ToFunc1Name.text.slice(0, -callPrefixLength);
const callSuffixLength = findCommandStartLength(func1ParamsToFunc2Name.text, func2ParamsToFunc1Result.text);
const callSuffixText = func1ParamsToFunc2Name.text.slice(0, callSuffixLength);
const parallelismBetweenCallsText = func1ParamsToFunc2Name.text.slice(callSuffixLength, -callPrefixLength);
const callParamsPrefixText = func1NameToFunc1Params.text;
const resultPrefixLength = findCommonEndLength(func2ParamsToFunc1Result.text, func1ResultToFunc2Result.text);
const resultPrefixText = func2ParamsToFunc1Result.text.slice(-resultPrefixLength);
const resultSuffixLength = findCommandStartLength(func1ResultToFunc2Result.text, func2ResultToModelMessage2.text);
const resultSuffixText = func1ResultToFunc2Result.text.slice(0, resultSuffixLength);
const parallelismResultBetweenResultsText = func1ResultToFunc2Result.text.slice(resultSuffixLength, -resultPrefixLength);
const parallelismResultSuffixText = func2ResultToModelMessage2.text.slice(resultSuffixLength);
const resolveParallelismBetweenSectionsParts = (betweenSectionsText) => {
const { index: endTokenIndex, text: endTokenId } = findFirstTextMatch(betweenSectionsText, [eosTokenId, eosTokenId]);
if (endTokenIndex >= 0 && endTokenId != null)
return {
parallelismCallSuffixText: betweenSectionsText.slice(0, endTokenIndex + endTokenId.length),
parallelismResultPrefix: betweenSectionsText.slice(endTokenIndex + endTokenId.length)
};
const bosIndex = betweenSectionsText.indexOf(bosTokenId);
if (bosIndex >= 0)
return {
parallelismCallSuffixText: betweenSectionsText.slice(0, bosIndex),
parallelismResultPrefix: betweenSectionsText.slice(bosIndex)
};
return {
parallelismCallSuffixText: betweenSectionsText,
parallelismResultPrefix: ""
};
};
const { parallelismCallSuffixText, parallelismResultPrefix } = resolveParallelismBetweenSectionsParts(func2ParamsToFunc1Result.text.slice(callSuffixLength, -resultPrefixLength));
let revivedCallPrefix = reviveSeparatorText(callPrefixText, idToStaticContent, contentIds);
const revivedParallelismCallSectionPrefix = removeCommonRevivedPrefix(reviveSeparatorText(parallelismCallPrefix, idToStaticContent, contentIds), !combineModelMessageAndToolCalls
? textBetween2TextualModelResponses
: LlamaText());
let revivedParallelismCallBetweenCalls = reviveSeparatorText(parallelismBetweenCallsText, idToStaticContent, contentIds);
if (revivedParallelismCallSectionPrefix.values.length === 0 && renderedOnlyCall != null) {
const userMessage1ToModelMessage1Start = getTextBetweenIds(rendered1Call, userMessage1, modelMessage1);
const onlyCallUserMessage1ToFunc1Name = getTextBetweenIds(renderedOnlyCall, userMessage1, func1name);
if (userMessage1ToModelMessage1Start.text != null && onlyCallUserMessage1ToFunc1Name.text != null) {
const onlyCallModelMessagePrefixLength = findCommandStartLength(userMessage1ToModelMessage1Start.text, onlyCallUserMessage1ToFunc1Name.text);
const onlyCallCallPrefixText = onlyCallUserMessage1ToFunc1Name.text.slice(onlyCallModelMessagePrefixLength);
const revivedOnlyCallCallPrefixText = reviveSeparatorText(onlyCallCallPrefixText, idToStaticContent, contentIds);
const optionalCallPrefix = removeCommonRevivedSuffix(revivedCallPrefix, revivedOnlyCallCallPrefixText);
if (optionalCallPrefix.values.length > 0) {
revivedCallPrefix = removeCommonRevivedPrefix(revivedCallPrefix, optionalCallPrefix);
revivedParallelismCallBetweenCalls = LlamaText([
optionalCallPrefix,
revivedParallelismCallBetweenCalls
]);
}
}
}
return {
stringifyParams,
stringifyResult,
combineModelMessageAndToolCalls,
settings: {
call: {
optionalPrefixSpace: true,
prefix: revivedCallPrefix,
paramsPrefix: reviveSeparatorText(callParamsPrefixText, idToStaticContent, contentIds),
suffix: reviveSeparatorText(callSuffixText, idToStaticContent, contentIds),
emptyCallParamsPlaceholder: {}
},
result: {
prefix: reviveSeparatorText(resultPrefixText, new Map([
...idToStaticContent.entries(),
[func1name, LlamaText("{{functionName}}")],
[func1params, LlamaText("{{functionParams}}")]
]), contentIds),
suffix: reviveSeparatorText(resultSuffixText, new Map([
...idToStaticContent.entries(),
[func1name, LlamaText("{{functionName}}")],
[func1params, LlamaText("{{functionParams}}")]
]), contentIds)
},
parallelism: {
call: {
sectionPrefix: revivedParallelismCallSectionPrefix,
betweenCalls: revivedParallelismCallBetweenCalls,
sectionSuffix: reviveSeparatorText(parallelismCallSuffixText, idToStaticContent, contentIds)
},
result: {
sectionPrefix: reviveSeparatorText(parallelismResultPrefix, idToStaticContent, contentIds),
betweenResults: reviveSeparatorText(parallelismResultBetweenResultsText, idToStaticContent, contentIds),
sectionSuffix: reviveSeparatorText(parallelismResultSuffixText, idToStaticContent, contentIds)
}
}
}
};
}
function getTextBetweenIds(text, startId, endId, startIndex = 0) {
const foundStartIndex = text.indexOf(startId, startIndex);
if (foundStartIndex < 0)
return { text: undefined, endIndex: -1 };
const foundEndIndex = text.indexOf(endId, foundStartIndex + startId.length);
if (foundEndIndex < 0)
return { text: undefined, endIndex: -1 };
return {
text: text.slice(foundStartIndex + startId.length, foundEndIndex),
endIndex: foundEndIndex
};
}
function reviveSeparatorText(text, idMap, contentIds) {
return LlamaText(splitText(text, [...new Set([...idMap.keys(), ...contentIds])])
.map((item) => {
if (typeof item === "string")
return new SpecialTokensText(item);
const mappedItem = idMap.get(item.separator);
if (mappedItem != null)
return mappedItem;
if (contentIds.has(item.separator))
throw new Error("Content ID found in separator text");
return new SpecialTokensText(item.separator);
}));
}
function removeCommonRevivedPrefix(target, matchStart) {
for (let commonStartLength = 0; commonStartLength < target.values.length && commonStartLength < matchStart.values.length; commonStartLength++) {
const targetValue = target.values[commonStartLength];
const matchStartValue = matchStart.values[commonStartLength];
if (typeof targetValue === "string" && typeof matchStartValue === "string") {
if (targetValue === matchStartValue)
continue;
}
else if (targetValue instanceof SpecialTokensText && matchStartValue instanceof SpecialTokensText) {
const commonLength = findCommandStartLength(targetValue.value, matchStartValue.value);
if (commonLength === targetValue.value.length && commonLength === matchStartValue.value.length)
continue;
return LlamaText([
new SpecialTokensText(targetValue.value.slice(commonLength)),
...target.values.slice(commonStartLength + 1)
]);
}
else if (targetValue instanceof SpecialToken && matchStartValue instanceof SpecialToken) {
if (targetValue.value === matchStartValue.value)
continue;
}
else if (LlamaText(targetValue ?? "").compare(LlamaText(matchStartValue ?? "")))
continue;
return LlamaText(target.values.slice(commonStartLength));
}
return LlamaText(target.values.slice(matchStart.values.length));
}
function removeCommonRevivedSuffix(target, matchEnd) {
for (let commonEndLength = 0; commonEndLength < target.values.length && commonEndLength < matchEnd.values.length; commonEndLength++) {
const targetValue = target.values[target.values.length - commonEndLength - 1];
const matchEndValue = matchEnd.values[matchEnd.values.length - commonEndLength - 1];
if (typeof targetValue === "string" && typeof matchEndValue === "string") {
if (targetValue === matchEndValue)
continue;
}
else if (targetValue instanceof SpecialTokensText && matchEndValue instanceof SpecialTokensText) {
const commonLength = findCommonEndLength(targetValue.value, matchEndValue.value);
if (commonLength === targetValue.value.length && commonLength === matchEndValue.value.length)
continue;
return LlamaText([
...target.values.slice(0, target.values.length - commonEndLength - 1),
new SpecialTokensText(targetValue.value.slice(0, targetValue.value.length - commonLength))
]);
}
else if (targetValue instanceof SpecialToken && matchEndValue instanceof SpecialToken) {
if (targetValue.value === matchEndValue.value)
continue;
}
else if (LlamaText(targetValue ?? "").compare(LlamaText(matchEndValue ?? "")))
continue;
return LlamaText(target.values.slice(0, target.values.length - commonEndLength - 1));
}
return LlamaText(target.values.slice(0, target.values.length - matchEnd.values.length));
}
function findCommandStartLength(text1, text2) {
let commonStartLength = 0;
while (commonStartLength < text1.length && commonStartLength < text2.length) {
if (text1[commonStartLength] !== text2[commonStartLength])
break;
commonStartLength++;
}
return commonStartLength;
}
function findCommonEndLength(text1, text2) {
let commonEndLength = 0;
while (commonEndLength < text1.length && commonEndLength < text2.length) {
if (text1[text1.length - commonEndLength - 1] !== text2[text2.length - commonEndLength - 1])
break;
commonEndLength++;
}
return commonEndLength;
}
function findFirstTextMatch(text, matchTexts, startIndex = 0) {
for (const matchText of matchTexts) {
const index = text.indexOf(matchText, startIndex);
if (index >= 0)
return { index, text: matchText };
}
return { index: -1, text: undefined };
}
//# sourceMappingURL=extractFunctionCallSettingsFromJinjaTemplate.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
import { ChatWrapperSettings, Tokenizer } from "../../../types.js";
export declare function extractSegmentSettingsFromTokenizerAndChatTemplate(chatTemplate: string | undefined, tokenizer?: Tokenizer): ChatWrapperSettings["segments"];

View File

@@ -0,0 +1,64 @@
import { LlamaText, SpecialTokensText } from "../../../utils/LlamaText.js";
import { removeUndefinedFields } from "../../../utils/removeNullFields.js";
export function extractSegmentSettingsFromTokenizerAndChatTemplate(chatTemplate, tokenizer) {
function tryMatchPrefixSuffixPair(tryMatchGroups) {
if (chatTemplate != null) {
for (const [prefix, suffix] of tryMatchGroups) {
if ((hasAll(chatTemplate.replaceAll(prefix + "\\n\\n" + suffix, ""), [
prefix + "\\n\\n",
"\\n\\n" + suffix
])) || (hasAll(chatTemplate.replaceAll(prefix + "\n\n" + suffix, ""), [
prefix + "\n\n",
"\n\n" + suffix
])))
return {
prefix: LlamaText(new SpecialTokensText(prefix + "\n\n")),
suffix: LlamaText(new SpecialTokensText("\n\n" + suffix))
};
if ((hasAll(chatTemplate.replaceAll(prefix + "\\n" + suffix, ""), [
prefix + "\\n",
"\\n" + suffix
])) || (hasAll(chatTemplate.replaceAll(prefix + "\n" + suffix, ""), [
prefix + "\n",
"\n" + suffix
])))
return {
prefix: LlamaText(new SpecialTokensText(prefix + "\n")),
suffix: LlamaText(new SpecialTokensText("\n" + suffix))
};
if (chatTemplate.includes(prefix) && chatTemplate.includes(suffix))
return {
prefix: LlamaText(new SpecialTokensText(prefix)),
suffix: LlamaText(new SpecialTokensText(suffix))
};
}
}
if (tokenizer != null) {
for (const [prefix, suffix] of tryMatchGroups) {
const thinkTokens = tokenizer(prefix, true, "trimLeadingSpace");
const thinkEndTokens = tokenizer(suffix, true, "trimLeadingSpace");
const [thinkToken] = thinkTokens;
const [thinkEndToken] = thinkEndTokens;
if (thinkTokens.length === 1 && thinkEndTokens.length === 1 &&
thinkToken != null && thinkEndToken != null) {
return {
prefix: LlamaText(new SpecialTokensText(prefix)),
suffix: LlamaText(new SpecialTokensText(suffix))
};
}
}
}
return undefined;
}
return removeUndefinedFields({
thought: tryMatchPrefixSuffixPair([
["<think>", "</think>"], // DeepSeek, QwQ
["<thought>", "</thought>"], // EXAONE Deep
["<|START_THINKING|>", "<|END_THINKING|>"] // Command R7B
])
});
}
function hasAll(text, matches) {
return matches.every((match) => text.includes(match));
}
//# sourceMappingURL=extractSegmentSettingsFromTokenizerAndChatTemplate.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"extractSegmentSettingsFromTokenizerAndChatTemplate.js","sourceRoot":"","sources":["../../../../src/chatWrappers/generic/utils/extractSegmentSettingsFromTokenizerAndChatTemplate.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,SAAS,EAAE,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAC;AAEzE,MAAM,UAAU,kDAAkD,CAC9D,YAAgC,EAAE,SAAqB;IAEvD,SAAS,wBAAwB,CAAC,cAAkD;QAChF,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;YACvB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,cAAc,EAAE,CAAC;gBAC5C,IACI,CACI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,GAAG,QAAQ,GAAG,MAAM,EAAE,EAAE,CAAC,EAAE;oBAC5D,MAAM,GAAG,QAAQ;oBACjB,QAAQ,GAAG,MAAM;iBACpB,CAAC,CACL,IAAI,CACD,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC,EAAE;oBAC1D,MAAM,GAAG,MAAM;oBACf,MAAM,GAAG,MAAM;iBAClB,CAAC,CACL;oBAED,OAAO;wBACH,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;wBACzD,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;qBAC5D,CAAC;gBAEN,IACI,CACI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,EAAE,CAAC,EAAE;oBACzD,MAAM,GAAG,KAAK;oBACd,KAAK,GAAG,MAAM;iBACjB,CAAC,CACL,IAAI,CACD,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,EAAE,CAAC,EAAE;oBACxD,MAAM,GAAG,IAAI;oBACb,IAAI,GAAG,MAAM;iBAChB,CAAC,CACL;oBAED,OAAO;wBACH,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;wBACvD,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;qBAC1D,CAAC;gBAEN,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAC9D,OAAO;wBACH,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;wBAChD,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;qBACnD,CAAC;YACV,CAAC;QACL,CAAC;QAED,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YACpB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,cAAc,EAAE,CAAC;gBAC5C,MAAM,WAAW,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;gBAChE,MAAM,cAAc,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;gBAEnE,MAAM,CAAC,UAAU,CAAC,GAAG,WAAW,CAAC;gBACjC,MAAM,CAAC,aAAa,CAAC,GAAG,cAAc,CAAC;gBAEvC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;oBACvD,UAAU,IAAI,IAAI,IAAI,aAAa,IAAI,IAAI,EAC7C,CAAC;oBACC,OAAO;wBACH,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;wBAChD,MAAM,EAAE,SAAS,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;qBACnD,CAAC;gBACN,CAAC;YACL,CAAC;QACL,CAAC;QAED,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,OAAO,qBAAqB,CAAC;QACzB,OAAO,EAAE,wBAAwB,CAAC;YAC9B,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,gBAAgB;YACzC,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,cAAc;YAC3C,CAAC,oBAAoB,EAAE,kBAAkB,CAAC,CAAC,cAAc;SAC5D,CAAC;KACL,CAAC,CAAC;AACP,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,OAAiB;IAC3C,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1D,CAAC"}

View File

@@ -0,0 +1,6 @@
/**
* Call the functions in the array one by one and return the result of the first one that doesn't throw an error.
*
* If all functions throw an error, throw the error of the last function.
*/
export declare function getFirstValidResult<const T extends (() => any)[]>(options: T): ReturnType<T[number]>;

View File

@@ -0,0 +1,19 @@
/**
* Call the functions in the array one by one and return the result of the first one that doesn't throw an error.
*
* If all functions throw an error, throw the error of the last function.
*/
export function getFirstValidResult(options) {
for (let i = 0; i < options.length; i++) {
if (i === options.length - 1)
return options[i]();
try {
return options[i]();
}
catch (err) {
// do nothing
}
}
throw new Error("All options failed");
}
//# sourceMappingURL=getFirstValidResult.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getFirstValidResult.js","sourceRoot":"","sources":["../../../../src/chatWrappers/generic/utils/getFirstValidResult.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAgC,OAAU;IACzE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,CAAC;YACxB,OAAO,OAAO,CAAC,CAAC,CAAE,EAAE,CAAC;QAEzB,IAAI,CAAC;YACD,OAAO,OAAO,CAAC,CAAC,CAAE,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,aAAa;QACjB,CAAC;IACL,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAC1C,CAAC"}

View File

@@ -0,0 +1,2 @@
import { ChatHistoryItem } from "../../../types.js";
export declare function squashChatHistoryItems(history: readonly ChatHistoryItem[]): ChatHistoryItem[];

View File

@@ -0,0 +1,35 @@
import { LlamaText } from "../../../utils/LlamaText.js";
export function squashChatHistoryItems(history) {
const res = [];
for (const item of history) {
const lastItem = res.at(-1);
if (lastItem == null) {
res.push(structuredClone(item));
continue;
}
if (lastItem.type === "system" && item.type === "system")
lastItem.text = LlamaText.joinValues("\n\n", [
LlamaText.fromJSON(lastItem.text),
LlamaText.fromJSON(item.text)
]).toJSON();
else if (lastItem.type === "user" && item.type === "user")
lastItem.text += "\n\n" + item.text;
else if (lastItem.type === "model" && item.type === "model") {
const responsesToAdd = ["\n\n", ...item.response];
while (typeof responsesToAdd[0] === "string" && typeof lastItem.response.at(-1) === "string") {
const lastResponses = lastItem.response.pop();
if (typeof lastResponses !== "string") {
lastItem.response.push(lastResponses);
break;
}
lastItem.response.push(lastResponses + responsesToAdd.shift());
}
while (responsesToAdd.length > 0)
lastItem.response.push(responsesToAdd.shift());
}
else
res.push(structuredClone(item));
}
return res;
}
//# sourceMappingURL=squashChatHistoryItems.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"squashChatHistoryItems.js","sourceRoot":"","sources":["../../../../src/chatWrappers/generic/utils/squashChatHistoryItems.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAEtD,MAAM,UAAU,sBAAsB,CAAC,OAAmC;IACtE,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;YACnB,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;YAChC,SAAS;QACb,CAAC;QAED,IAAI,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YACpD,QAAQ,CAAC,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE;gBACzC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;gBACjC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;aAChC,CAAC,CAAC,MAAM,EAAE,CAAC;aACX,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;YACrD,QAAQ,CAAC,IAAI,IAAI,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC;aACnC,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC1D,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;YAElD,OAAO,OAAO,cAAc,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,OAAO,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAC3F,MAAM,aAAa,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,EAAG,CAAC;gBAC/C,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC;oBACpC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;oBACtC,MAAM;gBACV,CAAC;gBAED,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,GAAG,cAAc,CAAC,KAAK,EAAG,CAAC,CAAC;YACpE,CAAC;YAED,OAAO,cAAc,CAAC,MAAM,GAAG,CAAC;gBAC5B,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,EAAG,CAAC,CAAC;QACxD,CAAC;;YACG,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,GAAG,CAAC;AACf,CAAC"}

View File

@@ -0,0 +1,22 @@
import { ChatWrapperSettings } from "../../../types.js";
export declare function templateSegmentOptionsToChatWrapperSettings(templateOptions?: TemplateChatWrapperSegmentsOptions): ChatWrapperSettings["segments"];
export type TemplateChatWrapperSegmentsOptions = {
/** Template for a thought segment */
thoughtTemplate?: `${string}{{content}}${string}`;
/**
* Automatically reopen a thought segment after function calls.
*
* Useful for aligning the output of models that assume that a thought segment is already open after function calls.
*
* Defaults to `false`.
*/
reopenThoughtAfterFunctionCalls?: boolean;
/** Consider all segments to be closed when this text is detected */
closeAllSegmentsTemplate?: string;
/**
* After function calls, reiterate the stack of the active segments to remind the model of the context.
*
* Defaults to `false`.
*/
reiterateStackAfterFunctionCalls?: boolean;
};

View File

@@ -0,0 +1,28 @@
import { parseTextTemplate } from "../../../utils/parseTextTemplate.js";
import { removeUndefinedFields } from "../../../utils/removeNullFields.js";
export function templateSegmentOptionsToChatWrapperSettings(templateOptions) {
if (templateOptions == null)
return {};
function getThoughtSegmentOptions() {
if (templateOptions?.thoughtTemplate == null)
return undefined;
const parsedThoughtTemplate = parseTextTemplate(templateOptions.thoughtTemplate, [{
text: "{{content}}",
key: "content"
}]);
const prefix = parsedThoughtTemplate.content.prefix;
if (prefix.length === 0)
throw new Error("Thought template must have text before \"{{content}}\"");
return removeUndefinedFields({
prefix,
suffix: parsedThoughtTemplate.content.suffix || undefined,
reopenAfterFunctionCalls: templateOptions.reopenThoughtAfterFunctionCalls
});
}
return removeUndefinedFields({
closeAllSegments: templateOptions.closeAllSegmentsTemplate || undefined,
reiterateStackAfterFunctionCalls: templateOptions.reiterateStackAfterFunctionCalls,
thought: getThoughtSegmentOptions()
});
}
//# sourceMappingURL=templateSegmentOptionsToChatWrapperSettings.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"templateSegmentOptionsToChatWrapperSettings.js","sourceRoot":"","sources":["../../../../src/chatWrappers/generic/utils/templateSegmentOptionsToChatWrapperSettings.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,iBAAiB,EAAC,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAC;AAEzE,MAAM,UAAU,2CAA2C,CACvD,eAAoD;IAEpD,IAAI,eAAe,IAAI,IAAI;QACvB,OAAO,EAAE,CAAC;IAEd,SAAS,wBAAwB;QAC7B,IAAI,eAAe,EAAE,eAAe,IAAI,IAAI;YACxC,OAAO,SAAS,CAAC;QAErB,MAAM,qBAAqB,GAAG,iBAAiB,CAAC,eAAe,CAAC,eAAe,EAAE,CAAC;gBAC9E,IAAI,EAAE,aAAa;gBACnB,GAAG,EAAE,SAAS;aACjB,CAAC,CAAC,CAAC;QAEJ,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,MAAM,CAAC;QACpD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAE9E,OAAO,qBAAqB,CAAC;YACzB,MAAM;YACN,MAAM,EAAE,qBAAqB,CAAC,OAAO,CAAC,MAAM,IAAI,SAAS;YACzD,wBAAwB,EAAE,eAAe,CAAC,+BAA+B;SAC5E,CAAC,CAAC;IACP,CAAC;IAED,OAAO,qBAAqB,CAAC;QACzB,gBAAgB,EAAE,eAAe,CAAC,wBAAwB,IAAI,SAAS;QACvE,gCAAgC,EAAE,eAAe,CAAC,gCAAgC;QAElF,OAAO,EAAE,wBAAwB,EAAE;KACtC,CAAC,CAAC;AACP,CAAC"}