efe8e165cf
按 leo 鐵律(2026-06-14)把插件從「直接 SQL 操作基本盤表」改寫成 「只透過基本盤 arcrun/kbdb HTTP API 讀寫」。零建表、零 migration、零 SQL。 - 新增 src/lib/kbdb-client.ts:唯一對外通道,封裝 entries/templates/records API - 新增 src/lib/templates.ts:triplet/entity template 定義(替代建表) - 改寫 21 個違規 action(triplet/graph/entity/search)→ 走 client,圖在插件層記憶體組裝 - 移除所有 migrations、D1/Vectorize/AI 綁定;embedding/語意搜尋歸基本盤 optional 模組 - index.ts 只掛 triplets/graph/entities/search 路由;基本盤路由歸 arcrun/kbdb - 測試改走 mock client(純 node);裁剪 CLAUDE.md 只留 graph 插件 + 鐵律 - 修正 SDD design.md「讀現狀推翻鐵律」的錯誤判斷(共用 D1 → API-as-Wall) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
// 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 | null,
|
||
): 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 ?? undefined,
|
||
});
|
||
return true;
|
||
}
|