// Methodology Reseed Utility // P3-2 후속: pseudo-embedding으로 시드된 methodology_vectors 행을 // embed-text Edge Function(OpenAI text-embedding-3-small)을 통해 실제 의미 임베딩으로 교체. // // 사용 방법: // 1) Supabase Secrets에 OPENAI_API_KEY 설정 // 2) supabase functions deploy embed-text // 3) admin 유저 세션에서: // import { reseedAllMethodologyVectors } from './lib/methodologyReseed' // await reseedAllMethodologyVectors({ onProgress: (i,n)=>console.log(i,'/',n) }) // 또는 UI 유틸 페이지에서 버튼 클릭으로 실행. import { supabase } from './supabase' import { invokeEdgeFunction } from './apiAdapter' export interface ReseedProgress { methodologyCode: string status: 'updated' | 'failed' | 'skipped' similaritySample?: number error?: string } export interface ReseedOptions { /** 진행률 콜백 (1-based index, total) */ onProgress?: (current: number, total: number, last: ReseedProgress) => void /** 개별 rate limit 대기(ms). 기본 250ms (OpenAI 제한 대비 여유). */ throttleMs?: number /** 드라이런 — 실제 UPDATE는 건너뛰고 embedding 생성만 시도 */ dryRun?: boolean } export interface ReseedResult { total: number updated: number failed: number skipped: number log: ReseedProgress[] } /** * embed-text Edge Function 호출 → 문자열 임베딩 벡터(1536 dim). */ async function fetchEmbedding(text: string): Promise<{ embedding: number[]; source: string }> { const payload = await invokeEdgeFunction<{ text: string }, { embedding: number[]; source: string }>('embed-text', { text }) if (!Array.isArray(payload.embedding) || payload.embedding.length !== 1536) { throw new Error(`잘못된 embedding 차원: ${payload.embedding?.length}`) } return payload } /** * 단일 methodology 행을 재시드한다. * 재시드 input은 `name_ko + applicability` — 사용자 쿼리와 의미 공간을 공유하기 위해. */ async function reseedOne( row: { id: string methodology_code: string name_ko: string applicability: string }, dryRun: boolean, ): Promise { try { const seedText = `${row.name_ko} ${row.applicability}`.trim() const { embedding, source } = await fetchEmbedding(seedText) if (dryRun) { return { methodologyCode: row.methodology_code, status: 'skipped', similaritySample: source === 'openai' ? 1 : 0, } } // pgvector는 배열 리터럴로 업데이트. supabase-js는 배열 그대로 전달하면 vector 타입으로 변환됨. const { error } = await supabase .from('methodology_vectors') .update({ embedding: embedding, updated_at: new Date().toISOString() }) .eq('id', row.id) if (error) throw error return { methodologyCode: row.methodology_code, status: 'updated', } } catch (e) { return { methodologyCode: row.methodology_code, status: 'failed', error: e instanceof Error ? e.message : String(e), } } } /** * methodology_vectors 전체 행 재시드. * 관리자 권한(RLS `Admins manage methodology vectors`)이 필요하다. */ export async function reseedAllMethodologyVectors( opts: ReseedOptions = {}, ): Promise { const { onProgress, throttleMs = 250, dryRun = false } = opts const { data: rows, error } = await supabase .from('methodology_vectors') .select('id, methodology_code, name_ko, applicability') .order('methodology_code', { ascending: true }) if (error) throw error const total = rows?.length ?? 0 const log: ReseedProgress[] = [] let updated = 0 let failed = 0 let skipped = 0 for (let i = 0; i < total; i++) { const row = rows![i] as { id: string methodology_code: string name_ko: string applicability: string } const progress = await reseedOne(row, dryRun) log.push(progress) if (progress.status === 'updated') updated += 1 else if (progress.status === 'failed') failed += 1 else skipped += 1 onProgress?.(i + 1, total, progress) if (throttleMs > 0 && i < total - 1) { await new Promise((r) => setTimeout(r, throttleMs)) } } return { total, updated, failed, skipped, log } } /** * 개발자 편의용: 콘솔에서 바로 호출. * window.carboany.reseedMethodologies() * 프로덕션 번들에 포함되지 않도록 main.tsx에서 import.meta.env.DEV 가드 권장. */ export function exposeReseedToWindow() { if (typeof window === 'undefined') return const w = window as unknown as Record if (!w.carboany) w.carboany = {} const api = w.carboany as Record api.reseedMethodologies = async (dryRun = false) => { const result = await reseedAllMethodologyVectors({ dryRun, onProgress: (i, n, p) => { console.log(`[reseed] ${i}/${n} ${p.methodologyCode} → ${p.status}`, p.error ?? '') }, }) console.table(result.log) console.log(`total=${result.total} updated=${result.updated} failed=${result.failed} skipped=${result.skipped}`) return result } }