UNPKG

genkitx-ollama

Version:

Genkit AI framework plugin for Ollama APIs.

413 lines 11.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var index_exports = {}; __export(index_exports, { OllamaConfigSchema: () => OllamaConfigSchema, ollama: () => ollama }); module.exports = __toCommonJS(index_exports); var import_genkit = require("genkit"); var import_logging = require("genkit/logging"); var import_model = require("genkit/model"); var import_plugin = require("genkit/plugin"); var import_embeddings = require("./embeddings.js"); const ANY_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#" }; const GENERIC_MODEL_INFO = { supports: { multiturn: true, media: true, tools: true, toolChoice: true, systemRole: true, constrained: "all" } }; const DEFAULT_OLLAMA_SERVER_ADDRESS = "http://localhost:11434"; async function initializer(ai, serverAddress, params) { params?.models?.map( (model) => defineOllamaModel(ai, model, serverAddress, params?.requestHeaders) ); params?.embedders?.map( (model) => (0, import_embeddings.defineOllamaEmbedder)(ai, { name: model.name, modelName: model.name, dimensions: model.dimensions, options: params }) ); } function resolveAction(ai, actionType, actionName, serverAddress, requestHeaders) { if (actionType === "model") { defineOllamaModel( ai, { name: actionName }, serverAddress, requestHeaders ); } } async function listActions(serverAddress, requestHeaders) { const models = await listLocalModels(serverAddress, requestHeaders); return models?.filter((m) => m.model && !m.model.includes("embed")).map( (m) => (0, import_genkit.modelActionMetadata)({ name: `ollama/${m.model}`, info: GENERIC_MODEL_INFO }) ) || []; } function ollamaPlugin(params) { if (!params) { params = {}; } if (!params.serverAddress) { params.serverAddress = DEFAULT_OLLAMA_SERVER_ADDRESS; } const serverAddress = params.serverAddress; return (0, import_plugin.genkitPlugin)( "ollama", async (ai) => { await initializer(ai, serverAddress, params); }, async (ai, actionType, actionName) => { resolveAction( ai, actionType, actionName, serverAddress, params?.requestHeaders ); }, async () => await listActions(serverAddress, params?.requestHeaders) ); } async function listLocalModels(serverAddress, requestHeaders) { let res; try { res = await fetch(serverAddress + "/api/tags", { method: "GET", headers: { "Content-Type": "application/json", ...await getHeaders(serverAddress, requestHeaders) } }); } catch (e) { throw new Error(`Make sure the Ollama server is running.`, { cause: e }); } const modelResponse = JSON.parse(await res.text()); return modelResponse.models; } const OllamaConfigSchema = import_model.GenerationCommonConfigSchema.extend({ temperature: import_genkit.z.number().min(0).max(1).describe( import_model.GenerationCommonConfigDescriptions.temperature + " The default value is 0.8." ).optional(), topK: import_genkit.z.number().describe( import_model.GenerationCommonConfigDescriptions.topK + " The default value is 40." ).optional(), topP: import_genkit.z.number().min(0).max(1).describe( import_model.GenerationCommonConfigDescriptions.topP + " The default value is 0.9." ).optional() }); function defineOllamaModel(ai, model, serverAddress, requestHeaders) { return ai.defineModel( { name: `ollama/${model.name}`, label: `Ollama - ${model.name}`, configSchema: OllamaConfigSchema, supports: { multiturn: !model.type || model.type === "chat", systemRole: true, tools: model.supports?.tools } }, async (input, streamingCallback) => { const { topP, topK, stopSequences, maxOutputTokens, ...rest } = input.config; const options = { ...rest }; if (topP !== void 0) { options.top_p = topP; } if (topK !== void 0) { options.top_k = topK; } if (stopSequences !== void 0) { options.stop = stopSequences.join(""); } if (maxOutputTokens !== void 0) { options.num_predict = maxOutputTokens; } const type = model.type ?? "chat"; const request = toOllamaRequest( model.name, input, options, type, !!streamingCallback ); import_logging.logger.debug(request, `ollama request (${type})`); const extraHeaders = await getHeaders( serverAddress, requestHeaders, model, input ); let res; try { res = await fetch( serverAddress + (type === "chat" ? "/api/chat" : "/api/generate"), { method: "POST", body: JSON.stringify(request), headers: { "Content-Type": "application/json", ...extraHeaders } } ); } catch (e) { const cause = e.cause; if (cause && cause instanceof Error && cause.message?.includes("ECONNREFUSED")) { cause.message += ". Make sure the Ollama server is running."; throw cause; } throw e; } if (!res.body) { throw new Error("Response has no body"); } let message; if (streamingCallback) { const reader = res.body.getReader(); const textDecoder = new TextDecoder(); let textResponse = ""; for await (const chunk of readChunks(reader)) { const chunkText = textDecoder.decode(chunk); const json = JSON.parse(chunkText); const message2 = parseMessage(json, type); streamingCallback({ index: 0, content: message2.content }); textResponse += message2.content[0].text; } message = { role: "model", content: [ { text: textResponse } ] }; } else { const txtBody = await res.text(); const json = JSON.parse(txtBody); import_logging.logger.debug(txtBody, "ollama raw response"); message = parseMessage(json, type); } return { message, usage: (0, import_model.getBasicUsageStats)(input.messages, message), finishReason: "stop" }; } ); } function parseMessage(response, type) { if (response.error) { throw new Error(response.error); } if (type === "chat") { if (response.message.tool_calls && response.message.tool_calls.length > 0) { return { role: toGenkitRole(response.message.role), content: toGenkitToolRequest(response.message.tool_calls) }; } else { return { role: toGenkitRole(response.message.role), content: [ { text: response.message.content } ] }; } } else { return { role: "model", content: [ { text: response.response } ] }; } } async function getHeaders(serverAddress, requestHeaders, model, input) { return requestHeaders ? typeof requestHeaders === "function" ? await requestHeaders( { serverAddress, model }, input ) : requestHeaders : {}; } function toOllamaRequest(name, input, options, type, stream) { const request = { model: name, options, stream, tools: input.tools?.filter(isValidOllamaTool).map(toOllamaTool) }; if (type === "chat") { const messages = []; input.messages.forEach((m) => { let messageText = ""; const role = toOllamaRole(m.role); const images = []; const toolRequests = []; const toolResponses = []; m.content.forEach((c) => { if (c.text) { messageText += c.text; } if (c.media) { let imageUri = c.media.url; if (imageUri.startsWith("data:")) { imageUri = imageUri.substring(imageUri.indexOf(",") + 1); } images.push(imageUri); } if (c.toolRequest) { toolRequests.push(c.toolRequest); } if (c.toolResponse) { toolResponses.push(c.toolResponse); } }); toolResponses.forEach((t) => { messages.push({ role, content: typeof t.output === "string" ? t.output : JSON.stringify(t.output) }); }); messages.push({ role, content: toolRequests.length > 0 ? "" : messageText, images: images.length > 0 ? images : void 0, tool_calls: toolRequests.length > 0 ? toOllamaToolCall(toolRequests) : void 0 }); }); request.messages = messages; } else { request.prompt = getPrompt(input); request.system = getSystemMessage(input); } return request; } function toOllamaRole(role) { if (role === "model") { return "assistant"; } return role; } function toGenkitRole(role) { if (role === "assistant") { return "model"; } return role; } function toOllamaTool(tool) { return { type: "function", function: { name: tool.name, description: tool.description, parameters: tool.inputSchema ?? ANY_JSON_SCHEMA } }; } function toOllamaToolCall(toolRequests) { return toolRequests.map((t) => ({ function: { name: t.name, // This should be safe since we already filtered tools that don't accept // objects arguments: t.input } })); } function toGenkitToolRequest(tool_calls) { return tool_calls.map((t) => ({ toolRequest: { name: t.function.name, ref: t.function.index ? t.function.index.toString() : void 0, input: t.function.arguments } })); } function readChunks(reader) { return { async *[Symbol.asyncIterator]() { let readResult = await reader.read(); while (!readResult.done) { yield readResult.value; readResult = await reader.read(); } } }; } function getPrompt(input) { return input.messages.filter((m) => m.role !== "system").map((m) => m.content.map((c) => c.text).join()).join(); } function getSystemMessage(input) { return input.messages.filter((m) => m.role === "system").map((m) => m.content.map((c) => c.text).join()).join(); } function isValidOllamaTool(tool) { if (tool.inputSchema?.type !== "object") { throw new Error( `Unsupported tool: '${tool.name}'. Ollama only supports tools with object inputs` ); } return true; } const ollama = ollamaPlugin; ollama.model = (name, config) => { return (0, import_model.modelRef)({ name: `ollama/${name}`, config, configSchema: OllamaConfigSchema }); }; ollama.embedder = (name, config) => { return (0, import_genkit.embedderRef)({ name: `ollama/${name}`, config }); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { OllamaConfigSchema, ollama }); //# sourceMappingURL=index.js.map