ex-brain 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/package.json +2 -1
- package/src/ai/compiler.ts +18 -53
- package/src/ai/entity-link.ts +31 -62
- package/src/ai/llm-client.ts +291 -0
- package/src/ai/timeline-extractor.ts +29 -62
- package/src/commands/index.ts +612 -86
- package/src/db/client.ts +121 -15
- package/src/db/errors.ts +178 -0
- package/src/db/schema.ts +1 -0
- package/src/mcp/server.ts +400 -237
- package/src/repositories/brain-repo.ts +576 -358
- package/src/settings.ts +23 -2
- package/src/types/index.ts +1 -0
- package/src/utils/cli-output.ts +569 -0
- package/src/utils/query-sanitizer.ts +63 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ResolvedLLM } from "../settings";
|
|
2
2
|
import type { TimelineEntry } from "../types";
|
|
3
|
+
import { callLLM, resolveApiKey, isLLMConfigured } from "./llm-client";
|
|
4
|
+
import { jsonrepair } from "jsonrepair";
|
|
3
5
|
|
|
4
6
|
// ---------------------------------------------------------------------------
|
|
5
7
|
// Types
|
|
@@ -50,14 +52,14 @@ export async function extractTimelineEvents(
|
|
|
50
52
|
input: TimelineExtractionInput,
|
|
51
53
|
llm: ResolvedLLM,
|
|
52
54
|
): Promise<TimelineExtractionResult> {
|
|
53
|
-
|
|
54
|
-
if (!apiKey) {
|
|
55
|
+
if (!isLLMConfigured(llm)) {
|
|
55
56
|
// Fallback: regex-based extraction
|
|
56
57
|
return fallbackExtract(input);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const prompt = buildExtractionPrompt(input);
|
|
60
|
-
const
|
|
61
|
+
const systemPrompt = "You are a timeline extraction assistant. Extract events from unstructured text. Always output valid JSON array. Be concise and factual.";
|
|
62
|
+
const resp = await callLLM(llm, prompt, 2048, systemPrompt);
|
|
61
63
|
|
|
62
64
|
if (!resp) {
|
|
63
65
|
return fallbackExtract(input);
|
|
@@ -94,7 +96,8 @@ export async function extractTimelineFromRelation(
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
const prompt = buildRelationTimelinePrompt(relation, defaultDate);
|
|
97
|
-
const
|
|
99
|
+
const systemPrompt = "You are a timeline extraction assistant. Extract events from relationships. Always output valid JSON array.";
|
|
100
|
+
const resp = await callLLM(llm, prompt, 512, systemPrompt);
|
|
98
101
|
|
|
99
102
|
if (!resp) return null;
|
|
100
103
|
|
|
@@ -186,50 +189,6 @@ Examples:
|
|
|
186
189
|
/no_think`;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
// LLM Call
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
async function callLLM(llm: ResolvedLLM, prompt: string, maxTokens: number): Promise<string> {
|
|
194
|
-
const apiKey = resolveApiKey(llm);
|
|
195
|
-
if (!apiKey) return "";
|
|
196
|
-
|
|
197
|
-
const body = {
|
|
198
|
-
model: llm.model,
|
|
199
|
-
messages: [
|
|
200
|
-
{ role: "system", content: "You are a timeline extraction assistant. Extract events from unstructured text. Always output valid JSON array. Be concise and factual." },
|
|
201
|
-
{ role: "user", content: prompt },
|
|
202
|
-
],
|
|
203
|
-
temperature: 0.1,
|
|
204
|
-
max_tokens: maxTokens,
|
|
205
|
-
enable_thinking: false,
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const resp = await fetch(
|
|
210
|
-
llm.baseURL.endsWith("/") ? llm.baseURL + "chat/completions" : llm.baseURL + "/chat/completions",
|
|
211
|
-
{
|
|
212
|
-
method: "POST",
|
|
213
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
214
|
-
body: JSON.stringify(body),
|
|
215
|
-
},
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
if (!resp.ok) {
|
|
219
|
-
const text = await resp.text();
|
|
220
|
-
console.warn(`[timeline-extractor] LLM call failed (${resp.status}): ${text.slice(0, 200)}`);
|
|
221
|
-
return "";
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const data = await resp.json();
|
|
225
|
-
return data.choices?.[0]?.message?.content?.trim() ?? "";
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
228
|
-
console.warn(`[timeline-extractor] LLM call error: ${msg}`);
|
|
229
|
-
return "";
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
192
|
// ---------------------------------------------------------------------------
|
|
234
193
|
// Response Parsing
|
|
235
194
|
// ---------------------------------------------------------------------------
|
|
@@ -239,7 +198,9 @@ function parseExtractionResponse(resp: string, pageSlug: string): TimelineEntry[
|
|
|
239
198
|
if (!match) return [];
|
|
240
199
|
|
|
241
200
|
try {
|
|
242
|
-
|
|
201
|
+
// Use jsonrepair to fix common LLM JSON issues
|
|
202
|
+
const repaired = jsonrepair(match[0]);
|
|
203
|
+
const parsed = JSON.parse(repaired) as unknown[];
|
|
243
204
|
const entries: TimelineEntry[] = [];
|
|
244
205
|
|
|
245
206
|
for (const e of parsed) {
|
|
@@ -249,12 +210,18 @@ function parseExtractionResponse(resp: string, pageSlug: string): TimelineEntry[
|
|
|
249
210
|
const date = normalizeDate(String(entry.date ?? ""));
|
|
250
211
|
if (!date) continue;
|
|
251
212
|
|
|
213
|
+
// Get importance from the response, default to 3
|
|
214
|
+
const importance = typeof entry.importance === "number"
|
|
215
|
+
? Math.max(1, Math.min(5, Math.round(entry.importance)))
|
|
216
|
+
: 3;
|
|
217
|
+
|
|
252
218
|
entries.push({
|
|
253
219
|
pageSlug,
|
|
254
220
|
date,
|
|
255
221
|
source: "extracted",
|
|
256
222
|
summary: String(entry.summary ?? "").slice(0, 120),
|
|
257
223
|
detail: String(entry.detail ?? ""),
|
|
224
|
+
importance,
|
|
258
225
|
});
|
|
259
226
|
}
|
|
260
227
|
|
|
@@ -344,15 +311,19 @@ function normalizeDate(raw: string, defaultDate?: string): string {
|
|
|
344
311
|
const chineseMatch = trimmed.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
|
|
345
312
|
if (chineseMatch) {
|
|
346
313
|
const [, year, month, day] = chineseMatch;
|
|
347
|
-
|
|
314
|
+
if (year && month && day) {
|
|
315
|
+
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
|
316
|
+
}
|
|
348
317
|
}
|
|
349
318
|
|
|
350
319
|
// Chinese format without year: 1月15日
|
|
351
320
|
const chineseNoYearMatch = trimmed.match(/(\d{1,2})月(\d{1,2})日/);
|
|
352
321
|
if (chineseNoYearMatch && defaultDate) {
|
|
353
322
|
const [, month, day] = chineseNoYearMatch;
|
|
354
|
-
|
|
355
|
-
|
|
323
|
+
if (month && day) {
|
|
324
|
+
const year = defaultDate.slice(0, 4);
|
|
325
|
+
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
|
326
|
+
}
|
|
356
327
|
}
|
|
357
328
|
|
|
358
329
|
// English month names
|
|
@@ -374,10 +345,12 @@ function normalizeDate(raw: string, defaultDate?: string): string {
|
|
|
374
345
|
const englishMatch = trimmed.match(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+(\d{1,2})(?:st|nd|rd|th)?(?:,?\s+(\d{4}))?/i);
|
|
375
346
|
if (englishMatch) {
|
|
376
347
|
const [, monthName, day, year] = englishMatch;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
348
|
+
if (monthName && day) {
|
|
349
|
+
const month = monthMap[monthName.toLowerCase().slice(0, 3)];
|
|
350
|
+
if (month) {
|
|
351
|
+
const finalYear = year || (defaultDate ? defaultDate.slice(0, 4) : new Date().getFullYear().toString());
|
|
352
|
+
return `${finalYear}-${month}-${day.padStart(2, "0")}`;
|
|
353
|
+
}
|
|
381
354
|
}
|
|
382
355
|
}
|
|
383
356
|
|
|
@@ -416,12 +389,6 @@ function normalizeDate(raw: string, defaultDate?: string): string {
|
|
|
416
389
|
// Helpers
|
|
417
390
|
// ---------------------------------------------------------------------------
|
|
418
391
|
|
|
419
|
-
function resolveApiKey(llm: ResolvedLLM): string {
|
|
420
|
-
if (llm.apiKey) return llm.apiKey;
|
|
421
|
-
if (llm.apiKeyEnv) return process.env[llm.apiKeyEnv] ?? "";
|
|
422
|
-
return "";
|
|
423
|
-
}
|
|
424
|
-
|
|
425
392
|
function deduplicateEntries(entries: TimelineEntry[]): TimelineEntry[] {
|
|
426
393
|
const seen = new Map<string, TimelineEntry>();
|
|
427
394
|
|