Files
kbdb-graph-plugin/src/actions/triplet-extract.ts
T
Claude 4190fbcf90 feat(graph): owner_id 寫入鏈必經(根治無主資料)——配合 base owner-mandatory(D28)
- kbdb-client requireOwner 守衛:漏 owner 插件端即 throw,不再靜默送 owner:''
- owner 一路 thread:persistNodes/ingestEnvelope/entity-crud/triplet-crud 全必填
- 病灶修:POST /triplets/ingest 原本沒傳 owner→加 owner_id query+400
- 寫入 route 缺 owner→400;vitest 23→27
- 註:gloss-bridge 分支的 backfill/gloss-entry 另需套 owner 必填(附說明),不跨支疊改
2026-07-05 11:00:30 +00:00

78 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// triplet-extract.ts — LLM 三元組萃取 + 寫入工具函數
// 萃取=純 LLM(不碰 DB);寫入=走基本盤 API(零 SQL / 零 D1)。
// 供 block-ingest.ts 和 block-process.ts 共用
import type { KbdbClient } from '../lib/kbdb-client';
import { createTriplet, queryTriplets } from './triplet-crud';
const EXTRACT_PROMPT = `你是知識萃取助手,從文章中萃取知識三元組。
只萃取:性格特質、關鍵經歷精華、核心觀點信念、說話方式與風格。
禁止:具體年份日期、純事實統計、流水帳年表。
格式:[{"subject":"...","predicate":"2-6字","object":"15-50字","confidence":0.8}]
直接輸出 JSON Array,第一字元 [,最後字元 ]。不要其他文字。`;
export interface LLMTriplet {
subject: string;
predicate: string;
object: string;
confidence: number;
}
/** Workers AI 萃取三元組,每段獨立呼叫,單段失敗不中斷 */
export async function extractTripletsViaLLM(ai: Ai, chunks: string[]): Promise<LLMTriplet[]> {
const results: LLMTriplet[] = [];
for (const chunk of chunks) {
if (!chunk.trim()) continue;
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const raw = await (ai as any).run('@cf/meta/llama-3.3-70b-instruct-fp8-fast', {
messages: [
{ role: 'system', content: EXTRACT_PROMPT },
{ role: 'user', content: `萃取三元組:\n${chunk}` },
],
max_tokens: 512,
temperature: 0.1,
});
const text = (typeof raw === 'string' ? raw : (raw?.response ?? ''))
.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
const m = text.match(/\[[\s\S]*\]/);
if (!m) continue;
const parsed = JSON.parse(m[0]);
if (!Array.isArray(parsed)) continue;
for (const t of parsed) {
const s = String(t?.subject ?? '').trim();
const p = String(t?.predicate ?? '').trim();
const o = String(t?.object ?? '').trim();
if (s && p && o) results.push({ subject: s, predicate: p, object: o, confidence: Number(t?.confidence) || 0.8 });
}
} catch { /* 單段失敗跳過 */ }
}
return results;
}
/** 寫入一條三元組(走基本盤 API),已存在回傳 false,新寫入回傳 true。
* 查重 + 寫入全走 KbdbClient → triplet-crud,零 SQL / 零 D1。 */
export async function writeTripletToDb(
client: KbdbClient,
t: { subject: string; predicate: string; object: string; confidence?: number },
owner: string, // 必經:萃取寫入必帶 owner(不再收 null → 不會 owner_id: undefined
): Promise<boolean> {
// 查重:以 S-P-O 三欄精確比對(queryTriplets 取 template record 後在插件層 filter
const { count } = await queryTriplets(client, {
subject: t.subject,
predicate: t.predicate,
object: t.object,
limit: 1,
});
if (count > 0) return false;
await createTriplet(client, {
subject: t.subject,
predicate: t.predicate,
object: t.object,
confidence: t.confidence ?? 0.8,
owner_id: owner,
});
return true;
}