// Analyze pass: extract taxonomy terms from prose. import "server-only"; import { defaultGateway, chatJSON, type Message } from "../gateway"; import { loadPrompt } from "../prompts"; import type { DetectedTerm } from "../../db/repo"; interface RawTerm { label?: string; parentLabel?: string | null; synonyms?: string[]; } const taxonomyJsonSchema = { type: "object", additionalProperties: false, required: ["terms"], properties: { terms: { type: "array", items: { type: "object", additionalProperties: false, required: ["label"], properties: { label: { type: "string", minLength: 1 }, parentLabel: { type: ["string", "null"] }, synonyms: { type: "array", items: { type: "string" } }, }, }, }, }, } as const; export interface TaxonomyResult { terms: DetectedTerm[]; inputTokens: number; outputTokens: number; provider: string; model: string; } export async function analyzeTaxonomy(documentText: string): Promise { const prompt = loadPrompt("socrates/analyze-taxonomy.md"); const gateway = defaultGateway(); const messages: Message[] = [ { role: "system", content: prompt }, { role: "user", content: `Document:\n\n${documentText}` }, ]; const { value, result } = await chatJSON<{ terms?: RawTerm[] }>(gateway, messages, { temperature: 0.2, maxTokens: 1500, jsonSchema: { name: "taxonomy", schema: taxonomyJsonSchema as Record }, jsonObjectMode: true, maxRepairs: 2, }); const seen = new Set(); const terms: DetectedTerm[] = []; for (const t of value?.terms ?? []) { const label = (t.label ?? "").trim(); if (!label) continue; const key = label.toLowerCase(); if (seen.has(key)) continue; seen.add(key); terms.push({ label, parentLabel: t.parentLabel?.trim() || null, synonyms: Array.isArray(t.synonyms) ? t.synonyms.filter(s => typeof s === "string") : [], }); } return { terms, inputTokens: result.inputTokens, outputTokens: result.outputTokens, provider: gateway.provider, model: gateway.model, }; }