2025-03-11 17:30:50 +00:00
|
|
|
import options from '../../options.js';
|
|
|
|
import { BaseAIService } from '../base_ai_service.js';
|
2025-03-28 22:50:15 +00:00
|
|
|
import type { Message, ChatCompletionOptions, ChatResponse } from '../ai_interface.js';
|
|
|
|
import sanitizeHtml from 'sanitize-html';
|
|
|
|
import { OllamaMessageFormatter } from '../formatters/ollama_formatter.js';
|
2025-04-06 20:50:08 +00:00
|
|
|
import log from '../../log.js';
|
|
|
|
import type { ToolCall } from '../tools/tool_interfaces.js';
|
|
|
|
import toolRegistry from '../tools/tool_registry.js';
|
|
|
|
|
|
|
|
interface OllamaFunctionArguments {
|
|
|
|
[key: string]: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface OllamaFunctionCall {
|
|
|
|
function: {
|
|
|
|
name: string;
|
|
|
|
arguments: OllamaFunctionArguments | string;
|
|
|
|
};
|
|
|
|
id?: string;
|
|
|
|
}
|
2025-03-02 19:39:10 -08:00
|
|
|
|
2025-03-28 21:47:28 +00:00
|
|
|
interface OllamaMessage {
|
|
|
|
role: string;
|
|
|
|
content: string;
|
2025-04-06 20:50:08 +00:00
|
|
|
tool_calls?: OllamaFunctionCall[];
|
2025-03-28 21:47:28 +00:00
|
|
|
}
|
|
|
|
|
2025-03-28 22:50:15 +00:00
|
|
|
interface OllamaResponse {
|
|
|
|
model: string;
|
|
|
|
created_at: string;
|
|
|
|
message: OllamaMessage;
|
|
|
|
done: boolean;
|
2025-04-06 20:50:08 +00:00
|
|
|
done_reason?: string;
|
2025-03-28 22:50:15 +00:00
|
|
|
total_duration: number;
|
|
|
|
load_duration: number;
|
|
|
|
prompt_eval_count: number;
|
|
|
|
prompt_eval_duration: number;
|
|
|
|
eval_count: number;
|
|
|
|
eval_duration: number;
|
|
|
|
}
|
|
|
|
|
2025-03-02 19:39:10 -08:00
|
|
|
export class OllamaService extends BaseAIService {
|
2025-03-28 22:50:15 +00:00
|
|
|
private formatter: OllamaMessageFormatter;
|
|
|
|
|
2025-03-02 19:39:10 -08:00
|
|
|
constructor() {
|
|
|
|
super('Ollama');
|
2025-03-28 22:50:15 +00:00
|
|
|
this.formatter = new OllamaMessageFormatter();
|
2025-03-02 19:39:10 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
isAvailable(): boolean {
|
2025-03-28 22:50:15 +00:00
|
|
|
return super.isAvailable() && !!options.getOption('ollamaBaseUrl');
|
2025-03-02 19:39:10 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
async generateChatCompletion(messages: Message[], opts: ChatCompletionOptions = {}): Promise<ChatResponse> {
|
|
|
|
if (!this.isAvailable()) {
|
2025-03-28 22:50:15 +00:00
|
|
|
throw new Error('Ollama service is not available. Check API URL in settings.');
|
2025-03-02 19:39:10 -08:00
|
|
|
}
|
|
|
|
|
2025-03-28 22:50:15 +00:00
|
|
|
const apiBase = options.getOption('ollamaBaseUrl');
|
2025-04-08 21:24:56 +00:00
|
|
|
|
|
|
|
// Get the model name and strip the "ollama:" prefix if it exists
|
|
|
|
let model = opts.model || options.getOption('ollamaDefaultModel') || 'llama3';
|
|
|
|
if (model.startsWith('ollama:')) {
|
|
|
|
model = model.substring(7); // Remove the "ollama:" prefix
|
|
|
|
log.info(`Stripped 'ollama:' prefix from model name, using: ${model}`);
|
|
|
|
}
|
|
|
|
|
2025-03-02 19:39:10 -08:00
|
|
|
const temperature = opts.temperature !== undefined
|
|
|
|
? opts.temperature
|
|
|
|
: parseFloat(options.getOption('aiTemperature') || '0.7');
|
|
|
|
|
|
|
|
const systemPrompt = this.getSystemPrompt(opts.systemPrompt || options.getOption('aiSystemPrompt'));
|
|
|
|
|
|
|
|
try {
|
2025-04-01 21:42:09 +00:00
|
|
|
// Determine whether to use the formatter or send messages directly
|
|
|
|
let messagesToSend: Message[];
|
2025-03-28 22:50:15 +00:00
|
|
|
|
2025-04-01 21:42:09 +00:00
|
|
|
if (opts.bypassFormatter) {
|
|
|
|
// Bypass the formatter entirely - use messages as is
|
|
|
|
messagesToSend = [...messages];
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(`Bypassing formatter for Ollama request with ${messages.length} messages`);
|
2025-04-01 21:42:09 +00:00
|
|
|
} else {
|
|
|
|
// Use the formatter to prepare messages
|
|
|
|
messagesToSend = this.formatter.formatMessages(
|
|
|
|
messages,
|
|
|
|
systemPrompt,
|
|
|
|
undefined, // context
|
|
|
|
opts.preserveSystemPrompt
|
|
|
|
);
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(`Sending to Ollama with formatted messages: ${messagesToSend.length}`);
|
2025-04-01 21:42:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check if this is a request that expects JSON response
|
|
|
|
const expectsJsonResponse = opts.expectsJsonResponse || false;
|
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Build request body
|
|
|
|
const requestBody: any = {
|
|
|
|
model,
|
|
|
|
messages: messagesToSend,
|
|
|
|
options: {
|
|
|
|
temperature,
|
2025-04-08 23:55:04 +00:00
|
|
|
// Add num_ctx parameter based on model capabilities
|
|
|
|
num_ctx: await this.getModelContextWindowTokens(model),
|
2025-04-06 20:50:08 +00:00
|
|
|
// Add response_format for requests that expect JSON
|
|
|
|
...(expectsJsonResponse ? { response_format: { type: "json_object" } } : {})
|
|
|
|
},
|
|
|
|
stream: false
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add tools if enabled - put them at the top level for Ollama
|
|
|
|
if (opts.enableTools !== false) {
|
|
|
|
// Get tools from registry if not provided in options
|
|
|
|
if (!opts.tools || opts.tools.length === 0) {
|
|
|
|
try {
|
|
|
|
// Get tool definitions from registry
|
|
|
|
const tools = toolRegistry.getAllToolDefinitions();
|
|
|
|
requestBody.tools = tools;
|
|
|
|
log.info(`Adding ${tools.length} tools to request`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// If no tools found, reinitialize
|
|
|
|
if (tools.length === 0) {
|
|
|
|
log.info('No tools found in registry, re-initializing...');
|
|
|
|
try {
|
|
|
|
const toolInitializer = await import('../tools/tool_initializer.js');
|
|
|
|
await toolInitializer.default.initializeTools();
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Try again
|
|
|
|
requestBody.tools = toolRegistry.getAllToolDefinitions();
|
|
|
|
log.info(`After re-initialization: ${requestBody.tools.length} tools available`);
|
|
|
|
} catch (err: any) {
|
|
|
|
log.error(`Failed to re-initialize tools: ${err.message}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
log.error(`Error getting tools: ${error.message || String(error)}`);
|
|
|
|
// Create default empty tools array if we couldn't load the tools
|
|
|
|
requestBody.tools = [];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
requestBody.tools = opts.tools;
|
|
|
|
}
|
|
|
|
log.info(`Adding ${requestBody.tools.length} tools to Ollama request`);
|
|
|
|
} else {
|
|
|
|
log.info('Tools are explicitly disabled for this request');
|
2025-04-01 21:42:09 +00:00
|
|
|
}
|
2025-03-28 22:50:15 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log key request details
|
|
|
|
log.info(`========== OLLAMA API REQUEST ==========`);
|
|
|
|
log.info(`Model: ${requestBody.model}, Messages: ${requestBody.messages.length}, Tools: ${requestBody.tools ? requestBody.tools.length : 0}`);
|
|
|
|
log.info(`Temperature: ${temperature}, Stream: ${requestBody.stream}, JSON response expected: ${expectsJsonResponse}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Check message structure and log detailed information about each message
|
|
|
|
requestBody.messages.forEach((msg: any, index: number) => {
|
|
|
|
const keys = Object.keys(msg);
|
|
|
|
log.info(`Message ${index}, Role: ${msg.role}, Keys: ${keys.join(', ')}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log message content preview
|
|
|
|
if (msg.content && typeof msg.content === 'string') {
|
2025-04-07 21:57:18 +00:00
|
|
|
const contentPreview = msg.content.length > 200
|
|
|
|
? `${msg.content.substring(0, 200)}...`
|
2025-04-06 20:50:08 +00:00
|
|
|
: msg.content;
|
|
|
|
log.info(`Message ${index} content: ${contentPreview}`);
|
|
|
|
}
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log tool-related details
|
|
|
|
if (keys.includes('tool_calls')) {
|
|
|
|
log.info(`Message ${index} has ${msg.tool_calls.length} tool calls:`);
|
|
|
|
msg.tool_calls.forEach((call: any, callIdx: number) => {
|
|
|
|
log.info(` Tool call ${callIdx}: ${call.function?.name || 'unknown'}, ID: ${call.id || 'unspecified'}`);
|
|
|
|
if (call.function?.arguments) {
|
2025-04-07 21:57:18 +00:00
|
|
|
const argsPreview = typeof call.function.arguments === 'string'
|
|
|
|
? call.function.arguments.substring(0, 100)
|
2025-04-06 20:50:08 +00:00
|
|
|
: JSON.stringify(call.function.arguments).substring(0, 100);
|
|
|
|
log.info(` Arguments: ${argsPreview}...`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
if (keys.includes('tool_call_id')) {
|
|
|
|
log.info(`Message ${index} is a tool response for tool call ID: ${msg.tool_call_id}`);
|
|
|
|
}
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
if (keys.includes('name') && msg.role === 'tool') {
|
|
|
|
log.info(`Message ${index} is from tool: ${msg.name}`);
|
|
|
|
}
|
|
|
|
});
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log tool definitions
|
|
|
|
if (requestBody.tools && requestBody.tools.length > 0) {
|
|
|
|
log.info(`Sending ${requestBody.tools.length} tool definitions:`);
|
|
|
|
requestBody.tools.forEach((tool: any, toolIdx: number) => {
|
|
|
|
log.info(` Tool ${toolIdx}: ${tool.function?.name || 'unnamed'}`);
|
|
|
|
if (tool.function?.description) {
|
|
|
|
log.info(` Description: ${tool.function.description.substring(0, 100)}...`);
|
|
|
|
}
|
|
|
|
if (tool.function?.parameters) {
|
2025-04-07 21:57:18 +00:00
|
|
|
const paramNames = tool.function.parameters.properties
|
|
|
|
? Object.keys(tool.function.parameters.properties)
|
2025-04-06 20:50:08 +00:00
|
|
|
: [];
|
|
|
|
log.info(` Parameters: ${paramNames.join(', ')}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-08 22:08:52 +00:00
|
|
|
// Log full request body (with improved logging for debug purposes)
|
2025-04-06 20:50:08 +00:00
|
|
|
const requestStr = JSON.stringify(requestBody);
|
2025-04-08 22:08:52 +00:00
|
|
|
log.info(`========== FULL OLLAMA REQUEST ==========`);
|
|
|
|
|
|
|
|
// Log request in manageable chunks
|
|
|
|
const maxChunkSize = 4000;
|
|
|
|
if (requestStr.length > maxChunkSize) {
|
|
|
|
let i = 0;
|
|
|
|
while (i < requestStr.length) {
|
|
|
|
const chunk = requestStr.substring(i, i + maxChunkSize);
|
|
|
|
log.info(`Request part ${Math.floor(i/maxChunkSize) + 1}/${Math.ceil(requestStr.length/maxChunkSize)}: ${chunk}`);
|
|
|
|
i += maxChunkSize;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.info(`Full request: ${requestStr}`);
|
|
|
|
}
|
|
|
|
log.info(`========== END FULL OLLAMA REQUEST ==========`);
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(`========== END OLLAMA REQUEST ==========`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Make API request
|
2025-03-28 22:50:15 +00:00
|
|
|
const response = await fetch(`${apiBase}/api/chat`, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
2025-04-06 20:50:08 +00:00
|
|
|
body: JSON.stringify(requestBody)
|
2025-03-28 21:47:28 +00:00
|
|
|
});
|
2025-03-28 22:29:33 +00:00
|
|
|
|
2025-03-28 22:50:15 +00:00
|
|
|
if (!response.ok) {
|
|
|
|
const errorBody = await response.text();
|
2025-04-06 20:50:08 +00:00
|
|
|
log.error(`Ollama API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
2025-03-28 22:50:15 +00:00
|
|
|
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
2025-03-28 22:29:33 +00:00
|
|
|
}
|
|
|
|
|
2025-03-28 22:50:15 +00:00
|
|
|
const data: OllamaResponse = await response.json();
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log response details
|
|
|
|
log.info(`========== OLLAMA API RESPONSE ==========`);
|
|
|
|
log.info(`Model: ${data.model}, Content length: ${data.message.content.length} chars`);
|
|
|
|
log.info(`Tokens: ${data.prompt_eval_count} prompt, ${data.eval_count} completion, ${data.prompt_eval_count + data.eval_count} total`);
|
|
|
|
log.info(`Duration: ${data.total_duration}ns total, ${data.prompt_eval_duration}ns prompt, ${data.eval_duration}ns completion`);
|
|
|
|
log.info(`Done: ${data.done}, Reason: ${data.done_reason || 'not specified'}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log content preview
|
2025-04-09 00:42:15 +00:00
|
|
|
const contentPreview = data.message.content && data.message.content.length > 300
|
2025-04-07 21:57:18 +00:00
|
|
|
? `${data.message.content.substring(0, 300)}...`
|
2025-04-06 20:50:08 +00:00
|
|
|
: data.message.content;
|
|
|
|
log.info(`Response content: ${contentPreview}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-09 00:42:15 +00:00
|
|
|
// Log the full raw response for debugging
|
|
|
|
log.info(`========== FULL OLLAMA RESPONSE ==========`);
|
|
|
|
log.info(`Raw response object: ${JSON.stringify(data)}`);
|
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Handle the response and extract tool calls if present
|
|
|
|
const chatResponse: ChatResponse = {
|
2025-03-28 22:50:15 +00:00
|
|
|
text: data.message.content,
|
|
|
|
model: data.model,
|
|
|
|
provider: this.getName(),
|
|
|
|
usage: {
|
|
|
|
promptTokens: data.prompt_eval_count,
|
|
|
|
completionTokens: data.eval_count,
|
|
|
|
totalTokens: data.prompt_eval_count + data.eval_count
|
|
|
|
}
|
|
|
|
};
|
2025-04-06 20:50:08 +00:00
|
|
|
|
|
|
|
// Add tool calls if present
|
|
|
|
if (data.message.tool_calls && data.message.tool_calls.length > 0) {
|
2025-04-07 21:57:18 +00:00
|
|
|
log.info(`========== OLLAMA TOOL CALLS DETECTED ==========`);
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(`Ollama response includes ${data.message.tool_calls.length} tool calls`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log detailed information about each tool call
|
|
|
|
const transformedToolCalls: ToolCall[] = [];
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Log detailed information about the tool calls in the response
|
|
|
|
log.info(`========== OLLAMA TOOL CALLS IN RESPONSE ==========`);
|
|
|
|
data.message.tool_calls.forEach((toolCall, index) => {
|
|
|
|
log.info(`Tool call ${index + 1}:`);
|
|
|
|
log.info(` Name: ${toolCall.function?.name || 'unknown'}`);
|
|
|
|
log.info(` ID: ${toolCall.id || `auto-${index + 1}`}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Generate a unique ID if none is provided
|
|
|
|
const id = toolCall.id || `tool-call-${Date.now()}-${index}`;
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Handle arguments based on their type
|
|
|
|
let processedArguments: Record<string, any> | string;
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
if (typeof toolCall.function.arguments === 'string') {
|
|
|
|
// Log raw string arguments in full for debugging
|
|
|
|
log.info(` Raw string arguments: ${toolCall.function.arguments}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Try to parse JSON string arguments
|
|
|
|
try {
|
|
|
|
processedArguments = JSON.parse(toolCall.function.arguments);
|
|
|
|
log.info(` Successfully parsed arguments to object with keys: ${Object.keys(processedArguments).join(', ')}`);
|
|
|
|
log.info(` Parsed argument values:`);
|
|
|
|
Object.entries(processedArguments).forEach(([key, value]) => {
|
2025-04-07 21:57:18 +00:00
|
|
|
const valuePreview = typeof value === 'string'
|
2025-04-06 20:50:08 +00:00
|
|
|
? (value.length > 100 ? `${value.substring(0, 100)}...` : value)
|
|
|
|
: JSON.stringify(value);
|
|
|
|
log.info(` ${key}: ${valuePreview}`);
|
|
|
|
});
|
2025-04-07 21:57:18 +00:00
|
|
|
} catch (e: unknown) {
|
2025-04-06 20:50:08 +00:00
|
|
|
// If parsing fails, keep as string and log the error
|
|
|
|
processedArguments = toolCall.function.arguments;
|
2025-04-07 21:57:18 +00:00
|
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
|
|
log.info(` Could not parse arguments as JSON: ${errorMessage}`);
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(` Keeping as string: ${processedArguments.substring(0, 200)}${processedArguments.length > 200 ? '...' : ''}`);
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Try to clean and parse again with more aggressive methods
|
|
|
|
try {
|
|
|
|
const cleaned = toolCall.function.arguments
|
|
|
|
.replace(/^['"]|['"]$/g, '') // Remove surrounding quotes
|
|
|
|
.replace(/\\"/g, '"') // Replace escaped quotes
|
|
|
|
.replace(/([{,])\s*'([^']+)'\s*:/g, '$1"$2":') // Replace single quotes around property names
|
|
|
|
.replace(/([{,])\s*(\w+)\s*:/g, '$1"$2":'); // Add quotes around unquoted property names
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(` Attempting to parse cleaned argument: ${cleaned}`);
|
|
|
|
const reparseArg = JSON.parse(cleaned);
|
|
|
|
log.info(` Successfully parsed cleaned argument with keys: ${Object.keys(reparseArg).join(', ')}`);
|
2025-04-09 00:42:15 +00:00
|
|
|
// Use reparsed arguments if successful
|
|
|
|
processedArguments = reparseArg;
|
2025-04-07 21:57:18 +00:00
|
|
|
} catch (cleanErr: unknown) {
|
|
|
|
const cleanErrMessage = cleanErr instanceof Error ? cleanErr.message : String(cleanErr);
|
|
|
|
log.info(` Failed to parse cleaned arguments: ${cleanErrMessage}`);
|
2025-04-06 20:50:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If it's already an object, use it directly and log details
|
|
|
|
processedArguments = toolCall.function.arguments;
|
|
|
|
log.info(` Object arguments with keys: ${Object.keys(processedArguments).join(', ')}`);
|
|
|
|
log.info(` Argument values:`);
|
|
|
|
Object.entries(processedArguments).forEach(([key, value]) => {
|
2025-04-07 21:57:18 +00:00
|
|
|
const valuePreview = typeof value === 'string'
|
2025-04-06 20:50:08 +00:00
|
|
|
? (value.length > 100 ? `${value.substring(0, 100)}...` : value)
|
|
|
|
: JSON.stringify(value);
|
|
|
|
log.info(` ${key}: ${valuePreview}`);
|
|
|
|
});
|
|
|
|
}
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-09 00:42:15 +00:00
|
|
|
// If arguments are still empty or invalid, create a default argument
|
|
|
|
if (!processedArguments ||
|
|
|
|
(typeof processedArguments === 'object' && Object.keys(processedArguments).length === 0)) {
|
|
|
|
log.info(` Empty or invalid arguments for tool ${toolCall.function.name}, creating default`);
|
|
|
|
|
|
|
|
// Get tool definition to determine required parameters
|
|
|
|
const allToolDefs = toolRegistry.getAllToolDefinitions();
|
|
|
|
const toolDef = allToolDefs.find(t => t.function?.name === toolCall.function.name);
|
|
|
|
|
|
|
|
if (toolDef && toolDef.function && toolDef.function.parameters) {
|
|
|
|
const params = toolDef.function.parameters;
|
|
|
|
processedArguments = {};
|
|
|
|
|
|
|
|
// Create default values for required parameters
|
|
|
|
if (params.required && Array.isArray(params.required)) {
|
|
|
|
params.required.forEach((param: string) => {
|
|
|
|
// Extract text from the response to use as default value
|
|
|
|
const defaultValue = data.message.content?.includes(param)
|
|
|
|
? extractValueFromText(data.message.content, param)
|
|
|
|
: "default";
|
|
|
|
|
|
|
|
(processedArguments as Record<string, any>)[param] = defaultValue;
|
|
|
|
log.info(` Added default value for required param ${param}: ${defaultValue}`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Convert to our standard ToolCall format
|
|
|
|
transformedToolCalls.push({
|
|
|
|
id,
|
|
|
|
type: 'function',
|
|
|
|
function: {
|
|
|
|
name: toolCall.function.name,
|
|
|
|
arguments: processedArguments
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2025-04-07 21:57:18 +00:00
|
|
|
|
2025-04-06 20:50:08 +00:00
|
|
|
// Add transformed tool calls to response
|
|
|
|
chatResponse.tool_calls = transformedToolCalls;
|
|
|
|
log.info(`Transformed ${transformedToolCalls.length} tool calls for execution`);
|
2025-04-07 21:57:18 +00:00
|
|
|
log.info(`Tool calls after transformation: ${JSON.stringify(chatResponse.tool_calls)}`);
|
|
|
|
|
|
|
|
// Ensure tool_calls is properly exposed and formatted
|
|
|
|
// This is to make sure the pipeline can detect and execute the tools
|
|
|
|
if (transformedToolCalls.length > 0) {
|
|
|
|
// Make sure the tool_calls are exposed in the exact format expected by pipeline
|
|
|
|
chatResponse.tool_calls = transformedToolCalls.map(tc => ({
|
|
|
|
id: tc.id,
|
|
|
|
type: 'function',
|
|
|
|
function: {
|
|
|
|
name: tc.function.name,
|
|
|
|
arguments: tc.function.arguments
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
// If the content is empty, use a placeholder to avoid issues
|
|
|
|
if (!chatResponse.text) {
|
|
|
|
chatResponse.text = "Processing your request...";
|
|
|
|
}
|
|
|
|
|
|
|
|
log.info(`Final tool_calls format for pipeline: ${JSON.stringify(chatResponse.tool_calls)}`);
|
|
|
|
}
|
2025-04-06 20:50:08 +00:00
|
|
|
log.info(`========== END OLLAMA TOOL CALLS ==========`);
|
2025-04-07 21:57:18 +00:00
|
|
|
} else {
|
|
|
|
log.info(`========== NO OLLAMA TOOL CALLS DETECTED ==========`);
|
|
|
|
log.info(`Checking raw message response format: ${JSON.stringify(data.message)}`);
|
2025-04-09 00:42:15 +00:00
|
|
|
|
|
|
|
// Attempt to analyze the response to see if it contains tool call intent
|
|
|
|
const responseText = data.message.content || '';
|
|
|
|
if (responseText.includes('search_notes') ||
|
|
|
|
responseText.includes('create_note') ||
|
|
|
|
responseText.includes('function') ||
|
|
|
|
responseText.includes('tool')) {
|
|
|
|
log.info(`Response may contain tool call intent but isn't formatted properly`);
|
|
|
|
log.info(`Content that might indicate tool call intent: ${responseText.substring(0, 500)}`);
|
|
|
|
}
|
2025-04-06 20:50:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
log.info(`========== END OLLAMA RESPONSE ==========`);
|
|
|
|
return chatResponse;
|
|
|
|
} catch (error: any) {
|
2025-04-09 00:42:15 +00:00
|
|
|
// Enhanced error handling with detailed diagnostics
|
2025-04-06 20:50:08 +00:00
|
|
|
log.error(`Ollama service error: ${error.message || String(error)}`);
|
2025-04-09 00:42:15 +00:00
|
|
|
if (error.stack) {
|
|
|
|
log.error(`Error stack trace: ${error.stack}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (error.message && error.message.includes('Cannot read properties of null')) {
|
|
|
|
log.error('Tool registry connection issue detected. Tool may not be properly registered or available.');
|
|
|
|
log.error('Check tool registry initialization and tool availability before execution.');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Propagate the original error
|
2025-03-28 22:50:15 +00:00
|
|
|
throw error;
|
2025-03-28 21:47:28 +00:00
|
|
|
}
|
2025-03-02 19:39:10 -08:00
|
|
|
}
|
2025-04-08 23:55:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the context window size in tokens for a given model
|
|
|
|
* @param modelName The name of the model
|
|
|
|
* @returns The context window size in tokens
|
|
|
|
*/
|
|
|
|
private async getModelContextWindowTokens(modelName: string): Promise<number> {
|
|
|
|
try {
|
|
|
|
// Import model capabilities service
|
|
|
|
const modelCapabilitiesService = (await import('../model_capabilities_service.js')).default;
|
|
|
|
|
|
|
|
// Get model capabilities
|
|
|
|
const modelCapabilities = await modelCapabilitiesService.getModelCapabilities(modelName);
|
|
|
|
|
|
|
|
// Get context window tokens with a default fallback
|
|
|
|
const contextWindowTokens = modelCapabilities.contextWindowTokens || 8192;
|
|
|
|
|
|
|
|
log.info(`Using context window size for ${modelName}: ${contextWindowTokens} tokens`);
|
|
|
|
|
|
|
|
return contextWindowTokens;
|
|
|
|
} catch (error: any) {
|
|
|
|
// Log error but provide a reasonable default
|
|
|
|
log.error(`Error getting model context window: ${error.message}`);
|
|
|
|
return 8192; // Default to 8192 tokens if there's an error
|
|
|
|
}
|
|
|
|
}
|
2025-03-02 19:39:10 -08:00
|
|
|
}
|
2025-04-09 00:42:15 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple utility to extract a value from text based on a parameter name
|
|
|
|
* @param text The text to search in
|
|
|
|
* @param param The parameter name to look for
|
|
|
|
* @returns Extracted value or default
|
|
|
|
*/
|
|
|
|
function extractValueFromText(text: string, param: string): string {
|
|
|
|
// Simple regex to find "param: value" or "param = value" or "param value" patterns
|
|
|
|
const patterns = [
|
|
|
|
new RegExp(`${param}[\\s]*:[\\s]*["']?([^"',\\s]+)["']?`, 'i'),
|
|
|
|
new RegExp(`${param}[\\s]*=[\\s]*["']?([^"',\\s]+)["']?`, 'i'),
|
|
|
|
new RegExp(`${param}[\\s]+["']?([^"',\\s]+)["']?`, 'i')
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const pattern of patterns) {
|
|
|
|
const match = text.match(pattern);
|
|
|
|
if (match && match[1]) {
|
|
|
|
return match[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "default_value";
|
|
|
|
}
|