@naktibalda/jorel
Version:
The easiest way to use LLMs, including streams, images, documents, tools and various agent scenarios.
302 lines (301 loc) • 14.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.JorElCoreStore = void 0;
const logger_1 = require("../logger");
const providers_1 = require("../providers");
const get_overrides_1 = require("../providers/get-overrides");
const model_parameter_overrides_1 = require("../providers/model-parameter-overrides");
const shared_1 = require("../shared");
const jorel_models_1 = require("./jorel.models");
const jorel_providers_1 = require("./jorel.providers");
class JorElCoreStore {
defaultConfig = {};
logger;
providerManager;
modelManager;
constructor(config = {}) {
this.defaultConfig = config;
if (config.logger instanceof logger_1.LogService) {
this.logger = config.logger;
if (config.logLevel) {
this.logger.logLevel = config.logLevel;
}
}
else {
this.logger = this.logger = new logger_1.LogService(config.logger, config.logLevel);
}
this.providerManager = new jorel_providers_1.JorElProviderManager(this.logger);
this.modelManager = new jorel_models_1.JorElModelManager(this.logger);
this.logger.debug("Core", "Core store initialized");
this.logger.silly("Core", "Core store config", {
config: (0, shared_1.omit)(config, ["logger"]),
});
}
/**
* Applies model-specific overrides to messages and config
* @param messages - The messages to apply the overrides to
* @param config - The config to apply the overrides to
* @param modelName - The name of the model to apply the overrides to
*/
applyModelOverrides(messages, config, modelName) {
const overrides = (0, get_overrides_1.getModelOverrides)(modelName, model_parameter_overrides_1.modelParameterOverrides);
if (overrides.noSystemMessage && messages.some((m) => m.role === "system")) {
this.logger.debug("Core", `System messages are not supported for ${modelName} and will be ignored`);
}
if (overrides.noTemperature && typeof config.temperature === "number") {
this.logger.debug("Core", `Temperature is not supported for ${modelName} and will be ignored`);
}
return {
messages: overrides.noSystemMessage ? messages.filter((m) => m.role !== "system") : messages,
config: (0, shared_1.shallowFilterUndefined)({
...config,
temperature: overrides.noTemperature ? null : config.temperature,
}),
};
}
/**
* Generate a response for a given set of messages
* @param messages - The messages to generate a response for
* @param config - The config to use for this generation
* @param config.model - Model to use for this generation (optional)
* @param config.systemMessage - System message to include in this request (optional)
* @param config.temperature - Temperature for this request (optional)
* @param config.tools - Tools to use for this request (optional)
*/
async generate(messages, config = {}) {
const modelEntry = this.modelManager.getModel(config.model || this.modelManager.getDefaultModel());
const provider = this.providerManager.getProvider(modelEntry.provider);
const { messages: messagesWithOverrides, config: configWithOverrides } = this.applyModelOverrides(messages, config, modelEntry.model);
this.logger.debug("Core", `Starting to generate response with model ${modelEntry.model} and provider ${modelEntry.provider}`);
this.logger.silly("Core", `Generate inputs`, {
model: modelEntry.model,
provider: modelEntry.provider,
messagesWithOverrides,
...(0, shared_1.omit)(configWithOverrides, ["secureContext"]),
secureContext: config.secureContext ? (0, shared_1.maskAll)(config.secureContext) : undefined,
});
const response = await provider.generateResponse(modelEntry.model, messagesWithOverrides, {
...this.defaultConfig,
...configWithOverrides,
logger: this.logger,
});
this.logger.debug("Core", `Finished generating response in ${response.meta.durationMs}ms. ${response.meta.inputTokens} input tokens, ${response.meta.outputTokens} output tokens`);
this.logger.silly("Core", "Generate output", {
response,
});
return response;
}
/**
* Internal method to generate a response and process tool calls until a final response is generated
* @param messages - The messages to generate a response for
* @param config - The config to use for this generation
* @param autoApprove - Whether to auto-approve tool calls
*/
async generateAndProcessTools(messages, config = {}, autoApprove = false) {
const _messages = [...messages];
if (config.tools && config.tools.tools.some((t) => t.type !== "function")) {
throw new Error("Only tools with a function executor can be used in this context");
}
const maxToolCalls = (config.maxToolCalls ?? config.maxAttempts) || 5;
const maxToolCallErrors = config.maxToolCallErrors || 3;
const maxAttempts = Math.max(maxToolCalls, maxToolCallErrors);
let toolCallErrors = 0;
let toolCalls = 0;
let generation;
for (let i = 0; i < maxAttempts; i++) {
generation = await this.generate(_messages, config);
if (generation.role === "assistant" || !config.tools) {
break;
}
else {
generation = autoApprove ? config.tools.approveCalls(generation) : generation;
this.logger.debug("Core", `Starting to process tool calls`);
this.logger.silly("Core", `Tool call inputs`, {
generation,
autoApprove,
context: config.context,
secureContext: config.secureContext ? (0, shared_1.maskAll)(config.secureContext) : undefined,
});
generation = await config.tools.processCalls(generation, {
context: config.context,
secureContext: config.secureContext,
maxErrors: Math.max(0, maxToolCallErrors - toolCallErrors),
maxCalls: Math.max(0, maxToolCalls - toolCalls),
});
toolCalls += generation.toolCalls.length;
toolCallErrors += generation.toolCalls.filter((t) => t.executionState === "error").length;
this.logger.debug("Core", `Finished processing tool calls`);
this.logger.silly("Core", `Tool call outputs`, {
generation,
});
_messages.push((0, providers_1.generateAssistantMessage)(generation.content, generation.toolCalls));
}
}
if (!generation) {
throw new Error("Unable to generate a response");
}
_messages.push((0, providers_1.generateAssistantMessage)(generation.content));
return {
output: generation,
messages: _messages,
};
}
/**
* Generate a stream of response chunks for a given set of messages
* @param messages - The messages to generate a response for
* @param config - The config to use for this generation
*/
async *generateContentStream(messages, config = {}) {
const modelEntry = this.modelManager.getModel(config.model || this.modelManager.getDefaultModel());
const provider = this.providerManager.getProvider(modelEntry.provider);
const { messages: messagesWithOverrides, config: configWithOverrides } = this.applyModelOverrides(messages, config, modelEntry.model);
this.logger.debug("Core", `Starting to generate response stream with model ${modelEntry.model} and provider ${modelEntry.provider}`);
this.logger.silly("Core", `Response stream inputs`, {
model: modelEntry.model,
provider: modelEntry.provider,
messages: messagesWithOverrides,
...(0, shared_1.omit)(configWithOverrides, []),
});
const stream = provider.generateResponseStream(modelEntry.model, messagesWithOverrides, {
...this.defaultConfig,
...configWithOverrides,
logger: this.logger,
});
for await (const chunk of stream) {
if (chunk.type === "toolCallStarted" || chunk.type === "toolCallCompleted") {
// Do nothing
}
else {
yield chunk;
}
if (chunk.type === "response") {
this.logger.debug("Core", "Finished generating response stream");
this.logger.silly("Core", "Response stream output", {
chunk,
});
}
}
}
/**
* Generate a stream of response chunks for a given set of messages and process tool calls until a final response is generated
* @param messages - The messages to generate a response for
* @param config - The config to use for this generation
* @param autoApprove - Whether to auto-approve tool calls
*/
async *generateStreamAndProcessTools(messages, config = {}, autoApprove = false) {
if (config.tools && config.tools.tools.some((t) => t.type !== "function")) {
throw new Error("Only tools with a function executor can be used in this context");
}
const maxToolCalls = (config.maxToolCalls ?? config.maxAttempts) || 5;
const maxToolCallErrors = config.maxToolCallErrors || 3;
const maxAttempts = Math.max(maxToolCalls, maxToolCallErrors);
let toolCallErrors = 0;
let toolCalls = 0;
let response = undefined;
for (let i = 0; i < maxAttempts; i++) {
const stream = this.generateContentStream(messages, config);
for await (const chunk of stream) {
if (chunk.type === "chunk") {
yield chunk;
}
else {
response = chunk;
}
}
if (!response)
throw new Error("Unable to generate a response");
if (response.role === "assistant" || !config.tools)
break;
response = autoApprove ? config.tools.approveCalls(response) : response;
this.logger.debug("Core", "Processing tool calls");
// Emit tool call started events for each tool call
for (const toolCall of response.toolCalls) {
yield {
type: "toolCallStarted",
toolCall: {
id: toolCall.id,
executionState: "pending",
approvalState: toolCall.approvalState,
request: toolCall.request,
result: null,
},
};
}
const processedToolCalls = [];
for (const toolCall of response.toolCalls) {
if (toolCallErrors >= maxToolCallErrors) {
this.setCallToError(toolCall, "Too many tool call errors");
processedToolCalls.push(toolCall);
continue;
}
if (toolCalls >= maxToolCalls) {
this.setCallToError(toolCall, "Too many tool calls");
processedToolCalls.push(toolCall);
continue;
}
if (toolCall.executionState !== "pending") {
continue;
}
const result = await config.tools.processToolCall(toolCall, {
context: config.context,
secureContext: config.secureContext,
});
processedToolCalls.push(result.toolCall);
if (result.toolCall.executionState === "completed" || result.toolCall.executionState === "error") {
yield {
type: "toolCallCompleted",
toolCall: result.toolCall,
};
}
if (result.toolCall.executionState === "error") {
toolCallErrors++;
}
toolCalls++;
}
response = {
...response,
toolCalls: processedToolCalls,
};
this.logger.debug("Core", "Finished processing tool calls");
messages.push((0, providers_1.generateAssistantMessage)(response.content, response.toolCalls));
}
if (response) {
yield response;
messages.push((0, providers_1.generateAssistantMessage)(response.content));
}
yield {
type: "messages",
messages,
};
}
/**
* Helper method to set a tool call to error state
*/
setCallToError(toolCall, errorMessage) {
toolCall.executionState = "error";
toolCall.error = {
type: "ToolExecutionError",
message: errorMessage,
numberOfAttempts: 1,
lastAttempt: new Date(),
};
toolCall.result = null;
}
/**
* Generate an embedding for a given text
* @param text - The text to generate an embedding for
* @param model - The model to use for this generation (optional)
*/
async generateEmbedding(text, model) {
const modelEntry = this.modelManager.getEmbeddingModel(model || this.modelManager.getDefaultEmbeddingModel());
const provider = this.providerManager.getProvider(modelEntry.provider);
this.logger.debug("Core", `Generating embedding with model ${modelEntry.model} and provider ${modelEntry.provider}`);
this.logger.silly("Core", `Embedding inputs`, {
model: modelEntry.model,
provider: modelEntry.provider,
text,
});
return await provider.createEmbedding(modelEntry.model, text);
}
}
exports.JorElCoreStore = JorElCoreStore;