UNPKG

@endlessblink/like-i-said-v2

Version:

Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.

322 lines (274 loc) 11.2 kB
/** * Ollama Local AI Client for Memory Enhancement * Provides privacy-focused, local AI processing for title/summary generation */ export class OllamaClient { constructor(baseUrl = 'http://localhost:11434', options = {}) { this.baseUrl = baseUrl this.options = { model: options.model || 'llama3.1:8b', temperature: options.temperature || 0.1, maxTokens: options.maxTokens || 200, batchSize: options.batchSize || 5, requestTimeout: options.requestTimeout || 30000, ...options } } /** * Check if Ollama server is available */ async isAvailable() { try { const response = await fetch(`${this.baseUrl}/api/version`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(5000) }) return response.ok } catch (error) { console.warn('Ollama server not available:', error.message) return false } } /** * List available models */ async listModels() { try { const response = await fetch(`${this.baseUrl}/api/tags`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const data = await response.json() return data.models || [] } catch (error) { console.error('Failed to list Ollama models:', error) return [] } } /** * Generate title and summary for a single memory */ async enhanceMemory(content, metadata = {}) { const prompt = this.buildEnhancementPrompt(content, metadata) try { const response = await this.generateCompletion(prompt) return this.parseEnhancementResponse(response) } catch (error) { console.error('Ollama enhancement error:', error) throw new Error(`Local AI enhancement failed: ${error.message}`) } } /** * Batch process multiple memories */ async enhanceMemoriesBatch(memories, onProgress = null) { const results = [] const batches = this.createBatches(memories, this.options.batchSize) console.log(`🤖 Processing ${memories.length} memories in ${batches.length} batches of ${this.options.batchSize}`) for (let i = 0; i < batches.length; i++) { const batch = batches[i] console.log(`📦 Processing batch ${i + 1}/${batches.length}`) const batchPromises = batch.map(async (memory, index) => { try { const enhancement = await this.enhanceMemory(memory.content, memory) onProgress?.(i * this.options.batchSize + index + 1, memories.length) return { memory, enhancement, success: true } } catch (error) { console.error(`Failed to enhance memory ${memory.id}:`, error) return { memory, error: error.message, success: false } } }) const batchResults = await Promise.all(batchPromises) results.push(...batchResults) // Rate limiting between batches if (i < batches.length - 1) { await this.delay(1000) } } return results } /** * Generate completion using Ollama API */ async generateCompletion(prompt) { const payload = { model: this.options.model, prompt: prompt, stream: false, options: { temperature: this.options.temperature, num_predict: this.options.maxTokens, stop: ['</json>', '\n\n---', '\n\nEnd:'] } } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), this.options.requestTimeout) try { const response = await fetch(`${this.baseUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: controller.signal }) clearTimeout(timeoutId) if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`) } const data = await response.json() if (!data.response) { throw new Error('No response from Ollama model') } return data.response.trim() } catch (error) { clearTimeout(timeoutId) if (error.name === 'AbortError') { throw new Error('Request timeout - model may be too slow') } throw error } } /** * Build enhancement prompt for memory content */ buildEnhancementPrompt(content, metadata = {}) { const category = metadata.category || 'general' // Enhanced category-specific instructions based on comprehensive rules const categoryInstructions = { code: `Use format: [Technology] [Component]: [Purpose/Action]. Focus on language, framework, component name, and main purpose. Prioritize function over implementation details. Include version numbers and specific technologies when relevant.`, work: `Use format: [Project] [Action]: [Specific Task]. Include project context, action verb, and specific outcome. Prioritize deliverables over process. Reference project phases, sprints, or milestones when relevant.`, research: `Use format: [Topic]: [Specific Focus/Finding]. Include domain and specific area of focus. Prioritize key insights over general topics. Reference methodologies and measurable outcomes.`, conversations: `Use format: [Context]: [Main Topic/Decision]. Include communication type, key participants, and main topic. Prioritize decisions and outcomes over general discussion.`, personal: `Use format: [Category]: [Specific Preference/Insight]. Include personal context and specific area. Prioritize actionable insights over general thoughts.`, preferences: `Use format: [Category]: [Specific Preference/Insight]. Include personal context and specific area. Prioritize actionable insights over general thoughts.`, general: `Use appropriate format based on content type. Focus on specificity, clarity, and actionability. Avoid vague terms like "stuff", "things", "various".` } const instruction = categoryInstructions[category] || categoryInstructions.general return `You are a memory enhancement specialist. Generate a concise title and description following these strict rules: CONTENT CATEGORY: ${category} CATEGORY RULES: ${instruction} MANDATORY REQUIREMENTS: - Title: Maximum 50 characters, use title case, no periods unless abbreviation - Description: Maximum 140 characters, active voice, complement title (no repetition) - Use specific terms over generic ones (avoid "implementation", "system", "process") - Include key identifiers (project names, technologies, specific issues) - For technical content: include technologies, versions, specific components - For tasks: use action-oriented language with clear outcomes - For research: emphasize findings and applications - For conversations: focus on decisions and key outcomes FORBIDDEN PATTERNS: - Vague descriptors: "various", "multiple", "different", "several" - Redundant prefixes: "How to", "Guide to", "Information about" - Obvious statements: "This is about", "Contains information on" - Excessive adjectives: "comprehensive", "ultimate", "complete", "full" - Generic project names: "Project", "System", "Application" CONTENT TO ANALYZE: ${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''} Generate ONLY a JSON response with: - title: Specific, clear, follows category format rules - summary: Informative, complements title, includes context/impact JSON format: { "title": "your title here", "summary": "your summary here" } Respond with ONLY the JSON, no additional text:` } /** * Parse AI response and extract title/summary */ parseEnhancementResponse(response) { try { // Clean the response to extract JSON const cleanResponse = response .replace(/^[^{]*/, '') // Remove text before first { .replace(/[^}]*$/, '') // Remove text after last } .replace(/```json\s*/, '') // Remove markdown code blocks .replace(/```\s*$/, '') .trim() const parsed = JSON.parse(cleanResponse) if (!parsed.title || !parsed.summary) { throw new Error('Missing title or summary in response') } return { title: parsed.title.substring(0, 50).trim(), summary: parsed.summary.substring(0, 140).trim() } } catch (error) { // Fallback: try to extract with regex const titleMatch = response.match(/"title":\s*"([^"]+)"/i) const summaryMatch = response.match(/"summary":\s*"([^"]+)"/i) if (titleMatch && summaryMatch) { return { title: titleMatch[1].substring(0, 50).trim(), summary: summaryMatch[1].substring(0, 140).trim() } } throw new Error(`Failed to parse AI response: ${error.message}`) } } /** * Create batches for processing */ createBatches(items, batchSize) { const batches = [] for (let i = 0; i < items.length; i += batchSize) { batches.push(items.slice(i, i + batchSize)) } return batches } /** * Delay utility for rate limiting */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } /** * Get model recommendations based on system resources */ static getModelRecommendations() { return { lightweight: [ { name: 'llama3.1:8b', description: 'Fast, good quality (4GB RAM)' }, { name: 'llama3.2:3b', description: 'Very fast, decent quality (2GB RAM)' }, { name: 'phi3:mini', description: 'Ultra-fast, basic quality (1GB RAM)' } ], balanced: [ { name: 'llama3.1:8b', description: 'Best all-around choice (4GB RAM)' }, { name: 'mistral:7b', description: 'Good alternative (4GB RAM)' }, { name: 'codellama:7b', description: 'Better for code content (4GB RAM)' } ], quality: [ { name: 'llama3.1:70b', description: 'Highest quality (40GB+ RAM)' }, { name: 'mixtral:8x7b', description: 'Excellent quality (26GB RAM)' }, { name: 'llama3.1:13b', description: 'High quality (8GB RAM)' } ] } } /** * Estimate processing time */ static estimateProcessingTime(memoryCount, model = 'llama3.1:8b') { const timePerMemory = { 'phi3:mini': 2, 'llama3.2:3b': 3, 'llama3.1:8b': 5, 'mistral:7b': 6, 'llama3.1:13b': 8, 'mixtral:8x7b': 15, 'llama3.1:70b': 30 } const baseTime = timePerMemory[model] || 5 const totalSeconds = memoryCount * baseTime const minutes = Math.ceil(totalSeconds / 60) return { seconds: totalSeconds, minutes: minutes, estimate: minutes < 2 ? `~${totalSeconds} seconds` : `~${minutes} minutes` } } } export default OllamaClient