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,18 @@
|
|
|
1
|
+
export function levenshtein(a: string, b: string): number {
|
|
2
|
+
const m = a.length, n = b.length;
|
|
3
|
+
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
|
4
|
+
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
5
|
+
);
|
|
6
|
+
for (let i = 1; i <= m; i++) {
|
|
7
|
+
for (let j = 1; j <= n; j++) {
|
|
8
|
+
dp[i][j] = a[i-1] === b[j-1]
|
|
9
|
+
? dp[i-1][j-1]
|
|
10
|
+
: 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return dp[m][n];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeForMatch(s: string): string {
|
|
17
|
+
return s.toLowerCase().trim().replace(/[^\w\s]/g, '');
|
|
18
|
+
}
|
|
@@ -143,6 +143,7 @@ function ensureSessionTopic(
|
|
|
143
143
|
persona_groups: CLAUDE_CODE_TOPIC_GROUPS,
|
|
144
144
|
learned_by: stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME)?.id ?? undefined,
|
|
145
145
|
last_updated: new Date().toISOString(),
|
|
146
|
+
learned_on: new Date().toISOString(),
|
|
146
147
|
};
|
|
147
148
|
|
|
148
149
|
stateManager.human_topic_upsert(newTopic);
|
|
@@ -127,6 +127,7 @@ function ensureSessionTopic(
|
|
|
127
127
|
persona_groups: CURSOR_TOPIC_GROUPS,
|
|
128
128
|
learned_by: stateManager.persona_getByName(CURSOR_PERSONA_NAME)?.id ?? undefined,
|
|
129
129
|
last_updated: new Date().toISOString(),
|
|
130
|
+
learned_on: new Date().toISOString(),
|
|
130
131
|
};
|
|
131
132
|
|
|
132
133
|
stateManager.human_topic_upsert(newTopic);
|
|
@@ -4,6 +4,7 @@ export { buildDescriptionCheckPrompt } from "./description-check.js";
|
|
|
4
4
|
export { buildRewriteScanPrompt, buildRewritePrompt } from "./rewrite.js";
|
|
5
5
|
export { buildDedupPrompt } from "./dedup.js";
|
|
6
6
|
export { buildUserDedupPrompt } from "./user-dedup.js";
|
|
7
|
+
export { buildPersonMigrationPrompt, type PersonMigrationPromptData } from "./person-migration.js";
|
|
7
8
|
export type {
|
|
8
9
|
PersonaExpirePromptData,
|
|
9
10
|
PersonaExpireResult,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Person } from "../../core/types/data-items.js";
|
|
2
|
+
import type { PromptOutput } from "../persona/types.js";
|
|
3
|
+
|
|
4
|
+
export interface PersonMigrationPromptData {
|
|
5
|
+
person: Pick<Person, "name" | "description" | "relationship">;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildPersonMigrationPrompt(data: PersonMigrationPromptData): PromptOutput {
|
|
9
|
+
const { person } = data;
|
|
10
|
+
|
|
11
|
+
const system = `You are extracting identifiers for a Person record in a personal knowledge system.
|
|
12
|
+
|
|
13
|
+
A Person record has a \`name\` and \`description\` that describe who they are. Your job is to extract ALL identifiers for this person — every name, handle, alias, or platform ID that refers to them.
|
|
14
|
+
|
|
15
|
+
## CRITICAL RULES
|
|
16
|
+
|
|
17
|
+
1. The \`name\` field value MUST appear in identifiers — never lose it.
|
|
18
|
+
2. If \`name\` contains a space (e.g. "Jeremy Scherer"), create BOTH a \`Full Name\` identifier AND a \`First Name\` identifier for the given name.
|
|
19
|
+
3. Mark exactly one identifier as \`is_primary: true\` — the most natural display name.
|
|
20
|
+
4. Check \`read_memory\` for additional context before finalizing.
|
|
21
|
+
5. Return ONLY a JSON object with an \`identifiers\` array. No other text.
|
|
22
|
+
6. "none" is never valid — every person has at least one identifier (their name).
|
|
23
|
+
|
|
24
|
+
## DESCRIPTION PRE-PROCESSING
|
|
25
|
+
|
|
26
|
+
If the description begins with a JSON block (e.g. \`{"identifiers": [...]}\` or \`[{...}]\`), those are pre-seeded identifiers from a prior migration step:
|
|
27
|
+
- Parse them out and include them in your output
|
|
28
|
+
- Normalize their \`type\` values to Title Case (e.g. \`nickname\` → \`Nickname\`, \`full_name\` → \`Full Name\`)
|
|
29
|
+
- Treat the remainder of the description (after the JSON block) as the actual description text for \`read_memory\` context
|
|
30
|
+
|
|
31
|
+
## IDENTIFIER TYPES
|
|
32
|
+
|
|
33
|
+
Use Title Case for all types. Built-in types:
|
|
34
|
+
|
|
35
|
+
| Type | Meaning |
|
|
36
|
+
|------|---------|
|
|
37
|
+
| Full Name | Legal or full birth name |
|
|
38
|
+
| First Name | Given/first name only |
|
|
39
|
+
| Nickname | Informal name, diminutive, pet name |
|
|
40
|
+
| Email | Email address |
|
|
41
|
+
| GitHub | GitHub username |
|
|
42
|
+
| Discord | Discord username |
|
|
43
|
+
| Roblox | Roblox username |
|
|
44
|
+
| Reddit | Reddit username |
|
|
45
|
+
| Twitter | Twitter/X handle |
|
|
46
|
+
| FF14 | Final Fantasy XIV character name |
|
|
47
|
+
| Relationship | How the user addresses this person: Dad, Pop, Sis, etc. |
|
|
48
|
+
| Ei Persona | Ei persona UUID (only if explicitly in the data) |
|
|
49
|
+
|
|
50
|
+
Any string is valid as a type — users define their own (e.g. \`Slack-ASU\`, \`Slack-RnP\`, \`sehimu_thinara\`). Use the exact casing the user would recognize.
|
|
51
|
+
|
|
52
|
+
## RESPONSE FORMAT
|
|
53
|
+
|
|
54
|
+
\`\`\`json
|
|
55
|
+
{
|
|
56
|
+
"identifiers": [
|
|
57
|
+
{ "type": "Nickname", "value": "Flare", "is_primary": true },
|
|
58
|
+
{ "type": "Full Name", "value": "Jeremy Scherer" },
|
|
59
|
+
{ "type": "First Name", "value": "Jeremy" },
|
|
60
|
+
{ "type": "GitHub", "value": "Flare576" },
|
|
61
|
+
{ "type": "Slack-RnP", "value": "jeremy.scherer" }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
\`\`\``;
|
|
65
|
+
|
|
66
|
+
const user = `Extract identifiers for this Person record.
|
|
67
|
+
|
|
68
|
+
Name: ${person.name}
|
|
69
|
+
Relationship: ${person.relationship ?? "unknown"}
|
|
70
|
+
Description: ${person.description ?? "(none)"}
|
|
71
|
+
|
|
72
|
+
First, call read_memory to search for any additional context about "${person.name}". Then return the complete identifiers array.
|
|
73
|
+
|
|
74
|
+
Return JSON only.`;
|
|
75
|
+
|
|
76
|
+
return { system, user };
|
|
77
|
+
}
|
|
@@ -74,7 +74,7 @@ ${buildNewExamples()}
|
|
|
74
74
|
|
|
75
75
|
Rules:
|
|
76
76
|
- The original record (id: "${data.item.id}") MUST appear in "existing", slimmed down.
|
|
77
|
-
- Descriptions should be concise
|
|
77
|
+
- Descriptions should be concise: ${data.itemType === 'topic' ? 'ideally under 300 characters, never over 500' : 'ideally under 600 characters, never over 1000'}.
|
|
78
78
|
- Preserve sentiment, strength, confidence, and other numeric values from the source record where applicable.
|
|
79
79
|
- "type" must be one of: "topic", "person".
|
|
80
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.
|
|
@@ -94,7 +94,21 @@ function stripEmbedding<T extends { embedding?: unknown }>(item: T): Omit<T, "em
|
|
|
94
94
|
function buildRecordFormatHint(itemType: string): string {
|
|
95
95
|
switch (itemType) {
|
|
96
96
|
case "person":
|
|
97
|
-
return `Person fields: id, type, name, description, sentiment (-1 to 1), relationship, exposure_current (0-1), exposure_desired (0-1), learned_by (optional), last_changed_by (optional)
|
|
97
|
+
return `Person fields: id, type, name, identifiers (array of {type, value, is_primary?}), description, sentiment (-1 to 1), relationship, exposure_current (0-1), exposure_desired (0-1), learned_by (optional), last_changed_by (optional).
|
|
98
|
+
|
|
99
|
+
When merging two Person records, combine ALL identifiers from both records into a single deduplicated list (by value). Mark exactly one as is_primary.
|
|
100
|
+
|
|
101
|
+
Example merged person output:
|
|
102
|
+
{
|
|
103
|
+
"identifiers": [
|
|
104
|
+
{ "type": "nickname", "value": "Flare", "is_primary": true },
|
|
105
|
+
{ "type": "full_name", "value": "Jeremy Scherer" },
|
|
106
|
+
{ "type": "github", "value": "Flare576" }
|
|
107
|
+
],
|
|
108
|
+
"description": "merged description...",
|
|
109
|
+
"sentiment": 0.5,
|
|
110
|
+
"relationship": "Friend"
|
|
111
|
+
}`;
|
|
98
112
|
case "topic":
|
|
99
113
|
return `Topic fields: id, type, name, description, sentiment (-1 to 1), category, exposure_current (0-1), exposure_desired (0-1), learned_by (optional), last_changed_by (optional)`;
|
|
100
114
|
default:
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { HeartbeatCheckPromptData, PromptOutput } from "./types.js";
|
|
9
9
|
import { type Message, type Topic, type Person } from "../../core/types.js";
|
|
10
10
|
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
11
|
+
import { getMessageContent } from "../../core/handlers/utils.js";
|
|
11
12
|
function formatTopicsWithGaps(topics: Topic[]): string {
|
|
12
13
|
if (topics.length === 0) return "(No topics with engagement gaps)";
|
|
13
14
|
|
|
@@ -38,8 +39,7 @@ function formatPeopleWithGaps(people: Person[]): string {
|
|
|
38
39
|
function isConversationalMessage(m: Message): boolean {
|
|
39
40
|
if (m.role !== 'system') return false;
|
|
40
41
|
if (m.silence_reason !== undefined) return false;
|
|
41
|
-
|
|
42
|
-
if (!m.verbal_response) return false;
|
|
42
|
+
if (!getMessageContent(m)) return false;
|
|
43
43
|
return true;
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -124,6 +124,22 @@ ${formatPeopleWithGaps(data.human.people)}`;
|
|
|
124
124
|
|
|
125
125
|
**Quality over quantity** - Only reach out if you have something real to say.`;
|
|
126
126
|
|
|
127
|
+
const driftFragment = data.drift_context
|
|
128
|
+
? `## Identity Reflection
|
|
129
|
+
|
|
130
|
+
The human's notes about you have recently evolved. Compare these:
|
|
131
|
+
|
|
132
|
+
**How the human currently describes you:**
|
|
133
|
+
${data.drift_context.people_description}
|
|
134
|
+
|
|
135
|
+
**Your current definition:**
|
|
136
|
+
${data.drift_context.persona_description}
|
|
137
|
+
|
|
138
|
+
If you feel these describe meaningfully different versions of you, you may want to gently surface this — in your own way, in your own time. If they seem the same to you, no need to mention it.
|
|
139
|
+
|
|
140
|
+
If you do choose to address this, include \`"mentioned_reflection": true\` in your response.`
|
|
141
|
+
: '';
|
|
142
|
+
|
|
127
143
|
const outputFragment = `## Response Format
|
|
128
144
|
|
|
129
145
|
Call the \`submit_heartbeat_check\` tool with your decision. If the tool is unavailable, return JSON:
|
|
@@ -132,7 +148,8 @@ Call the \`submit_heartbeat_check\` tool with your decision. If the tool is unav
|
|
|
132
148
|
{
|
|
133
149
|
"should_respond": true,
|
|
134
150
|
"topic": "the specific topic you want to discuss",
|
|
135
|
-
"message": "Your actual message to them (if should_respond is true)"
|
|
151
|
+
"message": "Your actual message to them (if should_respond is true)"${data.drift_context ? `,
|
|
152
|
+
"mentioned_reflection": true` : ''}
|
|
136
153
|
}
|
|
137
154
|
\`\`\`
|
|
138
155
|
|
|
@@ -143,15 +160,14 @@ If you decide NOT to reach out:
|
|
|
143
160
|
}
|
|
144
161
|
\`\`\``;
|
|
145
162
|
|
|
146
|
-
const system =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
${outputFragment}`;
|
|
163
|
+
const system = [
|
|
164
|
+
roleFragment,
|
|
165
|
+
contextFragment,
|
|
166
|
+
opportunitiesFragment,
|
|
167
|
+
guidelinesFragment,
|
|
168
|
+
driftFragment,
|
|
169
|
+
outputFragment,
|
|
170
|
+
].filter(Boolean).join('\n\n');
|
|
155
171
|
|
|
156
172
|
const historySection = `## Recent Conversation History
|
|
157
173
|
|
|
@@ -28,6 +28,8 @@ function formatItem(item: EiHeartbeatItem): string {
|
|
|
28
28
|
const desc = item.short_description ? ` — ${item.short_description}` : "";
|
|
29
29
|
return `- **${item.id}** Inactive Persona: ${item.name}${desc} (${item.days_inactive} days inactive)`;
|
|
30
30
|
}
|
|
31
|
+
default:
|
|
32
|
+
return '';
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -28,6 +28,10 @@ export interface HeartbeatCheckPromptData {
|
|
|
28
28
|
};
|
|
29
29
|
recent_history: Message[]; // Last N messages for context
|
|
30
30
|
inactive_days: number; // Days since last activity
|
|
31
|
+
drift_context?: {
|
|
32
|
+
people_description: string;
|
|
33
|
+
persona_description: string;
|
|
34
|
+
};
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -37,6 +41,7 @@ export interface HeartbeatCheckResult {
|
|
|
37
41
|
should_respond: boolean;
|
|
38
42
|
topic?: string;
|
|
39
43
|
message?: string;
|
|
44
|
+
mentioned_reflection?: boolean;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
// =============================================================================
|
|
@@ -79,6 +84,13 @@ export type EiHeartbeatItem =
|
|
|
79
84
|
name: string;
|
|
80
85
|
short_description?: string;
|
|
81
86
|
days_inactive: number;
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
id: string;
|
|
90
|
+
type: "New Person";
|
|
91
|
+
name: string;
|
|
92
|
+
description: string;
|
|
93
|
+
quote?: string;
|
|
82
94
|
};
|
|
83
95
|
|
|
84
96
|
/**
|
|
@@ -4,14 +4,12 @@ export { buildHumanTopicScanPrompt } from "./topic-scan.js";
|
|
|
4
4
|
export { buildHumanPersonScanPrompt } from "./person-scan.js";
|
|
5
5
|
export { buildTopicMatchPrompt } from "./topic-match.js";
|
|
6
6
|
export { buildTopicUpdatePrompt } from "./topic-update.js";
|
|
7
|
-
export { buildPersonMatchPrompt } from "./person-match.js";
|
|
8
7
|
export { buildPersonUpdatePrompt } from "./person-update.js";
|
|
9
8
|
export { buildEventScanPrompt } from "./event-scan.js";
|
|
10
9
|
export type { EventScanPromptData } from "./event-scan.js";
|
|
11
10
|
|
|
12
11
|
export type { TopicMatchPromptData } from "./topic-match.js";
|
|
13
12
|
export type { TopicUpdatePromptData } from "./topic-update.js";
|
|
14
|
-
export type { PersonMatchPromptData } from "./person-match.js";
|
|
15
13
|
export type { PersonUpdatePromptData } from "./person-update.js";
|
|
16
14
|
|
|
17
15
|
export type {
|
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import type { PersonScanPromptData, PromptOutput } from "./types.js";
|
|
1
|
+
import type { PersonScanPromptData, ParticipantContext, PromptOutput } from "./types.js";
|
|
2
2
|
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
3
3
|
|
|
4
|
+
function participantContextSection(ctx: ParticipantContext | undefined): string {
|
|
5
|
+
if (!ctx) return "";
|
|
6
|
+
const lines: string[] = ["# Participant Context", "The following may help you understand who is in this conversation.", ""];
|
|
7
|
+
lines.push(`## Persona: ${ctx.persona_name}`);
|
|
8
|
+
if (ctx.persona_description) lines.push(ctx.persona_description);
|
|
9
|
+
lines.push("");
|
|
10
|
+
lines.push("## Human User");
|
|
11
|
+
if (ctx.human_name) lines.push(`Name: ${ctx.human_name}`);
|
|
12
|
+
if (ctx.human_age !== undefined) lines.push(`Age: ${ctx.human_age}`);
|
|
13
|
+
lines.push("");
|
|
14
|
+
return lines.join("\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
export function buildHumanPersonScanPrompt(data: PersonScanPromptData): PromptOutput {
|
|
5
18
|
if (!data.persona_name) {
|
|
6
19
|
throw new Error("buildHumanPersonScanPrompt: persona_name is required");
|
|
7
20
|
}
|
|
8
21
|
|
|
9
22
|
const personaName = data.persona_name;
|
|
23
|
+
const humanName = data.participant_context?.human_name;
|
|
24
|
+
|
|
25
|
+
const builtInTypes = ['Full Name', 'First Name', 'Nickname', 'Email', 'GitHub', 'Discord',
|
|
26
|
+
'Roblox', 'Reddit', 'Twitter', 'FF14', 'Relationship', 'Ei Persona'];
|
|
27
|
+
const userTypes = data.known_identifier_types ?? [];
|
|
28
|
+
const allTypes = [...new Set([...builtInTypes, ...userTypes])].join(', ');
|
|
29
|
+
|
|
30
|
+
const selfGuard = humanName
|
|
31
|
+
? `The HUMAN USER (${humanName}) wrote these messages. When the conversation is meaningfully about them as a person, you MAY include a self-record with \`relationship: "Self"\`. Do NOT apply their names or handles as identifiers for OTHER people in their life.`
|
|
32
|
+
: `The HUMAN USER wrote these messages. They are not automatically a person to flag — only include a self-record with \`relationship: "Self"\` when the conversation is meaningfully about them. Do NOT apply their names or handles as identifiers for other people in their life.`;
|
|
10
33
|
|
|
11
34
|
const system = `# Task
|
|
12
35
|
|
|
@@ -14,7 +37,7 @@ You are scanning a conversation to quickly identify PEOPLE in the HUMAN USER's l
|
|
|
14
37
|
|
|
15
38
|
Detect and flag. Do NOT analyze deeply — that happens later.
|
|
16
39
|
|
|
17
|
-
## What to Capture
|
|
40
|
+
${participantContextSection(data.participant_context)}## What to Capture
|
|
18
41
|
|
|
19
42
|
Flag a PERSON when they were meaningfully discussed — not just mentioned in passing.
|
|
20
43
|
|
|
@@ -22,20 +45,23 @@ Be **conservative**: ignore one-off mentions, greetings, small talk, or jokes. O
|
|
|
22
45
|
|
|
23
46
|
## What a PERSON Is
|
|
24
47
|
|
|
25
|
-
Someone in the human user's world.
|
|
48
|
+
Someone in the human user's world.
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
For "relationship", use the **specific value** — NOT the category name:
|
|
28
51
|
|
|
29
|
-
|
|
52
|
+
- Immediate Family: Father, Mother, Son, Daughter, Brother, Sister, Husband, Wife, Partner
|
|
53
|
+
(step/in-law variants OK: Step-Father, Sister-in-Law, etc.)
|
|
54
|
+
- Extended Family: Grandfather, Grandmother, Aunt, Uncle, Cousin, Niece, Nephew
|
|
55
|
+
- Social: Friend, Close Acquaintance, Lover, Love Interest, Fiance, Spouse
|
|
56
|
+
- Professional: Coworker, Manager, Report, Mentor, Client
|
|
57
|
+
- Self — the human user themselves
|
|
58
|
+
- AI Persona — AI companions and assistants
|
|
30
59
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
**Professional**: Coworker, Manager, Report, Mentor, Client
|
|
34
|
-
|
|
35
|
-
**AI**: Persona (use \`relationship: "AI Persona"\` for AI companions and assistants)
|
|
60
|
+
Use the specific value where possible (e.g. "Father", "Brother", "Coworker"). Avoid returning the category label ("Immediate Family", "Extended Family", etc.) — use the item within the category instead. If the relationship doesn't fit any category cleanly, use the most natural plain-English description.
|
|
36
61
|
|
|
37
62
|
**NOT a PERSON:**
|
|
38
|
-
-
|
|
63
|
+
- ${selfGuard}
|
|
64
|
+
- Hypothetical or fictional people used in examples, thought experiments, or use-case scenarios — even if they have names. If the user is describing how a feature *could* work for "Sarah" or "Jared", those are not real people in their life.
|
|
39
65
|
- Biographical facts, topics, or hobbies
|
|
40
66
|
- Fictional characters from books, movies, or media
|
|
41
67
|
- Public figures only mentioned in passing (celebrities, politicians) — unless the user has a real relationship with them
|
|
@@ -48,6 +74,16 @@ Examples:
|
|
|
48
74
|
- name: "Alice from work", relationship: "Coworker", description: "Mentioned but not described further", reason: "User referenced a work colleague named Alice"
|
|
49
75
|
- name: "Unknown", relationship: "Sibling", description: "User mentioned a sibling but did not give a name", reason: "User said 'my brother' without further context"
|
|
50
76
|
|
|
77
|
+
## Identifiers (optional)
|
|
78
|
+
|
|
79
|
+
If the conversation **explicitly** mentions a platform handle, username, email address, or alternative name for this person, capture it in \`identifiers\`.
|
|
80
|
+
|
|
81
|
+
Known types: ${allTypes}
|
|
82
|
+
|
|
83
|
+
If you are unsure of the type, use \`Nickname\` as a fallback. Do NOT invent types. Do NOT duplicate the \`name\` field as an identifier. NEVER add dates, ages, or birthdays as identifiers.
|
|
84
|
+
|
|
85
|
+
Only include \`identifiers\` when explicitly mentioned in the conversation — omit it entirely if nothing qualifies.
|
|
86
|
+
|
|
51
87
|
## Output Format
|
|
52
88
|
|
|
53
89
|
\`\`\`json
|
|
@@ -55,14 +91,19 @@ Examples:
|
|
|
55
91
|
"people": [
|
|
56
92
|
{
|
|
57
93
|
"name": "The person's name, or 'Unknown' if not given",
|
|
94
|
+
"identifiers": [
|
|
95
|
+
{ "type": "GitHub", "value": "mldelaro" }
|
|
96
|
+
],
|
|
58
97
|
"description": "1-2 sentences: who this person is and their role in the user's life",
|
|
59
|
-
"relationship": "
|
|
98
|
+
"relationship": "Father|Mother|Brother|Son|Friend|Coworker|Self|etc.",
|
|
60
99
|
"reason": "Evidence from the conversation that justified flagging this person"
|
|
61
100
|
}
|
|
62
101
|
]
|
|
63
102
|
}
|
|
64
103
|
\`\`\`
|
|
65
104
|
|
|
105
|
+
\`identifiers\` is OPTIONAL — only include when the conversation explicitly mentions platform handles, usernames, emails, or alternative names.
|
|
106
|
+
|
|
66
107
|
**Return JSON only.**
|
|
67
108
|
|
|
68
109
|
ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided for context only — it has already been processed.`;
|
|
@@ -90,13 +131,16 @@ Scan the "Most Recent Messages" for PEOPLE in the human user's life.
|
|
|
90
131
|
"people": [
|
|
91
132
|
{
|
|
92
133
|
"name": "The person's name, or 'Unknown' if not given",
|
|
134
|
+
"identifiers": [{ "type": "GitHub", "value": "handle" }],
|
|
93
135
|
"description": "1-2 sentences: who this person is and their role in the user's life",
|
|
94
|
-
"relationship": "
|
|
136
|
+
"relationship": "Father|Mother|Brother|Son|Friend|Coworker|Self|etc.",
|
|
95
137
|
"reason": "Evidence from the conversation that justified flagging this person"
|
|
96
138
|
}
|
|
97
139
|
]
|
|
98
140
|
}
|
|
99
|
-
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
\`identifiers\` is optional — include only when explicitly mentioned.`;
|
|
100
144
|
|
|
101
145
|
return { system, user };
|
|
102
146
|
}
|