Notes/src/services/llm/formatters/ollama_formatter.ts

127 lines
4.7 KiB
TypeScript
Raw Normal View History

import type { Message } from '../ai_interface.js';
import { BaseMessageFormatter } from './base_formatter.js';
import sanitizeHtml from 'sanitize-html';
2025-03-28 23:07:02 +00:00
import { PROVIDER_PROMPTS, FORMATTING_PROMPTS } from '../constants/llm_prompt_constants.js';
2025-03-28 23:25:06 +00:00
import { LLM_CONSTANTS } from '../constants/provider_constants.js';
import {
HTML_ALLOWED_TAGS,
HTML_ALLOWED_ATTRIBUTES,
OLLAMA_CLEANING,
FORMATTER_LOGS
} from '../constants/formatter_constants.js';
/**
* Ollama-specific message formatter
* Handles the unique requirements of the Ollama API
*/
export class OllamaMessageFormatter extends BaseMessageFormatter {
/**
* Maximum recommended context length for Ollama
* Smaller than other providers due to Ollama's handling of context
*/
2025-03-28 23:25:06 +00:00
private static MAX_CONTEXT_LENGTH = LLM_CONSTANTS.CONTEXT_WINDOW.OLLAMA;
/**
* Format messages for the Ollama API
* @param messages Messages to format
* @param systemPrompt Optional system prompt to use
* @param context Optional context to include
* @param preserveSystemPrompt When true, preserves existing system messages rather than replacing them
*/
formatMessages(messages: Message[], systemPrompt?: string, context?: string, preserveSystemPrompt?: boolean): Message[] {
const formattedMessages: Message[] = [];
// First identify user and system messages
const systemMessages = messages.filter(msg => msg.role === 'system');
const userMessages = messages.filter(msg => msg.role === 'user' || msg.role === 'assistant');
// Determine if we should preserve the existing system message
if (preserveSystemPrompt && systemMessages.length > 0) {
// Preserve the existing system message
formattedMessages.push(systemMessages[0]);
} else {
// Use provided systemPrompt or default
const basePrompt = systemPrompt || PROVIDER_PROMPTS.COMMON.DEFAULT_ASSISTANT_INTRO;
formattedMessages.push({
role: 'system',
content: basePrompt
});
}
// If we have context, inject it into the first user message
if (context && userMessages.length > 0) {
let injectedContext = false;
for (let i = 0; i < userMessages.length; i++) {
const msg = userMessages[i];
if (msg.role === 'user' && !injectedContext) {
// Simple context injection directly in the user's message
const cleanedContext = this.cleanContextContent(context);
2025-03-28 23:07:02 +00:00
const formattedContext = PROVIDER_PROMPTS.OLLAMA.CONTEXT_INJECTION(
cleanedContext,
msg.content
);
formattedMessages.push({
role: 'user',
content: formattedContext
});
injectedContext = true;
} else {
formattedMessages.push(msg);
}
}
} else {
// No context, just add all messages as-is
for (const msg of userMessages) {
formattedMessages.push(msg);
}
}
console.log(FORMATTER_LOGS.OLLAMA.PROCESSED(messages.length, formattedMessages.length));
return formattedMessages;
}
/**
* Clean up HTML and other problematic content before sending to Ollama
* Ollama needs a more aggressive cleaning than other models
*/
override cleanContextContent(content: string): string {
if (!content) return '';
try {
// First use the parent class to do standard cleaning
let sanitized = super.cleanContextContent(content);
// Then apply Ollama-specific aggressive cleaning
// Remove any remaining HTML using sanitizeHtml
let plaintext = sanitizeHtml(sanitized, {
allowedTags: HTML_ALLOWED_TAGS.NONE,
allowedAttributes: HTML_ALLOWED_ATTRIBUTES.NONE,
textFilter: (text) => text
});
// Apply all Ollama-specific cleaning patterns
const ollamaPatterns = OLLAMA_CLEANING;
for (const pattern of Object.values(ollamaPatterns)) {
plaintext = plaintext.replace(pattern.pattern, pattern.replacement);
}
return plaintext.trim();
} catch (error) {
console.error(FORMATTER_LOGS.ERROR.CONTEXT_CLEANING("Ollama"), error);
return content; // Return original if cleaning fails
}
}
/**
* Get the maximum recommended context length for Ollama
*/
getMaxContextLength(): number {
return OllamaMessageFormatter.MAX_CONTEXT_LENGTH;
}
}