Mostly works

This commit is contained in:
2026-05-03 11:13:06 -04:00
parent 6f4519894b
commit 60663b8d4a
21 changed files with 3188 additions and 1706 deletions
+34 -1
View File
@@ -6,6 +6,15 @@ class GeminiProvider {
constructor(config) {
this.config = config;
this.session = null;
this.tools = [];
}
supportsTools() {
return true;
}
setTools(tools) {
this.tools = tools;
}
async start(history) {
@@ -18,7 +27,7 @@ class GeminiProvider {
}
__settings(history) {
return {
const settings = {
generationConfig: {
temperature: this.config.temperature || 1,
topP: this.config.topP || 0.95,
@@ -55,6 +64,19 @@ class GeminiProvider {
},
],
};
// Add tools if configured
if (this.tools && this.tools.length > 0) {
settings.tools = this.tools.map(tool => ({
functionDeclarations: [{
name: tool.name,
description: tool.description,
parameters: tool.parameters
}]
}));
}
return settings;
}
async chat(message, retryCount = 0) {
@@ -65,6 +87,9 @@ class GeminiProvider {
if (retryCount > 3) {
throw new Error(`Gemini 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));
// Recover by removing last history entry and restarting
this.session.params.history.pop();
await this.start(this.session.params.history);
@@ -80,6 +105,14 @@ class GeminiProvider {
return result.response.text();
}
getToolCalls(result) {
// Gemini returns function calls in result.response.functionCalls()
if (result.response && typeof result.response.functionCalls === 'function') {
return result.response.functionCalls();
}
return null;
}
async close() {
this.session = null;
}
+8 -1
View File
@@ -22,4 +22,11 @@ module.exports = {
ProviderFactory,
GeminiProvider,
OllamaProvider
};
};
/**
* Provider interface for tool support
* All providers must implement these methods:
* - setTools(tools): Configure available tools for function calling
* - supportsTools(): boolean - whether this provider supports tool calling
*/
+67 -22
View File
@@ -10,12 +10,20 @@ class OllamaProvider {
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) {
// Convert Gemini-style history to Ollama format if needed
this.messages = history || [];
if (this.config.prompt) {
console.log('Ollama provider initialized with model:', this.model);
}
@@ -27,13 +35,12 @@ class OllamaProvider {
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 'json'
/* return {
return {
type: 'array',
items: {
type: 'object',
@@ -43,12 +50,26 @@ class OllamaProvider {
},
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 {
// Build conversation from prompt + history
const messages = [
{
role: 'system',
@@ -64,33 +85,45 @@ class OllamaProvider {
}
];
// console.log('Ollama messages', messages)
const requestBody = {
model: this.model,
messages: messages,
stream: false,
think: false,
format: this.__jsonFormat(),
options: this.__settings()
};
// console.log('Ollama request:', JSON.stringify(requestBody, null, 2));
// 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,
{
// timeout: this.config.timeout || 30000,
headers: {
'Content-Type': 'application/json'
}
}
);
// Log raw response for debugging
const rawContent = response.data.message.content;
const messageData = response.data.message;
console.log('Ollama response', rawContent)
// console.log('Ollama raw response:', JSON.stringify(rawContent));
// console.log('Ollama raw response length:', rawContent?.length);
// Update history
this.messages.push({
@@ -105,14 +138,25 @@ class OllamaProvider {
content: rawContent
});
// Return in a format compatible with the Ai class
return {
// The text() closure strips markdown code fences so consumers
// (processResponse, getToolCalls) get clean content.
const result = {
response: {
text: () => response.data.message.content
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) {
// Log detailed error information
const errorDetails = {
message: error.message,
status: error.response?.status,
@@ -125,8 +169,9 @@ class OllamaProvider {
if (retryCount > 3) {
throw new Error(`Ollama API error after ${retryCount} retries: ${error.message}`);
}
// Retry after delay
await new Promise(resolve => setTimeout(resolve, 500 * (retryCount + 1)));
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);
}
}
@@ -144,4 +189,4 @@ class OllamaProvider {
}
}
module.exports = OllamaProvider;
module.exports = OllamaProvider;