ex-brain 0.2.3 → 0.2.4
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 +1 -1
- package/package.json +2 -1
- package/src/ai/ax-adapter.ts +80 -0
- package/src/ai/compiler.ts +148 -428
- package/src/ai/entity-link.ts +102 -109
- package/src/ai/timeline-extractor.ts +149 -306
- package/src/commands/index.ts +1 -1
- package/src/ai/llm-client.ts +0 -291
package/src/ai/compiler.ts
CHANGED
|
@@ -1,494 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent Compilation — Ax Signature version.
|
|
3
|
+
*
|
|
4
|
+
* Uses f.json() for complex multi-line output (compiledTruth contains markdown
|
|
5
|
+
* with multiple lines, which breaks Ax's line-based field parsing).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Declaraive input/output contracts
|
|
9
|
+
* - Automatic validation + retry on failure
|
|
10
|
+
* - Ready for GEPA optimization
|
|
11
|
+
* - Fallback to append when LLM unavailable
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ax, f } from "@ax-llm/ax";
|
|
1
15
|
import type { ResolvedLLM } from "../settings";
|
|
2
16
|
import type { TimelineEntry } from "../types";
|
|
3
|
-
import {
|
|
4
|
-
import { jsonrepair } from "jsonrepair";
|
|
17
|
+
import { createAxAI } from "./ax-adapter";
|
|
5
18
|
|
|
6
19
|
// ---------------------------------------------------------------------------
|
|
7
|
-
// Types
|
|
20
|
+
// Types (preserved for API compatibility with BrainRepository)
|
|
8
21
|
// ---------------------------------------------------------------------------
|
|
9
22
|
|
|
10
23
|
export interface CompileInput {
|
|
11
|
-
/** Current compiled truth content */
|
|
12
24
|
currentTruth: string;
|
|
13
|
-
/** Timeline entries for context */
|
|
14
25
|
timeline: TimelineEntry[];
|
|
15
|
-
/** New information to process */
|
|
16
26
|
newInfo: string;
|
|
17
|
-
/** Source of the new information */
|
|
18
27
|
source: string;
|
|
19
|
-
/** Date of the new information (ISO or YYYY-MM-DD) */
|
|
20
28
|
date: string;
|
|
21
|
-
|
|
22
|
-
pageContext?: {
|
|
23
|
-
slug: string;
|
|
24
|
-
type: string;
|
|
25
|
-
title: string;
|
|
26
|
-
};
|
|
29
|
+
pageContext?: { slug: string; type: string; title: string };
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface CompileResult {
|
|
30
|
-
/** Updated compiled truth */
|
|
31
33
|
compiledTruth: string;
|
|
32
|
-
/** Whether any update was made */
|
|
33
34
|
changed: boolean;
|
|
34
|
-
/** Type of change */
|
|
35
35
|
changeType: "append" | "update" | "replace" | "none" | "conflict";
|
|
36
|
-
/** Human-readable summary of what changed */
|
|
37
36
|
changeSummary: string;
|
|
38
|
-
/** Timeline entries to add (extracted from new info) */
|
|
39
37
|
timelineEntries: TimelineEntry[];
|
|
40
|
-
/** Confidence score */
|
|
41
38
|
confidence: number;
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/** Information type classification */
|
|
48
|
-
infoType: "status_update" | "new_event" | "correction" | "confirmation" | "new_entity";
|
|
49
|
-
/** Entities mentioned */
|
|
50
|
-
entities: string[];
|
|
51
|
-
/** Temporal context */
|
|
52
|
-
temporalContext: string;
|
|
53
|
-
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Signature definition (using json for multi-line compiledTruth)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
45
|
+
const compileSig = f()
|
|
46
|
+
.input("currentTruth", f.string("Current compiled truth content"))
|
|
47
|
+
.input("newInfo", f.string("New information to compile"))
|
|
48
|
+
.input("infoSource", f.string("Source of the new information"))
|
|
49
|
+
.input("infoDate", f.string("Date of the new information in YYYY-MM-DD format"))
|
|
50
|
+
.input("context", f.string("Page type, title, and recent timeline for context"))
|
|
51
|
+
.output("compilationResult", f.json(
|
|
52
|
+
"Compilation result as JSON object with: " +
|
|
53
|
+
"changeType (append|update|replace|none|conflict), " +
|
|
54
|
+
"compiledTruth (full updated markdown), " +
|
|
55
|
+
"changeSummary (one-line summary), " +
|
|
56
|
+
"confidence (0-1)"
|
|
57
|
+
))
|
|
58
|
+
.build();
|
|
59
|
+
|
|
60
|
+
const compileGen = ax(compileSig);
|
|
61
|
+
|
|
62
|
+
// Timeline extraction sub-signature
|
|
63
|
+
const timelineSig = f()
|
|
64
|
+
.input("newInfo", f.string("Information to extract timeline events from"))
|
|
65
|
+
.input("infoSource", f.string("Source identifier"))
|
|
66
|
+
.input("infoDate", f.string("Date of the information in YYYY-MM-DD format"))
|
|
67
|
+
.output("events", f.json(
|
|
68
|
+
"Array of timeline events with: date (YYYY-MM-DD), summary (max 120 chars), detail (optional)"
|
|
69
|
+
))
|
|
70
|
+
.build();
|
|
71
|
+
|
|
72
|
+
const timelineGen = ax(timelineSig);
|
|
69
73
|
|
|
70
74
|
// ---------------------------------------------------------------------------
|
|
71
|
-
//
|
|
75
|
+
// Public API
|
|
72
76
|
// ---------------------------------------------------------------------------
|
|
73
77
|
|
|
74
|
-
/**
|
|
75
|
-
* Intelligent compilation: analyze new info, merge/update compiled truth.
|
|
76
|
-
* Uses LLM to understand semantic changes and update appropriately.
|
|
77
|
-
*/
|
|
78
78
|
export async function compileTruth(
|
|
79
79
|
input: CompileInput,
|
|
80
80
|
llm: ResolvedLLM,
|
|
81
81
|
): Promise<CompileResult> {
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
84
|
-
return {
|
|
85
|
-
compiledTruth: appendFact(input.currentTruth, input.newInfo, input.source),
|
|
86
|
-
changed: true,
|
|
87
|
-
changeType: "append",
|
|
88
|
-
changeSummary: "LLM not configured, appended as simple fact",
|
|
89
|
-
timelineEntries: [],
|
|
90
|
-
confidence: 0.5,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Step 1: Analyze the new information
|
|
95
|
-
const analysis = await analyzeNewInfo(input, llm);
|
|
96
|
-
|
|
97
|
-
// Step 2: Generate updated compiled truth
|
|
98
|
-
const updateResult = await generateUpdatedTruth(input, analysis, llm);
|
|
99
|
-
|
|
100
|
-
// Step 3: Extract timeline entries from new info
|
|
101
|
-
const timelineEntries = await extractTimelineFromInfo(input, analysis, llm);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
compiledTruth: updateResult.compiledTruth,
|
|
105
|
-
changed: updateResult.changed,
|
|
106
|
-
changeType: updateResult.changeType,
|
|
107
|
-
changeSummary: updateResult.changeSummary,
|
|
108
|
-
timelineEntries,
|
|
109
|
-
confidence: analysis.facts.reduce((sum, f) => sum + f.confidence, 0) / Math.max(analysis.facts.length, 1),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Step 1: Analyze new information to understand what it means
|
|
115
|
-
*/
|
|
116
|
-
async function analyzeNewInfo(
|
|
117
|
-
input: CompileInput,
|
|
118
|
-
llm: ResolvedLLM,
|
|
119
|
-
): Promise<FactAnalysis> {
|
|
120
|
-
const prompt = buildAnalysisPrompt(input);
|
|
121
|
-
const resp = await callLLM(llm, prompt, 2048, COMPILER_SYSTEM_PROMPT);
|
|
122
|
-
const parsed = parseAnalysisResponse(resp);
|
|
123
|
-
|
|
124
|
-
return parsed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Step 2: Generate updated compiled truth based on analysis
|
|
129
|
-
*/
|
|
130
|
-
async function generateUpdatedTruth(
|
|
131
|
-
input: CompileInput,
|
|
132
|
-
analysis: FactAnalysis,
|
|
133
|
-
llm: ResolvedLLM,
|
|
134
|
-
): Promise<{ compiledTruth: string; changed: boolean; changeType: CompileResult["changeType"]; changeSummary: string }> {
|
|
135
|
-
// If no facts extracted, no change needed
|
|
136
|
-
if (analysis.facts.length === 0) {
|
|
137
|
-
return {
|
|
138
|
-
compiledTruth: input.currentTruth,
|
|
139
|
-
changed: false,
|
|
140
|
-
changeType: "none",
|
|
141
|
-
changeSummary: "No actionable facts extracted",
|
|
142
|
-
};
|
|
143
|
-
}
|
|
82
|
+
const aiClient = createAxAI(llm);
|
|
83
|
+
if (!aiClient) return fallbackAppend(input);
|
|
144
84
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
85
|
+
try {
|
|
86
|
+
// Step 1: Main compilation
|
|
87
|
+
const context = buildContext(input);
|
|
88
|
+
const result = await compileGen.forward(aiClient, {
|
|
89
|
+
currentTruth: input.currentTruth || "(empty)",
|
|
90
|
+
newInfo: input.newInfo,
|
|
91
|
+
infoSource: input.source,
|
|
92
|
+
infoDate: input.date,
|
|
93
|
+
context,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Parse the JSON result
|
|
97
|
+
const compiled = parseCompileResult(result.compilationResult);
|
|
98
|
+
if (!compiled) return fallbackAppend(input);
|
|
99
|
+
|
|
100
|
+
// Step 2: Extract timeline entries
|
|
101
|
+
const timelineEntries = await extractTimeline(input, aiClient);
|
|
149
102
|
|
|
150
|
-
// For new events/entities, append
|
|
151
|
-
if (analysis.infoType === "new_event" || analysis.infoType === "new_entity") {
|
|
152
103
|
return {
|
|
153
|
-
compiledTruth:
|
|
154
|
-
changed:
|
|
155
|
-
changeType:
|
|
156
|
-
changeSummary:
|
|
104
|
+
compiledTruth: compiled.compiledTruth,
|
|
105
|
+
changed: compiled.changeType !== "none",
|
|
106
|
+
changeType: compiled.changeType,
|
|
107
|
+
changeSummary: compiled.changeSummary,
|
|
108
|
+
timelineEntries,
|
|
109
|
+
confidence: compiled.confidence,
|
|
157
110
|
};
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
113
|
+
console.warn(`[ebrain] Ax compilation failed, falling back to append: ${msg}`);
|
|
114
|
+
return fallbackAppend(input);
|
|
158
115
|
}
|
|
159
|
-
|
|
160
|
-
// Default: append with source attribution
|
|
161
|
-
return {
|
|
162
|
-
compiledTruth: appendFact(input.currentTruth, input.newInfo, input.source),
|
|
163
|
-
changed: true,
|
|
164
|
-
changeType: "append",
|
|
165
|
-
changeSummary: "Appended new information with source attribution",
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Smart merge: LLM understands semantic updates and rewrites compiled truth
|
|
171
|
-
*/
|
|
172
|
-
async function smartMergeTruth(
|
|
173
|
-
input: CompileInput,
|
|
174
|
-
analysis: FactAnalysis,
|
|
175
|
-
llm: ResolvedLLM,
|
|
176
|
-
): Promise<{ compiledTruth: string; changed: boolean; changeType: CompileResult["changeType"]; changeSummary: string }> {
|
|
177
|
-
const prompt = buildMergePrompt(input, analysis);
|
|
178
|
-
const resp = await callLLM(llm, prompt, 4096, COMPILER_SYSTEM_PROMPT);
|
|
179
|
-
const result = parseMergeResponse(resp);
|
|
180
|
-
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Step 3: Extract timeline entries from new information
|
|
186
|
-
*/
|
|
187
|
-
async function extractTimelineFromInfo(
|
|
188
|
-
input: CompileInput,
|
|
189
|
-
analysis: FactAnalysis,
|
|
190
|
-
llm: ResolvedLLM,
|
|
191
|
-
): Promise<TimelineEntry[]> {
|
|
192
|
-
// Only extract timeline for significant events
|
|
193
|
-
if (analysis.infoType === "status_update" || analysis.infoType === "new_event") {
|
|
194
|
-
const prompt = buildTimelinePrompt(input, analysis);
|
|
195
|
-
const resp = await callLLM(llm, prompt, 1024, COMPILER_SYSTEM_PROMPT);
|
|
196
|
-
return parseTimelineResponse(resp, input.pageContext?.slug ?? "");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return [];
|
|
200
116
|
}
|
|
201
117
|
|
|
202
118
|
// ---------------------------------------------------------------------------
|
|
203
|
-
//
|
|
119
|
+
// Helpers
|
|
204
120
|
// ---------------------------------------------------------------------------
|
|
205
121
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Current Compiled Truth:
|
|
212
|
-
${input.currentTruth || "(empty)"}
|
|
213
|
-
|
|
214
|
-
Recent Timeline (for temporal context):
|
|
215
|
-
${input.timeline.slice(0, 10).map(t => `- ${t.date} | ${t.source}: ${t.summary}`).join("\n") || "(no timeline)"}
|
|
216
|
-
|
|
217
|
-
## New Information
|
|
218
|
-
Source: ${input.source}
|
|
219
|
-
Date: ${input.date}
|
|
220
|
-
Content: ${input.newInfo}
|
|
221
|
-
|
|
222
|
-
## Task
|
|
223
|
-
Classify this information and extract key facts. Output ONLY JSON.
|
|
224
|
-
|
|
225
|
-
Schema:
|
|
226
|
-
{
|
|
227
|
-
"facts": [
|
|
228
|
-
{
|
|
229
|
-
"category": "funding_stage|valuation|ceo|employee_count|product_status|partnership|...",
|
|
230
|
-
"oldValue": "previous value if this updates something (null if new)",
|
|
231
|
-
"newValue": "the new value",
|
|
232
|
-
"action": "replace|add",
|
|
233
|
-
"sourceSentence": "exact sentence from new info",
|
|
234
|
-
"confidence": 0.0-1.0
|
|
235
|
-
}
|
|
236
|
-
],
|
|
237
|
-
"infoType": "status_update|new_event|correction|confirmation|new_entity",
|
|
238
|
-
"entities": ["list of entities mentioned"],
|
|
239
|
-
"temporalContext": "when this happened or is valid for"
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
Rules:
|
|
243
|
-
1. "status_update" = information that changes/updates existing state (e.g., funding stage change)
|
|
244
|
-
2. "new_event" = discrete event that happened (e.g., product launch)
|
|
245
|
-
3. "correction" = explicitly correcting previous information
|
|
246
|
-
4. "confirmation" = confirming existing information without change
|
|
247
|
-
5. "new_entity" = introducing new entity/aspect not previously tracked
|
|
248
|
-
6. Extract ALL actionable facts, not just the most prominent one
|
|
249
|
-
7. Use high confidence (0.8+) for clear, explicit statements; lower for ambiguous ones
|
|
250
|
-
|
|
251
|
-
/no_think`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function buildMergePrompt(input: CompileInput, analysis: FactAnalysis): string {
|
|
255
|
-
const factSummaries = analysis.facts.map(f =>
|
|
256
|
-
`- ${f.category}: ${f.oldValue ? `"${f.oldValue}" → "${f.newValue}"` : `"${f.newValue}"`} (${f.action}, confidence: ${f.confidence})`
|
|
257
|
-
).join("\n");
|
|
258
|
-
|
|
259
|
-
return `Rewrite the compiled truth to incorporate the analyzed changes.
|
|
260
|
-
|
|
261
|
-
## Current Compiled Truth
|
|
262
|
-
${input.currentTruth || "(empty)"}
|
|
263
|
-
|
|
264
|
-
## Changes to Apply
|
|
265
|
-
${factSummaries}
|
|
266
|
-
|
|
267
|
-
## Source Attribution
|
|
268
|
-
Source: ${input.source}
|
|
269
|
-
Date: ${input.date}
|
|
270
|
-
|
|
271
|
-
## Change Type
|
|
272
|
-
${analysis.infoType}
|
|
273
|
-
|
|
274
|
-
## Task
|
|
275
|
-
Rewrite the compiled truth. Output ONLY JSON with this schema:
|
|
276
|
-
{
|
|
277
|
-
"compiledTruth": "the full rewritten compiled truth content (markdown format)",
|
|
278
|
-
"changed": true|false,
|
|
279
|
-
"changeType": "append|update|replace|conflict|none",
|
|
280
|
-
"changeSummary": "human-readable summary of what changed"
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
Rules:
|
|
284
|
-
1. For "replace" actions: remove the old value, add the new value
|
|
285
|
-
2. For "add" actions: append the new fact in appropriate section
|
|
286
|
-
3. Preserve the overall structure and style of existing content
|
|
287
|
-
4. Add source attribution: append " (Source: ${input.source}, ${input.date})" to updated facts
|
|
288
|
-
5. If structure doesn't exist, create appropriate sections (## Status, ## Facts, etc.)
|
|
289
|
-
6. "update" = modified existing content; "replace" = replaced entire section; "conflict" = contradictory info (keep both with notes)
|
|
290
|
-
7. Do NOT remove historical context - keep timeline references
|
|
291
|
-
8. Format as clean markdown
|
|
292
|
-
|
|
293
|
-
Example output for funding stage update:
|
|
294
|
-
{
|
|
295
|
-
"compiledTruth": "## Status\n\n- **Funding Stage**: Series A (Source: meeting_notes, 2024-05-20)\n- **Valuation**: ~$50M (estimated)\n\n## History\n\n- Previously: Seed stage (until 2024-05-20)\n\n## Facts\n\n- ...",
|
|
296
|
-
"changed": true,
|
|
297
|
-
"changeType": "update",
|
|
298
|
-
"changeSummary": "Updated funding stage from Seed to Series A"
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/no_think`;
|
|
122
|
+
interface ParsedCompileResult {
|
|
123
|
+
changeType: CompileResult["changeType"];
|
|
124
|
+
compiledTruth: string;
|
|
125
|
+
changeSummary: string;
|
|
126
|
+
confidence: number;
|
|
302
127
|
}
|
|
303
128
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
## Analysis
|
|
313
|
-
Type: ${analysis.infoType}
|
|
314
|
-
Key Facts: ${analysis.facts.map(f => f.newValue).join(", ")}
|
|
315
|
-
|
|
316
|
-
## Task
|
|
317
|
-
Create timeline entries. Output ONLY JSON array:
|
|
318
|
-
[
|
|
319
|
-
{
|
|
320
|
-
"date": "YYYY-MM-DD",
|
|
321
|
-
"source": "${input.source}",
|
|
322
|
-
"summary": "one-line summary (max 80 chars)",
|
|
323
|
-
"detail": "optional additional detail (markdown)"
|
|
129
|
+
function parseCompileResult(raw: unknown): ParsedCompileResult | null {
|
|
130
|
+
let obj: Record<string, unknown>;
|
|
131
|
+
if (typeof raw === "string") {
|
|
132
|
+
try { obj = JSON.parse(raw); } catch { return null; }
|
|
133
|
+
} else if (typeof raw === "object" && raw !== null) {
|
|
134
|
+
obj = raw as Record<string, unknown>;
|
|
135
|
+
} else {
|
|
136
|
+
return null;
|
|
324
137
|
}
|
|
325
|
-
]
|
|
326
|
-
|
|
327
|
-
Rules:
|
|
328
|
-
1. Use the provided date, or extract exact date from content if mentioned
|
|
329
|
-
2. Summary should be concise and factual
|
|
330
|
-
3. Only create entries for significant events worth tracking
|
|
331
|
-
4. Max 2 entries per input
|
|
332
|
-
5. Empty array if nothing significant
|
|
333
138
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// ---------------------------------------------------------------------------
|
|
338
|
-
// LLM Call
|
|
339
|
-
// ---------------------------------------------------------------------------
|
|
139
|
+
const changeType = String(obj.changeType ?? "none");
|
|
140
|
+
const validTypes = ["append", "update", "replace", "none", "conflict"];
|
|
141
|
+
const normalizedType = validTypes.includes(changeType) ? changeType as CompileResult["changeType"] : "append";
|
|
340
142
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
|
-
// Response Parsing
|
|
346
|
-
// ---------------------------------------------------------------------------
|
|
347
|
-
|
|
348
|
-
function parseAnalysisResponse(resp: string): FactAnalysis {
|
|
349
|
-
const match = resp.match(/\{[\s\S]*\}/);
|
|
350
|
-
if (!match) {
|
|
351
|
-
return { facts: [], infoType: "new_entity", entities: [], temporalContext: "" };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
// Use jsonrepair to fix common LLM JSON issues
|
|
356
|
-
const repaired = jsonrepair(match[0]);
|
|
357
|
-
const parsed = JSON.parse(repaired) as Record<string, unknown>;
|
|
358
|
-
|
|
359
|
-
const facts: ExtractedFact[] = [];
|
|
360
|
-
const rawFacts = parsed.facts as unknown[] ?? [];
|
|
361
|
-
for (const f of rawFacts) {
|
|
362
|
-
if (typeof f !== "object" || f === null) continue;
|
|
363
|
-
const fact = f as Record<string, unknown>;
|
|
364
|
-
facts.push({
|
|
365
|
-
category: String(fact.category ?? "other"),
|
|
366
|
-
oldValue: fact.oldValue ? String(fact.oldValue) : undefined,
|
|
367
|
-
newValue: String(fact.newValue ?? ""),
|
|
368
|
-
action: fact.action === "replace" ? "replace" : "add",
|
|
369
|
-
sourceSentence: String(fact.sourceSentence ?? ""),
|
|
370
|
-
confidence: typeof fact.confidence === "number" ? fact.confidence : 0.8,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
143
|
+
const compiledTruth = String(obj.compiledTruth ?? "");
|
|
144
|
+
if (!compiledTruth) return null;
|
|
373
145
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
} catch {
|
|
381
|
-
return { facts: [], infoType: "new_entity", entities: [], temporalContext: "" };
|
|
382
|
-
}
|
|
146
|
+
return {
|
|
147
|
+
changeType: normalizedType,
|
|
148
|
+
compiledTruth,
|
|
149
|
+
changeSummary: String(obj.changeSummary ?? ""),
|
|
150
|
+
confidence: typeof obj.confidence === "number" ? obj.confidence : 0.8,
|
|
151
|
+
};
|
|
383
152
|
}
|
|
384
153
|
|
|
385
|
-
function
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
compiledTruth: "",
|
|
390
|
-
changed: false,
|
|
391
|
-
changeType: "none",
|
|
392
|
-
changeSummary: "Failed to parse LLM response",
|
|
393
|
-
};
|
|
154
|
+
function buildContext(input: CompileInput): string {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
if (input.pageContext) {
|
|
157
|
+
parts.push(`Page: ${input.pageContext.title} (${input.pageContext.type})`);
|
|
394
158
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const parsed = JSON.parse(repaired) as Record<string, unknown>;
|
|
400
|
-
return {
|
|
401
|
-
compiledTruth: String(parsed.compiledTruth ?? ""),
|
|
402
|
-
changed: Boolean(parsed.changed),
|
|
403
|
-
changeType: normalizeChangeType(String(parsed.changeType ?? "none")),
|
|
404
|
-
changeSummary: String(parsed.changeSummary ?? ""),
|
|
405
|
-
};
|
|
406
|
-
} catch {
|
|
407
|
-
return {
|
|
408
|
-
compiledTruth: "",
|
|
409
|
-
changed: false,
|
|
410
|
-
changeType: "none",
|
|
411
|
-
changeSummary: "Failed to parse LLM response",
|
|
412
|
-
};
|
|
159
|
+
if (input.timeline.length > 0) {
|
|
160
|
+
const recent = input.timeline.slice(0, 10);
|
|
161
|
+
parts.push("Recent Timeline:");
|
|
162
|
+
parts.push(recent.map(t => ` - ${t.date} | ${t.source}: ${t.summary}`).join("\n"));
|
|
413
163
|
}
|
|
164
|
+
return parts.join("\n\n") || "(no additional context)";
|
|
414
165
|
}
|
|
415
166
|
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
167
|
+
async function extractTimeline(
|
|
168
|
+
input: CompileInput,
|
|
169
|
+
aiClient: ReturnType<typeof createAxAI>,
|
|
170
|
+
): Promise<TimelineEntry[]> {
|
|
171
|
+
if (!aiClient) return [];
|
|
420
172
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return entries;
|
|
173
|
+
const result = await timelineGen.forward(aiClient, {
|
|
174
|
+
newInfo: input.newInfo,
|
|
175
|
+
infoSource: input.source,
|
|
176
|
+
infoDate: input.date,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const rawEvents = parseEvents(result.events);
|
|
180
|
+
const pageSlug = input.pageContext?.slug ?? "";
|
|
181
|
+
return rawEvents.map(e => ({
|
|
182
|
+
pageSlug,
|
|
183
|
+
date: String(e.date ?? input.date),
|
|
184
|
+
source: input.source,
|
|
185
|
+
summary: String(e.summary ?? "").slice(0, 120),
|
|
186
|
+
detail: String(e.detail ?? ""),
|
|
187
|
+
}));
|
|
439
188
|
} catch {
|
|
440
189
|
return [];
|
|
441
190
|
}
|
|
442
191
|
}
|
|
443
192
|
|
|
444
|
-
|
|
445
|
-
// Helpers
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
|
|
448
|
-
function normalizeInfoType(raw: string): FactAnalysis["infoType"] {
|
|
449
|
-
const valid = ["status_update", "new_event", "correction", "confirmation", "new_entity"] as const;
|
|
450
|
-
const lower = raw.toLowerCase().trim();
|
|
451
|
-
if (valid.includes(lower as typeof valid[number])) return lower as typeof valid[number];
|
|
452
|
-
return "new_entity";
|
|
453
|
-
}
|
|
193
|
+
interface RawEvent { date?: string; summary?: string; detail?: string; }
|
|
454
194
|
|
|
455
|
-
function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return "none";
|
|
195
|
+
function parseEvents(raw: unknown): RawEvent[] {
|
|
196
|
+
if (Array.isArray(raw)) return raw as RawEvent[];
|
|
197
|
+
if (typeof raw === "string") { try { return JSON.parse(raw) as RawEvent[]; } catch { return []; } }
|
|
198
|
+
return [];
|
|
460
199
|
}
|
|
461
200
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
201
|
+
function fallbackAppend(input: CompileInput): CompileResult {
|
|
202
|
+
const timestamp = input.date || new Date().toISOString().slice(0, 10);
|
|
203
|
+
const newLine = `- ${input.newInfo.trim()} (Source: ${input.source}, ${timestamp})`;
|
|
204
|
+
const current = input.currentTruth || "";
|
|
205
|
+
let compiledTruth: string;
|
|
468
206
|
if (!current.trim()) {
|
|
469
|
-
|
|
207
|
+
compiledTruth = `## Facts\n\n${newLine}`;
|
|
208
|
+
} else if (!current.includes("## Facts")) {
|
|
209
|
+
compiledTruth = `${current}\n\n## Facts\n\n${newLine}`;
|
|
210
|
+
} else {
|
|
211
|
+
compiledTruth = `${current}\n${newLine}`;
|
|
470
212
|
}
|
|
471
|
-
|
|
472
|
-
if (!current.includes("## Facts")) {
|
|
473
|
-
return `${current}\n\n## Facts\n\n${newLine}`;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return `${current}\n${newLine}`;
|
|
213
|
+
return { compiledTruth, changed: true, changeType: "append", changeSummary: "LLM unavailable, appended as simple fact", timelineEntries: [], confidence: 0.5 };
|
|
477
214
|
}
|
|
478
|
-
|
|
479
|
-
function appendStructuredFacts(current: string, facts: ExtractedFact[], source: string): string {
|
|
480
|
-
const timestamp = new Date().toISOString().slice(0, 10);
|
|
481
|
-
const newLines = facts.map(f =>
|
|
482
|
-
`- **${f.category}**: ${f.newValue} (Source: ${source}, ${timestamp})`
|
|
483
|
-
).join("\n");
|
|
484
|
-
|
|
485
|
-
if (!current.trim()) {
|
|
486
|
-
return `## Facts\n\n${newLines}`;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (!current.includes("## Facts")) {
|
|
490
|
-
return `${current}\n\n## Facts\n\n${newLines}`;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return `${current}\n${newLines}`;
|
|
494
|
-
}
|