CarboAny / src /lib /methodologyReseed.ts
Esketch's picture
deploy: Campaign Control Plane + Security Hardening Release (Orphan Clean Build v3)
5c98911
Raw
History Blame Contribute Delete
5.17 kB
// 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<ReseedProgress> {
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<ReseedResult> {
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<string, unknown>
if (!w.carboany) w.carboany = {}
const api = w.carboany as Record<string, unknown>
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
}
}