ei-tui 0.6.0 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
@@ -122,10 +122,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
122
122
  const personaDisplayName = response.request.data.personaDisplayName as string;
123
123
  const roomId = response.request.data.roomId as string | undefined;
124
124
  const candidateCategory = response.request.data.candidateCategory as string | undefined;
125
-
126
- if (!result.name || !result.description || result.sentiment === undefined) {
127
- throw new Error(`[handleTopicUpdate] Missing required fields: name=${result.name}, description=${!!result.description}, sentiment=${result.sentiment}`);
128
- }
125
+ const candidateName = response.request.data.candidateName as string | undefined;
129
126
 
130
127
  const personaIds = personaId.split("|").filter(Boolean);
131
128
  const primaryId = personaIds[0] ?? personaId;
@@ -147,14 +144,21 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
147
144
 
148
145
  const existingTopic = isNewItem ? undefined : human.topics.find(t => t.id === existingItemId);
149
146
 
147
+ const resolvedName = result.name || existingTopic?.name || candidateName;
148
+ const resolvedDescription = typeof result.description === 'string' ? result.description : existingTopic?.description;
149
+
150
+ if (!resolvedName || !resolvedDescription || result.sentiment === undefined) {
151
+ throw new Error(`[handleTopicUpdate] Missing required fields: name=${resolvedName}, description=${!!resolvedDescription}, sentiment=${result.sentiment}`);
152
+ }
153
+
150
154
  let embedding: number[] | undefined;
151
155
  try {
152
156
  const embeddingService = getEmbeddingService();
153
157
  const category = result.category ?? candidateCategory ?? existingTopic?.category;
154
- const text = getTopicEmbeddingText({ name: result.name, category, description: result.description });
158
+ const text = getTopicEmbeddingText({ name: resolvedName, category, description: resolvedDescription });
155
159
  embedding = await embeddingService.embed(text);
156
160
  } catch (err) {
157
- console.warn(`[handleTopicUpdate] Failed to compute embedding for topic "${result.name}":`, err);
161
+ console.warn(`[handleTopicUpdate] Failed to compute embedding for topic "${resolvedName}":`, err);
158
162
  }
159
163
 
160
164
  const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
@@ -167,8 +171,8 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
167
171
 
168
172
  const topic: Topic = {
169
173
  id: itemId,
170
- name: result.name,
171
- description: result.description,
174
+ name: resolvedName,
175
+ description: resolvedDescription,
172
176
  sentiment: result.sentiment,
173
177
  category: result.category ?? candidateCategory ?? existingTopic?.category,
174
178
  exposure_current: calculateExposureCurrent(exposureImpact, existingTopic?.exposure_current ?? 0),
@@ -189,7 +193,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
189
193
  : state.messages_get(personaId);
190
194
  await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
191
195
 
192
- console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${result.name}"`);
196
+ console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${resolvedName}"`);
193
197
  }
194
198
 
