// Single-pass "concepts" analyzer — replaces the legacy taxonomy + glossary // pair (T3 of the integration plan). One LLM call emits both the term list // (with hierarchy + synonyms) and definitions, so the two layers can never // drift out of sync. // // The legacy `taxonomy` / `glossary` analyzers (analyze/taxonomy.ts and // analyze/glossary.ts) are still in the tree as deprecated fallbacks for one // release; nothing in the runtime path imports them anymore. 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[]; definition?: string; } const conceptsJsonSchema = { 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" } }, definition: { type: "string" }, }, }, }, }, } as const; export interface ConceptsResult { terms: DetectedTerm[]; definitions: Array<{ label: string; definition: string }>; inputTokens: number; outputTokens: number; provider: string; model: string; } export async function analyzeConcepts(documentText: string): Promise { const prompt = loadPrompt("socrates/analyze-concepts.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: 2400, jsonSchema: { name: "concepts", schema: conceptsJsonSchema as Record }, jsonObjectMode: true, maxRepairs: 2, }); const seen = new Set(); const terms: DetectedTerm[] = []; const definitions: Array<{ label: string; definition: string }> = []; 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") : [], }); const def = (t.definition ?? "").trim(); if (def) definitions.push({ label, definition: def }); } return { terms, definitions, inputTokens: result.inputTokens, outputTokens: result.outputTokens, provider: gateway.provider, model: gateway.model, }; }