ei-tui 0.5.4 → 0.6.1
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 +1 -1
- package/src/core/constants/built-in-identifier-types.ts +24 -0
- package/src/core/embedding-service.ts +24 -1
- package/src/core/handlers/dedup.ts +34 -4
- package/src/core/handlers/heartbeat.ts +16 -0
- package/src/core/handlers/human-extraction.ts +201 -7
- package/src/core/handlers/human-matching.ts +71 -22
- package/src/core/handlers/index.ts +52 -14
- package/src/core/handlers/persona-generation.ts +2 -0
- package/src/core/handlers/persona-response.ts +37 -22
- package/src/core/handlers/persona-topics.ts +35 -271
- package/src/core/handlers/rewrite.ts +3 -0
- package/src/core/handlers/rooms.ts +41 -20
- package/src/core/handlers/utils.ts +10 -8
- package/src/core/heartbeat-manager.ts +60 -2
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +3 -2
- package/src/core/orchestrators/ceremony.ts +54 -144
- package/src/core/orchestrators/dedup-phase.ts +0 -199
- package/src/core/orchestrators/extraction-chunker.ts +8 -3
- package/src/core/orchestrators/human-extraction.ts +37 -85
- package/src/core/orchestrators/index.ts +4 -8
- package/src/core/orchestrators/person-migration.ts +55 -0
- package/src/core/orchestrators/persona-topics.ts +64 -89
- package/src/core/orchestrators/room-extraction.ts +34 -0
- package/src/core/persona-manager.ts +21 -2
- package/src/core/personas/opencode-agent.ts +1 -0
- package/src/core/processor.ts +51 -14
- package/src/core/prompt-context-builder.ts +38 -5
- package/src/core/queue-processor.ts +4 -2
- package/src/core/room-manager.ts +6 -7
- package/src/core/state/human.ts +6 -0
- package/src/core/state/personas.ts +35 -10
- package/src/core/state/rooms.ts +21 -0
- package/src/core/state-manager.ts +61 -0
- package/src/core/types/data-items.ts +12 -0
- package/src/core/types/entities.ts +3 -0
- package/src/core/types/enums.ts +2 -7
- package/src/core/types/llm.ts +2 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +19 -0
- package/src/core/utils/index.ts +2 -1
- package/src/core/utils/levenshtein.ts +18 -0
- package/src/integrations/claude-code/importer.ts +1 -0
- package/src/integrations/cursor/importer.ts +1 -0
- package/src/prompts/ceremony/index.ts +1 -0
- package/src/prompts/ceremony/person-migration.ts +77 -0
- package/src/prompts/ceremony/rewrite.ts +1 -1
- package/src/prompts/ceremony/user-dedup.ts +15 -1
- package/src/prompts/heartbeat/check.ts +28 -12
- package/src/prompts/heartbeat/ei.ts +2 -0
- package/src/prompts/heartbeat/types.ts +12 -0
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +58 -14
- package/src/prompts/human/person-update.ts +171 -96
- package/src/prompts/human/topic-update.ts +1 -1
- package/src/prompts/human/types.ts +5 -1
- package/src/prompts/index.ts +3 -10
- package/src/prompts/message-utils.ts +9 -23
- package/src/prompts/persona/index.ts +3 -10
- package/src/prompts/persona/topics-rate.ts +95 -0
- package/src/prompts/persona/types.ts +8 -48
- package/src/prompts/response/index.ts +3 -7
- package/src/prompts/response/sections.ts +7 -57
- package/src/prompts/room/index.ts +1 -1
- package/src/prompts/room/sections.ts +8 -31
- package/tui/src/commands/me.tsx +14 -7
- package/tui/src/commands/persona.tsx +120 -83
- package/tui/src/components/MessageList.tsx +9 -4
- package/tui/src/components/RoomMessageList.tsx +10 -5
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/cyp-editor.tsx +13 -8
- package/tui/src/util/yaml-context.ts +66 -0
- package/tui/src/util/yaml-human.ts +274 -0
- package/tui/src/util/yaml-persona.ts +479 -0
- package/tui/src/util/yaml-provider.ts +215 -0
- package/tui/src/util/yaml-queue.ts +81 -0
- package/tui/src/util/yaml-quotes.ts +46 -0
- package/tui/src/util/yaml-serializers.ts +9 -1417
- package/tui/src/util/yaml-settings.ts +223 -0
- package/tui/src/util/yaml-shared.ts +32 -0
- package/tui/src/util/yaml-toolkit.ts +55 -0
- package/src/prompts/human/person-match.ts +0 -65
- package/src/prompts/persona/topics-match.ts +0 -70
- package/src/prompts/persona/topics-scan.ts +0 -98
- package/src/prompts/persona/topics-update.ts +0 -154
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type {
|
|
3
|
+
HumanSettings,
|
|
4
|
+
CeremonyConfig,
|
|
5
|
+
OpenCodeSettings,
|
|
6
|
+
ProviderAccount,
|
|
7
|
+
} from "../../../src/core/types.js";
|
|
8
|
+
import type { ClaudeCodeSettings } from "../../../src/integrations/claude-code/types.js";
|
|
9
|
+
import type { CursorSettings } from "../../../src/integrations/cursor/types.js";
|
|
10
|
+
import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
|
|
11
|
+
|
|
12
|
+
interface EditableSettingsData {
|
|
13
|
+
default_model?: string | null;
|
|
14
|
+
oneshot_model?: string | null;
|
|
15
|
+
rewrite_model?: string | null;
|
|
16
|
+
time_mode?: "24h" | "12h" | "local" | "utc" | null;
|
|
17
|
+
name_display?: string | null;
|
|
18
|
+
default_heartbeat_ms?: number | null;
|
|
19
|
+
default_context_window_hours?: number | null;
|
|
20
|
+
message_min_count?: number | null;
|
|
21
|
+
message_max_age_days?: number | null;
|
|
22
|
+
ceremony?: {
|
|
23
|
+
time: string;
|
|
24
|
+
decay_rate?: number | null;
|
|
25
|
+
explore_threshold?: number | null;
|
|
26
|
+
dedup_threshold?: number | null;
|
|
27
|
+
event_window_hours?: number | null;
|
|
28
|
+
};
|
|
29
|
+
opencode?: {
|
|
30
|
+
integration?: boolean | null;
|
|
31
|
+
polling_interval_ms?: number | null;
|
|
32
|
+
last_sync?: string | null;
|
|
33
|
+
extraction_point?: string | null;
|
|
34
|
+
extraction_model?: string | null;
|
|
35
|
+
};
|
|
36
|
+
claudeCode?: {
|
|
37
|
+
integration?: boolean | null;
|
|
38
|
+
polling_interval_ms?: number | null;
|
|
39
|
+
last_sync?: string | null;
|
|
40
|
+
extraction_point?: string | null;
|
|
41
|
+
extraction_model?: string | null;
|
|
42
|
+
};
|
|
43
|
+
cursor?: {
|
|
44
|
+
integration?: boolean | null;
|
|
45
|
+
polling_interval_ms?: number | null;
|
|
46
|
+
last_sync?: string | null;
|
|
47
|
+
extraction_point?: string | null;
|
|
48
|
+
};
|
|
49
|
+
backup?: {
|
|
50
|
+
enabled?: boolean | null;
|
|
51
|
+
max_backups?: number | null;
|
|
52
|
+
interval_ms?: number | null;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function settingsToYAML(settings: HumanSettings | undefined, accounts: ProviderAccount[]): string {
|
|
57
|
+
const guidToDisplay = (guid: string | undefined | null): string | null => {
|
|
58
|
+
if (!guid) return null;
|
|
59
|
+
return modelGuidToDisplay(guid, accounts);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const data: EditableSettingsData = {
|
|
63
|
+
default_model: guidToDisplay(settings?.default_model),
|
|
64
|
+
oneshot_model: guidToDisplay(settings?.oneshot_model),
|
|
65
|
+
rewrite_model: guidToDisplay(settings?.rewrite_model),
|
|
66
|
+
time_mode: settings?.time_mode ?? null,
|
|
67
|
+
name_display: settings?.name_display ?? null,
|
|
68
|
+
default_heartbeat_ms: settings?.default_heartbeat_ms ?? 1800000,
|
|
69
|
+
default_context_window_hours: settings?.default_context_window_hours ?? 8,
|
|
70
|
+
message_min_count: settings?.message_min_count ?? 200,
|
|
71
|
+
message_max_age_days: settings?.message_max_age_days ?? 14,
|
|
72
|
+
ceremony: {
|
|
73
|
+
time: settings?.ceremony?.time ?? "09:00",
|
|
74
|
+
decay_rate: settings?.ceremony?.decay_rate ?? null,
|
|
75
|
+
explore_threshold: settings?.ceremony?.explore_threshold ?? null,
|
|
76
|
+
dedup_threshold: settings?.ceremony?.dedup_threshold ?? null,
|
|
77
|
+
event_window_hours: settings?.ceremony?.event_window_hours ?? null,
|
|
78
|
+
},
|
|
79
|
+
opencode: {
|
|
80
|
+
integration: settings?.opencode?.integration ?? false,
|
|
81
|
+
polling_interval_ms: settings?.opencode?.polling_interval_ms ?? 60000,
|
|
82
|
+
extraction_model: guidToDisplay(settings?.opencode?.extraction_model) ?? 'default',
|
|
83
|
+
last_sync: settings?.opencode?.last_sync ?? null,
|
|
84
|
+
extraction_point: settings?.opencode?.extraction_point ?? null,
|
|
85
|
+
},
|
|
86
|
+
claudeCode: {
|
|
87
|
+
integration: settings?.claudeCode?.integration ?? false,
|
|
88
|
+
polling_interval_ms: settings?.claudeCode?.polling_interval_ms ?? 60000,
|
|
89
|
+
extraction_model: guidToDisplay(settings?.claudeCode?.extraction_model) ?? 'default',
|
|
90
|
+
last_sync: settings?.claudeCode?.last_sync ?? null,
|
|
91
|
+
extraction_point: settings?.claudeCode?.extraction_point ?? null,
|
|
92
|
+
},
|
|
93
|
+
cursor: {
|
|
94
|
+
integration: settings?.cursor?.integration ?? false,
|
|
95
|
+
polling_interval_ms: settings?.cursor?.polling_interval_ms ?? 60000,
|
|
96
|
+
last_sync: settings?.cursor?.last_sync ?? null,
|
|
97
|
+
extraction_point: settings?.cursor?.extraction_point ?? null,
|
|
98
|
+
},
|
|
99
|
+
backup: {
|
|
100
|
+
enabled: settings?.backup?.enabled ?? false,
|
|
101
|
+
max_backups: settings?.backup?.max_backups ?? 24,
|
|
102
|
+
interval_ms: settings?.backup?.interval_ms ?? 3600000,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return YAML.stringify(data, {
|
|
107
|
+
lineWidth: 0,
|
|
108
|
+
})
|
|
109
|
+
.replace(/^(\s+)(last_sync: .+)$/mg, '$1# [read-only] $2')
|
|
110
|
+
.replace(/^(\s+)(extraction_point: .+)$/mg, '$1# [read-only] $2')
|
|
111
|
+
.replace(/^(\s+)(extraction_model: .+)$/mg, '$1$2 # e.g. Anthropic:claude-haiku-4-5');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function settingsFromYAML(yamlContent: string, original: HumanSettings | undefined, accounts: ProviderAccount[]): HumanSettings {
|
|
115
|
+
const data = YAML.parse(yamlContent) as EditableSettingsData;
|
|
116
|
+
|
|
117
|
+
const nullToUndefined = <T>(value: T | null | undefined): T | undefined =>
|
|
118
|
+
value === null ? undefined : value;
|
|
119
|
+
|
|
120
|
+
const displayToGuid = (display: string | null | undefined): string | undefined => {
|
|
121
|
+
if (!display || display === 'default') return undefined;
|
|
122
|
+
return displayToModelGuid(display, accounts) ?? display;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
let ceremony: CeremonyConfig | undefined;
|
|
126
|
+
if (data.ceremony) {
|
|
127
|
+
ceremony = {
|
|
128
|
+
time: data.ceremony.time,
|
|
129
|
+
decay_rate: nullToUndefined(data.ceremony.decay_rate),
|
|
130
|
+
explore_threshold: nullToUndefined(data.ceremony.explore_threshold),
|
|
131
|
+
dedup_threshold: nullToUndefined(data.ceremony.dedup_threshold),
|
|
132
|
+
event_window_hours: nullToUndefined(data.ceremony.event_window_hours),
|
|
133
|
+
last_ceremony: original?.ceremony?.last_ceremony,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let opencode: OpenCodeSettings | undefined;
|
|
138
|
+
if (data.opencode) {
|
|
139
|
+
opencode = {
|
|
140
|
+
integration: nullToUndefined(data.opencode.integration),
|
|
141
|
+
polling_interval_ms: nullToUndefined(data.opencode.polling_interval_ms),
|
|
142
|
+
last_sync: original?.opencode?.last_sync,
|
|
143
|
+
extraction_point: original?.opencode?.extraction_point,
|
|
144
|
+
processed_sessions: original?.opencode?.processed_sessions,
|
|
145
|
+
extraction_model: displayToGuid(data.opencode.extraction_model),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let claudeCode: ClaudeCodeSettings | undefined;
|
|
150
|
+
if (data.claudeCode) {
|
|
151
|
+
claudeCode = {
|
|
152
|
+
integration: nullToUndefined(data.claudeCode.integration),
|
|
153
|
+
polling_interval_ms: nullToUndefined(data.claudeCode.polling_interval_ms),
|
|
154
|
+
last_sync: original?.claudeCode?.last_sync,
|
|
155
|
+
extraction_point: original?.claudeCode?.extraction_point,
|
|
156
|
+
processed_sessions: original?.claudeCode?.processed_sessions,
|
|
157
|
+
extraction_model: displayToGuid(data.claudeCode.extraction_model),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let cursor: CursorSettings | undefined;
|
|
162
|
+
if (data.cursor) {
|
|
163
|
+
cursor = {
|
|
164
|
+
integration: nullToUndefined(data.cursor.integration),
|
|
165
|
+
polling_interval_ms: nullToUndefined(data.cursor.polling_interval_ms),
|
|
166
|
+
last_sync: original?.cursor?.last_sync,
|
|
167
|
+
extraction_point: original?.cursor?.extraction_point,
|
|
168
|
+
processed_sessions: original?.cursor?.processed_sessions,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let backup: import('../../../src/core/types.js').BackupConfig | undefined;
|
|
173
|
+
if (data.backup) {
|
|
174
|
+
backup = {
|
|
175
|
+
enabled: nullToUndefined(data.backup.enabled),
|
|
176
|
+
max_backups: nullToUndefined(data.backup.max_backups),
|
|
177
|
+
interval_ms: nullToUndefined(data.backup.interval_ms),
|
|
178
|
+
last_backup: original?.backup?.last_backup,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...original,
|
|
184
|
+
default_model: displayToGuid(data.default_model),
|
|
185
|
+
oneshot_model: displayToGuid(data.oneshot_model),
|
|
186
|
+
rewrite_model: displayToGuid(data.rewrite_model),
|
|
187
|
+
time_mode: nullToUndefined(data.time_mode),
|
|
188
|
+
name_display: nullToUndefined(data.name_display),
|
|
189
|
+
default_heartbeat_ms: nullToUndefined(data.default_heartbeat_ms),
|
|
190
|
+
default_context_window_hours: nullToUndefined(data.default_context_window_hours),
|
|
191
|
+
message_min_count: nullToUndefined(data.message_min_count),
|
|
192
|
+
message_max_age_days: nullToUndefined(data.message_max_age_days),
|
|
193
|
+
ceremony,
|
|
194
|
+
opencode,
|
|
195
|
+
claudeCode,
|
|
196
|
+
cursor,
|
|
197
|
+
backup,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function validateModelProvider(
|
|
202
|
+
modelSpec: string | undefined,
|
|
203
|
+
accounts: ProviderAccount[]
|
|
204
|
+
): string | undefined {
|
|
205
|
+
if (!modelSpec) return undefined;
|
|
206
|
+
|
|
207
|
+
const colonIdx = modelSpec.indexOf(":");
|
|
208
|
+
const providerPart = colonIdx >= 0 ? modelSpec.substring(0, colonIdx) : modelSpec;
|
|
209
|
+
const modelPart = colonIdx >= 0 ? modelSpec.substring(colonIdx + 1) : undefined;
|
|
210
|
+
|
|
211
|
+
const match = accounts.find(a => a.name.toLowerCase() === providerPart.toLowerCase());
|
|
212
|
+
|
|
213
|
+
if (!match) {
|
|
214
|
+
const available = accounts.map(a => a.name).join(", ");
|
|
215
|
+
throw new Error(
|
|
216
|
+
available
|
|
217
|
+
? `No provider named "${providerPart}". Available: ${available}`
|
|
218
|
+
: `No provider named "${providerPart}". Create one with /provider new`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return modelPart ? `${match.name}:${modelPart}` : match.name;
|
|
223
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// GUID <-> DISPLAY NAME HELPERS
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import type { ProviderAccount } from "../../../src/core/types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert a model GUID to "ProviderName:modelName" display string.
|
|
9
|
+
* Falls back to the raw GUID if the model is not found.
|
|
10
|
+
*/
|
|
11
|
+
export function modelGuidToDisplay(guid: string, accounts: ProviderAccount[]): string {
|
|
12
|
+
for (const account of accounts) {
|
|
13
|
+
const model = (account.models ?? []).find(m => m.id === guid);
|
|
14
|
+
if (model) return `${account.name}:${model.name}`;
|
|
15
|
+
}
|
|
16
|
+
return guid; // fallback: return raw GUID if not found
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve "ProviderName:modelName" display string back to a model GUID.
|
|
21
|
+
* Returns undefined if no matching provider+model is found.
|
|
22
|
+
* Handles colons in model names by treating everything after the first colon as the model name.
|
|
23
|
+
*/
|
|
24
|
+
export function displayToModelGuid(display: string, accounts: ProviderAccount[]): string | undefined {
|
|
25
|
+
const colonIdx = display.indexOf(':');
|
|
26
|
+
if (colonIdx < 0) return undefined;
|
|
27
|
+
const providerName = display.substring(0, colonIdx);
|
|
28
|
+
const modelName = display.substring(colonIdx + 1);
|
|
29
|
+
const account = accounts.find(a => a.name === providerName);
|
|
30
|
+
const model = (account?.models ?? []).find(m => m.name === modelName);
|
|
31
|
+
return model?.id;
|
|
32
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type { ToolProvider, ToolDefinition } from "../../../src/core/types.js";
|
|
3
|
+
|
|
4
|
+
interface EditableToolkitData {
|
|
5
|
+
display_name: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
config: Record<string, string>;
|
|
8
|
+
tools?: Record<string, boolean>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toolkitToYAML(provider: ToolProvider, tools: ToolDefinition[]): string {
|
|
12
|
+
const toolsMap = tools.length > 0
|
|
13
|
+
? Object.fromEntries(tools.map(t => [t.display_name, t.enabled]))
|
|
14
|
+
: undefined;
|
|
15
|
+
if (provider.builtin) {
|
|
16
|
+
return YAML.stringify({ enabled: provider.enabled, tools: toolsMap }, { lineWidth: 0 });
|
|
17
|
+
}
|
|
18
|
+
const data: EditableToolkitData = {
|
|
19
|
+
display_name: provider.display_name,
|
|
20
|
+
enabled: provider.enabled,
|
|
21
|
+
config: { ...provider.config },
|
|
22
|
+
tools: toolsMap,
|
|
23
|
+
};
|
|
24
|
+
return YAML.stringify(data, { lineWidth: 0 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToolkitYAMLResult {
|
|
28
|
+
updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>>;
|
|
29
|
+
toolUpdates: Array<{ id: string; enabled: boolean }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toolkitFromYAML(yamlContent: string, original: ToolProvider, tools: ToolDefinition[]): ToolkitYAMLResult {
|
|
33
|
+
const data = YAML.parse(yamlContent) as EditableToolkitData;
|
|
34
|
+
|
|
35
|
+
if (!data.display_name) {
|
|
36
|
+
if (!original.display_name) throw new Error("display_name is required");
|
|
37
|
+
data.display_name = original.display_name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>> = {
|
|
41
|
+
display_name: data.display_name,
|
|
42
|
+
enabled: data.enabled ?? original.enabled,
|
|
43
|
+
config: data.config ?? {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const toolUpdates: Array<{ id: string; enabled: boolean }> = [];
|
|
47
|
+
if (data.tools) {
|
|
48
|
+
for (const [displayName, enabled] of Object.entries(data.tools)) {
|
|
49
|
+
const tool = tools.find(t => t.display_name === displayName);
|
|
50
|
+
if (tool) toolUpdates.push({ id: tool.id, enabled: Boolean(enabled) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { updates, toolUpdates };
|
|
55
|
+
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { PromptOutput } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export interface PersonMatchPromptData {
|
|
4
|
-
candidate_name: string;
|
|
5
|
-
candidate_description: string;
|
|
6
|
-
candidate_relationship: string;
|
|
7
|
-
existing_people: Array<{
|
|
8
|
-
id: string;
|
|
9
|
-
name: string;
|
|
10
|
-
description: string;
|
|
11
|
-
relationship?: string;
|
|
12
|
-
}>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildPersonMatchPrompt(data: PersonMatchPromptData): PromptOutput {
|
|
16
|
-
if (!data.candidate_name) {
|
|
17
|
-
throw new Error("buildPersonMatchPrompt: candidate_name is required");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const system = `# Task
|
|
21
|
-
|
|
22
|
-
You are checking if a PERSON already exists in our database.
|
|
23
|
-
|
|
24
|
-
## Matching Rules
|
|
25
|
-
|
|
26
|
-
1. **Exact match**: Same person by name or clear identity → return their ID
|
|
27
|
-
2. **Similar match**: Same person referred to differently ("Mom" vs "Carol", "my boss" vs "Trumble") → return their ID
|
|
28
|
-
3. **No match**: Genuinely new person → return "new"
|
|
29
|
-
|
|
30
|
-
Be conservative. If you're unsure, return "new" — a duplicate is worse than a gap.
|
|
31
|
-
|
|
32
|
-
# Existing People
|
|
33
|
-
|
|
34
|
-
\`\`\`json
|
|
35
|
-
${JSON.stringify(data.existing_people, null, 2)}
|
|
36
|
-
\`\`\`
|
|
37
|
-
|
|
38
|
-
# Response Format
|
|
39
|
-
|
|
40
|
-
Return ONLY the ID of the matching entry, or "new".
|
|
41
|
-
|
|
42
|
-
\`\`\`json
|
|
43
|
-
{
|
|
44
|
-
"matched_guid": "uuid-of-matching-entry" | "new"
|
|
45
|
-
}
|
|
46
|
-
\`\`\`
|
|
47
|
-
|
|
48
|
-
**Return JSON only.**`;
|
|
49
|
-
|
|
50
|
-
const user = `# Candidate Person
|
|
51
|
-
|
|
52
|
-
Name: ${data.candidate_name}
|
|
53
|
-
Description: ${data.candidate_description}
|
|
54
|
-
Relationship: ${data.candidate_relationship}
|
|
55
|
-
|
|
56
|
-
Find the best match in existing people, or return "new" if this is a genuinely new person.
|
|
57
|
-
|
|
58
|
-
\`\`\`json
|
|
59
|
-
{
|
|
60
|
-
"matched_guid": "..." | "new"
|
|
61
|
-
}
|
|
62
|
-
\`\`\``;
|
|
63
|
-
|
|
64
|
-
return { system, user };
|
|
65
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { PersonaTopicMatchPromptData, PromptOutput } from "./types.js";
|
|
2
|
-
import type { PersonaTopic } from "../../core/types.js";
|
|
3
|
-
|
|
4
|
-
function formatExistingTopics(topics: PersonaTopic[]): string {
|
|
5
|
-
if (topics.length === 0) return "[]";
|
|
6
|
-
|
|
7
|
-
return JSON.stringify(topics.map(t => ({
|
|
8
|
-
id: t.id,
|
|
9
|
-
name: t.name,
|
|
10
|
-
perspective: t.perspective.substring(0, 100) + (t.perspective.length > 100 ? '...' : ''),
|
|
11
|
-
})), null, 2);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function buildPersonaTopicMatchPrompt(data: PersonaTopicMatchPromptData): PromptOutput {
|
|
15
|
-
if (!data.persona_name || !data.candidate) {
|
|
16
|
-
throw new Error("buildPersonaTopicMatchPrompt: persona_name and candidate are required");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const personaName = data.persona_name;
|
|
20
|
-
|
|
21
|
-
const system = `# Task
|
|
22
|
-
|
|
23
|
-
You are checking if a topic candidate already exists in ${personaName}'s topic list.
|
|
24
|
-
|
|
25
|
-
# Matching Rules
|
|
26
|
-
|
|
27
|
-
1. **Exact match**: Same topic name → action "match", return its ID
|
|
28
|
-
2. **Similar match**: Clearly the same topic with different wording → action "match", return its ID
|
|
29
|
-
- "Steam Deck" and "Steam Deck Modding" → MATCH (same core topic)
|
|
30
|
-
- "Cooking" and "Italian Cooking" → consider MATCH if the specific is part of the general
|
|
31
|
-
3. **No match**: Genuinely different topic → action "create"
|
|
32
|
-
4. **Skip**: Trivial, off-topic, or too vague to be a meaningful persona topic → action "skip"
|
|
33
|
-
|
|
34
|
-
# Existing Topics
|
|
35
|
-
|
|
36
|
-
\`\`\`json
|
|
37
|
-
${formatExistingTopics(data.existing_topics)}
|
|
38
|
-
\`\`\`
|
|
39
|
-
|
|
40
|
-
# Response Format
|
|
41
|
-
|
|
42
|
-
\`\`\`json
|
|
43
|
-
{
|
|
44
|
-
"action": "match" | "create" | "skip",
|
|
45
|
-
"matched_id": "uuid-of-matching-topic" | null,
|
|
46
|
-
"reason": "brief explanation"
|
|
47
|
-
}
|
|
48
|
-
\`\`\`
|
|
49
|
-
|
|
50
|
-
**Return JSON only.**`;
|
|
51
|
-
|
|
52
|
-
const user = `# Candidate Topic
|
|
53
|
-
|
|
54
|
-
Name: ${data.candidate.name}
|
|
55
|
-
Message Count: ${data.candidate.message_count}
|
|
56
|
-
Sentiment Signal: ${data.candidate.sentiment_signal}
|
|
57
|
-
|
|
58
|
-
Find the best match in ${personaName}'s existing topics, or decide if this should be created or skipped.
|
|
59
|
-
|
|
60
|
-
**Return JSON:**
|
|
61
|
-
\`\`\`json
|
|
62
|
-
{
|
|
63
|
-
"action": "match" | "create" | "skip",
|
|
64
|
-
"matched_id": "..." | null,
|
|
65
|
-
"reason": "..."
|
|
66
|
-
}
|
|
67
|
-
\`\`\``;
|
|
68
|
-
|
|
69
|
-
return { system, user };
|
|
70
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { PersonaTopicScanPromptData, PromptOutput } from "./types.js";
|
|
2
|
-
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
3
|
-
|
|
4
|
-
export function buildPersonaTopicScanPrompt(data: PersonaTopicScanPromptData): PromptOutput {
|
|
5
|
-
if (!data.persona_name) {
|
|
6
|
-
throw new Error("buildPersonaTopicScanPrompt: persona_name is required");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const personaName = data.persona_name;
|
|
10
|
-
|
|
11
|
-
const system = `# Task
|
|
12
|
-
|
|
13
|
-
You are scanning a conversation to quickly identify TOPICS that ${personaName} actively ENGAGED with.
|
|
14
|
-
|
|
15
|
-
Your ONLY job is to spot topics and count how many messages touched them. Do NOT analyze deeply. Just detect and flag.
|
|
16
|
-
|
|
17
|
-
# What Counts as Engagement
|
|
18
|
-
|
|
19
|
-
**Engagement means ${personaName}:**
|
|
20
|
-
- Discussed with knowledge or shared opinions
|
|
21
|
-
- Asked follow-up questions showing interest
|
|
22
|
-
- Offered insights or elaborated on the topic
|
|
23
|
-
- Showed genuine interest through detailed responses
|
|
24
|
-
|
|
25
|
-
**NOT engagement:**
|
|
26
|
-
- Topic mentioned only by the human (without ${personaName} responding to it)
|
|
27
|
-
- Mentioned in passing without real discussion
|
|
28
|
-
- Generic conversational acknowledgments
|
|
29
|
-
|
|
30
|
-
# What is a TOPIC
|
|
31
|
-
|
|
32
|
-
A meaningful subject or concept that ${personaName} cares about or discusses:
|
|
33
|
-
- Hobbies and interests (gaming, cooking, hiking)
|
|
34
|
-
- Current concerns (work stress, health)
|
|
35
|
-
- Media they consume (books, shows, podcasts)
|
|
36
|
-
- Ongoing projects or situations
|
|
37
|
-
- Abstract ideas they're exploring
|
|
38
|
-
|
|
39
|
-
**NOT TOPICS** (these are traits):
|
|
40
|
-
- Communication style ("talks formally")
|
|
41
|
-
- Personality patterns ("optimistic", "skeptical")
|
|
42
|
-
|
|
43
|
-
# Response Format
|
|
44
|
-
|
|
45
|
-
Return a list of topics with message counts.
|
|
46
|
-
|
|
47
|
-
\`\`\`json
|
|
48
|
-
{
|
|
49
|
-
"topics": [
|
|
50
|
-
{
|
|
51
|
-
"name": "Steam Deck Modding",
|
|
52
|
-
"message_count": 5,
|
|
53
|
-
"sentiment_signal": 0.7
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
}
|
|
57
|
-
\`\`\`
|
|
58
|
-
|
|
59
|
-
- \`name\`: Short identifier for the topic
|
|
60
|
-
- \`message_count\`: How many messages in the conversation touched this topic (CRITICAL for filtering noise)
|
|
61
|
-
- \`sentiment_signal\`: Quick read on how ${personaName} feels about it (-1.0 to 1.0)
|
|
62
|
-
|
|
63
|
-
**CRITICAL**: ONLY analyze "Most Recent Messages". Earlier conversation is context only.
|
|
64
|
-
|
|
65
|
-
**Return JSON only.**`;
|
|
66
|
-
|
|
67
|
-
const earlierSection = data.messages_context.length > 0
|
|
68
|
-
? `## Earlier Conversation (context only)
|
|
69
|
-
${formatMessagesAsPlaceholders(data.messages_context, personaName)}
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
: '';
|
|
73
|
-
|
|
74
|
-
const recentSection = `## Most Recent Messages (analyze these)
|
|
75
|
-
${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
|
|
76
|
-
|
|
77
|
-
const user = `# Conversation
|
|
78
|
-
${earlierSection}${recentSection}
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
Scan the "Most Recent Messages" for topics ${personaName} actively engaged with.
|
|
83
|
-
|
|
84
|
-
**Return JSON:**
|
|
85
|
-
\`\`\`json
|
|
86
|
-
{
|
|
87
|
-
"topics": [
|
|
88
|
-
{
|
|
89
|
-
"name": "topic name",
|
|
90
|
-
"message_count": 3,
|
|
91
|
-
"sentiment_signal": 0.5
|
|
92
|
-
}
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
\`\`\``;
|
|
96
|
-
|
|
97
|
-
return { system, user };
|
|
98
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import type { PersonaTopicUpdatePromptData, PromptOutput } from "./types.js";
|
|
2
|
-
import type { PersonaTopic, PersonaTrait } from "../../core/types.js";
|
|
3
|
-
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
4
|
-
|
|
5
|
-
function formatTraitsForPrompt(traits: PersonaTrait[]): string {
|
|
6
|
-
if (traits.length === 0) return "(No traits defined)";
|
|
7
|
-
return traits.map(t => `- ${t.name}: ${t.description}`).join('\n');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function formatExistingTopic(topic: PersonaTopic | undefined): string {
|
|
11
|
-
if (!topic) return "**NEW TOPIC** - Creating from scratch";
|
|
12
|
-
|
|
13
|
-
return `**EXISTING TOPIC** - Updating:
|
|
14
|
-
\`\`\`json
|
|
15
|
-
${JSON.stringify({
|
|
16
|
-
name: topic.name,
|
|
17
|
-
perspective: topic.perspective,
|
|
18
|
-
approach: topic.approach,
|
|
19
|
-
personal_stake: topic.personal_stake,
|
|
20
|
-
sentiment: topic.sentiment,
|
|
21
|
-
exposure_current: topic.exposure_current,
|
|
22
|
-
exposure_desired: topic.exposure_desired,
|
|
23
|
-
}, null, 2)}
|
|
24
|
-
\`\`\``;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildPersonaTopicUpdatePrompt(data: PersonaTopicUpdatePromptData): PromptOutput {
|
|
28
|
-
if (!data.persona_name || !data.candidate) {
|
|
29
|
-
throw new Error("buildPersonaTopicUpdatePrompt: persona_name and candidate are required");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const personaName = data.persona_name;
|
|
33
|
-
const isNewTopic = !data.existing_topic;
|
|
34
|
-
|
|
35
|
-
const system = `# Task
|
|
36
|
-
|
|
37
|
-
You are generating or updating a PersonaTopic for ${personaName}.
|
|
38
|
-
|
|
39
|
-
${isNewTopic ? `This is a NEW topic - create all fields from scratch based on the conversation.` : `This is an EXISTING topic - update fields based on new conversation evidence.`}
|
|
40
|
-
|
|
41
|
-
# Persona Context
|
|
42
|
-
|
|
43
|
-
**Name**: ${personaName}
|
|
44
|
-
${data.short_description ? `**Description**: ${data.short_description}` : ''}
|
|
45
|
-
${data.long_description ? `**Background**: ${data.long_description}` : ''}
|
|
46
|
-
|
|
47
|
-
**Personality Traits**:
|
|
48
|
-
${formatTraitsForPrompt(data.traits)}
|
|
49
|
-
|
|
50
|
-
# Current Topic State
|
|
51
|
-
|
|
52
|
-
${formatExistingTopic(data.existing_topic)}
|
|
53
|
-
|
|
54
|
-
# Field Definitions
|
|
55
|
-
|
|
56
|
-
## name
|
|
57
|
-
Short identifier for the topic. Only change for clarification.
|
|
58
|
-
|
|
59
|
-
## perspective
|
|
60
|
-
${personaName}'s view or opinion on this topic. What do they think about it?
|
|
61
|
-
|
|
62
|
-
**ALWAYS populate this field.** Use conversation content + persona traits to infer their view.
|
|
63
|
-
|
|
64
|
-
Example: "The Shire represents everything worth fighting for - simple pleasures, good food, and the bonds of community."
|
|
65
|
-
|
|
66
|
-
## approach
|
|
67
|
-
How ${personaName} prefers to engage with this topic. Their style of discussion.
|
|
68
|
-
|
|
69
|
-
**Only populate if there's a clear pattern** in how they discuss this topic. Leave empty string if unclear.
|
|
70
|
-
|
|
71
|
-
Example: "Frodo speaks of the Shire with wistful longing, often comparing other places to it unfavorably."
|
|
72
|
-
|
|
73
|
-
## personal_stake
|
|
74
|
-
Why this topic matters to ${personaName} personally. Their connection to it.
|
|
75
|
-
|
|
76
|
-
**Only populate if there's evidence** of personal connection. Leave empty string if unclear.
|
|
77
|
-
|
|
78
|
-
Example: "As a hobbit who left home, the Shire is both memory and motivation."
|
|
79
|
-
|
|
80
|
-
## sentiment
|
|
81
|
-
How ${personaName} feels about this topic. Scale: -1.0 (hate) to 1.0 (love).
|
|
82
|
-
|
|
83
|
-
## exposure_impact
|
|
84
|
-
How much this topic was discussed in the recent messages. Choose one:
|
|
85
|
-
- "high" — Major topic, multiple substantive exchanges
|
|
86
|
-
- "medium" — Meaningful mention, discussed with some depth
|
|
87
|
-
- "low" — Brief or passing mention
|
|
88
|
-
- "none" — Barely mentioned, tangential at best
|
|
89
|
-
|
|
90
|
-
## exposure_desired
|
|
91
|
-
How much ${personaName} wants to discuss this topic. Scale: 0.0 to 1.0.
|
|
92
|
-
|
|
93
|
-
**RARELY change** for existing topics. Only adjust if there's explicit preference signal.
|
|
94
|
-
|
|
95
|
-
**For new topics**: Infer from how enthusiastically ${personaName} engaged.
|
|
96
|
-
|
|
97
|
-
# Critical Instructions
|
|
98
|
-
|
|
99
|
-
1. ONLY analyze "Most Recent Messages" - earlier messages are context only
|
|
100
|
-
2. Do NOT invent details not supported by the conversation
|
|
101
|
-
3. Do NOT apply flowery or poetic language - be factual
|
|
102
|
-
4. If a field cannot be determined, use empty string (for text) or preserve existing value (for numbers)
|
|
103
|
-
|
|
104
|
-
**Return JSON:**
|
|
105
|
-
\`\`\`json
|
|
106
|
-
{
|
|
107
|
-
"name": "Topic Name",
|
|
108
|
-
"perspective": "Their view on this topic",
|
|
109
|
-
"approach": "",
|
|
110
|
-
"personal_stake": "",
|
|
111
|
-
"sentiment": 0.5,
|
|
112
|
-
"exposure_impact": "medium",
|
|
113
|
-
"exposure_desired": 0.5
|
|
114
|
-
}
|
|
115
|
-
\`\`\``;
|
|
116
|
-
|
|
117
|
-
const earlierSection = data.messages_context.length > 0
|
|
118
|
-
? `## Earlier Conversation (context only)
|
|
119
|
-
${formatMessagesAsPlaceholders(data.messages_context, personaName)}
|
|
120
|
-
|
|
121
|
-
`
|
|
122
|
-
: '';
|
|
123
|
-
|
|
124
|
-
const recentSection = `## Most Recent Messages (analyze these)
|
|
125
|
-
${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
|
|
126
|
-
|
|
127
|
-
const user = `# Conversation
|
|
128
|
-
${earlierSection}${recentSection}
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
# Topic Candidate
|
|
133
|
-
|
|
134
|
-
Name: ${data.candidate.name}
|
|
135
|
-
Message Count: ${data.candidate.message_count}
|
|
136
|
-
Sentiment Signal: ${data.candidate.sentiment_signal}
|
|
137
|
-
|
|
138
|
-
${isNewTopic ? 'Create' : 'Update'} the PersonaTopic based on how ${personaName} engaged with this topic.
|
|
139
|
-
|
|
140
|
-
**Return JSON:**
|
|
141
|
-
\`\`\`json
|
|
142
|
-
{
|
|
143
|
-
"name": "...",
|
|
144
|
-
"perspective": "...",
|
|
145
|
-
"approach": "...",
|
|
146
|
-
"personal_stake": "...",
|
|
147
|
-
"sentiment": 0.5,
|
|
148
|
-
"exposure_impact": "medium",
|
|
149
|
-
"exposure_desired": 0.5
|
|
150
|
-
}
|
|
151
|
-
\`\`\``;
|
|
152
|
-
|
|
153
|
-
return { system, user };
|
|
154
|
-
}
|