forked from wmantly/mc-bot-town
200 lines
4.7 KiB
JavaScript
200 lines
4.7 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
|
|
});
|
|
|
|
// Cap history — unbounded growth eventually overflows num_ctx,
|
|
// which silently truncates the system prompt (memories, tool docs)
|
|
const maxHistory = this.config.maxHistory || 30;
|
|
if (this.messages.length > maxHistory) {
|
|
this.messages.splice(0, this.messages.length - maxHistory);
|
|
}
|
|
|
|
// 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;
|