feat: Add distillery enrichment cache

Caches AI enrichment results per distillery to save API calls:
- New table: enrichment_cache (distillery, tags, hit_count)
- New service: cache-enrichment.ts (get, save, increment, stats)
- enrich-data.ts checks cache before AI query
- Saves to cache after successful AI response
- Returns cached: true/false flag for transparency

Benefits:
- 0 API cost for repeated distillery scans
- Near-instant response for cached distilleries
- Shared across all users
This commit is contained in:
2025-12-26 22:12:27 +01:00
parent 537081cd1f
commit daf6c86633
4 changed files with 177 additions and 1 deletions

View File

@@ -0,0 +1,32 @@
-- Enrichment Cache Table
-- Caches AI enrichment results per distillery to save API calls
CREATE TABLE IF NOT EXISTS enrichment_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
distillery TEXT NOT NULL UNIQUE,
suggested_tags TEXT[],
suggested_custom_tags TEXT[],
search_string TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
hit_count INTEGER DEFAULT 0
);
-- Index for fast lookups
CREATE INDEX IF NOT EXISTS idx_enrichment_cache_distillery
ON enrichment_cache(distillery);
-- RLS: Allow all authenticated users to read (it's shared cache)
ALTER TABLE enrichment_cache ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "enrichment_cache_select" ON enrichment_cache;
CREATE POLICY "enrichment_cache_select" ON enrichment_cache
FOR SELECT TO authenticated USING (true);
DROP POLICY IF EXISTS "enrichment_cache_insert" ON enrichment_cache;
CREATE POLICY "enrichment_cache_insert" ON enrichment_cache
FOR INSERT TO authenticated WITH CHECK (true);
DROP POLICY IF EXISTS "enrichment_cache_update" ON enrichment_cache;
CREATE POLICY "enrichment_cache_update" ON enrichment_cache
FOR UPDATE TO authenticated USING (true);

View File

@@ -6,6 +6,7 @@ import { trackApiUsage } from '@/services/track-api-usage';
import { deductCredits } from '@/services/credit-service'; import { deductCredits } from '@/services/credit-service';
import { getAllSystemTags } from '@/services/tags'; import { getAllSystemTags } from '@/services/tags';
import { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } from '@/lib/openrouter'; import { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } from '@/lib/openrouter';
import { getEnrichmentCache, saveEnrichmentCache, incrementCacheHit } from '@/services/cache-enrichment';
// Native Schema Definition for Enrichment Data // Native Schema Definition for Enrichment Data
const enrichmentSchema = { const enrichmentSchema = {
@@ -146,6 +147,31 @@ export async function enrichData(name: string, distillery: string, availableTags
return { success: false, error: 'OPENROUTER_API_KEY is not configured.' }; return { success: false, error: 'OPENROUTER_API_KEY is not configured.' };
} }
// ========================================
// CACHE CHECK - Return cached data if available
// ========================================
if (distillery?.trim()) {
try {
const cached = await getEnrichmentCache(distillery);
if (cached) {
console.log(`[EnrichData] ✅ CACHE HIT for: ${distillery}`);
// Fire and forget: increment hit counter
incrementCacheHit(distillery).catch(() => { });
return {
success: true,
data: cached,
cached: true,
provider: 'cache',
perf: { apiDuration: 0 }
};
}
console.log(`[EnrichData] ❌ Cache MISS for: ${distillery}`);
} catch (cacheError) {
console.warn('[EnrichData] Cache lookup failed:', cacheError);
// Continue with AI query
}
}
let supabase; let supabase;
try { try {
let tagsToUse = availableTags; let tagsToUse = availableTags;
@@ -184,6 +210,15 @@ Instructions:
console.log('[EnrichData] Response:', result.data); console.log('[EnrichData] Response:', result.data);
// ========================================
// SAVE TO CACHE - Store for future lookups
// ========================================
if (distillery?.trim()) {
saveEnrichmentCache(distillery, result.data).catch((err) => {
console.warn('[EnrichData] Cache save failed:', err);
});
}
// Track usage // Track usage
await trackApiUsage({ await trackApiUsage({
userId: userId, userId: userId,
@@ -197,6 +232,7 @@ Instructions:
return { return {
success: true, success: true,
data: result.data, data: result.data,
cached: false,
provider, provider,
perf: { perf: {
apiDuration: result.apiTime apiDuration: result.apiTime

View File

@@ -0,0 +1,108 @@
'use server';
import { createClient } from '@/lib/supabase/server';
interface EnrichmentCacheData {
suggested_tags: string[] | null;
suggested_custom_tags: string[] | null;
search_string: string | null;
}
/**
* Get cached enrichment data for a distillery
*/
export async function getEnrichmentCache(distillery: string): Promise<EnrichmentCacheData | null> {
if (!distillery?.trim()) return null;
const supabase = await createClient();
const normalizedDistillery = distillery.trim().toLowerCase();
const { data, error } = await supabase
.from('enrichment_cache')
.select('suggested_tags, suggested_custom_tags, search_string')
.eq('distillery', normalizedDistillery)
.single();
if (error || !data) {
return null;
}
return data;
}
/**
* Save enrichment data to cache
*/
export async function saveEnrichmentCache(
distillery: string,
enrichmentData: EnrichmentCacheData
): Promise<boolean> {
if (!distillery?.trim()) return false;
const supabase = await createClient();
const normalizedDistillery = distillery.trim().toLowerCase();
const { error } = await supabase
.from('enrichment_cache')
.upsert({
distillery: normalizedDistillery,
suggested_tags: enrichmentData.suggested_tags,
suggested_custom_tags: enrichmentData.suggested_custom_tags,
search_string: enrichmentData.search_string,
updated_at: new Date().toISOString(),
}, {
onConflict: 'distillery'
});
if (error) {
console.error('[EnrichmentCache] Save error:', error);
return false;
}
console.log(`[EnrichmentCache] Saved cache for: ${normalizedDistillery}`);
return true;
}
/**
* Increment cache hit counter
*/
export async function incrementCacheHit(distillery: string): Promise<void> {
if (!distillery?.trim()) return;
const supabase = await createClient();
const normalizedDistillery = distillery.trim().toLowerCase();
// Simple increment via update - no RPC needed
const { data: current } = await supabase
.from('enrichment_cache')
.select('hit_count')
.eq('distillery', normalizedDistillery)
.single();
if (current) {
await supabase
.from('enrichment_cache')
.update({ hit_count: (current.hit_count || 0) + 1 })
.eq('distillery', normalizedDistillery);
}
}
/**
* Get cache statistics
*/
export async function getEnrichmentCacheStats() {
const supabase = await createClient();
const { data, error } = await supabase
.from('enrichment_cache')
.select('distillery, hit_count, created_at')
.order('hit_count', { ascending: false })
.limit(20);
if (error) {
console.error('[EnrichmentCache] Stats error:', error);
return [];
}
return data || [];
}

File diff suppressed because one or more lines are too long