/** * FosterFlow AI Worker — runs on a machine with local GPU/Ollama access. * Polls the FosterFlow API for pending AI jobs, processes them through local Ollama, * and posts results back. * * Usage: * PUBLIC_SITE_URL=https://yourrescue.fosterflow.app WORKER_API_KEY=your-key npx tsx scripts/ai-worker.ts * * Environment variables: * PUBLIC_SITE_URL — FosterFlow base URL (default: http://localhost:5173) * WORKER_API_KEY — Shared secret for authenticating with the FosterFlow API * OLLAMA_URL — Ollama API URL (default: http://localhost:11434) * POLL_INTERVAL — Milliseconds between polls (default: 10000) * * NOTE: Old completed/failed jobs should be cleaned up periodically. * Consider a cron job: DELETE FROM ai_jobs WHERE status IN ('completed','failed') AND completed_at < NOW() - INTERVAL 7 DAY; */ const API_URL = process.env.PUBLIC_SITE_URL || 'http://localhost:5173'; const WORKER_KEY = process.env.WORKER_API_KEY || ''; const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; const POLL_INTERVAL = parseInt(process.env.POLL_INTERVAL || '10000', 10); if (!WORKER_KEY) { console.error('ERROR: WORKER_API_KEY environment variable is required'); process.exit(1); } async function fetchNextJob() { const res = await fetch(`${API_URL}/api/worker/jobs`, { headers: { Authorization: `Bearer ${WORKER_KEY}` } }); if (!res.ok) return null; return res.json(); } async function reportResult(jobId: number, output: unknown) { await fetch(`${API_URL}/api/worker/jobs/${jobId}`, { method: 'POST', headers: { Authorization: `Bearer ${WORKER_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'completed', output }) }); } async function reportError(jobId: number, error: string) { await fetch(`${API_URL}/api/worker/jobs/${jobId}`, { method: 'POST', headers: { Authorization: `Bearer ${WORKER_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'failed', error }) }); } async function generateNames(input: { species: string; theme: string; count: number; existingNames?: string }) { const themes: Record = { food: 'food and cooking themed names (like Biscuit, Waffles, Chimichurri, Mochi, Pancake)', disney: 'Disney character themed names (like Simba, Nala, Elsa, Olaf, Stitch)', celebrity: 'celebrity or pop culture themed names (like Bowie, Dolly, Elvis, Beyonce)', nature: 'nature themed names (like Willow, Storm, River, Daisy, Bear)', classic: 'classic pet names (like Max, Bella, Charlie, Luna, Cooper)', funny: 'funny and punny names (like Sir Barks-a-Lot, Bark Twain, Chewbarka, Purrlock Holmes)', random: 'creative and unique names' }; const themePrompt = themes[input.theme] || themes.random; const count = input.count || 10; const prompt = `Generate exactly ${count} creative ${input.species || 'dog'} names. Theme: ${themePrompt}. IMPORTANT: These names are ALREADY TAKEN and must NOT be reused: ${input.existingNames || 'none'} Rules: - Just the names, one per line - No numbers, no explanations, no extra text - Be creative and fun - Mix single names and two-word names - Do NOT repeat any name from the taken list above`; const res = await fetch(`${OLLAMA_URL}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gemma2:9b', prompt, stream: false, options: { temperature: 1.2, top_p: 0.95, num_predict: 200 } }) }); if (!res.ok) throw new Error(`Ollama error: ${res.status}: ${await res.text()}`); const data = await res.json(); const names = (data.response || '') .split('\n') .map((n: string) => n.replace(/^\d+[\.\)]\s*/, '').replace(/^[-*•]\s*/, '').trim()) .filter((name: string) => name && name.length > 1 && name.length < 40 && !name.includes(':')) .filter((name: string, index: number, self: string[]) => self.indexOf(name) === index) .slice(0, count); return { names }; } async function poll() { console.log('FosterFlow AI Worker started'); console.log(` Polling: ${API_URL} every ${POLL_INTERVAL}ms`); console.log(` Ollama: ${OLLAMA_URL}`); // Run immediately on start, then on interval const tick = async () => { try { const job = await fetchNextJob(); if (job && job.id) { console.log(`Processing job ${job.id} (${job.type})`); try { if (job.type === 'pet-names') { const result = await generateNames(job.input); await reportResult(job.id, result); console.log(`Job ${job.id} completed`); } else { await reportError(job.id, `Unknown job type: ${job.type}`); } } catch (err: any) { await reportError(job.id, err.message); console.error(`Job ${job.id} failed:`, err.message); } } } catch (err) { // Connection error — will retry on next interval } }; tick(); setInterval(tick, POLL_INTERVAL); } poll();