ei-tui 0.1.3
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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContextStatus,
|
|
3
|
+
LLMNextStep,
|
|
4
|
+
ValidationLevel,
|
|
5
|
+
type LLMResponse,
|
|
6
|
+
type Message,
|
|
7
|
+
type Trait,
|
|
8
|
+
type Topic,
|
|
9
|
+
type PersonaTopic,
|
|
10
|
+
type Fact,
|
|
11
|
+
type Person,
|
|
12
|
+
type Quote,
|
|
13
|
+
type DataItemType,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
import type { StateManager } from "../state-manager.js";
|
|
16
|
+
import type { HeartbeatCheckResult, EiHeartbeatResult } from "../../prompts/heartbeat/types.js";
|
|
17
|
+
import type { PersonaGenerationResult, PersonaDescriptionsResult } from "../../prompts/generation/types.js";
|
|
18
|
+
import type {
|
|
19
|
+
TraitResult,
|
|
20
|
+
PersonaTopicScanResult,
|
|
21
|
+
PersonaTopicScanCandidate,
|
|
22
|
+
PersonaTopicMatchResult,
|
|
23
|
+
PersonaTopicUpdateResult,
|
|
24
|
+
} from "../../prompts/persona/types.js";
|
|
25
|
+
import type { EiValidationResult } from "../../prompts/validation/types.js";
|
|
26
|
+
import type {
|
|
27
|
+
PersonaExpireResult,
|
|
28
|
+
PersonaExploreResult,
|
|
29
|
+
DescriptionCheckResult,
|
|
30
|
+
} from "../../prompts/ceremony/types.js";
|
|
31
|
+
import {
|
|
32
|
+
orchestratePersonaGeneration,
|
|
33
|
+
queueItemMatch,
|
|
34
|
+
queueItemUpdate,
|
|
35
|
+
queueExplorePhase,
|
|
36
|
+
queueDescriptionCheck,
|
|
37
|
+
queuePersonaTopicMatch,
|
|
38
|
+
queuePersonaTopicUpdate,
|
|
39
|
+
type PartialPersona,
|
|
40
|
+
type ExtractionContext,
|
|
41
|
+
type PersonaTopicContext,
|
|
42
|
+
} from "../orchestrators/index.js";
|
|
43
|
+
import { buildPersonaDescriptionsPrompt } from "../../prompts/generation/index.js";
|
|
44
|
+
import type {
|
|
45
|
+
FactScanResult,
|
|
46
|
+
TraitScanResult,
|
|
47
|
+
TopicScanResult,
|
|
48
|
+
PersonScanResult,
|
|
49
|
+
ItemMatchResult,
|
|
50
|
+
ItemUpdateResult,
|
|
51
|
+
ExposureImpact,
|
|
52
|
+
} from "../../prompts/human/types.js";
|
|
53
|
+
import { buildEiValidationPrompt } from "../../prompts/validation/index.js";
|
|
54
|
+
import { LLMRequestType, LLMPriority, LLMNextStep as NextStep } from "../types.js";
|
|
55
|
+
import { getEmbeddingService, getItemEmbeddingText } from "../embedding-service.js";
|
|
56
|
+
|
|
57
|
+
export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
|
|
58
|
+
|
|
59
|
+
function splitMessagesByTimestamp(
|
|
60
|
+
messages: Message[],
|
|
61
|
+
analyzeFromTimestamp: string | null
|
|
62
|
+
): { messages_context: Message[]; messages_analyze: Message[] } {
|
|
63
|
+
if (!analyzeFromTimestamp) {
|
|
64
|
+
return { messages_context: [], messages_analyze: messages };
|
|
65
|
+
}
|
|
66
|
+
const splitTime = new Date(analyzeFromTimestamp).getTime();
|
|
67
|
+
const splitIndex = messages.findIndex(m => new Date(m.timestamp).getTime() >= splitTime);
|
|
68
|
+
if (splitIndex === -1) {
|
|
69
|
+
return { messages_context: messages, messages_analyze: [] };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
messages_context: messages.slice(0, splitIndex),
|
|
73
|
+
messages_analyze: messages.slice(splitIndex),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type ExtractionFlag = "f" | "r" | "p" | "o";
|
|
78
|
+
|
|
79
|
+
function markMessagesExtracted(
|
|
80
|
+
response: LLMResponse,
|
|
81
|
+
state: StateManager,
|
|
82
|
+
flag: ExtractionFlag
|
|
83
|
+
): void {
|
|
84
|
+
const personaId = response.request.data.personaId as string | undefined;
|
|
85
|
+
const messageIds = response.request.data.message_ids_to_mark as string[] | undefined;
|
|
86
|
+
|
|
87
|
+
if (!personaId || !messageIds?.length) return;
|
|
88
|
+
|
|
89
|
+
const count = state.messages_markExtracted(personaId, messageIds, flag);
|
|
90
|
+
if (count > 0) {
|
|
91
|
+
console.log(`[markMessagesExtracted] Marked ${count} messages with flag '${flag}' for persona ${personaId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handlePersonaResponse(response: LLMResponse, state: StateManager): void {
|
|
96
|
+
const personaId = response.request.data.personaId as string;
|
|
97
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
98
|
+
if (!personaId) {
|
|
99
|
+
console.error("[handlePersonaResponse] No personaId in request data");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Always mark user messages as read - even if persona chooses not to respond,
|
|
104
|
+
// the messages were "seen" and processed
|
|
105
|
+
state.messages_markPendingAsRead(personaId);
|
|
106
|
+
|
|
107
|
+
if (!response.content) {
|
|
108
|
+
console.log(`[handlePersonaResponse] No content in response (${personaDisplayName} chose not to respond)`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const message: Message = {
|
|
113
|
+
id: crypto.randomUUID(),
|
|
114
|
+
role: "system",
|
|
115
|
+
content: response.content,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
read: false,
|
|
118
|
+
context_status: ContextStatus.Default,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
state.messages_append(personaId, message);
|
|
122
|
+
console.log(`[handlePersonaResponse] Appended response to ${personaDisplayName}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleHeartbeatCheck(response: LLMResponse, state: StateManager): void {
|
|
126
|
+
const personaId = response.request.data.personaId as string;
|
|
127
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
128
|
+
if (!personaId) {
|
|
129
|
+
console.error("[handleHeartbeatCheck] No personaId in request data");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = response.parsed as HeartbeatCheckResult | undefined;
|
|
134
|
+
if (!result) {
|
|
135
|
+
console.error("[handleHeartbeatCheck] No parsed result");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
state.persona_update(personaId, { last_heartbeat: now });
|
|
141
|
+
|
|
142
|
+
if (!result.should_respond) {
|
|
143
|
+
console.log(`[handleHeartbeatCheck] ${personaDisplayName} chose not to reach out`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (result.message) {
|
|
148
|
+
const message: Message = {
|
|
149
|
+
id: crypto.randomUUID(),
|
|
150
|
+
role: "system",
|
|
151
|
+
content: result.message,
|
|
152
|
+
timestamp: now,
|
|
153
|
+
read: false,
|
|
154
|
+
context_status: ContextStatus.Default,
|
|
155
|
+
};
|
|
156
|
+
state.messages_append(personaId, message);
|
|
157
|
+
console.log(`[handleHeartbeatCheck] ${personaDisplayName} proactively messaged about: ${result.topic ?? "general"}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function handleEiHeartbeat(response: LLMResponse, state: StateManager): void {
|
|
162
|
+
const result = response.parsed as EiHeartbeatResult | undefined;
|
|
163
|
+
if (!result) {
|
|
164
|
+
console.error("[handleEiHeartbeat] No parsed result");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const now = new Date().toISOString();
|
|
169
|
+
state.persona_update("ei", { last_heartbeat: now });
|
|
170
|
+
|
|
171
|
+
if (!result.should_respond) {
|
|
172
|
+
console.log("[handleEiHeartbeat] Ei chose not to reach out");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (result.message) {
|
|
177
|
+
const message: Message = {
|
|
178
|
+
id: crypto.randomUUID(),
|
|
179
|
+
role: "system",
|
|
180
|
+
content: result.message,
|
|
181
|
+
timestamp: now,
|
|
182
|
+
read: false,
|
|
183
|
+
context_status: ContextStatus.Default,
|
|
184
|
+
};
|
|
185
|
+
state.messages_append("ei", message);
|
|
186
|
+
console.log("[handleEiHeartbeat] Ei proactively messaged");
|
|
187
|
+
if (result.priorities) {
|
|
188
|
+
console.log("[handleEiHeartbeat] Priorities:", result.priorities.map(p => p.name).join(", "));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handlePersonaGeneration(response: LLMResponse, state: StateManager): void {
|
|
194
|
+
const personaId = response.request.data.personaId as string;
|
|
195
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
196
|
+
if (!personaId) {
|
|
197
|
+
console.error("[handlePersonaGeneration] No personaId in request data");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = response.parsed as PersonaGenerationResult | undefined;
|
|
202
|
+
const existingPartial = (response.request.data.partial as PartialPersona) ?? { id: personaId, name: personaDisplayName };
|
|
203
|
+
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
|
|
206
|
+
const traits: Trait[] = (result?.traits || []).map(t => ({
|
|
207
|
+
id: crypto.randomUUID(),
|
|
208
|
+
name: t.name,
|
|
209
|
+
description: t.description,
|
|
210
|
+
sentiment: t.sentiment,
|
|
211
|
+
strength: t.strength,
|
|
212
|
+
last_updated: now,
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
const topics: PersonaTopic[] = (result?.topics || []).map(t => ({
|
|
216
|
+
id: crypto.randomUUID(),
|
|
217
|
+
name: t.name,
|
|
218
|
+
perspective: t.perspective || "",
|
|
219
|
+
approach: t.approach || "",
|
|
220
|
+
personal_stake: t.personal_stake || "",
|
|
221
|
+
sentiment: t.sentiment,
|
|
222
|
+
exposure_current: t.exposure_current,
|
|
223
|
+
exposure_desired: t.exposure_desired,
|
|
224
|
+
last_updated: now,
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
const updatedPartial: PartialPersona = {
|
|
228
|
+
...existingPartial,
|
|
229
|
+
short_description: result?.short_description ?? existingPartial.short_description,
|
|
230
|
+
long_description: existingPartial.long_description ?? result?.long_description,
|
|
231
|
+
traits: traits.length > 0 ? traits : existingPartial.traits,
|
|
232
|
+
topics: topics.length > 0 ? topics : existingPartial.topics,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
orchestratePersonaGeneration(updatedPartial, state);
|
|
236
|
+
console.log(`[handlePersonaGeneration] Orchestrated: ${personaDisplayName}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handlePersonaDescriptions(response: LLMResponse, state: StateManager): void {
|
|
240
|
+
const personaId = response.request.data.personaId as string;
|
|
241
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
242
|
+
if (!personaId) {
|
|
243
|
+
console.error("[handlePersonaDescriptions] No personaId in request data");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = response.parsed as PersonaDescriptionsResult | undefined;
|
|
248
|
+
if (!result) {
|
|
249
|
+
console.error("[handlePersonaDescriptions] No parsed result");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (result.no_change) {
|
|
254
|
+
console.log(`[handlePersonaDescriptions] No change needed for ${personaDisplayName}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
state.persona_update(personaId, {
|
|
259
|
+
short_description: result.short_description,
|
|
260
|
+
long_description: result.long_description,
|
|
261
|
+
last_updated: new Date().toISOString(),
|
|
262
|
+
});
|
|
263
|
+
console.log(`[handlePersonaDescriptions] Updated descriptions for ${personaDisplayName}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function handlePersonaTraitExtraction(response: LLMResponse, state: StateManager): void {
|
|
267
|
+
const personaId = response.request.data.personaId as string;
|
|
268
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
269
|
+
if (!personaId) {
|
|
270
|
+
console.error("[handlePersonaTraitExtraction] No personaId in request data");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = response.parsed as TraitResult[] | undefined;
|
|
275
|
+
if (!result || !Array.isArray(result)) {
|
|
276
|
+
console.error("[handlePersonaTraitExtraction] Invalid parsed result");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const now = new Date().toISOString();
|
|
281
|
+
const traits: Trait[] = result.map(t => ({
|
|
282
|
+
id: crypto.randomUUID(),
|
|
283
|
+
name: t.name,
|
|
284
|
+
description: t.description,
|
|
285
|
+
sentiment: t.sentiment,
|
|
286
|
+
strength: t.strength,
|
|
287
|
+
last_updated: now,
|
|
288
|
+
}));
|
|
289
|
+
|
|
290
|
+
state.persona_update(personaId, { traits, last_updated: now });
|
|
291
|
+
console.log(`[handlePersonaTraitExtraction] Updated ${traits.length} traits for ${personaDisplayName}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function handleEiValidation(response: LLMResponse, state: StateManager): void {
|
|
295
|
+
const validationId = response.request.data.validationId as string;
|
|
296
|
+
const dataType = response.request.data.dataType as string;
|
|
297
|
+
const itemName = response.request.data.itemName as string;
|
|
298
|
+
|
|
299
|
+
const result = response.parsed as EiValidationResult | undefined;
|
|
300
|
+
if (!result) {
|
|
301
|
+
console.error("[handleEiValidation] No parsed result");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(`[handleEiValidation] Decision for ${dataType} "${itemName}": ${result.decision} - ${result.reason}`);
|
|
306
|
+
|
|
307
|
+
if (result.decision === "reject") {
|
|
308
|
+
if (validationId) {
|
|
309
|
+
state.queue_clearValidations([validationId]);
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const itemToApply = result.decision === "modify" && result.modified_item
|
|
315
|
+
? result.modified_item
|
|
316
|
+
: response.request.data.proposedItem;
|
|
317
|
+
|
|
318
|
+
if (itemToApply && dataType) {
|
|
319
|
+
const now = new Date().toISOString();
|
|
320
|
+
const item = { ...itemToApply, last_updated: now };
|
|
321
|
+
|
|
322
|
+
switch (dataType) {
|
|
323
|
+
case "fact":
|
|
324
|
+
state.human_fact_upsert({
|
|
325
|
+
...item,
|
|
326
|
+
validated: ValidationLevel.Ei,
|
|
327
|
+
validated_date: now,
|
|
328
|
+
} as Fact);
|
|
329
|
+
break;
|
|
330
|
+
case "trait":
|
|
331
|
+
state.human_trait_upsert(item as Trait);
|
|
332
|
+
break;
|
|
333
|
+
case "topic":
|
|
334
|
+
state.human_topic_upsert(item as Topic);
|
|
335
|
+
break;
|
|
336
|
+
case "person":
|
|
337
|
+
state.human_person_upsert(item as Person);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
console.log(`[handleEiValidation] Applied ${result.decision} for ${dataType} "${itemName}"`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (validationId) {
|
|
344
|
+
state.queue_clearValidations([validationId]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function handleOneShot(_response: LLMResponse, _state: StateManager): void {
|
|
349
|
+
// One-shot is handled specially in Processor to fire onOneShotReturned
|
|
350
|
+
// This handler is a no-op placeholder
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function handlePersonaExpire(response: LLMResponse, state: StateManager): void {
|
|
354
|
+
const personaId = response.request.data.personaId as string;
|
|
355
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
356
|
+
if (!personaId) {
|
|
357
|
+
console.error("[handlePersonaExpire] No personaId in request data");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const result = response.parsed as PersonaExpireResult | undefined;
|
|
362
|
+
const persona = state.persona_getById(personaId);
|
|
363
|
+
|
|
364
|
+
if (!persona) {
|
|
365
|
+
console.error(`[handlePersonaExpire] Persona not found: ${personaDisplayName}`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const idsToRemove = new Set(result?.topic_ids_to_remove ?? []);
|
|
370
|
+
const remainingTopics = persona.topics.filter((t: PersonaTopic) => !idsToRemove.has(t.id));
|
|
371
|
+
const removedCount = persona.topics.length - remainingTopics.length;
|
|
372
|
+
|
|
373
|
+
if (removedCount > 0) {
|
|
374
|
+
state.persona_update(personaId, {
|
|
375
|
+
topics: remainingTopics,
|
|
376
|
+
last_updated: new Date().toISOString(),
|
|
377
|
+
});
|
|
378
|
+
console.log(`[handlePersonaExpire] Removed ${removedCount} topic(s) from ${personaDisplayName}`);
|
|
379
|
+
} else {
|
|
380
|
+
console.log(`[handlePersonaExpire] No topics removed for ${personaDisplayName}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const human = state.getHuman();
|
|
384
|
+
const exploreThreshold = human.settings?.ceremony?.explore_threshold ?? 3;
|
|
385
|
+
|
|
386
|
+
if (remainingTopics.length < exploreThreshold) {
|
|
387
|
+
console.log(`[handlePersonaExpire] ${personaDisplayName} has ${remainingTopics.length} topic(s) (< ${exploreThreshold}), triggering Explore`);
|
|
388
|
+
queueExplorePhase(personaId, state);
|
|
389
|
+
} else {
|
|
390
|
+
queueDescriptionCheck(personaId, state);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function handlePersonaExplore(response: LLMResponse, state: StateManager): void {
|
|
395
|
+
const personaId = response.request.data.personaId as string;
|
|
396
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
397
|
+
if (!personaId) {
|
|
398
|
+
console.error("[handlePersonaExplore] No personaId in request data");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const result = response.parsed as PersonaExploreResult | undefined;
|
|
403
|
+
const persona = state.persona_getById(personaId);
|
|
404
|
+
|
|
405
|
+
if (!persona) {
|
|
406
|
+
console.error(`[handlePersonaExplore] Persona not found: ${personaDisplayName}`);
|
|
407
|
+
queueDescriptionCheck(personaId, state);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const newTopics = result?.new_topics ?? [];
|
|
412
|
+
if (newTopics.length === 0) {
|
|
413
|
+
console.log(`[handlePersonaExplore] No new topics generated for ${personaDisplayName}`);
|
|
414
|
+
queueDescriptionCheck(personaId, state);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const now = new Date().toISOString();
|
|
419
|
+
const existingNames = new Set(persona.topics.map((t: PersonaTopic) => t.name.toLowerCase()));
|
|
420
|
+
|
|
421
|
+
const topicsToAdd: PersonaTopic[] = newTopics
|
|
422
|
+
.filter(t => !existingNames.has(t.name.toLowerCase()))
|
|
423
|
+
.map(t => ({
|
|
424
|
+
id: crypto.randomUUID(),
|
|
425
|
+
name: t.name,
|
|
426
|
+
perspective: t.perspective || "",
|
|
427
|
+
approach: t.approach || "",
|
|
428
|
+
personal_stake: t.personal_stake || "",
|
|
429
|
+
sentiment: t.sentiment,
|
|
430
|
+
exposure_current: t.exposure_current ?? 0.2,
|
|
431
|
+
exposure_desired: t.exposure_desired ?? 0.6,
|
|
432
|
+
last_updated: now,
|
|
433
|
+
}));
|
|
434
|
+
|
|
435
|
+
if (topicsToAdd.length > 0) {
|
|
436
|
+
const allTopics = [...persona.topics, ...topicsToAdd];
|
|
437
|
+
state.persona_update(personaId, {
|
|
438
|
+
topics: allTopics,
|
|
439
|
+
last_updated: now,
|
|
440
|
+
});
|
|
441
|
+
console.log(`[handlePersonaExplore] Added ${topicsToAdd.length} new topic(s) to ${personaDisplayName}: ${topicsToAdd.map(t => t.name).join(", ")}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
queueDescriptionCheck(personaId, state);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function handleDescriptionCheck(response: LLMResponse, state: StateManager): void {
|
|
448
|
+
const personaId = response.request.data.personaId as string;
|
|
449
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
450
|
+
if (!personaId) {
|
|
451
|
+
console.error("[handleDescriptionCheck] No personaId in request data");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const result = response.parsed as DescriptionCheckResult | undefined;
|
|
456
|
+
if (!result) {
|
|
457
|
+
console.error("[handleDescriptionCheck] No parsed result");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
console.log(`[handleDescriptionCheck] ${personaDisplayName}: ${result.should_update ? "UPDATE NEEDED" : "No update needed"} - ${result.reason ?? "no reason given"}`);
|
|
462
|
+
|
|
463
|
+
if (!result.should_update) {
|
|
464
|
+
console.log(`[handleDescriptionCheck] Ceremony complete for ${personaDisplayName}`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const persona = state.persona_getById(personaId);
|
|
469
|
+
if (!persona) {
|
|
470
|
+
console.error(`[handleDescriptionCheck] Persona not found: ${personaDisplayName}`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const prompt = buildPersonaDescriptionsPrompt({
|
|
475
|
+
name: persona.display_name,
|
|
476
|
+
aliases: persona.aliases ?? [],
|
|
477
|
+
traits: persona.traits,
|
|
478
|
+
topics: persona.topics,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
state.queue_enqueue({
|
|
482
|
+
type: LLMRequestType.JSON,
|
|
483
|
+
priority: LLMPriority.Low,
|
|
484
|
+
system: prompt.system,
|
|
485
|
+
user: prompt.user,
|
|
486
|
+
next_step: LLMNextStep.HandlePersonaDescriptions,
|
|
487
|
+
data: { personaId, personaDisplayName },
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
console.log(`[handleDescriptionCheck] Queued description regeneration for ${personaDisplayName}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function handleHumanFactScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
494
|
+
const result = response.parsed as FactScanResult | undefined;
|
|
495
|
+
|
|
496
|
+
// Mark messages as scanned regardless of whether facts were found
|
|
497
|
+
markMessagesExtracted(response, state, "f");
|
|
498
|
+
|
|
499
|
+
if (!result?.facts || !Array.isArray(result.facts)) {
|
|
500
|
+
console.log("[handleHumanFactScan] No facts detected or invalid result");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const context = response.request.data as unknown as ExtractionContext;
|
|
505
|
+
if (!context?.personaId) return;
|
|
506
|
+
|
|
507
|
+
for (const candidate of result.facts) {
|
|
508
|
+
await queueItemMatch("fact", candidate, context, state);
|
|
509
|
+
}
|
|
510
|
+
console.log(`[handleHumanFactScan] Queued ${result.facts.length} fact(s) for matching`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function handleHumanTraitScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
514
|
+
const result = response.parsed as TraitScanResult | undefined;
|
|
515
|
+
|
|
516
|
+
markMessagesExtracted(response, state, "r");
|
|
517
|
+
|
|
518
|
+
if (!result?.traits || !Array.isArray(result.traits)) {
|
|
519
|
+
console.log("[handleHumanTraitScan] No traits detected or invalid result");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const context = response.request.data as unknown as ExtractionContext;
|
|
524
|
+
if (!context?.personaId) return;
|
|
525
|
+
|
|
526
|
+
for (const candidate of result.traits) {
|
|
527
|
+
await queueItemMatch("trait", candidate, context, state);
|
|
528
|
+
}
|
|
529
|
+
console.log(`[handleHumanTraitScan] Queued ${result.traits.length} trait(s) for matching`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function handleHumanTopicScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
533
|
+
const result = response.parsed as TopicScanResult | undefined;
|
|
534
|
+
|
|
535
|
+
markMessagesExtracted(response, state, "p");
|
|
536
|
+
|
|
537
|
+
if (!result?.topics || !Array.isArray(result.topics)) {
|
|
538
|
+
console.log("[handleHumanTopicScan] No topics detected or invalid result");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const context = response.request.data as unknown as ExtractionContext;
|
|
543
|
+
if (!context?.personaId) return;
|
|
544
|
+
|
|
545
|
+
for (const candidate of result.topics) {
|
|
546
|
+
await queueItemMatch("topic", candidate, context, state);
|
|
547
|
+
}
|
|
548
|
+
console.log(`[handleHumanTopicScan] Queued ${result.topics.length} topic(s) for matching`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function handleHumanPersonScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
552
|
+
const result = response.parsed as PersonScanResult | undefined;
|
|
553
|
+
|
|
554
|
+
markMessagesExtracted(response, state, "o");
|
|
555
|
+
|
|
556
|
+
if (!result?.people || !Array.isArray(result.people)) {
|
|
557
|
+
console.log("[handleHumanPersonScan] No people detected or invalid result");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const context = response.request.data as unknown as ExtractionContext;
|
|
562
|
+
if (!context?.personaId) return;
|
|
563
|
+
|
|
564
|
+
for (const candidate of result.people) {
|
|
565
|
+
await queueItemMatch("person", candidate, context, state);
|
|
566
|
+
}
|
|
567
|
+
console.log(`[handleHumanPersonScan] Queued ${result.people.length} person(s) for matching`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function handleHumanItemMatch(response: LLMResponse, state: StateManager): void {
|
|
571
|
+
const result = response.parsed as ItemMatchResult | undefined;
|
|
572
|
+
if (!result) {
|
|
573
|
+
console.error("[handleHumanItemMatch] No parsed result");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// "new" isn't a valid guid and is used as a marker for LLMs to signify "no match" - update for processing
|
|
577
|
+
if (result?.matched_guid === "new") {
|
|
578
|
+
result.matched_guid = null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const candidateType = response.request.data.candidateType as DataItemType;
|
|
582
|
+
const itemName = response.request.data.itemName as string;
|
|
583
|
+
const itemValue = response.request.data.itemValue as string;
|
|
584
|
+
const personaId = response.request.data.personaId as string;
|
|
585
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
586
|
+
const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
|
|
587
|
+
|
|
588
|
+
const allMessages = state.messages_get(personaId);
|
|
589
|
+
const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
|
|
590
|
+
|
|
591
|
+
if (result.matched_guid) {
|
|
592
|
+
const human = state.getHuman();
|
|
593
|
+
const matchedFact = human.facts.find(f => f.id === result.matched_guid);
|
|
594
|
+
if (matchedFact?.validated === ValidationLevel.Human) {
|
|
595
|
+
console.log(`[handleHumanItemMatch] Skipping locked fact "${matchedFact.name}" (human-validated)`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const context: ExtractionContext & { itemName: string; itemValue: string; itemCategory?: string } = {
|
|
601
|
+
personaId,
|
|
602
|
+
personaDisplayName,
|
|
603
|
+
messages_context,
|
|
604
|
+
messages_analyze,
|
|
605
|
+
itemName,
|
|
606
|
+
itemValue,
|
|
607
|
+
itemCategory: candidateType === "topic" ? itemValue : undefined,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
queueItemUpdate(candidateType, result, context, state);
|
|
611
|
+
const matched = result.matched_guid ? `matched GUID "${result.matched_guid}"` : "no match (new item)";
|
|
612
|
+
console.log(`[handleHumanItemMatch] ${candidateType} "${itemName}": ${matched}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function handleHumanItemUpdate(response: LLMResponse, state: StateManager): Promise<void> {
|
|
616
|
+
const result = response.parsed as ItemUpdateResult | undefined;
|
|
617
|
+
|
|
618
|
+
if (!result || Object.keys(result).length === 0) {
|
|
619
|
+
console.log("[handleHumanItemUpdate] No changes needed (empty result)");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const candidateType = response.request.data.candidateType as DataItemType;
|
|
624
|
+
const isNewItem = response.request.data.isNewItem as boolean;
|
|
625
|
+
const existingItemId = response.request.data.existingItemId as string | undefined;
|
|
626
|
+
const personaId = response.request.data.personaId as string;
|
|
627
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
628
|
+
|
|
629
|
+
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
630
|
+
console.error("[handleHumanItemUpdate] Missing required fields in result");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const now = new Date().toISOString();
|
|
635
|
+
const itemId = isNewItem ? crypto.randomUUID() : (existingItemId ?? crypto.randomUUID());
|
|
636
|
+
|
|
637
|
+
const persona = state.persona_getById(personaId);
|
|
638
|
+
const personaGroup = persona?.group_primary ?? null;
|
|
639
|
+
const isEi = personaDisplayName.toLowerCase() === "ei";
|
|
640
|
+
|
|
641
|
+
const human = state.getHuman();
|
|
642
|
+
const getExistingItem = (): { learned_by?: string; persona_groups?: string[] } | undefined => {
|
|
643
|
+
if (isNewItem) return undefined;
|
|
644
|
+
switch (candidateType) {
|
|
645
|
+
case "fact": return human.facts.find(f => f.id === existingItemId);
|
|
646
|
+
case "trait": return human.traits.find(t => t.id === existingItemId);
|
|
647
|
+
case "topic": return human.topics.find(t => t.id === existingItemId);
|
|
648
|
+
case "person": return human.people.find(p => p.id === existingItemId);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const existingItem = getExistingItem();
|
|
652
|
+
|
|
653
|
+
const mergeGroups = (existing: string[] | undefined): string[] | undefined => {
|
|
654
|
+
if (!personaGroup) return existing;
|
|
655
|
+
if (isNewItem) return [personaGroup];
|
|
656
|
+
const groups = new Set(existing ?? []);
|
|
657
|
+
groups.add(personaGroup);
|
|
658
|
+
return Array.from(groups);
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
let embedding: number[] | undefined;
|
|
662
|
+
try {
|
|
663
|
+
const embeddingService = getEmbeddingService();
|
|
664
|
+
const text = getItemEmbeddingText({ name: result.name, description: result.description });
|
|
665
|
+
embedding = await embeddingService.embed(text);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
console.warn(`[handleHumanItemUpdate] Failed to compute embedding for ${candidateType} "${result.name}":`, err);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
switch (candidateType) {
|
|
671
|
+
case "fact": {
|
|
672
|
+
const fact: Fact = {
|
|
673
|
+
id: itemId,
|
|
674
|
+
name: result.name,
|
|
675
|
+
description: result.description,
|
|
676
|
+
sentiment: result.sentiment,
|
|
677
|
+
validated: ValidationLevel.None,
|
|
678
|
+
validated_date: now,
|
|
679
|
+
last_updated: now,
|
|
680
|
+
learned_by: isNewItem ? personaDisplayName : existingItem?.learned_by,
|
|
681
|
+
persona_groups: mergeGroups(existingItem?.persona_groups),
|
|
682
|
+
embedding,
|
|
683
|
+
};
|
|
684
|
+
applyOrValidate(state, "fact", fact, personaDisplayName, isEi, personaGroup);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "trait": {
|
|
688
|
+
const trait: Trait = {
|
|
689
|
+
id: itemId,
|
|
690
|
+
name: result.name,
|
|
691
|
+
description: result.description,
|
|
692
|
+
sentiment: result.sentiment,
|
|
693
|
+
strength: (result as any).strength ?? 0.5,
|
|
694
|
+
last_updated: now,
|
|
695
|
+
learned_by: isNewItem ? personaDisplayName : existingItem?.learned_by,
|
|
696
|
+
persona_groups: mergeGroups(existingItem?.persona_groups),
|
|
697
|
+
embedding,
|
|
698
|
+
};
|
|
699
|
+
applyOrValidate(state, "trait", trait, personaDisplayName, isEi, personaGroup);
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "topic": {
|
|
703
|
+
const exposureImpact = (result as any).exposure_impact as ExposureImpact | undefined;
|
|
704
|
+
const itemCategory = response.request.data.itemCategory as string | undefined;
|
|
705
|
+
const existingTopic = human.topics.find(t => t.id === existingItemId);
|
|
706
|
+
const topic: Topic = {
|
|
707
|
+
id: itemId,
|
|
708
|
+
name: result.name,
|
|
709
|
+
description: result.description,
|
|
710
|
+
sentiment: result.sentiment,
|
|
711
|
+
category: (result as any).category ?? itemCategory ?? existingTopic?.category,
|
|
712
|
+
exposure_current: calculateExposureCurrent(exposureImpact),
|
|
713
|
+
exposure_desired: (result as any).exposure_desired ?? 0.5,
|
|
714
|
+
last_updated: now,
|
|
715
|
+
learned_by: isNewItem ? personaDisplayName : existingItem?.learned_by,
|
|
716
|
+
persona_groups: mergeGroups(existingItem?.persona_groups),
|
|
717
|
+
embedding,
|
|
718
|
+
};
|
|
719
|
+
applyOrValidate(state, "topic", topic, personaDisplayName, isEi, personaGroup);
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
case "person": {
|
|
723
|
+
const exposureImpact = (result as any).exposure_impact as ExposureImpact | undefined;
|
|
724
|
+
const person: Person = {
|
|
725
|
+
id: itemId,
|
|
726
|
+
name: result.name,
|
|
727
|
+
description: result.description,
|
|
728
|
+
sentiment: result.sentiment,
|
|
729
|
+
relationship: (result as any).relationship ?? "Unknown",
|
|
730
|
+
exposure_current: calculateExposureCurrent(exposureImpact),
|
|
731
|
+
exposure_desired: (result as any).exposure_desired ?? 0.5,
|
|
732
|
+
last_updated: now,
|
|
733
|
+
learned_by: isNewItem ? personaDisplayName : existingItem?.learned_by,
|
|
734
|
+
persona_groups: mergeGroups(existingItem?.persona_groups),
|
|
735
|
+
embedding,
|
|
736
|
+
};
|
|
737
|
+
applyOrValidate(state, "person", person, personaDisplayName, isEi, personaGroup);
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const allMessages = state.messages_get(personaId);
|
|
743
|
+
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
744
|
+
|
|
745
|
+
console.log(`[handleHumanItemUpdate] ${isNewItem ? "Created" : "Updated"} ${candidateType} "${result.name}"`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function validateAndStoreQuotes(
|
|
749
|
+
candidates: Array<{ text: string; reason: string }> | undefined,
|
|
750
|
+
messages: Message[],
|
|
751
|
+
dataItemId: string,
|
|
752
|
+
personaName: string,
|
|
753
|
+
personaGroup: string | null,
|
|
754
|
+
state: StateManager
|
|
755
|
+
): Promise<void> {
|
|
756
|
+
if (!candidates || candidates.length === 0) return;
|
|
757
|
+
|
|
758
|
+
for (const candidate of candidates) {
|
|
759
|
+
let found = false;
|
|
760
|
+
for (const message of messages) {
|
|
761
|
+
const start = message.content.indexOf(candidate.text);
|
|
762
|
+
if (start !== -1) {
|
|
763
|
+
const end = start + candidate.text.length;
|
|
764
|
+
|
|
765
|
+
const existing = state.human_quote_getForMessage(message.id);
|
|
766
|
+
const existingQuote = existing.find(q => q.start === start && q.end === end);
|
|
767
|
+
|
|
768
|
+
if (existingQuote) {
|
|
769
|
+
if (!existingQuote.data_item_ids.includes(dataItemId)) {
|
|
770
|
+
state.human_quote_update(existingQuote.id, {
|
|
771
|
+
data_item_ids: [...existingQuote.data_item_ids, dataItemId],
|
|
772
|
+
});
|
|
773
|
+
console.log(`[extraction] Linked existing quote to "${dataItemId}": "${candidate.text.slice(0, 30)}..."`);
|
|
774
|
+
} else {
|
|
775
|
+
console.log(`[extraction] Quote already linked to "${dataItemId}": "${candidate.text.slice(0, 30)}..."`);
|
|
776
|
+
}
|
|
777
|
+
found = true;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
let embedding: number[] | undefined;
|
|
782
|
+
try {
|
|
783
|
+
const embeddingService = getEmbeddingService();
|
|
784
|
+
embedding = await embeddingService.embed(candidate.text);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
console.warn(`[extraction] Failed to compute embedding for quote: "${candidate.text.slice(0, 30)}..."`, err);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const quote: Quote = {
|
|
790
|
+
id: crypto.randomUUID(),
|
|
791
|
+
message_id: message.id,
|
|
792
|
+
data_item_ids: [dataItemId],
|
|
793
|
+
persona_groups: [personaGroup || "General"],
|
|
794
|
+
text: candidate.text,
|
|
795
|
+
speaker: message.role === "human" ? "human" : personaName,
|
|
796
|
+
timestamp: message.timestamp,
|
|
797
|
+
start: start,
|
|
798
|
+
end: end,
|
|
799
|
+
created_at: new Date().toISOString(),
|
|
800
|
+
created_by: "extraction",
|
|
801
|
+
embedding,
|
|
802
|
+
};
|
|
803
|
+
state.human_quote_add(quote);
|
|
804
|
+
console.log(`[extraction] Captured quote: "${candidate.text.slice(0, 50)}..."`);
|
|
805
|
+
found = true;
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (!found) {
|
|
810
|
+
console.log(`[extraction] Quote not found in messages, skipping: "${candidate.text?.slice(0, 50)}..."`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function calculateExposureCurrent(impact: ExposureImpact | undefined): number {
|
|
816
|
+
switch (impact) {
|
|
817
|
+
case "high": return 0.9;
|
|
818
|
+
case "medium": return 0.6;
|
|
819
|
+
case "low": return 0.3;
|
|
820
|
+
case "none": return 0.1;
|
|
821
|
+
default: return 0.5;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function applyOrValidate(
|
|
826
|
+
state: StateManager,
|
|
827
|
+
dataType: DataItemType,
|
|
828
|
+
item: Fact | Trait | Topic | Person,
|
|
829
|
+
personaName: string,
|
|
830
|
+
isEi: boolean,
|
|
831
|
+
personaGroup: string | null
|
|
832
|
+
): void {
|
|
833
|
+
const isGeneralGroup = !personaGroup || personaGroup.toLowerCase() === "general";
|
|
834
|
+
const needsValidation = !isEi && isGeneralGroup;
|
|
835
|
+
|
|
836
|
+
switch (dataType) {
|
|
837
|
+
case "fact": state.human_fact_upsert(item as Fact); break;
|
|
838
|
+
case "trait": state.human_trait_upsert(item as Trait); break;
|
|
839
|
+
case "topic": state.human_topic_upsert(item as Topic); break;
|
|
840
|
+
case "person": state.human_person_upsert(item as Person); break;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (needsValidation) {
|
|
844
|
+
queueEiValidation(state, dataType, item, personaName);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
|
|
849
|
+
|
|
850
|
+
function handlePersonaTopicScan(response: LLMResponse, state: StateManager): void {
|
|
851
|
+
const personaId = response.request.data.personaId as string;
|
|
852
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
853
|
+
if (!personaId || !personaDisplayName) {
|
|
854
|
+
console.error("[handlePersonaTopicScan] Missing personaId or personaDisplayName in request data");
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const result = response.parsed as PersonaTopicScanResult | undefined;
|
|
859
|
+
if (!result?.topics || !Array.isArray(result.topics)) {
|
|
860
|
+
console.log("[handlePersonaTopicScan] No topics detected or invalid result");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
|
|
865
|
+
const allMessages = state.messages_get(personaId);
|
|
866
|
+
const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
|
|
867
|
+
|
|
868
|
+
const context: PersonaTopicContext = {
|
|
869
|
+
personaId,
|
|
870
|
+
personaDisplayName,
|
|
871
|
+
messages_context,
|
|
872
|
+
messages_analyze,
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
for (const candidate of result.topics) {
|
|
876
|
+
queuePersonaTopicMatch(candidate, context, state);
|
|
877
|
+
}
|
|
878
|
+
console.log(`[handlePersonaTopicScan] Queued ${result.topics.length} topic(s) for matching`);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function handlePersonaTopicMatch(response: LLMResponse, state: StateManager): void {
|
|
882
|
+
const personaId = response.request.data.personaId as string;
|
|
883
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
884
|
+
const candidate = response.request.data.candidate as PersonaTopicScanCandidate;
|
|
885
|
+
const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
|
|
886
|
+
|
|
887
|
+
if (!personaId || !personaDisplayName || !candidate) {
|
|
888
|
+
console.error("[handlePersonaTopicMatch] Missing required data");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const result = response.parsed as PersonaTopicMatchResult | undefined;
|
|
893
|
+
if (!result) {
|
|
894
|
+
console.error("[handlePersonaTopicMatch] No parsed result");
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (result.action === "match") {
|
|
899
|
+
console.log(`[handlePersonaTopicMatch] "${candidate.name}" matched existing topic`);
|
|
900
|
+
} else if (result.action === "create") {
|
|
901
|
+
if (candidate.message_count < MIN_MESSAGE_COUNT_FOR_CREATE) {
|
|
902
|
+
console.log(`[handlePersonaTopicMatch] "${candidate.name}" skipped: message_count ${candidate.message_count} < ${MIN_MESSAGE_COUNT_FOR_CREATE}`);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
console.log(`[handlePersonaTopicMatch] "${candidate.name}" will be created`);
|
|
906
|
+
} else if (result.action === "skip") {
|
|
907
|
+
console.log(`[handlePersonaTopicMatch] "${candidate.name}" skipped: ${result.reason}`);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const allMessages = state.messages_get(personaId);
|
|
912
|
+
const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
|
|
913
|
+
|
|
914
|
+
const context: PersonaTopicContext = {
|
|
915
|
+
personaId,
|
|
916
|
+
personaDisplayName,
|
|
917
|
+
messages_context,
|
|
918
|
+
messages_analyze,
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
queuePersonaTopicUpdate(candidate, result, context, state);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): void {
|
|
925
|
+
const personaId = response.request.data.personaId as string;
|
|
926
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
927
|
+
const existingTopicId = response.request.data.existingTopicId as string | null;
|
|
928
|
+
const isNewTopic = response.request.data.isNewTopic as boolean;
|
|
929
|
+
|
|
930
|
+
if (!personaId) {
|
|
931
|
+
console.error("[handlePersonaTopicUpdate] No personaId in request data");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const result = response.parsed as PersonaTopicUpdateResult | undefined;
|
|
936
|
+
if (!result) {
|
|
937
|
+
console.error("[handlePersonaTopicUpdate] No parsed result");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const persona = state.persona_getById(personaId);
|
|
942
|
+
if (!persona) {
|
|
943
|
+
console.error(`[handlePersonaTopicUpdate] Persona not found: ${personaDisplayName}`);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const now = new Date().toISOString();
|
|
948
|
+
|
|
949
|
+
if (isNewTopic) {
|
|
950
|
+
const newTopic: PersonaTopic = {
|
|
951
|
+
id: crypto.randomUUID(),
|
|
952
|
+
name: result.name,
|
|
953
|
+
perspective: result.perspective || "",
|
|
954
|
+
approach: result.approach || "",
|
|
955
|
+
personal_stake: result.personal_stake || "",
|
|
956
|
+
sentiment: result.sentiment,
|
|
957
|
+
exposure_current: result.exposure_current,
|
|
958
|
+
exposure_desired: result.exposure_desired,
|
|
959
|
+
last_updated: now,
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const allTopics = [...persona.topics, newTopic];
|
|
963
|
+
state.persona_update(personaId, { topics: allTopics, last_updated: now });
|
|
964
|
+
console.log(`[handlePersonaTopicUpdate] Created new topic "${result.name}" for ${personaDisplayName}`);
|
|
965
|
+
} else if (existingTopicId) {
|
|
966
|
+
const updatedTopics = persona.topics.map((t: PersonaTopic) => {
|
|
967
|
+
if (t.id !== existingTopicId) return t;
|
|
968
|
+
|
|
969
|
+
const newExposure = Math.min(1.0, t.exposure_current + (result.exposure_current - t.exposure_current));
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
...t,
|
|
973
|
+
name: result.name,
|
|
974
|
+
perspective: result.perspective || t.perspective,
|
|
975
|
+
approach: result.approach || t.approach,
|
|
976
|
+
personal_stake: result.personal_stake || t.personal_stake,
|
|
977
|
+
sentiment: result.sentiment,
|
|
978
|
+
exposure_current: newExposure,
|
|
979
|
+
exposure_desired: result.exposure_desired,
|
|
980
|
+
last_updated: now,
|
|
981
|
+
};
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
state.persona_update(personaId, { topics: updatedTopics, last_updated: now });
|
|
985
|
+
console.log(`[handlePersonaTopicUpdate] Updated topic "${result.name}" for ${personaDisplayName}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function queueEiValidation(
|
|
990
|
+
state: StateManager,
|
|
991
|
+
dataType: DataItemType,
|
|
992
|
+
item: Fact | Trait | Topic | Person,
|
|
993
|
+
sourcePersona: string
|
|
994
|
+
): void {
|
|
995
|
+
const human = state.getHuman();
|
|
996
|
+
let existingItem: Fact | Trait | Topic | Person | undefined;
|
|
997
|
+
|
|
998
|
+
switch (dataType) {
|
|
999
|
+
case "fact": existingItem = human.facts.find(f => f.id === item.id); break;
|
|
1000
|
+
case "trait": existingItem = human.traits.find(t => t.id === item.id); break;
|
|
1001
|
+
case "topic": existingItem = human.topics.find(t => t.id === item.id); break;
|
|
1002
|
+
case "person": existingItem = human.people.find(p => p.id === item.id); break;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const prompt = buildEiValidationPrompt({
|
|
1006
|
+
validation_type: "cross_persona",
|
|
1007
|
+
item_name: item.name,
|
|
1008
|
+
data_type: dataType,
|
|
1009
|
+
context: `Learned from conversation with ${sourcePersona}`,
|
|
1010
|
+
source_persona: sourcePersona,
|
|
1011
|
+
current_item: existingItem,
|
|
1012
|
+
proposed_item: item,
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// Cross-persona validation is always normal priority
|
|
1016
|
+
const priority = LLMPriority.Normal;
|
|
1017
|
+
|
|
1018
|
+
state.queue_enqueue({
|
|
1019
|
+
type: LLMRequestType.JSON,
|
|
1020
|
+
priority,
|
|
1021
|
+
system: prompt.system,
|
|
1022
|
+
user: prompt.user,
|
|
1023
|
+
next_step: NextStep.HandleEiValidation,
|
|
1024
|
+
data: {
|
|
1025
|
+
validationId: crypto.randomUUID(),
|
|
1026
|
+
dataType,
|
|
1027
|
+
itemName: item.name,
|
|
1028
|
+
proposedItem: item,
|
|
1029
|
+
sourcePersona,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
console.log(`[queueEiValidation] Queued ${dataType} "${item.name}" from ${sourcePersona} for Ei validation`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
1037
|
+
handlePersonaResponse,
|
|
1038
|
+
handlePersonaGeneration,
|
|
1039
|
+
handlePersonaDescriptions,
|
|
1040
|
+
handleHumanFactScan,
|
|
1041
|
+
handleHumanTraitScan,
|
|
1042
|
+
handleHumanTopicScan,
|
|
1043
|
+
handleHumanPersonScan,
|
|
1044
|
+
handleHumanItemMatch,
|
|
1045
|
+
handleHumanItemUpdate,
|
|
1046
|
+
handlePersonaTraitExtraction,
|
|
1047
|
+
handlePersonaTopicScan,
|
|
1048
|
+
handlePersonaTopicMatch,
|
|
1049
|
+
handlePersonaTopicUpdate,
|
|
1050
|
+
handleHeartbeatCheck,
|
|
1051
|
+
handleEiHeartbeat,
|
|
1052
|
+
handleEiValidation,
|
|
1053
|
+
handleOneShot,
|
|
1054
|
+
handlePersonaExpire,
|
|
1055
|
+
handlePersonaExplore,
|
|
1056
|
+
handleDescriptionCheck,
|
|
1057
|
+
};
|