ei-tui 0.1.25 → 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 +42 -0
- package/package.json +1 -1
- package/src/README.md +4 -11
- package/src/cli/README.md +4 -5
- package/src/cli/retrieval.ts +3 -25
- package/src/cli.ts +3 -7
- package/src/core/AGENTS.md +1 -1
- package/src/core/constants/built-in-facts.ts +49 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/context-utils.ts +0 -1
- package/src/core/embedding-service.ts +8 -0
- package/src/core/handlers/dedup.ts +10 -16
- package/src/core/handlers/heartbeat.ts +2 -3
- package/src/core/handlers/human-extraction.ts +95 -30
- package/src/core/handlers/human-matching.ts +326 -248
- package/src/core/handlers/index.ts +8 -6
- package/src/core/handlers/persona-generation.ts +8 -8
- package/src/core/handlers/rewrite.ts +4 -29
- package/src/core/handlers/utils.ts +23 -1
- package/src/core/heartbeat-manager.ts +2 -4
- package/src/core/human-data-manager.ts +5 -27
- package/src/core/message-manager.ts +10 -10
- package/src/core/orchestrators/ceremony.ts +50 -39
- package/src/core/orchestrators/dedup-phase.ts +0 -1
- package/src/core/orchestrators/human-extraction.ts +351 -207
- package/src/core/orchestrators/index.ts +6 -4
- package/src/core/orchestrators/persona-generation.ts +3 -3
- package/src/core/processor.ts +99 -17
- package/src/core/prompt-context-builder.ts +4 -6
- package/src/core/state/human.ts +1 -26
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +107 -14
- package/src/core/tools/builtin/read-memory.ts +7 -8
- package/src/core/types/data-items.ts +2 -4
- package/src/core/types/entities.ts +6 -4
- package/src/core/types/enums.ts +6 -9
- package/src/core/types/llm.ts +2 -2
- package/src/core/utils/crossFind.ts +2 -5
- package/src/core/utils/event-windows.ts +31 -0
- package/src/integrations/claude-code/importer.ts +8 -4
- package/src/integrations/claude-code/types.ts +2 -0
- package/src/integrations/opencode/importer.ts +7 -3
- package/src/prompts/AGENTS.md +73 -1
- package/src/prompts/ceremony/rewrite.ts +3 -22
- package/src/prompts/ceremony/types.ts +3 -3
- package/src/prompts/generation/descriptions.ts +2 -2
- package/src/prompts/generation/types.ts +2 -2
- package/src/prompts/heartbeat/types.ts +2 -2
- package/src/prompts/human/event-scan.ts +122 -0
- package/src/prompts/human/fact-find.ts +106 -0
- package/src/prompts/human/fact-scan.ts +0 -2
- package/src/prompts/human/index.ts +17 -10
- package/src/prompts/human/person-match.ts +65 -0
- package/src/prompts/human/person-scan.ts +52 -59
- package/src/prompts/human/person-update.ts +241 -0
- package/src/prompts/human/topic-match.ts +65 -0
- package/src/prompts/human/topic-scan.ts +51 -71
- package/src/prompts/human/topic-update.ts +295 -0
- package/src/prompts/human/types.ts +63 -40
- package/src/prompts/index.ts +4 -8
- package/src/prompts/persona/topics-update.ts +2 -2
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/persona/types.ts +3 -3
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +9 -12
- package/src/prompts/response/types.ts +2 -3
- package/src/storage/embeddings.ts +1 -1
- package/src/storage/index.ts +1 -0
- package/src/storage/indexed.ts +174 -0
- package/src/storage/merge.ts +67 -2
- package/tui/src/commands/me.tsx +5 -14
- package/tui/src/commands/settings.tsx +15 -0
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/util/yaml-serializers.ts +48 -33
- package/src/cli/commands/traits.ts +0 -25
- package/src/prompts/human/item-match.ts +0 -74
- package/src/prompts/human/item-update.ts +0 -364
- package/src/prompts/human/trait-scan.ts +0 -115
|
@@ -1,35 +1,72 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, type Message, type
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, type Message, type Topic, type Person } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
buildHumanTraitScanPrompt,
|
|
4
|
+
buildFactFindPrompt,
|
|
6
5
|
buildHumanTopicScanPrompt,
|
|
7
6
|
buildHumanPersonScanPrompt,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
buildTopicMatchPrompt,
|
|
8
|
+
buildTopicUpdatePrompt,
|
|
9
|
+
buildPersonMatchPrompt,
|
|
10
|
+
buildPersonUpdatePrompt,
|
|
11
|
+
buildEventScanPrompt,
|
|
12
12
|
type TopicScanCandidate,
|
|
13
13
|
type PersonScanCandidate,
|
|
14
14
|
type ItemMatchResult,
|
|
15
|
+
type ParticipantContext,
|
|
15
16
|
} from "../../prompts/human/index.js";
|
|
16
17
|
import { chunkExtractionContext } from "./extraction-chunker.js";
|
|
17
|
-
import { getEmbeddingService, findTopK } from "../embedding-service.js";
|
|
18
|
+
import { getEmbeddingService, findTopK, getTopicEmbeddingText, getPersonEmbeddingText } from "../embedding-service.js";
|
|
18
19
|
import { resolveTokenLimit } from "../llm-client.js";
|
|
20
|
+
import { BUILT_IN_FACT_NAMES } from "../constants/built-in-facts.js";
|
|
21
|
+
import { buildEventWindows } from "../utils/event-windows.js";
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
function buildParticipantContext(personaId: string, state: StateManager): ParticipantContext {
|
|
24
|
+
const persona = state.persona_getById(personaId);
|
|
25
|
+
const human = state.getHuman();
|
|
26
|
+
|
|
27
|
+
const persona_description = persona?.long_description || undefined;
|
|
28
|
+
|
|
29
|
+
const fullNameFact = human.facts.find(f => f.name === "Full Name");
|
|
30
|
+
const nicknameFact = human.facts.find(f => f.name === "Nickname/Preferred Name");
|
|
31
|
+
const fullName = fullNameFact?.description || "";
|
|
32
|
+
const nickname = nicknameFact?.description || "";
|
|
33
|
+
let human_name: string | undefined;
|
|
34
|
+
if (fullName && nickname) human_name = `${fullName} (${nickname})`;
|
|
35
|
+
else if (fullName) human_name = fullName;
|
|
36
|
+
else if (nickname) human_name = nickname;
|
|
37
|
+
|
|
38
|
+
let human_age: number | undefined;
|
|
39
|
+
const birthdayFact = human.facts.find(f => f.name === "Birthday");
|
|
40
|
+
if (birthdayFact?.description) {
|
|
41
|
+
const birth = new Date(birthdayFact.description);
|
|
42
|
+
if (!isNaN(birth.getTime())) {
|
|
43
|
+
human_age = Math.floor((Date.now() - birth.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
persona_name: persona?.display_name ?? personaId,
|
|
49
|
+
persona_description,
|
|
50
|
+
human_name,
|
|
51
|
+
human_age,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
21
54
|
|
|
22
55
|
export interface ExtractionContext {
|
|
23
56
|
personaId: string;
|
|
24
57
|
personaDisplayName: string;
|
|
25
58
|
messages_context: Message[];
|
|
26
59
|
messages_analyze: Message[];
|
|
27
|
-
extraction_flag?: "f" | "
|
|
60
|
+
extraction_flag?: "f" | "t" | "p" | "e";
|
|
28
61
|
}
|
|
29
62
|
|
|
30
63
|
export interface ExtractionOptions {
|
|
31
64
|
/** Ceremony phase number (1=Dedup, 2=Expose) */
|
|
32
65
|
ceremony_progress?: number;
|
|
66
|
+
/** Override model for extraction LLM calls */
|
|
67
|
+
extraction_model?: string;
|
|
68
|
+
/** Override token budget for chunking */
|
|
69
|
+
extraction_token_limit?: number;
|
|
33
70
|
}
|
|
34
71
|
|
|
35
72
|
function getAnalyzeFromTimestamp(context: ExtractionContext): string | null {
|
|
@@ -40,16 +77,27 @@ function getAnalyzeFromTimestamp(context: ExtractionContext): string | null {
|
|
|
40
77
|
const EXTRACTION_BUDGET_RATIO = 0.75;
|
|
41
78
|
const MIN_EXTRACTION_TOKENS = 10000;
|
|
42
79
|
|
|
43
|
-
function getExtractionMaxTokens(state: StateManager): number {
|
|
80
|
+
function getExtractionMaxTokens(state: StateManager, options?: ExtractionOptions): number {
|
|
81
|
+
if (options?.extraction_token_limit) {
|
|
82
|
+
return Math.max(MIN_EXTRACTION_TOKENS, Math.floor(options.extraction_token_limit * EXTRACTION_BUDGET_RATIO));
|
|
83
|
+
}
|
|
44
84
|
const human = state.getHuman();
|
|
45
|
-
const
|
|
85
|
+
const modelForTokenLimit = options?.extraction_model ?? human.settings?.default_model;
|
|
86
|
+
const tokenLimit = resolveTokenLimit(modelForTokenLimit, human.settings?.accounts);
|
|
46
87
|
return Math.max(MIN_EXTRACTION_TOKENS, Math.floor(tokenLimit * EXTRACTION_BUDGET_RATIO));
|
|
47
88
|
}
|
|
48
89
|
|
|
49
|
-
export function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
90
|
+
export function queueFactFind(context: ExtractionContext, state: StateManager, options?: ExtractionOptions): number {
|
|
91
|
+
const human = state.getHuman();
|
|
92
|
+
const extractionModel = options?.extraction_model;
|
|
93
|
+
const missing_fact_names = human.facts
|
|
94
|
+
.filter(f => !f.description || f.description === "")
|
|
95
|
+
.map(f => f.name)
|
|
96
|
+
.filter(name => BUILT_IN_FACT_NAMES.has(name));
|
|
97
|
+
|
|
98
|
+
if (missing_fact_names.length === 0) return 0;
|
|
99
|
+
|
|
100
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, options));
|
|
53
101
|
|
|
54
102
|
// Pre-mark messages before enqueuing — prevents duplicate scans if the
|
|
55
103
|
// queue check fires again during LLM latency (100ms loop × 5s call = 50 dupes)
|
|
@@ -58,8 +106,9 @@ export function queueFactScan(context: ExtractionContext, state: StateManager, o
|
|
|
58
106
|
}
|
|
59
107
|
|
|
60
108
|
for (const chunk of chunks) {
|
|
61
|
-
const prompt =
|
|
109
|
+
const prompt = buildFactFindPrompt({
|
|
62
110
|
persona_name: chunk.personaDisplayName,
|
|
111
|
+
missing_fact_names,
|
|
63
112
|
messages_context: chunk.messages_context,
|
|
64
113
|
messages_analyze: chunk.messages_analyze,
|
|
65
114
|
});
|
|
@@ -67,9 +116,10 @@ export function queueFactScan(context: ExtractionContext, state: StateManager, o
|
|
|
67
116
|
state.queue_enqueue({
|
|
68
117
|
type: LLMRequestType.JSON,
|
|
69
118
|
priority: LLMPriority.Low,
|
|
119
|
+
model: extractionModel,
|
|
70
120
|
system: prompt.system,
|
|
71
121
|
user: prompt.user,
|
|
72
|
-
next_step: LLMNextStep.
|
|
122
|
+
next_step: LLMNextStep.HandleFactFind,
|
|
73
123
|
data: {
|
|
74
124
|
...options,
|
|
75
125
|
personaId: chunk.personaId,
|
|
@@ -84,47 +134,16 @@ export function queueFactScan(context: ExtractionContext, state: StateManager, o
|
|
|
84
134
|
return chunks.length;
|
|
85
135
|
}
|
|
86
136
|
|
|
87
|
-
export function queueTraitScan(context: ExtractionContext, state: StateManager, options?: ExtractionOptions): number {
|
|
88
|
-
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state));
|
|
89
|
-
|
|
90
|
-
if (chunks.length === 0) return 0;
|
|
91
|
-
|
|
92
|
-
for (const chunk of chunks) {
|
|
93
|
-
const prompt = buildHumanTraitScanPrompt({
|
|
94
|
-
persona_name: chunk.personaDisplayName,
|
|
95
|
-
messages_context: chunk.messages_context,
|
|
96
|
-
messages_analyze: chunk.messages_analyze,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
state.queue_enqueue({
|
|
100
|
-
type: LLMRequestType.JSON,
|
|
101
|
-
priority: LLMPriority.Low,
|
|
102
|
-
system: prompt.system,
|
|
103
|
-
user: prompt.user,
|
|
104
|
-
next_step: LLMNextStep.HandleHumanTraitScan,
|
|
105
|
-
data: {
|
|
106
|
-
...options,
|
|
107
|
-
personaId: chunk.personaId,
|
|
108
|
-
personaDisplayName: chunk.personaDisplayName,
|
|
109
|
-
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
110
|
-
extraction_flag: context.extraction_flag,
|
|
111
|
-
message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return chunks.length;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
137
|
export function queueTopicScan(context: ExtractionContext, state: StateManager, options?: ExtractionOptions): number {
|
|
120
|
-
const
|
|
138
|
+
const extractionModel = options?.extraction_model;
|
|
139
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, options));
|
|
121
140
|
|
|
122
141
|
if (chunks.length === 0) return 0;
|
|
123
142
|
|
|
124
143
|
// Pre-mark messages before enqueuing — prevents duplicate scans if the
|
|
125
144
|
// queue check fires again during LLM latency (100ms loop × 5s call = 50 dupes)
|
|
126
145
|
for (const chunk of chunks) {
|
|
127
|
-
state.messages_markExtracted(chunk.personaId, chunk.messages_analyze.map(m => m.id), "
|
|
146
|
+
state.messages_markExtracted(chunk.personaId, chunk.messages_analyze.map(m => m.id), "t");
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
for (const chunk of chunks) {
|
|
@@ -132,11 +151,13 @@ export function queueTopicScan(context: ExtractionContext, state: StateManager,
|
|
|
132
151
|
persona_name: chunk.personaDisplayName,
|
|
133
152
|
messages_context: chunk.messages_context,
|
|
134
153
|
messages_analyze: chunk.messages_analyze,
|
|
154
|
+
participant_context: buildParticipantContext(context.personaId, state),
|
|
135
155
|
});
|
|
136
156
|
|
|
137
157
|
state.queue_enqueue({
|
|
138
158
|
type: LLMRequestType.JSON,
|
|
139
159
|
priority: LLMPriority.Low,
|
|
160
|
+
model: extractionModel,
|
|
140
161
|
system: prompt.system,
|
|
141
162
|
user: prompt.user,
|
|
142
163
|
next_step: LLMNextStep.HandleHumanTopicScan,
|
|
@@ -155,14 +176,15 @@ export function queueTopicScan(context: ExtractionContext, state: StateManager,
|
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
export function queuePersonScan(context: ExtractionContext, state: StateManager, options?: ExtractionOptions): number {
|
|
158
|
-
const
|
|
179
|
+
const extractionModel = options?.extraction_model;
|
|
180
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, options));
|
|
159
181
|
|
|
160
182
|
if (chunks.length === 0) return 0;
|
|
161
183
|
|
|
162
184
|
// Pre-mark messages before enqueuing — prevents duplicate scans if the
|
|
163
185
|
// queue check fires again during LLM latency (100ms loop × 5s call = 50 dupes)
|
|
164
186
|
for (const chunk of chunks) {
|
|
165
|
-
state.messages_markExtracted(chunk.personaId, chunk.messages_analyze.map(m => m.id), "
|
|
187
|
+
state.messages_markExtracted(chunk.personaId, chunk.messages_analyze.map(m => m.id), "p");
|
|
166
188
|
}
|
|
167
189
|
|
|
168
190
|
for (const chunk of chunks) {
|
|
@@ -175,6 +197,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
|
|
|
175
197
|
state.queue_enqueue({
|
|
176
198
|
type: LLMRequestType.JSON,
|
|
177
199
|
priority: LLMPriority.Low,
|
|
200
|
+
model: extractionModel,
|
|
178
201
|
system: prompt.system,
|
|
179
202
|
user: prompt.user,
|
|
180
203
|
next_step: LLMNextStep.HandleHumanPersonScan,
|
|
@@ -193,10 +216,10 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
|
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
export function queueAllScans(context: ExtractionContext, state: StateManager, options?: ExtractionOptions): void {
|
|
196
|
-
|
|
197
|
-
queueTraitScan(context, state, options);
|
|
219
|
+
queueFactFind(context, state, options);
|
|
198
220
|
queuePersonScan(context, state, options);
|
|
199
221
|
queueTopicScan(context, state, options);
|
|
222
|
+
queueEventSummary(context.personaId, state, options);
|
|
200
223
|
}
|
|
201
224
|
|
|
202
225
|
/**
|
|
@@ -214,32 +237,33 @@ export function queueAllScans(context: ExtractionContext, state: StateManager, o
|
|
|
214
237
|
export function queueDirectTopicUpdate(
|
|
215
238
|
topic: import("../types.js").Topic,
|
|
216
239
|
context: ExtractionContext,
|
|
217
|
-
state: StateManager
|
|
240
|
+
state: StateManager,
|
|
241
|
+
options?: ExtractionOptions
|
|
218
242
|
): number {
|
|
219
|
-
const
|
|
243
|
+
const extractionModel = options?.extraction_model;
|
|
244
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, options));
|
|
220
245
|
|
|
221
246
|
if (chunks.length === 0) return 0;
|
|
222
247
|
|
|
223
248
|
for (const chunk of chunks) {
|
|
224
|
-
const prompt =
|
|
225
|
-
data_type: "topic",
|
|
249
|
+
const prompt = buildTopicUpdatePrompt({
|
|
226
250
|
existing_item: topic,
|
|
227
251
|
messages_context: chunk.messages_context,
|
|
228
252
|
messages_analyze: chunk.messages_analyze,
|
|
229
253
|
persona_name: chunk.personaDisplayName,
|
|
254
|
+
participant_context: buildParticipantContext(context.personaId, state),
|
|
230
255
|
});
|
|
231
256
|
|
|
232
257
|
state.queue_enqueue({
|
|
233
258
|
type: LLMRequestType.JSON,
|
|
234
259
|
priority: LLMPriority.Normal,
|
|
260
|
+
model: extractionModel,
|
|
235
261
|
system: prompt.system,
|
|
236
262
|
user: prompt.user,
|
|
237
|
-
next_step: LLMNextStep.
|
|
263
|
+
next_step: LLMNextStep.HandleTopicUpdate,
|
|
238
264
|
data: {
|
|
239
265
|
personaId: context.personaId,
|
|
240
266
|
personaDisplayName: context.personaDisplayName,
|
|
241
|
-
candidateType: "topic",
|
|
242
|
-
matchedType: "topic",
|
|
243
267
|
isNewItem: false,
|
|
244
268
|
existingItemId: topic.id,
|
|
245
269
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
@@ -250,227 +274,345 @@ export function queueDirectTopicUpdate(
|
|
|
250
274
|
return chunks.length;
|
|
251
275
|
}
|
|
252
276
|
|
|
253
|
-
function truncateDescription(description: string, maxLength: number = 255): string {
|
|
254
|
-
if (description.length <= maxLength) return description;
|
|
255
|
-
return description.slice(0, maxLength) + "...";
|
|
256
|
-
}
|
|
257
|
-
|
|
258
277
|
const EMBEDDING_TOP_K = 20;
|
|
259
278
|
const EMBEDDING_MIN_SIMILARITY = 0.3;
|
|
260
279
|
|
|
261
280
|
/**
|
|
262
|
-
* Queue
|
|
263
|
-
*
|
|
264
|
-
* Instead of sending ALL items to the LLM, we:
|
|
265
|
-
* 1. Compute embedding for the candidate (name + value)
|
|
266
|
-
* 2. Find top-K most similar existing items via cosine similarity
|
|
267
|
-
* 3. Send only those candidates to the LLM for final matching decision
|
|
268
|
-
*
|
|
269
|
-
* This reduces prompt size from O(all_items) to O(K) where K=20.
|
|
281
|
+
* Queue a topic match request using embedding-based similarity (topics only).
|
|
270
282
|
*/
|
|
271
|
-
export async function
|
|
272
|
-
|
|
273
|
-
candidate: ScanCandidate,
|
|
283
|
+
export async function queueTopicMatch(
|
|
284
|
+
candidate: TopicScanCandidate,
|
|
274
285
|
context: ExtractionContext,
|
|
275
|
-
state: StateManager
|
|
286
|
+
state: StateManager,
|
|
287
|
+
extractionModel?: string
|
|
276
288
|
): Promise<void> {
|
|
277
289
|
const human = state.getHuman();
|
|
278
|
-
|
|
279
|
-
let itemName: string;
|
|
280
|
-
let itemValue: string;
|
|
281
|
-
|
|
282
|
-
switch (dataType) {
|
|
283
|
-
case "fact":
|
|
284
|
-
itemName = (candidate as FactScanCandidate).type_of_fact;
|
|
285
|
-
itemValue = (candidate as FactScanCandidate).value_of_fact;
|
|
286
|
-
break;
|
|
287
|
-
case "trait":
|
|
288
|
-
itemName = (candidate as TraitScanCandidate).type_of_trait;
|
|
289
|
-
itemValue = (candidate as TraitScanCandidate).value_of_trait;
|
|
290
|
-
break;
|
|
291
|
-
case "topic":
|
|
292
|
-
itemName = (candidate as TopicScanCandidate).value_of_topic;
|
|
293
|
-
itemValue = (candidate as TopicScanCandidate).type_of_topic;
|
|
294
|
-
break;
|
|
295
|
-
case "person":
|
|
296
|
-
itemName = (candidate as PersonScanCandidate).name_of_person;
|
|
297
|
-
itemValue = (candidate as PersonScanCandidate).type_of_person;
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
290
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
...human.traits.map(t => ({ ...t, data_type: "trait" as DataItemType })),
|
|
307
|
-
...(dataType !== "trait" ? human.topics.map(t => ({ ...t, data_type: "topic" as DataItemType })) : []),
|
|
308
|
-
...(dataType !== "trait" ? human.people.map(p => ({ ...p, data_type: "person" as DataItemType })) : []),
|
|
309
|
-
].filter(item => item.embedding && item.embedding.length > 0);
|
|
310
|
-
|
|
311
|
-
let topKItems: Array<{
|
|
312
|
-
data_type: DataItemType;
|
|
313
|
-
data_id: string;
|
|
314
|
-
data_name: string;
|
|
315
|
-
data_description: string;
|
|
316
|
-
}> = [];
|
|
317
|
-
|
|
318
|
-
if (allItemsWithEmbeddings.length > 0) {
|
|
291
|
+
const topicsWithEmbeddings = human.topics.filter(t => t.embedding && t.embedding.length > 0);
|
|
292
|
+
|
|
293
|
+
let topKItems: Array<{ id: string; name: string; description: string; category?: string }> = [];
|
|
294
|
+
|
|
295
|
+
if (topicsWithEmbeddings.length > 0) {
|
|
319
296
|
try {
|
|
320
297
|
const embeddingService = getEmbeddingService();
|
|
321
|
-
const candidateText =
|
|
298
|
+
const candidateText = getTopicEmbeddingText({
|
|
299
|
+
name: candidate.name,
|
|
300
|
+
category: candidate.category,
|
|
301
|
+
description: candidate.description,
|
|
302
|
+
});
|
|
322
303
|
const candidateVector = await embeddingService.embed(candidateText);
|
|
323
304
|
|
|
324
|
-
const topK = findTopK(candidateVector,
|
|
325
|
-
|
|
305
|
+
const topK = findTopK(candidateVector, topicsWithEmbeddings, EMBEDDING_TOP_K);
|
|
326
306
|
topKItems = topK
|
|
327
307
|
.filter(({ similarity }) => similarity >= EMBEDDING_MIN_SIMILARITY)
|
|
328
308
|
.map(({ item }) => ({
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
? item.description
|
|
334
|
-
: truncateDescription(item.description),
|
|
309
|
+
id: item.id,
|
|
310
|
+
name: item.name,
|
|
311
|
+
description: item.description,
|
|
312
|
+
category: item.category,
|
|
335
313
|
}));
|
|
336
314
|
|
|
337
|
-
console.log(`[
|
|
315
|
+
console.log(`[queueTopicMatch] Embedding search: ${topicsWithEmbeddings.length} topics → ${topKItems.length} candidates`);
|
|
338
316
|
} catch (err) {
|
|
339
|
-
console.error(`[
|
|
317
|
+
console.error(`[queueTopicMatch] Embedding search failed, falling back to all topics:`, err);
|
|
340
318
|
}
|
|
341
319
|
}
|
|
342
320
|
|
|
343
321
|
if (topKItems.length === 0) {
|
|
322
|
+
console.log(`[queueTopicMatch] No embeddings available, using all topics`);
|
|
323
|
+
topKItems = human.topics.map(t => ({
|
|
324
|
+
id: t.id,
|
|
325
|
+
name: t.name,
|
|
326
|
+
description: t.description,
|
|
327
|
+
category: t.category,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
344
330
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
331
|
+
const prompt = buildTopicMatchPrompt({
|
|
332
|
+
candidate_name: candidate.name,
|
|
333
|
+
candidate_description: candidate.description,
|
|
334
|
+
candidate_category: candidate.category,
|
|
335
|
+
existing_topics: topKItems,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
state.queue_enqueue({
|
|
339
|
+
type: LLMRequestType.JSON,
|
|
340
|
+
priority: LLMPriority.Normal,
|
|
341
|
+
model: extractionModel,
|
|
342
|
+
system: prompt.system,
|
|
343
|
+
user: prompt.user,
|
|
344
|
+
next_step: LLMNextStep.HandleTopicMatch,
|
|
345
|
+
data: {
|
|
346
|
+
...context,
|
|
347
|
+
candidateName: candidate.name,
|
|
348
|
+
candidateDescription: candidate.description,
|
|
349
|
+
candidateCategory: candidate.category,
|
|
350
|
+
extraction_model: extractionModel,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Queue a person match request using embedding-based similarity (people only).
|
|
357
|
+
*/
|
|
358
|
+
export async function queuePersonMatch(
|
|
359
|
+
candidate: PersonScanCandidate,
|
|
360
|
+
context: ExtractionContext,
|
|
361
|
+
state: StateManager,
|
|
362
|
+
extractionModel?: string
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const human = state.getHuman();
|
|
365
|
+
|
|
366
|
+
const peopleWithEmbeddings = human.people.filter(p => p.embedding && p.embedding.length > 0);
|
|
367
|
+
|
|
368
|
+
let topKItems: Array<{ id: string; name: string; description: string; relationship?: string }> = [];
|
|
357
369
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
370
|
+
if (peopleWithEmbeddings.length > 0) {
|
|
371
|
+
try {
|
|
372
|
+
const embeddingService = getEmbeddingService();
|
|
373
|
+
const candidateText = getPersonEmbeddingText({
|
|
374
|
+
name: candidate.name,
|
|
375
|
+
relationship: candidate.relationship,
|
|
376
|
+
description: candidate.description,
|
|
364
377
|
});
|
|
365
|
-
|
|
378
|
+
const candidateVector = await embeddingService.embed(candidateText);
|
|
379
|
+
|
|
380
|
+
const topK = findTopK(candidateVector, peopleWithEmbeddings, EMBEDDING_TOP_K);
|
|
381
|
+
topKItems = topK
|
|
382
|
+
.filter(({ similarity }) => similarity >= EMBEDDING_MIN_SIMILARITY)
|
|
383
|
+
.map(({ item }) => ({
|
|
384
|
+
id: item.id,
|
|
385
|
+
name: item.name,
|
|
386
|
+
description: item.description,
|
|
387
|
+
relationship: item.relationship,
|
|
388
|
+
}));
|
|
366
389
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
data_type: "topic",
|
|
371
|
-
data_id: topic.id,
|
|
372
|
-
data_name: topic.name,
|
|
373
|
-
data_description: dataType === "topic" ? topic.description : truncateDescription(topic.description),
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
for (const person of human.people) {
|
|
378
|
-
topKItems.push({
|
|
379
|
-
data_type: "person",
|
|
380
|
-
data_id: person.id,
|
|
381
|
-
data_name: person.name,
|
|
382
|
-
data_description: dataType === "person" ? person.description : truncateDescription(person.description),
|
|
383
|
-
});
|
|
384
|
-
}
|
|
390
|
+
console.log(`[queuePersonMatch] Embedding search: ${peopleWithEmbeddings.length} people → ${topKItems.length} candidates`);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(`[queuePersonMatch] Embedding search failed, falling back to all people:`, err);
|
|
385
393
|
}
|
|
386
394
|
}
|
|
387
395
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
if (topKItems.length === 0) {
|
|
397
|
+
console.log(`[queuePersonMatch] No embeddings available, using all people`);
|
|
398
|
+
topKItems = human.people.map(p => ({
|
|
399
|
+
id: p.id,
|
|
400
|
+
name: p.name,
|
|
401
|
+
description: p.description,
|
|
402
|
+
relationship: p.relationship,
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
395
405
|
|
|
406
|
+
const prompt = buildPersonMatchPrompt({
|
|
407
|
+
candidate_name: candidate.name,
|
|
408
|
+
candidate_description: candidate.description,
|
|
409
|
+
candidate_relationship: candidate.relationship,
|
|
410
|
+
existing_people: topKItems,
|
|
411
|
+
});
|
|
396
412
|
|
|
397
413
|
state.queue_enqueue({
|
|
398
414
|
type: LLMRequestType.JSON,
|
|
399
415
|
priority: LLMPriority.Normal,
|
|
416
|
+
model: extractionModel,
|
|
400
417
|
system: prompt.system,
|
|
401
418
|
user: prompt.user,
|
|
402
|
-
next_step: LLMNextStep.
|
|
419
|
+
next_step: LLMNextStep.HandlePersonMatch,
|
|
403
420
|
data: {
|
|
404
421
|
...context,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
422
|
+
candidateName: candidate.name,
|
|
423
|
+
candidateDescription: candidate.description,
|
|
424
|
+
candidateRelationship: candidate.relationship,
|
|
425
|
+
extraction_model: extractionModel,
|
|
408
426
|
},
|
|
409
427
|
});
|
|
410
428
|
}
|
|
411
429
|
|
|
412
|
-
export function
|
|
413
|
-
candidateType: DataItemType,
|
|
430
|
+
export function queueTopicUpdate(
|
|
414
431
|
matchResult: ItemMatchResult,
|
|
415
|
-
context: ExtractionContext & {
|
|
432
|
+
context: ExtractionContext & {
|
|
433
|
+
candidateName: string;
|
|
434
|
+
candidateDescription: string;
|
|
435
|
+
candidateCategory: string;
|
|
436
|
+
extraction_model?: string;
|
|
437
|
+
},
|
|
416
438
|
state: StateManager
|
|
417
439
|
): number {
|
|
418
440
|
const human = state.getHuman();
|
|
419
441
|
const matchedGuid = matchResult.matched_guid;
|
|
420
442
|
const isNewItem = matchedGuid === null;
|
|
421
443
|
|
|
422
|
-
let existingItem:
|
|
423
|
-
|
|
444
|
+
let existingItem: Topic | null = null;
|
|
445
|
+
if (!isNewItem && matchedGuid) {
|
|
446
|
+
existingItem = human.topics.find(t => t.id === matchedGuid) ?? null;
|
|
447
|
+
}
|
|
424
448
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (existingItem) matchedType = "fact";
|
|
449
|
+
const extractionOptions: ExtractionOptions = { extraction_model: context.extraction_model };
|
|
450
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, extractionOptions));
|
|
428
451
|
|
|
429
|
-
|
|
430
|
-
existingItem = human.traits.find(t => t.id === matchedGuid) ?? null;
|
|
431
|
-
if (existingItem) matchedType = "trait";
|
|
432
|
-
}
|
|
452
|
+
if (chunks.length === 0) return 0;
|
|
433
453
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
454
|
+
for (const chunk of chunks) {
|
|
455
|
+
const prompt = buildTopicUpdatePrompt({
|
|
456
|
+
existing_item: existingItem,
|
|
457
|
+
new_topic_name: isNewItem ? context.candidateName : undefined,
|
|
458
|
+
new_topic_description: isNewItem ? context.candidateDescription : undefined,
|
|
459
|
+
new_topic_category: isNewItem ? context.candidateCategory : undefined,
|
|
460
|
+
messages_context: chunk.messages_context,
|
|
461
|
+
messages_analyze: chunk.messages_analyze,
|
|
462
|
+
persona_name: chunk.personaDisplayName,
|
|
463
|
+
participant_context: buildParticipantContext(context.personaId, state),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
state.queue_enqueue({
|
|
467
|
+
type: LLMRequestType.JSON,
|
|
468
|
+
priority: LLMPriority.Normal,
|
|
469
|
+
model: context.extraction_model,
|
|
470
|
+
system: prompt.system,
|
|
471
|
+
user: prompt.user,
|
|
472
|
+
next_step: LLMNextStep.HandleTopicUpdate,
|
|
473
|
+
data: {
|
|
474
|
+
personaId: context.personaId,
|
|
475
|
+
personaDisplayName: context.personaDisplayName,
|
|
476
|
+
isNewItem,
|
|
477
|
+
existingItemId: existingItem?.id,
|
|
478
|
+
candidateCategory: context.candidateCategory,
|
|
479
|
+
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return chunks.length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function queueEventSummary(
|
|
488
|
+
personaId: string,
|
|
489
|
+
state: StateManager,
|
|
490
|
+
options?: ExtractionOptions
|
|
491
|
+
): number {
|
|
492
|
+
const persona = state.persona_getById(personaId);
|
|
493
|
+
if (!persona) {
|
|
494
|
+
console.error(`[queueEventSummary] Persona not found: ${personaId}`);
|
|
495
|
+
return 0;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const unextractedMessages = state.messages_getUnextracted(personaId, "e");
|
|
499
|
+
if (unextractedMessages.length === 0) {
|
|
500
|
+
console.log(`[queueEventSummary] No unprocessed messages for ${persona.display_name}`);
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const human = state.getHuman();
|
|
505
|
+
const gapHours = human.settings?.ceremony?.event_window_hours ?? 8;
|
|
506
|
+
|
|
507
|
+
const sorted = [...unextractedMessages].sort(
|
|
508
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const windows = buildEventWindows(sorted, gapHours);
|
|
512
|
+
|
|
513
|
+
const allMessages = state.messages_get(personaId);
|
|
514
|
+
const extractionModel = options?.extraction_model;
|
|
515
|
+
let totalChunks = 0;
|
|
516
|
+
|
|
517
|
+
state.messages_markExtracted(personaId, sorted.map(m => m.id), "e");
|
|
518
|
+
|
|
519
|
+
for (const windowMessages of windows) {
|
|
520
|
+
if (windowMessages.length === 0) continue;
|
|
521
|
+
|
|
522
|
+
const windowStartTime = new Date(windowMessages[0].timestamp).getTime();
|
|
523
|
+
const messages_context = allMessages.filter(
|
|
524
|
+
m => m.e === true && new Date(m.timestamp).getTime() < windowStartTime
|
|
525
|
+
);
|
|
438
526
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
527
|
+
const context: ExtractionContext = {
|
|
528
|
+
personaId,
|
|
529
|
+
personaDisplayName: persona.display_name,
|
|
530
|
+
messages_context,
|
|
531
|
+
messages_analyze: windowMessages,
|
|
532
|
+
extraction_flag: "e",
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, options));
|
|
536
|
+
|
|
537
|
+
for (const chunk of chunks) {
|
|
538
|
+
const prompt = buildEventScanPrompt({
|
|
539
|
+
persona_name: chunk.personaDisplayName,
|
|
540
|
+
messages_context: chunk.messages_context,
|
|
541
|
+
messages_analyze: chunk.messages_analyze,
|
|
542
|
+
participant_context: buildParticipantContext(personaId, state),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
state.queue_enqueue({
|
|
546
|
+
type: LLMRequestType.JSON,
|
|
547
|
+
priority: LLMPriority.Low,
|
|
548
|
+
model: extractionModel,
|
|
549
|
+
system: prompt.system,
|
|
550
|
+
user: prompt.user,
|
|
551
|
+
next_step: LLMNextStep.HandleEventScan,
|
|
552
|
+
data: {
|
|
553
|
+
...options,
|
|
554
|
+
personaId: chunk.personaId,
|
|
555
|
+
personaDisplayName: chunk.personaDisplayName,
|
|
556
|
+
extraction_flag: "e",
|
|
557
|
+
message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
totalChunks++;
|
|
442
561
|
}
|
|
443
562
|
}
|
|
444
563
|
|
|
445
|
-
|
|
564
|
+
console.log(`[queueEventSummary] Queued ${totalChunks} event scan chunk(s) for ${persona.display_name} (${windows.length} window(s))`);
|
|
565
|
+
return totalChunks;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function queuePersonUpdate(
|
|
569
|
+
matchResult: ItemMatchResult,
|
|
570
|
+
context: ExtractionContext & {
|
|
571
|
+
candidateName: string;
|
|
572
|
+
candidateDescription: string;
|
|
573
|
+
candidateRelationship: string;
|
|
574
|
+
extraction_model?: string;
|
|
575
|
+
},
|
|
576
|
+
state: StateManager
|
|
577
|
+
): number {
|
|
578
|
+
const human = state.getHuman();
|
|
579
|
+
const matchedGuid = matchResult.matched_guid;
|
|
580
|
+
const isNewItem = matchedGuid === null;
|
|
581
|
+
|
|
582
|
+
let existingItem: Person | null = null;
|
|
583
|
+
if (!isNewItem && matchedGuid) {
|
|
584
|
+
existingItem = human.people.find(p => p.id === matchedGuid) ?? null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const extractionOptions: ExtractionOptions = { extraction_model: context.extraction_model };
|
|
588
|
+
const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, extractionOptions));
|
|
446
589
|
|
|
447
590
|
if (chunks.length === 0) return 0;
|
|
448
591
|
|
|
449
592
|
for (const chunk of chunks) {
|
|
450
|
-
const prompt =
|
|
451
|
-
data_type: candidateType,
|
|
593
|
+
const prompt = buildPersonUpdatePrompt({
|
|
452
594
|
existing_item: existingItem,
|
|
595
|
+
new_person_name: isNewItem ? context.candidateName : undefined,
|
|
596
|
+
new_person_description: isNewItem ? context.candidateDescription : undefined,
|
|
597
|
+
new_person_relationship: isNewItem ? context.candidateRelationship : undefined,
|
|
453
598
|
messages_context: chunk.messages_context,
|
|
454
599
|
messages_analyze: chunk.messages_analyze,
|
|
455
600
|
persona_name: chunk.personaDisplayName,
|
|
456
|
-
new_item_name: isNewItem ? context.itemName : undefined,
|
|
457
|
-
new_item_value: isNewItem ? context.itemValue : undefined,
|
|
458
601
|
});
|
|
459
602
|
|
|
460
603
|
state.queue_enqueue({
|
|
461
604
|
type: LLMRequestType.JSON,
|
|
462
605
|
priority: LLMPriority.Normal,
|
|
606
|
+
model: context.extraction_model,
|
|
463
607
|
system: prompt.system,
|
|
464
608
|
user: prompt.user,
|
|
465
|
-
next_step: LLMNextStep.
|
|
609
|
+
next_step: LLMNextStep.HandlePersonUpdate,
|
|
466
610
|
data: {
|
|
467
611
|
personaId: context.personaId,
|
|
468
612
|
personaDisplayName: context.personaDisplayName,
|
|
469
|
-
candidateType,
|
|
470
|
-
matchedType,
|
|
471
613
|
isNewItem,
|
|
472
614
|
existingItemId: existingItem?.id,
|
|
473
|
-
|
|
615
|
+
candidateRelationship: context.candidateRelationship,
|
|
474
616
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
475
617
|
},
|
|
476
618
|
});
|
|
@@ -478,3 +620,5 @@ export function queueItemUpdate(
|
|
|
478
620
|
|
|
479
621
|
return chunks.length;
|
|
480
622
|
}
|
|
623
|
+
|
|
624
|
+
|