ei-tui 0.1.25 → 0.2.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.
Files changed (78) hide show
  1. package/README.md +42 -0
  2. package/package.json +1 -1
  3. package/src/README.md +4 -11
  4. package/src/cli/README.md +4 -5
  5. package/src/cli/retrieval.ts +3 -25
  6. package/src/cli.ts +3 -7
  7. package/src/core/AGENTS.md +1 -1
  8. package/src/core/constants/built-in-facts.ts +49 -0
  9. package/src/core/constants/index.ts +1 -0
  10. package/src/core/context-utils.ts +0 -1
  11. package/src/core/embedding-service.ts +8 -0
  12. package/src/core/handlers/dedup.ts +10 -16
  13. package/src/core/handlers/heartbeat.ts +2 -3
  14. package/src/core/handlers/human-extraction.ts +95 -30
  15. package/src/core/handlers/human-matching.ts +326 -248
  16. package/src/core/handlers/index.ts +8 -6
  17. package/src/core/handlers/persona-generation.ts +8 -8
  18. package/src/core/handlers/rewrite.ts +4 -29
  19. package/src/core/handlers/utils.ts +23 -1
  20. package/src/core/heartbeat-manager.ts +2 -4
  21. package/src/core/human-data-manager.ts +5 -27
  22. package/src/core/message-manager.ts +10 -10
  23. package/src/core/orchestrators/ceremony.ts +50 -39
  24. package/src/core/orchestrators/dedup-phase.ts +0 -1
  25. package/src/core/orchestrators/human-extraction.ts +351 -207
  26. package/src/core/orchestrators/index.ts +6 -4
  27. package/src/core/orchestrators/persona-generation.ts +3 -3
  28. package/src/core/processor.ts +99 -17
  29. package/src/core/prompt-context-builder.ts +4 -6
  30. package/src/core/state/human.ts +1 -26
  31. package/src/core/state/personas.ts +2 -2
  32. package/src/core/state-manager.ts +107 -14
  33. package/src/core/tools/builtin/read-memory.ts +7 -8
  34. package/src/core/types/data-items.ts +2 -4
  35. package/src/core/types/entities.ts +6 -4
  36. package/src/core/types/enums.ts +6 -9
  37. package/src/core/types/llm.ts +2 -2
  38. package/src/core/utils/crossFind.ts +2 -5
  39. package/src/core/utils/event-windows.ts +31 -0
  40. package/src/integrations/claude-code/importer.ts +8 -4
  41. package/src/integrations/claude-code/types.ts +2 -0
  42. package/src/integrations/opencode/importer.ts +7 -3
  43. package/src/prompts/AGENTS.md +73 -1
  44. package/src/prompts/ceremony/rewrite.ts +3 -22
  45. package/src/prompts/ceremony/types.ts +3 -3
  46. package/src/prompts/generation/descriptions.ts +2 -2
  47. package/src/prompts/generation/types.ts +2 -2
  48. package/src/prompts/heartbeat/types.ts +2 -2
  49. package/src/prompts/human/event-scan.ts +122 -0
  50. package/src/prompts/human/fact-find.ts +106 -0
  51. package/src/prompts/human/fact-scan.ts +0 -2
  52. package/src/prompts/human/index.ts +17 -10
  53. package/src/prompts/human/person-match.ts +65 -0
  54. package/src/prompts/human/person-scan.ts +52 -59
  55. package/src/prompts/human/person-update.ts +241 -0
  56. package/src/prompts/human/topic-match.ts +65 -0
  57. package/src/prompts/human/topic-scan.ts +51 -71
  58. package/src/prompts/human/topic-update.ts +295 -0
  59. package/src/prompts/human/types.ts +63 -40
  60. package/src/prompts/index.ts +4 -8
  61. package/src/prompts/persona/topics-update.ts +2 -2
  62. package/src/prompts/persona/traits.ts +2 -2
  63. package/src/prompts/persona/types.ts +3 -3
  64. package/src/prompts/response/index.ts +1 -1
  65. package/src/prompts/response/sections.ts +9 -12
  66. package/src/prompts/response/types.ts +2 -3
  67. package/src/storage/embeddings.ts +1 -1
  68. package/src/storage/index.ts +1 -0
  69. package/src/storage/indexed.ts +174 -0
  70. package/src/storage/merge.ts +67 -2
  71. package/tui/src/commands/me.tsx +5 -14
  72. package/tui/src/commands/settings.tsx +15 -0
  73. package/tui/src/context/ei.tsx +5 -14
  74. package/tui/src/util/yaml-serializers.ts +48 -33
  75. package/src/cli/commands/traits.ts +0 -25
  76. package/src/prompts/human/item-match.ts +0 -74
  77. package/src/prompts/human/item-update.ts +0 -364
  78. package/src/prompts/human/trait-scan.ts +0 -115
