Files
mc-bot-town-2/nodejs/controller/ai/providers/ollama.js
T
2026-05-03 11:13:06 -04:00

193 lines
4.4 KiB
JavaScript

'use strict';
const axios = require('axios');
axios.defaults.timeout = 0;
class OllamaProvider {
constructor(config) {
this.config = config;
this.baseUrl = config.baseUrl || 'http://localhost:11434';
this.model = config.model || 'llama3.2';
this.messages = [];
this.tools = [];
}
supportsTools() {
return true;
}
setTools(tools) {
this.tools = tools;
}
async start(history) {
this.messages = history || [];
if (this.config.prompt) {
console.log('Ollama provider initialized with model:', this.model);
}
}
__settings() {
return {
temperature: this.config.temperature || 1,
top_p: this.config.topP || 0.95,
top_k: this.config.topK || 64,
num_predict: this.config.maxOutputTokens || 2048,
num_ctx: this.config.num_ctx,
};
}
__jsonFormat() {
return {
type: 'array',
items: {
type: 'object',
properties: {
text: { type: 'string' },
delay: { type: 'number' }
},
required: ['text', 'delay']
}
};
}
/**
* Strip markdown code fences (```json ... ```) from a response string.
* Ollama models (especially smaller ones) sometimes wrap their output in fences
* even when the system prompt says not to.
*/
static stripMarkdownFences(text) {
if (!text || typeof text !== 'string') return text;
let cleaned = text.trim();
// Remove leading ```json or ``` fences
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '');
// Remove trailing ``` fences
cleaned = cleaned.replace(/\n?```\s*$/, '');
return cleaned.trim();
}
async chat(message, retryCount = 0) {
try {
const messages = [
{
role: 'system',
content: this.config.prompt || 'You are a helpful assistant.'
},
...this.messages.map(msg => ({
role: msg.role === 'model' ? 'assistant' : 'user',
content: msg.parts ? msg.parts.map(p => p.text).join('') : (msg.content || '')
})),
{
role: 'user',
content: message
}
];
const requestBody = {
model: this.model,
messages: messages,
stream: false,
options: this.__settings()
};
// Only set format when NO tools are configured.
// format + tools together confuses smaller models — they try to
// satisfy both constraints and produce garbage (_/empty responses).
const hasTools = this.tools && this.tools.length > 0;
if (!hasTools) {
requestBody.format = this.__jsonFormat();
}
if (hasTools) {
requestBody.tools = this.tools.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters
}
}));
}
const response = await axios.post(
`${this.baseUrl}/api/chat`,
requestBody,
{
headers: {
'Content-Type': 'application/json'
}
}
);
const rawContent = response.data.message.content;
const messageData = response.data.message;
console.log('Ollama response', rawContent)
// Update history
this.messages.push({
role: 'user',
parts: [{ text: message }],
content: message
});
this.messages.push({
role: 'model',
parts: [{ text: rawContent }],
content: rawContent
});
// The text() closure strips markdown code fences so consumers
// (processResponse, getToolCalls) get clean content.
const result = {
response: {
text: () => {
let content = messageData.content || rawContent;
content = OllamaProvider.stripMarkdownFences(content);
return content;
}
}
};
// Ollama may return tool calls in messageData.tool_calls
if (messageData.tool_calls) {
result.tool_calls = messageData.tool_calls;
}
return result;
} catch (error) {
const errorDetails = {
message: error.message,
status: error.response?.status,
data: error.response?.data,
url: error.config?.url,
retryCount: retryCount
};
console.log('Ollama API error details:', errorDetails);
if (retryCount > 3) {
throw new Error(`Ollama API error after ${retryCount} retries: ${error.message}`);
}
const baseDelay = Math.min(1000 * Math.pow(2, retryCount), 30000);
const jitter = Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, baseDelay + jitter));
return await this.chat(message, retryCount + 1);
}
}
setPrompt(prompt) {
this.config.prompt = prompt;
}
getResponse(result) {
return result.response.text();
}
async close() {
this.messages = [];
}
}
module.exports = OllamaProvider;