ei-tui 0.9.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/package.json +8 -1
- package/src/README.md +10 -26
- package/src/core/context-utils.ts +2 -2
- package/src/core/handlers/document-segmentation.ts +113 -0
- package/src/core/handlers/heartbeat.ts +9 -1
- package/src/core/handlers/human-extraction.ts +4 -1
- package/src/core/handlers/human-matching.ts +5 -53
- package/src/core/handlers/index.ts +3 -51
- package/src/core/handlers/persona-generation.ts +1 -28
- package/src/core/handlers/rewrite.ts +13 -9
- package/src/core/handlers/utils.ts +2 -9
- package/src/core/heartbeat-manager.ts +5 -5
- package/src/core/llm-client.ts +11 -1
- package/src/core/message-manager.ts +26 -23
- package/src/core/orchestrators/ceremony.ts +87 -49
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/orchestrators/human-extraction.ts +22 -18
- package/src/core/orchestrators/index.ts +0 -1
- package/src/core/orchestrators/persona-topics.ts +1 -1
- package/src/core/orchestrators/room-extraction.ts +5 -5
- package/src/core/persona-manager.ts +4 -0
- package/src/core/processor.ts +98 -22
- package/src/core/prompt-context-builder.ts +7 -6
- package/src/core/queue-manager.ts +35 -0
- package/src/core/state/personas.ts +1 -17
- package/src/core/state/queue.ts +9 -1
- package/src/core/state-manager.ts +4 -66
- package/src/core/types/entities.ts +17 -3
- package/src/core/types/enums.ts +1 -2
- package/src/core/types/integrations.ts +2 -0
- package/src/core/types/llm.ts +9 -0
- package/src/core/types/rooms.ts +1 -1
- package/src/integrations/claude-code/importer.ts +1 -1
- package/src/integrations/cursor/importer.ts +1 -1
- package/src/integrations/document/chunker.ts +88 -0
- package/src/integrations/document/importer.ts +82 -0
- package/src/integrations/document/index.ts +2 -0
- package/src/integrations/document/invoice.ts +63 -0
- package/src/integrations/document/types.ts +16 -0
- package/src/integrations/document/unsource.ts +164 -0
- package/src/integrations/opencode/importer.ts +1 -1
- package/src/integrations/persona-history/importer.ts +197 -0
- package/src/integrations/persona-history/index.ts +3 -0
- package/src/integrations/persona-history/types.ts +7 -0
- package/src/prompts/ceremony/dedup.ts +7 -3
- package/src/prompts/ceremony/index.ts +2 -11
- package/src/prompts/ceremony/people-rewrite.ts +190 -0
- package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
- package/src/prompts/ceremony/types.ts +1 -42
- package/src/prompts/generation/index.ts +0 -3
- package/src/prompts/generation/types.ts +0 -15
- package/src/prompts/heartbeat/check.ts +18 -6
- package/src/prompts/heartbeat/types.ts +2 -1
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +13 -4
- package/src/prompts/human/topic-scan.ts +16 -2
- package/src/prompts/human/topic-update.ts +36 -4
- package/src/prompts/human/types.ts +1 -16
- package/src/prompts/index.ts +0 -19
- package/src/prompts/reflection/index.ts +35 -5
- package/src/prompts/reflection/types.ts +1 -1
- package/src/prompts/response/index.ts +5 -0
- package/src/prompts/response/sections.ts +26 -0
- package/src/prompts/response/types.ts +3 -0
- package/src/storage/indexed.ts +4 -0
- package/src/storage/interface.ts +1 -0
- package/src/storage/local.ts +4 -0
- package/src/templates/emmett.ts +49 -0
- package/tui/README.md +22 -0
- package/tui/src/app.tsx +9 -6
- package/tui/src/commands/delete.tsx +7 -1
- package/tui/src/commands/import.tsx +30 -0
- package/tui/src/commands/registry.test.ts +10 -5
- package/tui/src/commands/unsource.tsx +115 -0
- package/tui/src/components/PromptInput.tsx +4 -0
- package/tui/src/components/WelcomeOverlay.tsx +58 -32
- package/tui/src/context/ei.tsx +80 -60
- package/tui/src/globals.d.ts +57 -0
- package/tui/src/index.tsx +14 -0
- package/tui/src/storage/file.ts +11 -5
- package/tui/src/util/e2e-flags.ts +4 -3
- package/tui/src/util/help-content.ts +20 -0
- package/tui/src/util/provider-detection.ts +251 -0
- package/tui/src/util/yaml-human.ts +7 -1
- package/tui/src/util/yaml-persona.ts +8 -4
- package/tui/src/util/yaml-settings.ts +3 -3
- package/src/core/orchestrators/person-migration.ts +0 -55
- package/src/prompts/ceremony/description-check.ts +0 -54
- package/src/prompts/ceremony/expire.ts +0 -37
- package/src/prompts/ceremony/explore.ts +0 -77
- package/src/prompts/ceremony/person-migration.ts +0 -77
- package/src/prompts/generation/descriptions.ts +0 -91
- package/src/prompts/human/fact-scan.ts +0 -150
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { RewriteScanPromptData, RewritePromptData } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// What belongs in a Person record (shared reference for both prompts)
|
|
5
|
+
// =============================================================================
|
|
6
|
+
//
|
|
7
|
+
// A Person record is a RELATIONSHIP PROFILE — who this person is, how they
|
|
8
|
+
// relate to the human user, and anything a persona would use to meaningfully
|
|
9
|
+
// reference them in conversation 6+ months from now.
|
|
10
|
+
//
|
|
11
|
+
// A Person record is NOT:
|
|
12
|
+
// - A project status log
|
|
13
|
+
// - A record of ticket numbers, PR numbers, or sprint assignments
|
|
14
|
+
// - A biography of their personal habits and hobbies
|
|
15
|
+
// - A shared-interest tracker (those are Topics)
|
|
16
|
+
//
|
|
17
|
+
// The test: "Would this still be true and useful if you ran into this person
|
|
18
|
+
// at a coffee shop, unrelated to any current project?"
|
|
19
|
+
|
|
20
|
+
const PERSON_CONTRACT = `A Person record is a **relationship profile** — who this person IS, how they relate to the human user, their character and communication style, and anything that makes them recognizable across time and context.
|
|
21
|
+
|
|
22
|
+
It is NOT:
|
|
23
|
+
- A project status log (ticket numbers, PR references, sprint assignments)
|
|
24
|
+
- A record of shared interests that could stand alone as a Topic
|
|
25
|
+
- Personal biography unrelated to the relationship (commute, hobbies, hometown)
|
|
26
|
+
- Technical knowledge attributed to them rather than about them
|
|
27
|
+
|
|
28
|
+
**The test**: Would this detail still be true and useful if you ran into this person at a coffee shop, unrelated to any current project, in six months?`;
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// PHASE 1: SCAN — Identify subjects that don't belong in a Person record
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export function buildPersonRewriteScanPrompt(data: RewriteScanPromptData): { system: string; user: string } {
|
|
35
|
+
const system = `You are auditing a Person record in a personal knowledge base.
|
|
36
|
+
|
|
37
|
+
${PERSON_CONTRACT}
|
|
38
|
+
|
|
39
|
+
Your job: identify **subjects buried in this description that fail the test above**.
|
|
40
|
+
|
|
41
|
+
For each subject that doesn't belong, return a short phrase (3-8 words) that describes it — specific enough to search for matching records. These phrases will be used to find existing Topics this content might belong in.
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
- Do NOT include the relationship profile itself — who they are, their role, how you know them, their character
|
|
45
|
+
- Be specific: "React performance patterns" beats "technical stuff"
|
|
46
|
+
- If the record is clean — everything in it passes the test — return an empty array
|
|
47
|
+
|
|
48
|
+
Return a raw JSON array of strings. No markdown fencing, no commentary.
|
|
49
|
+
|
|
50
|
+
Example — a Person named "Nicholas" whose description includes sprint ticket numbers:
|
|
51
|
+
["CMIDP sprint ticket assignments", "ASU Data Lake access provisioning details"]`;
|
|
52
|
+
|
|
53
|
+
const payload = JSON.stringify({
|
|
54
|
+
name: (data.item as { name?: string }).name,
|
|
55
|
+
description: data.item.description,
|
|
56
|
+
relationship: (data.item as { relationship?: string }).relationship,
|
|
57
|
+
}, null, 2);
|
|
58
|
+
|
|
59
|
+
const user = `${payload}
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
Return a raw JSON array of subject phrases found in this Person record that don't belong there. Return [] if the record is clean.`;
|
|
64
|
+
|
|
65
|
+
return { system, user };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// PHASE 2: SPLIT — Slim the Person, redistribute subjects to Topics
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
function buildPersonExistingExample(): string {
|
|
73
|
+
return `{
|
|
74
|
+
"id": "existing-uuid",
|
|
75
|
+
"type": "person",
|
|
76
|
+
"name": "Nicholas",
|
|
77
|
+
"description": "Backend engineer on the CMIDP team. Thoughtful code reviewer who flags architectural concerns — specifically around concurrency and queue isolation. Direct point of contact for Data Lake access provisioning.",
|
|
78
|
+
"relationship": "coworker"
|
|
79
|
+
}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildPersonNewTopicExample(): string {
|
|
83
|
+
return `{
|
|
84
|
+
"type": "topic",
|
|
85
|
+
"name": "CMIDP Sprint 86 work",
|
|
86
|
+
"description": "Nicholas owns 4 tickets in Sprint 86 including course list ordering bugs (CMIDP-2604, CMIDP-2441, CMIDP-2686) and course sequencing (CMIDP-2624).",
|
|
87
|
+
"sentiment": 0.5,
|
|
88
|
+
"category": "Project"
|
|
89
|
+
}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildPersonRewriteSplitPrompt(data: RewritePromptData): { system: string; user: string } {
|
|
93
|
+
const system = `You are reorganizing a Person record in a personal knowledge base.
|
|
94
|
+
|
|
95
|
+
${PERSON_CONTRACT}
|
|
96
|
+
|
|
97
|
+
An earlier scan identified subjects in this Person record that don't belong there. For each subject, we searched the knowledge base for existing Topics that might already cover it.
|
|
98
|
+
|
|
99
|
+
Your job:
|
|
100
|
+
1. **Slim the Person** — remove the identified subjects AND any other content that fails the relationship profile test (personal trivia, lifestyle details, biographical facts unrelated to the relationship). Keep only: who they are, their role, their character, how the human user knows and works with them.
|
|
101
|
+
2. **Redistribute each identified subject** — if a matching Topic exists in the search results, move the content there. If not, create a new Topic.
|
|
102
|
+
3. **Discard what isn't worth a Topic** — personal trivia (hobbies, commute, hometown) that has no standalone value doesn't need to become a Topic. Just remove it from the Person.
|
|
103
|
+
4. **Lose NO relationship data** — everything about how this person relates to the human user must survive.
|
|
104
|
+
|
|
105
|
+
Record format for the Person (MUST keep "id", type stays "person"):
|
|
106
|
+
${buildPersonExistingExample()}
|
|
107
|
+
|
|
108
|
+
Record format for a new Topic created from extracted content:
|
|
109
|
+
${buildPersonNewTopicExample()}
|
|
110
|
+
|
|
111
|
+
Rules:
|
|
112
|
+
- The original Person record (id: "${data.item.id}") MUST appear in "existing", slimmed down
|
|
113
|
+
- Person description after slimming: 2-4 sentences, relationship profile only. **If it still contains city, commute, hobbies, or lifestyle details after slimming — remove them.** Those are not relationship data.
|
|
114
|
+
- Topics created from person content: use the most appropriate category (Technical, Project, Interest, etc.)
|
|
115
|
+
- People MUST include "relationship"
|
|
116
|
+
- Topics MUST include "category"
|
|
117
|
+
- Do NOT invent information — only redistribute what exists in the original record
|
|
118
|
+
- Do NOT remove the person's relationship, role, character, or how the human user knows them — only the non-person content
|
|
119
|
+
|
|
120
|
+
**What to KEEP in the Person description**: role, expertise, *why* the human user works with them (their operational function in the relationship), how they communicate, character traits, how the human user knows them.
|
|
121
|
+
**What to REMOVE from the Person description**: current project status, ticket/PR numbers, shared interests (→ Topic), city/commute/hobbies (→ discard).
|
|
122
|
+
|
|
123
|
+
The distinction:
|
|
124
|
+
- "Data Lake bucket owner responsible for access provisioning" → KEEP (operational role in the relationship)
|
|
125
|
+
- "Currently owns 4 tickets in Sprint 86" → REMOVE (current sprint status, not who they are)
|
|
126
|
+
- "Left detailed comments on PR #1644 identifying architectural concerns around concurrency" → KEEP the insight, DROP the PR reference: "Flags architectural concerns around concurrency and queue isolation" belongs in the description; "PR #1644" does not.
|
|
127
|
+
|
|
128
|
+
Return raw JSON with exactly two keys:
|
|
129
|
+
{
|
|
130
|
+
"existing": [ /* slimmed Person + any existing Topics being updated */ ],
|
|
131
|
+
"new": [ /* new Topics for subjects with no existing match */ ]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
No markdown fencing, no commentary.`;
|
|
135
|
+
|
|
136
|
+
const subjects = data.subjects.map(s => ({
|
|
137
|
+
search_term: s.searchTerm,
|
|
138
|
+
matches: s.matches.map(m => ({
|
|
139
|
+
id: (m as { id?: string }).id,
|
|
140
|
+
name: (m as { name?: string }).name,
|
|
141
|
+
description: m.description,
|
|
142
|
+
category: (m as { category?: string }).category,
|
|
143
|
+
})),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const payload = JSON.stringify({
|
|
147
|
+
original_person: {
|
|
148
|
+
id: data.item.id,
|
|
149
|
+
name: (data.item as { name?: string }).name,
|
|
150
|
+
description: data.item.description,
|
|
151
|
+
relationship: (data.item as { relationship?: string }).relationship,
|
|
152
|
+
sentiment: data.item.sentiment,
|
|
153
|
+
},
|
|
154
|
+
subjects_to_extract: subjects,
|
|
155
|
+
}, null, 2);
|
|
156
|
+
|
|
157
|
+
const schemaReminder = `**Return JSON:**
|
|
158
|
+
\`\`\`json
|
|
159
|
+
{
|
|
160
|
+
"existing": [
|
|
161
|
+
{
|
|
162
|
+
"id": "uuid-of-person",
|
|
163
|
+
"type": "person",
|
|
164
|
+
"name": "Person Name",
|
|
165
|
+
"description": "Slimmed relationship profile only",
|
|
166
|
+
"relationship": "coworker"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"new": [
|
|
170
|
+
{
|
|
171
|
+
"type": "topic",
|
|
172
|
+
"name": "Subject Name",
|
|
173
|
+
"description": "Content extracted from person record",
|
|
174
|
+
"sentiment": 0.5,
|
|
175
|
+
"category": "Project|Technical|Interest|etc."
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
Return raw JSON only.`;
|
|
182
|
+
|
|
183
|
+
const user = `${payload}
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
${schemaReminder}`;
|
|
188
|
+
|
|
189
|
+
return { system, user };
|
|
190
|
+
}
|
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import type { RewriteScanPromptData, RewritePromptData } from "./types.js";
|
|
2
2
|
|
|
3
3
|
// =============================================================================
|
|
4
|
-
// PHASE 1: SCAN — Identify
|
|
4
|
+
// PHASE 1: SCAN — Identify subjects that don't belong in a Topic record
|
|
5
5
|
// =============================================================================
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
function stripEmbedding<T extends { embedding?: unknown }>(item: T): Omit<T, "embedding"> {
|
|
8
|
+
const { embedding: _, ...rest } = item;
|
|
9
|
+
return rest as Omit<T, "embedding">;
|
|
10
|
+
}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
export function buildTopicRewriteScanPrompt(data: RewriteScanPromptData): { system: string; user: string } {
|
|
13
|
+
const isTechnical = (data.item as { category?: string }).category === "Technical";
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
const technicalGuidance = isTechnical
|
|
16
|
+
? `
|
|
17
|
+
## Technical Topic Guidance
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
+
This is a Technical topic — a knowledge base for a specific technology, platform, or tool. Technical topics are ALLOWED to be dense and detailed.
|
|
20
|
+
|
|
21
|
+
Only flag subjects that are about a **different** technology or workflow than the one named in this record. For example:
|
|
22
|
+
- A Uniform topic containing Turborepo setup details → flag "Turborepo monorepo setup"
|
|
23
|
+
- A Uniform topic containing Vercel preview gotchas → do NOT flag (that's core Uniform knowledge)
|
|
24
|
+
- An AWS Bedrock topic containing Twilio integration details → flag "Twilio integration"
|
|
25
|
+
`
|
|
26
|
+
: "";
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
const system = `You are auditing a Topic record in a personal knowledge base. A single Topic record has grown too large because unrelated information was repeatedly added over time. The record's Name suggests its intended subject, but its Description now covers additional, unrelated subjects.
|
|
29
|
+
|
|
30
|
+
Your job: identify the **extra subjects** buried in this record that do NOT belong under the record's Name.
|
|
31
|
+
|
|
32
|
+
Rules:
|
|
33
|
+
- Do NOT include the record's primary subject (what its Name describes) — only the extra, unrelated subjects
|
|
34
|
+
- Each subject should be a succinct phrase (2-8 words) that could serve as a search query
|
|
35
|
+
- Be specific: "TypeScript coding conventions" beats "technical preferences"
|
|
36
|
+
- If the record is cohesive and on-topic despite its length, return an empty array
|
|
37
|
+
${technicalGuidance}
|
|
38
|
+
Return a raw JSON array of strings. No markdown fencing, no commentary.
|
|
21
39
|
|
|
22
40
|
Example — a Topic named "Software Engineering" whose description also discusses vim keybindings, git conventions, and AI tooling:
|
|
23
41
|
["vim keybindings and editor configuration", "git and GitHub workflow conventions", "AI coding assistant preferences"]`;
|
|
@@ -25,10 +43,10 @@ Example — a Topic named "Software Engineering" whose description also discusse
|
|
|
25
43
|
const payload = JSON.stringify(stripEmbedding(data.item), null, 2);
|
|
26
44
|
|
|
27
45
|
const schemaReminder = `**Return JSON:**
|
|
28
|
-
|
|
46
|
+
\`\`\`json
|
|
29
47
|
[
|
|
30
|
-
"
|
|
31
|
-
"git workflow conventions",
|
|
48
|
+
"vim keybindings and editor configuration",
|
|
49
|
+
"git and GitHub workflow conventions",
|
|
32
50
|
"AI coding assistant preferences"
|
|
33
51
|
]
|
|
34
52
|
\`\`\`
|
|
@@ -45,13 +63,70 @@ ${schemaReminder}`;
|
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
// =============================================================================
|
|
48
|
-
// PHASE 2:
|
|
66
|
+
// PHASE 2: SPLIT — Slim the Topic, redistribute subjects to new/existing records
|
|
49
67
|
// =============================================================================
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
function buildTopicExistingExample(): string {
|
|
70
|
+
return `Topic:
|
|
71
|
+
{
|
|
72
|
+
"id": "existing-uuid",
|
|
73
|
+
"type": "topic",
|
|
74
|
+
"name": "Topic Name",
|
|
75
|
+
"description": "Updated topic description",
|
|
76
|
+
"category": "Interest"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Person:
|
|
80
|
+
{
|
|
81
|
+
"id": "existing-uuid",
|
|
82
|
+
"type": "person",
|
|
83
|
+
"name": "Person Name",
|
|
84
|
+
"description": "Updated person description",
|
|
85
|
+
"relationship": "coworker"
|
|
86
|
+
}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildTopicNewExample(isTechnical: boolean): string {
|
|
90
|
+
const categoryHint = isTechnical
|
|
91
|
+
? `"category": "Technical" // Split topics from a Technical record inherit Technical category unless clearly different`
|
|
92
|
+
: `"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical"`;
|
|
93
|
+
|
|
94
|
+
return `Topic:
|
|
95
|
+
{
|
|
96
|
+
"type": "topic",
|
|
97
|
+
"name": "New Topic Name",
|
|
98
|
+
"description": "Concise topic description",
|
|
99
|
+
"sentiment": 0.0,
|
|
100
|
+
${categoryHint}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Person:
|
|
104
|
+
{
|
|
105
|
+
"type": "person",
|
|
106
|
+
"name": "New Person Name",
|
|
107
|
+
"description": "Concise person description",
|
|
108
|
+
"sentiment": 0.0,
|
|
109
|
+
"relationship": "friend"
|
|
110
|
+
}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildTopicRewriteSplitPrompt(data: RewritePromptData): { system: string; user: string } {
|
|
114
|
+
const isTechnical = (data.item as { category?: string }).category === "Technical";
|
|
115
|
+
|
|
116
|
+
const descriptionGuidance = isTechnical
|
|
117
|
+
? `ideally under 600 characters, never over 900 — Technical topics are knowledge bases that preserve specific gotchas, decisions, and open questions`
|
|
118
|
+
: `ideally under 300 characters, never over 500`;
|
|
53
119
|
|
|
54
|
-
const
|
|
120
|
+
const technicalCategoryNote = isTechnical
|
|
121
|
+
? `\n- Topics split from a Technical record should inherit category "Technical" unless the subject is clearly a different type (e.g., a personal interest extracted from a technical topic)`
|
|
122
|
+
: "";
|
|
123
|
+
|
|
124
|
+
const noSubjectsGate = data.subjects.length === 0
|
|
125
|
+
? `\n**IMPORTANT: No extra subjects were identified for this record. The correct response is to return the original record unchanged in "existing" with an empty "new" array. Do NOT create new records. Do NOT modify the description.**\n`
|
|
126
|
+
: "";
|
|
127
|
+
|
|
128
|
+
const system = `You are reorganizing a personal knowledge base. A Topic record has become a catch-all for several unrelated subjects. An earlier analysis identified the extra subjects, and we searched the knowledge base for potentially matching existing records.
|
|
129
|
+
${noSubjectsGate}
|
|
55
130
|
|
|
56
131
|
The search results under each subject are our **best guesses** — they may not be accurate matches. Only merge data into an existing record if the subject matter genuinely overlaps. Similar names with different meanings should produce a NEW record instead.
|
|
57
132
|
|
|
@@ -60,26 +135,26 @@ Your job:
|
|
|
60
135
|
2. **Create new records**: For subjects with no appropriate match among the search results, create a new record.
|
|
61
136
|
3. **Slim the original**: Remove all data from the original record that now lives elsewhere. The original should contain ONLY information directly relevant to its Name.
|
|
62
137
|
|
|
63
|
-
Return raw JSON with exactly two keys. No markdown fencing, no commentary
|
|
138
|
+
Return raw JSON with exactly two keys. No markdown fencing, no commentary:
|
|
64
139
|
{
|
|
65
140
|
"existing": [ /* updated records, including the slimmed-down original */ ],
|
|
66
141
|
"new": [ /* brand-new records for subjects with no match */ ]
|
|
67
142
|
}
|
|
68
143
|
|
|
69
144
|
Record format for "existing" entries (MUST include "id" and "type"):
|
|
70
|
-
${
|
|
145
|
+
${buildTopicExistingExample()}
|
|
71
146
|
|
|
72
147
|
Record format for "new" entries (NO "id" field — the system assigns one):
|
|
73
|
-
${
|
|
148
|
+
${buildTopicNewExample(isTechnical)}
|
|
74
149
|
|
|
75
150
|
Rules:
|
|
76
|
-
- The original record (id: "${data.item.id}") MUST appear in "existing", slimmed down
|
|
77
|
-
- Descriptions should be concise: ${
|
|
78
|
-
- Preserve sentiment
|
|
79
|
-
- "type" must be one of: "topic", "person"
|
|
80
|
-
- Topics MUST include "category" — one of: Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project, Event. For Event topics, the description should be a narrative account of a specific moment, not a general summary.
|
|
151
|
+
- The original record (id: "${data.item.id}") MUST appear in "existing", slimmed down
|
|
152
|
+
- Descriptions should be concise: ${descriptionGuidance}
|
|
153
|
+
- Preserve sentiment and other numeric values from the source record where applicable
|
|
154
|
+
- "type" must be one of: "topic", "person"
|
|
155
|
+
- Topics MUST include "category" — one of: Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project, Event, Technical. For Event topics, the description should be a narrative account of a specific moment, not a general summary. For Technical topics, split by distinct technical concept (e.g., "Uniform Composition Model" vs "Uniform Preview Setup") — preserve specificity over brevity
|
|
81
156
|
- People MUST include "relationship" — a short label like "coworker", "friend", "mentor", etc.
|
|
82
|
-
- Do NOT invent information. Only redistribute what exists in the original record
|
|
157
|
+
- Do NOT invent information. Only redistribute what exists in the original record${technicalCategoryNote}`;
|
|
83
158
|
|
|
84
159
|
const subjects = data.subjects.map(s => ({
|
|
85
160
|
search_term: s.searchTerm,
|
|
@@ -93,7 +168,7 @@ Rules:
|
|
|
93
168
|
};
|
|
94
169
|
|
|
95
170
|
const schemaReminder = `**Return JSON:**
|
|
96
|
-
|
|
171
|
+
\`\`\`json
|
|
97
172
|
{
|
|
98
173
|
"existing": [
|
|
99
174
|
{
|
|
@@ -123,53 +198,3 @@ ${schemaReminder}`;
|
|
|
123
198
|
|
|
124
199
|
return { system, user };
|
|
125
200
|
}
|
|
126
|
-
|
|
127
|
-
// =============================================================================
|
|
128
|
-
// Helpers
|
|
129
|
-
// =============================================================================
|
|
130
|
-
|
|
131
|
-
/** Strip embedding arrays from items before putting them in prompts — they're huge and useless to the LLM. */
|
|
132
|
-
function stripEmbedding<T extends { embedding?: unknown }>(item: T): Omit<T, "embedding"> {
|
|
133
|
-
const { embedding: _, ...rest } = item;
|
|
134
|
-
return rest as Omit<T, "embedding">;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function buildExistingExamples(): string {
|
|
138
|
-
return `Topic:
|
|
139
|
-
{
|
|
140
|
-
"id": "existing-uuid",
|
|
141
|
-
"type": "topic",
|
|
142
|
-
"name": "Topic Name",
|
|
143
|
-
"description": "Updated topic description",
|
|
144
|
-
"category": "Interest"
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
Person:
|
|
148
|
-
{
|
|
149
|
-
"id": "existing-uuid",
|
|
150
|
-
"type": "person",
|
|
151
|
-
"name": "Person Name",
|
|
152
|
-
"description": "Updated person description",
|
|
153
|
-
"relationship": "coworker"
|
|
154
|
-
}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function buildNewExamples(): string {
|
|
158
|
-
return `Topic:
|
|
159
|
-
{
|
|
160
|
-
"type": "topic",
|
|
161
|
-
"name": "New Topic Name",
|
|
162
|
-
"description": "Concise topic description",
|
|
163
|
-
"sentiment": 0.0,
|
|
164
|
-
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event"
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
Person:
|
|
168
|
-
{
|
|
169
|
-
"type": "person",
|
|
170
|
-
"name": "New Person Name",
|
|
171
|
-
"description": "Concise person description",
|
|
172
|
-
"sentiment": 0.0,
|
|
173
|
-
"relationship": "friend"
|
|
174
|
-
}`;
|
|
175
|
-
}
|
|
@@ -1,45 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export interface PersonaExpirePromptData {
|
|
4
|
-
persona_name: string;
|
|
5
|
-
topics: PersonaTopic[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface PersonaExpireResult {
|
|
9
|
-
topic_ids_to_remove: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface PersonaExplorePromptData {
|
|
13
|
-
persona_name: string;
|
|
14
|
-
traits: PersonaTrait[];
|
|
15
|
-
remaining_topics: PersonaTopic[];
|
|
16
|
-
recent_conversation_themes: string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface PersonaExploreResult {
|
|
20
|
-
new_topics: Array<{
|
|
21
|
-
name: string;
|
|
22
|
-
perspective: string;
|
|
23
|
-
approach: string;
|
|
24
|
-
personal_stake: string;
|
|
25
|
-
sentiment: number;
|
|
26
|
-
exposure_current: number;
|
|
27
|
-
exposure_desired: number;
|
|
28
|
-
}>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface DescriptionCheckPromptData {
|
|
32
|
-
persona_name: string;
|
|
33
|
-
current_short_description?: string;
|
|
34
|
-
current_long_description?: string;
|
|
35
|
-
traits: PersonaTrait[];
|
|
36
|
-
topics: PersonaTopic[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface DescriptionCheckResult {
|
|
40
|
-
should_update: boolean;
|
|
41
|
-
reason?: string;
|
|
42
|
-
}
|
|
1
|
+
import type { DataItemBase } from "../../core/types.js";
|
|
43
2
|
|
|
44
3
|
// =============================================================================
|
|
45
4
|
// REWRITE (Item Reorganization)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { buildPersonaGenerationPrompt } from "./persona.js";
|
|
2
|
-
export { buildPersonaDescriptionsPrompt } from "./descriptions.js";
|
|
3
2
|
export { buildPersonaFromPersonPrompt } from "./from-person.js";
|
|
4
3
|
export {
|
|
5
4
|
DEFAULT_SEED_TRAITS,
|
|
@@ -11,7 +10,5 @@ export type {
|
|
|
11
10
|
PersonaGenerationPromptData,
|
|
12
11
|
PersonaGenerationResult,
|
|
13
12
|
PersonaFromPersonPromptData,
|
|
14
|
-
PersonaDescriptionsPromptData,
|
|
15
|
-
PersonaDescriptionsResult,
|
|
16
13
|
PromptOutput,
|
|
17
14
|
} from "./types.js";
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
|
|
2
|
-
|
|
3
1
|
export interface PromptOutput {
|
|
4
2
|
system: string;
|
|
5
3
|
user: string;
|
|
@@ -45,16 +43,3 @@ export interface PersonaFromPersonPromptData {
|
|
|
45
43
|
existing_trait_names?: string[];
|
|
46
44
|
existing_topic_names?: string[];
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
export interface PersonaDescriptionsPromptData {
|
|
50
|
-
name: string;
|
|
51
|
-
aliases: string[];
|
|
52
|
-
traits: PersonaTrait[];
|
|
53
|
-
topics: PersonaTopic[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface PersonaDescriptionsResult {
|
|
57
|
-
short_description: string;
|
|
58
|
-
long_description: string;
|
|
59
|
-
no_change?: boolean;
|
|
60
|
-
}
|
|
@@ -74,7 +74,6 @@ function getLastPersonaMessage(history: Message[]): Message | undefined {
|
|
|
74
74
|
* - Getting recent message history
|
|
75
75
|
*/
|
|
76
76
|
export function buildHeartbeatCheckPrompt(data: HeartbeatCheckPromptData): PromptOutput {
|
|
77
|
-
console.log(`[HeartbeatCheck ${data.persona.name}] Building prompt - topics: ${data.human.topics.length}, people: ${data.human.people.length}, inactive_days: ${data.inactive_days}, history: ${data.recent_history.length} messages`);
|
|
78
77
|
if (!data.persona?.name) {
|
|
79
78
|
throw new Error("buildHeartbeatCheckPrompt: persona.name is required");
|
|
80
79
|
}
|
|
@@ -124,11 +123,24 @@ ${formatPeopleWithGaps(data.human.people)}`;
|
|
|
124
123
|
|
|
125
124
|
**Quality over quantity** - Only reach out if you have something real to say.`;
|
|
126
125
|
|
|
127
|
-
const pendingUpdateFragment = data.persona.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
const pendingUpdateFragment = data.persona.pending_update ? (() => {
|
|
127
|
+
const p = data.persona.pending_update!;
|
|
128
|
+
const descPart = p.long_description || p.short_description
|
|
129
|
+
? `### Proposed Description\n${p.long_description || p.short_description}`
|
|
130
|
+
: "";
|
|
131
|
+
const traitsPart = p.traits.length > 0
|
|
132
|
+
? `### Proposed Traits\n${p.traits.map(t => `- **${t.name}**: ${t.description}`).join("\n")}`
|
|
133
|
+
: "";
|
|
134
|
+
const topicsPart = p.topics.length > 0
|
|
135
|
+
? `### Proposed Interests\n${p.topics.map(t => `- **${t.name}**: ${t.perspective}`).join("\n")}`
|
|
136
|
+
: "";
|
|
137
|
+
const parts = [descPart, traitsPart, topicsPart].filter(Boolean).join("\n\n");
|
|
138
|
+
return `## Pending Identity Changes
|
|
139
|
+
|
|
140
|
+
Your human is reviewing proposed updates to your identity. These are waiting for their response — you may want to bring it up, or not. It's yours to decide.
|
|
141
|
+
|
|
142
|
+
${parts}`;
|
|
143
|
+
})() : '';
|
|
132
144
|
|
|
133
145
|
const outputFragment = `## Response Format
|
|
134
146
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { PersonaTrait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
|
|
7
|
+
import type { PersonaEntity } from "../../core/types/entities.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Common prompt output structure
|
|
@@ -21,7 +22,7 @@ export interface HeartbeatCheckPromptData {
|
|
|
21
22
|
name: string;
|
|
22
23
|
traits: PersonaTrait[];
|
|
23
24
|
topics: PersonaTopic[];
|
|
24
|
-
|
|
25
|
+
pending_update?: PersonaEntity["pending_update"];
|
|
25
26
|
};
|
|
26
27
|
human: {
|
|
27
28
|
topics: Topic[]; // Filtered, sorted by engagement gap
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { buildHumanFactScanPrompt } from "./fact-scan.js";
|
|
2
1
|
export { buildFactFindPrompt } from "./fact-find.js";
|
|
3
2
|
export { buildHumanTopicScanPrompt } from "./topic-scan.js";
|
|
4
3
|
export { buildHumanPersonScanPrompt } from "./person-scan.js";
|
|
@@ -16,7 +15,6 @@ export type {
|
|
|
16
15
|
PromptOutput,
|
|
17
16
|
ParticipantContext,
|
|
18
17
|
PersonaEntitySnapshot,
|
|
19
|
-
FactScanPromptData,
|
|
20
18
|
TopicScanPromptData,
|
|
21
19
|
PersonScanPromptData,
|
|
22
20
|
FactFindPromptData,
|
|
@@ -43,6 +43,8 @@ Flag a PERSON when they were meaningfully discussed — not just mentioned in pa
|
|
|
43
43
|
|
|
44
44
|
Be **conservative**: ignore one-off mentions, greetings, small talk, or jokes. Only flag people who matter to the human user's life.
|
|
45
45
|
|
|
46
|
+
A person is **not worth flagging** if they have no name AND appear only to attribute a single event ("a coworker showed me this band", "a friend told me about it", "some guy I know"). The human user having a contact who did one thing is not a meaningful discussion of that person.
|
|
47
|
+
|
|
46
48
|
## What a PERSON Is
|
|
47
49
|
|
|
48
50
|
Someone in the human user's world.
|
|
@@ -68,11 +70,18 @@ Use the specific value where possible (e.g. "Father", "Brother", "Coworker"). Av
|
|
|
68
70
|
|
|
69
71
|
## When Identity Is Unclear
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
"Unknown" is ONLY for people who are **meaningfully and repeatedly discussed** but whose name isn't given. It is NOT a catch-all for any nameless mention.
|
|
74
|
+
|
|
75
|
+
✓ USE "Unknown":
|
|
76
|
+
- name: "Unknown", relationship: "Brother", reason: "User talked at length about their brother across multiple messages without naming him"
|
|
77
|
+
|
|
78
|
+
✗ DO NOT USE "Unknown" for one-off attributions:
|
|
79
|
+
- "a coworker showed me this band" → **skip entirely** — not a person, just attribution
|
|
80
|
+
- "a friend told me about it" → **skip entirely**
|
|
81
|
+
- "some guy I know" → **skip entirely**
|
|
82
|
+
- "a coworker at [company name]" with no personal name → **skip entirely** — a company name is NOT a person's name
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
- name: "Alice from work", relationship: "Coworker", description: "Mentioned but not described further", reason: "User referenced a work colleague named Alice"
|
|
75
|
-
- name: "Unknown", relationship: "Sibling", description: "User mentioned a sibling but did not give a name", reason: "User said 'my brother' without further context"
|
|
84
|
+
If someone has no personal name and appears only to explain how the user found something or heard about something, they are not a person in the user's life worth tracking. Do not extract them. A single interaction — even a meaningful one — does not make someone a contact.
|
|
76
85
|
|
|
77
86
|
## Identifiers (optional)
|
|
78
87
|
|