import db from '../db.js'; const LLM_HOST = process.env.LLM_HOST || null; const LLM_MODEL = process.env.LLM_MODEL || 'qwen7b.Q4_K_M.gguf'; async function queryLLM(messages) { if (!LLM_HOST) return null; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 180000); try { const response = await fetch(`${LLM_HOST}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: LLM_MODEL, messages, max_tokens: 500, temperature: 0.8, stream: false }), signal: controller.signal, }); clearTimeout(timeout); if (!response.ok) return null; const data = await response.json(); return data.choices?.[0]?.message?.content || null; } catch (e) { clearTimeout(timeout); return null; } } const DEFAULT_KNOWLEDGE = { character_archetypes: [ { name: 'The Hero', traits: ['brave', 'selfless', 'determined', 'courageous'], backstory_template: 'A {adjective} individual driven by {motivation} to {goal}.', needs_priority: { Food: 3, Energy: 4, Intimate: 1 } }, { name: 'The Sage', traits: ['wise', 'patient', 'introspective', 'knowledgeable'], backstory_template: 'Through years of study and experience, this {adjective} soul has gained {knowledge}.', needs_priority: { Food: 1, Energy: 2, Intimate: 1 } }, { name: 'The Rogue', traits: ['cunning', 'adaptive', 'independent', 'resourceful'], backstory_template: 'Living on the edge, this {adjective} character uses their {skill} to survive.', needs_priority: { Food: 4, Energy: 3, Intimate: 3 } }, { name: 'The Caregiver', traits: ['nurturing', 'selfless', 'compassionate', 'protective'], backstory_template: 'Driven by {motivation}, this {adjective} soul puts others before themselves.', needs_priority: { Food: 2, Energy: 3, Intimate: 2 } }, { name: 'The Wild One', traits: ['untamed', 'instinctual', 'free-spirited', 'primal'], backstory_template: 'Born of {origin}, this {adjective} being follows the call of the wild.', needs_priority: { Food: 5, Energy: 5, Bladder: 4, Bowel: 4, Hormones: 5, Intimate: 5 } } ], name_generators: { fantasy: ['Kaelen', 'Lyra', 'Thorn', 'Elara', 'Bryn', 'Zephyr', 'Mira', 'Orion', 'Sable', 'Finn'], modern: ['Alex', 'Jordan', 'Riley', 'Sam', 'Taylor', 'Morgan', 'Casey', 'Avery', 'Quinn', 'Drew'], gothic: ['Vladimir', 'Isabella', 'Mortimer', 'Ophelia', 'Caspian', 'Seraphina', 'Damien', 'Raven', 'Lucian', 'Vesper'], cyberpunk: ['Neon', 'Pixel', 'Cipher', 'Blade', 'Vex', 'Synthia', 'Zero', 'Echo', 'Byte', 'Nyx'] }, motivations: ['justice', 'knowledge', 'power', 'love', 'survival', 'redemption', 'freedom', 'discovery', 'revenge', 'balance'], origins: ['the ancient forests', 'a forgotten kingdom', 'the stars above', 'the depths of the sea', 'a laboratory', 'the void between worlds', 'a nomadic tribe', 'an order of knights'] }; function getRandomItem(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function generateName(style) { const generators = DEFAULT_KNOWLEDGE.name_generators; const names = generators[style] || generators.fantasy; return getRandomItem(names); } function generateArchetype() { const archetypes = DEFAULT_KNOWLEDGE.character_archetypes; const archetype = getRandomItem(archetypes); const adjective = getRandomItem(archetype.traits); const motivation = getRandomItem(DEFAULT_KNOWLEDGE.motivations); const origin = getRandomItem(DEFAULT_KNOWLEDGE.origins); const goal = getRandomItem(['save their people', 'find the truth', 'protect the innocent', 'gain ultimate power', 'achieve inner peace', 'survive the coming storm']); const knowledge = getRandomItem(['ancient wisdom', 'forgotten secrets', 'the art of diplomacy', 'alchemical mastery', 'technological prowess']); const backstory = archetype.backstory_template .replace('{adjective}', adjective) .replace('{motivation}', motivation) .replace('{goal}', goal) .replace('{knowledge}', knowledge) .replace('{origin}', origin) .replace('{skill}', `${motivation} and ${adjective}ness`); return { archetype: archetype.name, traits: archetype.traits, backstory, needs_priority: archetype.needs_priority, motivation, origin }; } function getTrainingData(userId) { const data = db.prepare('SELECT prompt, response, category FROM ai_training_data WHERE user_id = ? ORDER BY created_at DESC LIMIT 50').all(userId); return data; } function generateCharacterSuggestion(prompt, userId) { const userTraining = getTrainingData(userId); const archetype = generateArchetype(); const nameStyle = prompt.toLowerCase().includes('fantasy') ? 'fantasy' : prompt.toLowerCase().includes('cyber') ? 'cyberpunk' : prompt.toLowerCase().includes('goth') ? 'gothic' : prompt.toLowerCase().includes('modern') ? 'modern' : 'fantasy'; const name = generateName(nameStyle); const defaultNeeds = [ { name: 'Food', enabled: true, initial_value: 80, min_value: 0, max_value: 100, decay_rate: 1.5, priority: 3 }, { name: 'Energy', enabled: true, initial_value: 90, min_value: 0, max_value: 100, decay_rate: 2, priority: 4 }, { name: 'Bladder', enabled: true, initial_value: 30, min_value: 0, max_value: 100, decay_rate: 0.8, priority: 2 }, { name: 'Bowel', enabled: false, initial_value: 20, min_value: 0, max_value: 100, decay_rate: 0.5, priority: 1 }, { name: 'Hormones', enabled: true, initial_value: 40, min_value: 0, max_value: 100, decay_rate: 0.3, priority: 1 }, { name: 'Intimate', enabled: true, initial_value: 30, min_value: 0, max_value: 100, decay_rate: 0.4, priority: 2 } ]; const needsWithPriority = defaultNeeds.map(n => ({ ...n, priority: archetype.needs_priority[n.name] || n.priority })); const response = { name, description: `A ${nameStyle.toLowerCase()} character embodying the ${archetype.archetype.toLowerCase()} archetype.`, personality_traits: archetype.traits, backstory: archetype.backstory, suggested_needs: needsWithPriority, suggested_ui_elements: needsWithPriority .filter(n => n.enabled) .map(n => ({ need_name: n.name, element_type: n.name === 'Energy' ? 'gauge' : 'progress_bar', config: { color: getColorForNeed(n.name), label: n.name, animation: 'smooth' } })), reasoning: `Based on your request, I drew inspiration from the ${archetype.archetype.toLowerCase()} archetype. ${archetype.motivation} drives this character forward. I prioritized needs that match their instincts.` }; if (userTraining.length > 0) { const recentTraining = userTraining[0]; response.training_influence = `Drawing from your past preferences: "${recentTraining.prompt}" -> "${recentTraining.response}"`; } return response; } function getColorForNeed(needName) { const colors = { Food: '#e74c3c', Energy: '#f39c12', Bladder: '#3498db', Bowel: '#8e44ad', Hormones: '#e91e63', Intimate: '#ff6b6b' }; return colors[needName] || '#95a5a6'; } async function processTrainingPrompt(prompt, userId) { try { const messages = [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: `Create a character based on this request: "${prompt}". Return ONLY valid JSON.` } ]; const ollamaResponse = await queryLLM(messages); if (ollamaResponse) { const cleaned = ollamaResponse.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim(); const parsed = JSON.parse(cleaned); const valid = validateSuggestion(parsed); if (valid) return parsed; } } catch { // Fallback to rule-based } return generateCharacterSuggestion(prompt, userId); } function validateSuggestion(s) { if (!s.name || !s.description || !Array.isArray(s.personality_traits)) return false; if (!Array.isArray(s.suggested_needs) || s.suggested_needs.length === 0) return false; return true; } export { generateCharacterSuggestion, processTrainingPrompt, DEFAULT_KNOWLEDGE };