195
199
  export async function handlePersonUpdate(response: LLMResponse, state: StateManager): Promise<void> {
@@ -422,6 +422,7 @@ export function queueTopicUpdate(
422
422
  roomId: context.roomId,
423
423
  isNewItem,
424
424
  existingItemId: existingItem?.id,
425
+ candidateName: isNewItem ? context.candidateName : undefined,
425
426
  candidateCategory: context.candidateCategory,
426
427
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
427
428
  },
@@ -250,7 +250,7 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
250
250
  ${jsonTemplate}
251
251
  \`\`\`
252
252
 
253
- When returning a record, **ALWAYS** include \`name\`, \`description\`, and \`sentiment\`.
253
+ When returning a record, always include \`sentiment\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`description\` when returning a record.
254
254
 
255
255
  If you find **NO EVIDENCE** of this TOPIC in the "Most Recent Messages", respond with: \`{}\`
256
256
 
@@ -80,21 +80,27 @@ export const meCommand: Command = {
80
80
  }
81
81
 
82
82
  for (const fact of parsed.facts) {
83
- await ctx.ei.upsertFact(fact);
83
+ if (parsed.changedFactIds.has(fact.id)) {
84
+ await ctx.ei.upsertFact(fact);
85
+ }
84
86
  }
85
87
  for (const topic of parsed.topics) {
86
- await ctx.ei.upsertTopic(topic);
88
+ if (parsed.changedTopicIds.has(topic.id)) {
89
+ await ctx.ei.upsertTopic(topic);
90
+ }
87
91
  }
88
92
  for (const person of parsed.people) {
89
- await ctx.ei.upsertPerson(person);
93
+ if (parsed.changedPersonIds.has(person.id)) {
94
+ await ctx.ei.upsertPerson(person);
95
+ }
90
96
  }
91
97
 
92
98
  const deleteCount = parsed.deletedFactIds.length +
93
99
  parsed.deletedTopicIds.length +
94
100
  parsed.deletedPersonIds.length;
95
- const updateCount = parsed.facts.length +
96
- parsed.topics.length +
97
- parsed.people.length;
101
+ const updateCount = parsed.changedFactIds.size +
102
+ parsed.changedTopicIds.size +
103
+ parsed.changedPersonIds.size;
98
104
 
99
105
  ctx.showNotification(`Updated ${updateCount} items, deleted ${deleteCount}`, "info");
100
106
  return;
@@ -0,0 +1,66 @@
1
+ import YAML from "yaml";
2
+ import type { Message } from "../../../src/core/types.js";
3
+ import { ContextStatus } from "../../../src/core/types.js";
4
+
5
+ interface EditableMessage {
6
+ id: string;
7
+ role: "human" | "system";
8
+ timestamp: string;
9
+ context_status: ContextStatus;
10
+ _delete?: boolean;
11
+ content?: string;
12
+ silence_reason?: string;
13
+ }
14
+
15
+ function getContent(m: { content?: string; verbal_response?: string; action_response?: string }): string {
16
+ if (m.content) return m.content;
17
+ const parts: string[] = [];
18
+ if (m.action_response) parts.push(`_${m.action_response}_`);
19
+ if (m.verbal_response) parts.push(m.verbal_response);
20
+ return parts.join('\n\n');
21
+ }
22
+
23
+ export function contextToYAML(messages: Message[]): string {
24
+ const header = [
25
+ "# context_status: default | always | never",
26
+ "# _delete: true — permanently removes the message",
27
+ "# content | silence_reason",
28
+ ].join("\n");
29
+
30
+ const data: EditableMessage[] = messages.map((m) => ({
31
+ id: m.id,
32
+ role: m.role,
33
+ timestamp: m.timestamp,
34
+ context_status: m.context_status,
35
+ _delete: false,
36
+ content: getContent(m) || undefined,
37
+ silence_reason: m.silence_reason,
38
+ }));
39
+
40
+ return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
41
+ }
42
+
43
+ export interface ContextYAMLResult {
44
+ messages: Array<{ id: string; context_status: ContextStatus }>;
45
+ deletedMessageIds: string[];
46
+ }
47
+
48
+ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
49
+ const data = YAML.parse(yamlContent) as EditableMessage[];
50
+
51
+ const deletedMessageIds: string[] = [];
52
+ const messages: Array<{ id: string; context_status: ContextStatus }> = [];
53
+
54
+ for (const m of data ?? []) {
55
+ if (m._delete) {
56
+ deletedMessageIds.push(m.id);
57
+ } else {
58
+ const normalized = (m.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
59
+ messages.push({ id: m.id, context_status: normalized });
60
+ }
61
+ }
62
+
63
+ return { messages, deletedMessageIds };
64
+ }
65
+
66
+
@@ -0,0 +1,274 @@
1
+ import YAML from "yaml";
2
+ import type {
3
+ HumanEntity,
4
+ Fact,
5
+ Topic,
6
+ Person,
7
+ PersonIdentifier,
8
+ } from "../../../src/core/types.js";
9
+ import { BUILT_IN_FACT_NAMES } from "../../../src/core/constants/built-in-facts.js";
10
+ import { BUILT_IN_IDENTIFIER_TYPES } from "../../../src/core/constants/built-in-identifier-types.js";
11
+
12
+ interface EditableTopic extends Omit<Topic, 'persona_groups'> {
13
+ _delete?: boolean;
14
+ persona_groups?: Record<string, boolean>[];
15
+ }
16
+
17
+ interface EditableFact extends Omit<Fact, 'persona_groups'> {
18
+ _delete?: boolean;
19
+ persona_groups?: Record<string, boolean>[];
20
+ }
21
+
22
+ interface YAMLPersonIdentifier {
23
+ type: string;
24
+ value: string;
25
+ primary?: true;
26
+ }
27
+
28
+ interface EditablePersonYAML extends Omit<Person, 'identifiers' | 'persona_groups'> {
29
+ identifiers: YAMLPersonIdentifier[];
30
+ _delete?: boolean;
31
+ persona_groups?: Record<string, boolean>[];
32
+ }
33
+
34
+ interface EditableHumanData {
35
+ facts: EditableFact[];
36
+ topics: EditableTopic[];
37
+ people: EditablePersonYAML[];
38
+ }
39
+
40
+ type WithReadOnlyFields = {
41
+ learned_on?: string;
42
+ learned_by?: string;
43
+ validated_date?: string;
44
+ last_mentioned?: string;
45
+ last_updated: string;
46
+ last_changed_by?: string;
47
+ };
48
+
49
+ function readOnlyToEnd<T extends WithReadOnlyFields>(item: T): T {
50
+ const { learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by, ...rest } = item;
51
+ return { ...rest, learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by } as T;
52
+ }
53
+
54
+ function buildGroupCheckboxMap(itemGroups: string[], allGroups: string[]): Record<string, boolean>[] {
55
+ const activeSet = new Set(itemGroups);
56
+ return [...new Set([...allGroups, ...itemGroups])].map(g => ({ [g]: activeSet.has(g) }));
57
+ }
58
+
59
+ function toYAMLIdentifiers(identifiers: PersonIdentifier[], personaLookup?: Map<string, string>): YAMLPersonIdentifier[] {
60
+ return identifiers.map(({ type, value, is_primary }) => {
61
+ const resolvedValue = type === 'Ei Persona' ? (personaLookup?.get(value) ?? value) : value;
62
+ const entry: YAMLPersonIdentifier = { type, value: resolvedValue };
63
+ if (is_primary) entry.primary = true;
64
+ return entry;
65
+ });
66
+ }
67
+
68
+ function knownTypesComment(personaLookup?: Map<string, string>): string {
69
+ const lines = [`# Valid types: ${BUILT_IN_IDENTIFIER_TYPES.join(', ')}`];
70
+ if (personaLookup && personaLookup.size > 0) {
71
+ lines.push(`# Personas: ${Array.from(personaLookup.values()).join(', ')}`);
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+
76
+ function parseGroupCheckboxMap(groups: Record<string, boolean>[] | undefined): string[] {
77
+ if (!groups) return [];
78
+ const result: string[] = [];
79
+ for (const record of groups) {
80
+ for (const [name, active] of Object.entries(record)) {
81
+ if (active) result.push(name);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>, allGroups: string[] = []): string {
88
+ const data: EditableHumanData = {
89
+ facts: human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
90
+ topics: human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
91
+ people: human.people.map(p => {
92
+ const { identifiers, interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(p);
93
+ return {
94
+ ...rest,
95
+ persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups),
96
+ identifiers: toYAMLIdentifiers(identifiers ?? [], personaLookup),
97
+ _delete: false as const,
98
+ };
99
+ }),
100
+ };
101
+
102
+ const personComment = knownTypesComment(personaLookup);
103
+
104
+ return YAML.stringify(data, {
105
+ lineWidth: 0,
106
+ })
107
+ .replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
108
+ .replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
109
+ const trimmed = val.trim();
110
+ const displayName = personaLookup?.get(trimmed) ?? trimmed;
111
+ return `${indent}# [read-only] ${key}${displayName}`;
112
+ })
113
+ .replace(/^(\s+)(validated_date: .+)$/mg, '$1# [read-only] $2')
114
+ .replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
115
+ .replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
116
+ .replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
117
+ const trimmed = val.trim();
118
+ const displayName = personaLookup?.get(trimmed) ?? trimmed;
119
+ return `${indent}# [read-only] ${key}${displayName}`;
120
+ })
121
+ .replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
122
+ return `${indent}${personComment}\n${indent}identifiers:`;
123
+ });
124
+ }
125
+
126
+ export interface HumanYAMLResult {
127
+ facts: Fact[];
128
+ topics: Topic[];
129
+ people: Person[];
130
+ deletedFactIds: string[];
131
+ deletedTopicIds: string[];
132
+ deletedPersonIds: string[];
133
+ changedFactIds: Set<string>;
134
+ changedTopicIds: Set<string>;
135
+ changedPersonIds: Set<string>;
136
+ }
137
+
138
+ function identifiersEqual(a: PersonIdentifier[] | undefined, b: PersonIdentifier[] | undefined): boolean {
139
+ const normalize = (ids: PersonIdentifier[] | undefined) =>
140
+ [...(ids ?? [])].sort((x, y) => `${x.type}:${x.value}`.localeCompare(`${y.type}:${y.value}`));
141
+ const na = normalize(a);
142
+ const nb = normalize(b);
143
+ if (na.length !== nb.length) return false;
144
+ return na.every((id, i) =>
145
+ id.type === nb[i].type &&
146
+ id.value === nb[i].value &&
147
+ Boolean(id.is_primary) === Boolean(nb[i].is_primary)
148
+ );
149
+ }
150
+
151
+ function groupsEqual(a: string[] | undefined, b: string[] | undefined): boolean {
152
+ const sa = [...(a ?? [])].sort();
153
+ const sb = [...(b ?? [])].sort();
154
+ if (sa.length !== sb.length) return false;
155
+ return sa.every((v, i) => v === sb[i]);
156
+ }
157
+
158
+ function factChanged(parsed: Fact, original: Fact): boolean {
159
+ const scalarFields: (keyof Fact)[] = [
160
+ 'name', 'description', 'sentiment',
161
+ ];
162
+ for (const field of scalarFields) {
163
+ if (parsed[field] !== original[field]) return true;
164
+ }
165
+ return !groupsEqual(parsed.persona_groups, original.persona_groups);
166
+ }
167
+
168
+ function topicChanged(parsed: Topic, original: Topic): boolean {
169
+ const scalarFields: (keyof Topic)[] = [
170
+ 'name', 'description', 'sentiment', 'exposure_current', 'exposure_desired', 'category',
171
+ ];
172
+ for (const field of scalarFields) {
173
+ if (parsed[field] !== original[field]) return true;
174
+ }
175
+ return !groupsEqual(parsed.persona_groups, original.persona_groups);
176
+ }
177
+
178
+ function personChanged(parsed: Person, original: Person): boolean {
179
+ const scalarFields: (keyof Person)[] = [
180
+ 'name', 'description', 'sentiment', 'relationship',
181
+ 'exposure_current', 'exposure_desired',
182
+ ];
183
+ for (const field of scalarFields) {
184
+ if (parsed[field] !== original[field]) return true;
185
+ }
186
+ if (!groupsEqual(parsed.persona_groups, original.persona_groups)) return true;
187
+ return !identifiersEqual(parsed.identifiers, original.identifiers);
188
+ }
189
+
190
+ export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
191
+ const stripped = yamlContent
192
+ .split('\n')
193
+ .filter(line => !/^\s*#\s*\[read-only\]/.test(line))
194
+ .join('\n');
195
+ const data = YAML.parse(stripped) as EditableHumanData;
196
+
197
+ const deletedFactIds: string[] = [];
198
+ const deletedTopicIds: string[] = [];
199
+ const deletedPersonIds: string[] = [];
200
+ const changedFactIds = new Set<string>();
201
+ const changedTopicIds = new Set<string>();
202
+ const changedPersonIds = new Set<string>();
203
+
204
+ const facts: Fact[] = [];
205
+ for (const f of data.facts ?? []) {
206
+ if (f._delete && !BUILT_IN_FACT_NAMES.has(f.name)) {
207
+ deletedFactIds.push(f.id);
208
+ } else {
209
+ const { _delete, persona_groups: groupMap, ...parsed } = f;
210
+ const originalFact = original?.facts.find(of => of.id === parsed.id);
211
+ const fact: Fact = originalFact
212
+ ? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
213
+ : { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
214
+ facts.push(fact);
215
+ if (!originalFact || factChanged(fact, originalFact)) {
216
+ if (fact.description && !originalFact?.validated_date) {
217
+ fact.validated_date = new Date().toISOString();
218
+ }
219
+ changedFactIds.add(fact.id);
220
+ }
221
+ }
222
+ }
223
+
224
+ const topics: Topic[] = [];
225
+ for (const t of data.topics ?? []) {
226
+ if (t._delete) {
227
+ deletedTopicIds.push(t.id);
228
+ } else {
229
+ const { _delete, persona_groups: groupMap, ...parsed } = t;
230
+ const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
231
+ const topic: Topic = originalTopic
232
+ ? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
233
+ : { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
234
+ topics.push(topic);
235
+ if (!originalTopic || topicChanged(topic, originalTopic)) {
236
+ changedTopicIds.add(topic.id);
237
+ }
238
+ }
239
+ }
240
+
241
+ const people: Person[] = [];
242
+ for (const p of data.people ?? []) {
243
+ if (p._delete) {
244
+ deletedPersonIds.push(p.id);
245
+ } else {
246
+ const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
247
+ const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
248
+ type,
249
+ value,
250
+ ...(primary ? { is_primary: true } : {}),
251
+ }));
252
+ const originalPerson = original?.people.find(op => op.id === parsed.id);
253
+ const person: Person = originalPerson
254
+ ? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
255
+ : { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
256
+ people.push(person);
257
+ if (!originalPerson || personChanged(person, originalPerson)) {
258
+ changedPersonIds.add(person.id);
259
+ }
260
+ }
261
+ }
262
+
263
+ return {
264
+ facts,
265
+ topics,
266
+ people,
267
+ deletedFactIds,
268
+ deletedTopicIds,
269
+ deletedPersonIds,
270
+ changedFactIds,
271
+ changedTopicIds,
272
+ changedPersonIds,
273
+ };
274
+ }