@@ -0,0 +1,65 @@
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
+ }
@@ -8,72 +8,64 @@ export function buildHumanPersonScanPrompt(data: PersonScanPromptData): PromptOu
8
8
 
9
9
  const personaName = data.persona_name;
10
10
 
11
- const taskFragment = `# Task
12
-
13
- You are scanning a conversation to quickly identify PEOPLE of interest TO the HUMAN USER. Your ONLY job is to spot mentions of PEOPLE. Do NOT analyze them deeply. Just detect and flag.`;
14
-
15
- const specificNeedsFragment = `## Specific Needs
16
-
17
- Your job is to quickly identify:
18
- 1. Which PEOPLE were mentioned or relevant
19
- a. Only flag PEOPLE that were actually discussed, not just tangentially related
20
- b. Be CONSERVATIVE - only suggest genuinely important, long-term relevant PEOPLE
21
- i. Ignore: greetings, small talk, one-off mentions, jokes
22
- c. Be CLEAR - state your \`reason\` for including this PERSON with any evidence you used`;
23
-
24
- const guidelinesFragment = `## Guidelines
25
-
26
- 1. **Unknown Types and Names of PEOPLE**
27
- a. In some conversations, it may be impossible to identify which "Brother" or which "Bob" the user is referring to.
28
- - Use "Unknown" for the missing field and explain in the \`reason\`
29
- - This will trigger a later validation step to get more information!
30
- b. If you're adding a NEW PERSON, be as specific as you can, for example:
31
- - { "type_of_person": "Unknown", "name_of_person": "Alice from work", "reason": "Mentioned but relationship unclear" }
32
- - { "type_of_person": "Sibling", "name_of_person": "Name Unknown", "reason": "Mentioned a sibling, name not given" }
33
-
34
- **A PERSON Is**
35
- * Immediate Family: Father, Husband, Son, Brother, Mother, Wife, Daughter, Sister (and step/in-law variants)
36
- * Extended Family: Grandfather, Grandmother, Aunt, Uncle, Cousin, Niece, Nephew
37
- * Close Acquaintance
38
- * Friend
39
- * Lover / Love Interest
40
- * Fiance / Spouse
41
- * Coworker
42
- * AI Persona (use \`type_of_person: "Persona"\`)
43
-
44
- **A PERSON Is NOT**
45
- - Biographical data: Birthday, Location, Job, Marital Status, Gender, Eye Color, Hair Color
46
- - Other unchangeable Data: Wedding Day, Allergies
47
- - Trait: Personality patterns, communication style, behavioral tendencies
48
- - General Topic: Interests, Hobbies, General subjects
49
- - Characters: Fictitious entities from books, movies, stories, media, etc.`;
50
- const criticalFragment = `# CRITICAL INSTRUCTIONS
51
-
52
- ONLY ANALYZE the "Most Recent Messages" in the following conversation. The "Earlier Conversation" is provided for your context and has already been processed!
53
-
54
- The JSON format is:
11
+ const system = `# Task
12
+
13
+ You are scanning a conversation to quickly identify PEOPLE in the HUMAN USER's life.
14
+
15
+ Detect and flag. Do NOT analyze deeply — that happens later.
16
+
17
+ ## What to Capture
18
+
19
+ Flag a PERSON when they were meaningfully discussed not just mentioned in passing.
20
+
21
+ Be **conservative**: ignore one-off mentions, greetings, small talk, or jokes. Only flag people who matter to the human user's life.
22
+
23
+ ## What a PERSON Is
24
+
25
+ Someone in the human user's world. Use the relationship as the primary classifier:
26
+
27
+ **Immediate Family**: Father, Mother, Son, Daughter, Brother, Sister, Husband, Wife, Partner (and step/in-law variants)
28
+
29
+ **Extended Family**: Grandfather, Grandmother, Aunt, Uncle, Cousin, Niece, Nephew
30
+
31
+ **Social**: Friend, Close Acquaintance, Lover, Love Interest, Fiance, Spouse
32
+
33
+ **Professional**: Coworker, Manager, Report, Mentor, Client
34
+
35
+ **AI**: Persona (use \`relationship: "AI Persona"\` for AI companions and assistants)
36
+
37
+ **NOT a PERSON:**
38
+ - The user themselves
39
+ - Biographical facts, topics, or hobbies
40
+ - Fictional characters from books, movies, or media
41
+ - Public figures only mentioned in passing (celebrities, politicians) — unless the user has a real relationship with them
42
+
43
+ ## When Identity Is Unclear
44
+
45
+ If you can't identify which "Bob" or which "Brother" the user means, use "Unknown" and explain in the reason field. This triggers a later step to resolve ambiguity.
46
+
47
+ Examples:
48
+ - name: "Alice from work", relationship: "Coworker", description: "Mentioned but not described further", reason: "User referenced a work colleague named Alice"
49
+ - name: "Unknown", relationship: "Sibling", description: "User mentioned a sibling but did not give a name", reason: "User said 'my brother' without further context"
50
+
51
+ ## Output Format
55
52
 
56
53
  \`\`\`json
57
54
  {
58
55
  "people": [
59
56
  {
60
- "type_of_person": "The relationship from the list above",
61
- "name_of_person": "The person's name",
62
- "reason": "The justification of including this specific person"
57
+ "name": "The person's name, or 'Unknown' if not given",
58
+ "description": "1-2 sentences: who this person is and their role in the user's life",
59
+ "relationship": "Relationship type from the list above",
60
+ "reason": "Evidence from the conversation that justified flagging this person"
63
61
  }
64
62
  ]
65
63
  }
66
64
  \`\`\`
67
65
 
68
- **Return JSON only.**`;
69
-
70
- const system = `${taskFragment}
71
-
72
- ${specificNeedsFragment}
73
-
74
- ${guidelinesFragment}
66
+ **Return JSON only.**
75
67
 
76
- ${criticalFragment}`;
68
+ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided for context only — it has already been processed.`;
77
69
 
78
70
  const earlierSection = data.messages_context.length > 0
79
71
  ? `## Earlier Conversation
@@ -90,16 +82,17 @@ ${earlierSection}${recentSection}
90
82
 
91
83
  ---
92
84
 
93
- Scan the "Most Recent Messages" for PEOPLE mentioned by the human user.
85
+ Scan the "Most Recent Messages" for PEOPLE in the human user's life.
94
86
 
95
87
  **Return JSON:**
96
88
  \`\`\`json
97
89
  {
98
90
  "people": [
99
91
  {
100
- "type_of_person": "The relationship from the list above",
101
- "name_of_person": "The person's name",
102
- "reason": "The justification of including this specific person"
92
+ "name": "The person's name, or 'Unknown' if not given",
93
+ "description": "1-2 sentences: who this person is and their role in the user's life",
94
+ "relationship": "Relationship type from the list above",
95
+ "reason": "Evidence from the conversation that justified flagging this person"
103
96
  }
104
97
  ]
105
98
  }
@@ -0,0 +1,241 @@
1
+ import type { PromptOutput } from "./types.js";
2
+ import type { Person, Message } from "../../core/types.js";
3
+ import { formatMessagesAsPlaceholders } from "../message-utils.js";
4
+
5
+ export interface PersonUpdatePromptData {
6
+ existing_item: Person | null;
7
+ new_person_name?: string;
8
+ new_person_description?: string;
9
+ new_person_relationship?: string;
10
+ messages_context: Message[];
11
+ messages_analyze: Message[];
12
+ persona_name: string;
13
+ }
14
+
15
+ function formatExistingPerson(person: Person): string {
16
+ return JSON.stringify({
17
+ name: person.name,
18
+ description: person.description,
19
+ sentiment: person.sentiment,
20
+ relationship: person.relationship,
21
+ exposure_current: person.exposure_current,
22
+ exposure_desired: person.exposure_desired,
23
+ }, null, 2);
24
+ }
25
+
26
+ export function buildPersonUpdatePrompt(data: PersonUpdatePromptData): PromptOutput {
27
+ if (!data.persona_name) {
28
+ throw new Error("buildPersonUpdatePrompt: persona_name is required");
29
+ }
30
+
31
+ const personaName = data.persona_name;
32
+
33
+ const nameSection = `The person's actual name, or the clearest available identifier.
34
+
35
+ Only update when you learn something more specific.
36
+
37
+ Examples: "Unknown woman" → "Carol", "Mom" → "Carol (Mom)", "David" → "David Kim"`;
38
+
39
+ const descriptionSection = `A concise summary of who this person is and how they relate to the HUMAN USER. Personas use this to recognize this person and engage meaningfully when they come up.
40
+
41
+ ## CRITICAL: Synthesize, don't accumulate
42
+
43
+ Every update must **rewrite** the description as a current-state summary. Never append to it.
44
+
45
+ **Good description**: "Borfinda, partner of 12 years. Former marine biologist, now stay-at-home parent. Tends to ground the user when they spiral; dry sense of humor. Two kids together."
46
+
47
+ **Bad description**: "Borfinda was mentioned when the user talked about moving. In a later conversation she came up again during the work stress discussion. Most recently the user said she was supportive."
48
+
49
+ The description should:
50
+ - Capture who this person IS — their role, characteristics, relationship texture
51
+ - Include what the HUMAN USER has revealed about them over time
52
+ - Be useful to a persona who's never heard this person's name before
53
+ - Read as a brief, confident summary — not a log of when they were mentioned
54
+
55
+ The description should NOT:
56
+ - Append "Most recently:", "Latest mention:", or any temporal marker
57
+ - Accumulate a session-by-session history of every time this person came up
58
+ - Speculate about the person based on thin evidence
59
+ - Exceed 3-4 sentences under any circumstances
60
+
61
+ **ABSOLUTELY VITAL**: Do **NOT** embellish — personas use their own voice. Record what the user actually said or demonstrated, not your interpretation of its emotional significance.`;
62
+
63
+ const relationshipSection = `## Relationship (\`relationship\`)
64
+
65
+ How the HUMAN USER is currently related to this PERSON.
66
+
67
+ Once known, this field changes infrequently — a "Father" may later be clarified to "Step-Father", but is unlikely to become "Uncle".
68
+
69
+ Keep it concise and specific. Avoid vague labels.
70
+
71
+ Examples: "Unknown" → "Coworker", "Mother" → "Step-Mother", "Fiance" → "Spouse", "AI Persona" → "AI Companion"`;
72
+
73
+ const exposureSection = `## Desired Exposure (\`exposure_desired\`)
74
+
75
+ How much the HUMAN USER wants to talk about this PERSON.
76
+
77
+ Scale of 0.0 to 1.0:
78
+ - 0.0: Never wants to hear about this PERSON again
79
+ - 0.5: Average amount of engagement
80
+ - 1.0: This PERSON is the sole focus of their existence
81
+
82
+ Do not make micro-adjustments. Close enough is OK.
83
+
84
+ ## Exposure Impact (\`exposure_impact\`)
85
+
86
+ Not in the current data — but include it in your response.
87
+
88
+ How much this conversation should count toward exposure tracking:
89
+ - "high": Long, detailed conversation exclusively about this PERSON
90
+ - "medium": Long OR detailed conversation about this PERSON
91
+ - "low": The conversation touched on this PERSON briefly
92
+ - "none": Only alluded to or hinted at`;
93
+
94
+ const currentDetailsSection = data.existing_item
95
+ ? `\`\`\`json
96
+ ${formatExistingPerson(data.existing_item)}
97
+ \`\`\`
98
+
99
+ You are UPDATING an existing PERSON.`
100
+ : `**NEW PERSON — NOT YET IN SYSTEM**
101
+
102
+ You are CREATING a new PERSON from what was discovered:
103
+ \`\`\`json
104
+ {
105
+ "name": "${data.new_person_name ?? "Unknown"}",
106
+ "description": "${data.new_person_description ?? "Details unknown"}",
107
+ "relationship": "${data.new_person_relationship ?? "Unknown"}"
108
+ }
109
+ \`\`\`
110
+
111
+ Return all fields based on what you find in the conversation.`;
112
+
113
+ const jsonTemplate = `{
114
+ "name": "...",
115
+ "description": "...",
116
+ "sentiment": 0.0,
117
+ "relationship": "Mother|Friend|Coworker|AI Companion|etc.",
118
+ "exposure_desired": 0.5,
119
+ "exposure_impact": "high|medium|low|none",
120
+ "quotes": [
121
+ {
122
+ "text": "exact phrase from message",
123
+ "reason": "why this matters"
124
+ }
125
+ ]
126
+ }`;
127
+
128
+ const system = `# Task
129
+
130
+ You are scanning a conversation to deeply understand a PERSON in the HUMAN USER's life.
131
+
132
+ Your job is to take that analysis and apply it to the record we already have **IF DOING SO WILL PROVIDE THE HUMAN USER WITH A BETTER EXPERIENCE IN THE FUTURE**.
133
+
134
+ This means detail you add should:
135
+ 1. Be meaningful, accurate, or still true to the HUMAN USER in six months or more
136
+ 2. **NOT** already be present in the description or name of the PERSON
137
+
138
+ This PERSON will be recorded in the HUMAN USER's profile for agents and personas to later reference.
139
+
140
+ # Field Definitions
141
+
142
+ ## Name (\`name\`)
143
+ ${nameSection}
144
+
145
+ ## Description (\`description\`)
146
+ ${descriptionSection}
147
+
148
+ ## Sentiment (\`sentiment\`)
149
+
150
+ How the HUMAN USER feels about this PERSON overall.
151
+
152
+ Scale of -1.0 to 1.0:
153
+ - -1.0: No PERSON is more despised
154
+ - -0.5: Disliked or complicated relationship, but not without value
155
+ - 0: Neutral or unknown
156
+ - 0.5: Liked and valued
157
+ - 1.0: The most important person in their life
158
+
159
+ Do not make micro-adjustments. Close enough is OK.
160
+
161
+ ${relationshipSection}
162
+
163
+ ${exposureSection}
164
+
165
+ ## Quotes
166
+
167
+ In addition to updating the PERSON, identify any **memorable, funny, important, or stand-out phrases** from the Most Recent Messages that relate to this PERSON.
168
+
169
+ ### What Makes a Quote Worth Preserving
170
+
171
+ **Prioritize:**
172
+ - Humor, wit, colorful language, creative profanity
173
+ - Emotional outbursts (positive or negative) — the raw stuff
174
+ - Phrases that reveal how the HUMAN USER feels about this PERSON
175
+ - Things you'd quote back to them later to make them laugh or think
176
+ - Unique expressions or turns of phrase about or from this PERSON
177
+ - Quotable moments from EITHER speaker — humans AND AI personas both say memorable things
178
+
179
+ **NEVER extract these — they are NOT quotes:**
180
+ - Technical identifiers: ARNs, URLs, file paths, UUIDs, config keys
181
+ - AI agent self-talk: "I notice I'm in Plan Mode", "I'll start by...", status updates
182
+ - AI apologies or acknowledgments: "You're absolutely right", "I apologize for that"
183
+ - Generic statements that could apply to anyone
184
+ - Credentials, secrets, connection strings, or anything that looks like an access token
185
+
186
+ **The litmus test**: Would you bring this up at a bar with a friend? Would it make someone laugh, think, or feel something?
187
+ - "She's the only person who can make me feel simultaneously stupid and brilliant" → YES.
188
+ - "Borfinda was mentioned in the context of the Minnesota discussion" → NO. That's a note, not a quote.
189
+
190
+ **When in doubt, leave it out.** An empty quotes array is always acceptable.
191
+
192
+ **CRITICAL**: Return the EXACT text as it appears in the message. **WE CAN ONLY USE IT IF WE FIND IT IN THE TEXT.**
193
+
194
+ # CRITICAL INSTRUCTIONS
195
+
196
+ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided for context only — it has already been processed.
197
+
198
+ \`\`\`json
199
+ ${jsonTemplate}
200
+ \`\`\`
201
+
202
+ When returning a record, **ALWAYS** include \`name\`, \`description\`, and \`sentiment\`.
203
+
204
+ If you find **NO EVIDENCE** of this PERSON in the "Most Recent Messages", respond with: \`{}\`
205
+
206
+ If **NO CHANGES** are required, respond with: \`{}\`
207
+
208
+ An empty object is the MOST COMMON expected response.
209
+
210
+ # Current Details of PERSON
211
+
212
+ ${currentDetailsSection}
213
+ `;
214
+
215
+ const earlierSection =
216
+ data.messages_context.length > 0
217
+ ? `## Earlier Conversation
218
+ ${formatMessagesAsPlaceholders(data.messages_context, personaName)}
219
+
220
+ `
221
+ : "";
222
+
223
+ const recentSection = `## Most Recent Messages
224
+ ${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
225
+
226
+ const user = `# Conversation
227
+ ${earlierSection}${recentSection}
228
+
229
+ ---
230
+
231
+ Analyze the Most Recent Messages and update the PERSON if warranted.
232
+
233
+ **Return JSON:**
234
+ \`\`\`json
235
+ ${jsonTemplate}
236
+ \`\`\`
237
+
238
+ If no changes are needed, respond with: \`{}\``;
239
+
240
+ return { system, user };
241
+ }
@@ -0,0 +1,65 @@
1
+ import type { PromptOutput } from "./types.js";
2
+
3
+ export interface TopicMatchPromptData {
4
+ candidate_name: string;
5
+ candidate_description: string;
6
+ candidate_category: string;
7
+ existing_topics: Array<{
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ category?: string;
12
+ }>;
13
+ }
14
+
15
+ export function buildTopicMatchPrompt(data: TopicMatchPromptData): PromptOutput {
16
+ if (!data.candidate_name) {
17
+ throw new Error("buildTopicMatchPrompt: candidate_name is required");
18
+ }
19
+
20
+ const system = `# Task
21
+
22
+ You are checking if a TOPIC already exists in our database.
23
+
24
+ ## Matching Rules
25
+
26
+ 1. **Exact match**: Same name or concept → return its ID
27
+ 2. **Similar match**: Clearly the same topic with different wording → return its ID
28
+ 3. **No match**: Genuinely new information → return "new"
29
+
30
+ Be conservative. If you're unsure, return "new" — a duplicate is worse than a gap.
31
+
32
+ # Existing Topics
33
+
34
+ \`\`\`json
35
+ ${JSON.stringify(data.existing_topics, 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 Topic
51
+
52
+ Name: ${data.candidate_name}
53
+ Description: ${data.candidate_description}
54
+ Category: ${data.candidate_category}
55
+
56
+ Find the best match in existing topics, or return "new" if this is genuinely new.
57
+
58
+ \`\`\`json
59
+ {
60
+ "matched_guid": "..." | "new"
61
+ }
62
+ \`\`\``;
63
+
64
+ return { system, user };
65
+ }