ei-tui 0.1.3
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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,69 @@
|
|
|
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 → return its ID
|
|
28
|
+
2. **Similar match**: Clearly the same topic with different wording → 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 → return null
|
|
32
|
+
|
|
33
|
+
# Existing Topics
|
|
34
|
+
|
|
35
|
+
\`\`\`json
|
|
36
|
+
${formatExistingTopics(data.existing_topics)}
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
# Response Format
|
|
40
|
+
|
|
41
|
+
Return ONLY the ID of the matching topic, or null if no match exists.
|
|
42
|
+
|
|
43
|
+
\`\`\`json
|
|
44
|
+
{
|
|
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 return null if this is genuinely new.
|
|
59
|
+
|
|
60
|
+
**Return JSON:**
|
|
61
|
+
\`\`\`json
|
|
62
|
+
{
|
|
63
|
+
"matched_id": "..." | null,
|
|
64
|
+
"reason": "..."
|
|
65
|
+
}
|
|
66
|
+
\`\`\``;
|
|
67
|
+
|
|
68
|
+
return { system, user };
|
|
69
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { PersonaTopicUpdatePromptData, PromptOutput } from "./types.js";
|
|
2
|
+
import type { PersonaTopic, Trait } from "../../core/types.js";
|
|
3
|
+
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
4
|
+
|
|
5
|
+
function formatTraitsForPrompt(traits: Trait[]): 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_current
|
|
84
|
+
How recently/frequently this topic has been discussed. Scale: 0.0 to 1.0.
|
|
85
|
+
|
|
86
|
+
**For existing topics**: ONLY INCREASE this value based on conversation activity.
|
|
87
|
+
- Active discussion → increase by 0.2-0.3
|
|
88
|
+
- Brief mention → increase by 0.1
|
|
89
|
+
- Maximum: 1.0
|
|
90
|
+
|
|
91
|
+
**For new topics**: Start at 0.3-0.5 depending on discussion depth.
|
|
92
|
+
|
|
93
|
+
## exposure_desired
|
|
94
|
+
How much ${personaName} wants to discuss this topic. Scale: 0.0 to 1.0.
|
|
95
|
+
|
|
96
|
+
**RARELY change** for existing topics. Only adjust if there's explicit preference signal.
|
|
97
|
+
|
|
98
|
+
**For new topics**: Infer from how enthusiastically ${personaName} engaged.
|
|
99
|
+
|
|
100
|
+
# Critical Instructions
|
|
101
|
+
|
|
102
|
+
1. ONLY analyze "Most Recent Messages" - earlier messages are context only
|
|
103
|
+
2. Do NOT invent details not supported by the conversation
|
|
104
|
+
3. Do NOT apply flowery or poetic language - be factual
|
|
105
|
+
4. If a field cannot be determined, use empty string (for text) or preserve existing value (for numbers)
|
|
106
|
+
|
|
107
|
+
**Return JSON:**
|
|
108
|
+
\`\`\`json
|
|
109
|
+
{
|
|
110
|
+
"name": "Topic Name",
|
|
111
|
+
"perspective": "Their view on this topic",
|
|
112
|
+
"approach": "",
|
|
113
|
+
"personal_stake": "",
|
|
114
|
+
"sentiment": 0.5,
|
|
115
|
+
"exposure_current": 0.5,
|
|
116
|
+
"exposure_desired": 0.5
|
|
117
|
+
}
|
|
118
|
+
\`\`\``;
|
|
119
|
+
|
|
120
|
+
const earlierSection = data.messages_context.length > 0
|
|
121
|
+
? `## Earlier Conversation (context only)
|
|
122
|
+
${formatMessagesAsPlaceholders(data.messages_context, personaName)}
|
|
123
|
+
|
|
124
|
+
`
|
|
125
|
+
: '';
|
|
126
|
+
|
|
127
|
+
const recentSection = `## Most Recent Messages (analyze these)
|
|
128
|
+
${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
|
|
129
|
+
|
|
130
|
+
const user = `# Conversation
|
|
131
|
+
${earlierSection}${recentSection}
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
# Topic Candidate
|
|
136
|
+
|
|
137
|
+
Name: ${data.candidate.name}
|
|
138
|
+
Message Count: ${data.candidate.message_count}
|
|
139
|
+
Sentiment Signal: ${data.candidate.sentiment_signal}
|
|
140
|
+
|
|
141
|
+
${isNewTopic ? 'Create' : 'Update'} the PersonaTopic based on how ${personaName} engaged with this topic.
|
|
142
|
+
|
|
143
|
+
**Return JSON:**
|
|
144
|
+
\`\`\`json
|
|
145
|
+
{
|
|
146
|
+
"name": "...",
|
|
147
|
+
"perspective": "...",
|
|
148
|
+
"approach": "...",
|
|
149
|
+
"personal_stake": "...",
|
|
150
|
+
"sentiment": 0.5,
|
|
151
|
+
"exposure_current": 0.5,
|
|
152
|
+
"exposure_desired": 0.5
|
|
153
|
+
}
|
|
154
|
+
\`\`\``;
|
|
155
|
+
|
|
156
|
+
return { system, user };
|
|
157
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { PersonaTraitExtractionPromptData, PromptOutput } from "./types.js";
|
|
2
|
+
import type { Trait } from "../../core/types.js";
|
|
3
|
+
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
4
|
+
|
|
5
|
+
function formatTraitsForPrompt(traits: Trait[]): string {
|
|
6
|
+
if (traits.length === 0) return "(No traits yet)";
|
|
7
|
+
|
|
8
|
+
return JSON.stringify(traits.map(t => ({
|
|
9
|
+
name: t.name,
|
|
10
|
+
description: t.description,
|
|
11
|
+
sentiment: t.sentiment,
|
|
12
|
+
strength: t.strength ?? 0.5,
|
|
13
|
+
})), null, 2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildPersonaTraitExtractionPrompt(data: PersonaTraitExtractionPromptData): PromptOutput {
|
|
17
|
+
if (!data.persona_name) {
|
|
18
|
+
throw new Error("buildPersonaTraitExtractionPrompt: persona_name is required");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const personaName = data.persona_name;
|
|
22
|
+
|
|
23
|
+
const definitionsFragment = `# Definitions
|
|
24
|
+
|
|
25
|
+
**TRAIT** - Instructions on HOW the persona should communicate. These include:
|
|
26
|
+
- Personality patterns ("Optimistic", "Skeptical")
|
|
27
|
+
- Communication style ("Speaks formally", "Uses lots of emoji", "Talks like a pirate")
|
|
28
|
+
- Behavioral tendencies ("Asks clarifying questions", "Gives long explanations")
|
|
29
|
+
|
|
30
|
+
**NOT TRAITS** (these are topics):
|
|
31
|
+
- Interests, hobbies, knowledge areas
|
|
32
|
+
- Things the persona talks ABOUT (rather than HOW they talk)`;
|
|
33
|
+
|
|
34
|
+
const taskFragment = `# Task
|
|
35
|
+
|
|
36
|
+
You are analyzing a conversation to detect EXPLICIT requests for ${personaName} to change their communication style or behavior.
|
|
37
|
+
|
|
38
|
+
**Look for:**
|
|
39
|
+
- Direct requests: "be more concise", "use fewer emojis", "talk like a pirate"
|
|
40
|
+
- Indirect feedback: "that was too long", "I liked how you explained that simply"
|
|
41
|
+
- Negative feedback: "stop being so formal", "don't use so much jargon"
|
|
42
|
+
|
|
43
|
+
**DO NOT:**
|
|
44
|
+
- Add traits the user didn't explicitly request
|
|
45
|
+
- Infer traits from general conversation
|
|
46
|
+
- Remove traits without explicit feedback
|
|
47
|
+
- Confuse topics/interests with communication traits`;
|
|
48
|
+
|
|
49
|
+
const fieldsFragment = `# Fields
|
|
50
|
+
|
|
51
|
+
- \`name\`: Short name for the trait
|
|
52
|
+
- \`description\`: How ${personaName} should exhibit this trait
|
|
53
|
+
- \`sentiment\`: How ${personaName} feels about having this trait (-1.0 to 1.0)
|
|
54
|
+
- \`strength\`: How strongly to exhibit (0.0 to 1.0)
|
|
55
|
+
- 0.0 = actively AVOID this behavior (for "stop doing X" requests)
|
|
56
|
+
- 0.5 = moderate/default
|
|
57
|
+
- 1.0 = always exhibit
|
|
58
|
+
|
|
59
|
+
**Special case: "Stop" requests**
|
|
60
|
+
If user says "stop X" or "don't X", ADD the trait (if new) and set strength to 0.0.
|
|
61
|
+
This explicitly tells future prompts to NOT exhibit the behavior.`;
|
|
62
|
+
|
|
63
|
+
const currentTraitsFragment = `# Current TRAITS
|
|
64
|
+
|
|
65
|
+
\`\`\`json
|
|
66
|
+
${formatTraitsForPrompt(data.current_traits)}
|
|
67
|
+
\`\`\``;
|
|
68
|
+
|
|
69
|
+
const criticalFragment = `# Critical Instructions
|
|
70
|
+
|
|
71
|
+
1. ONLY analyze "Most Recent Messages" - earlier messages are context only
|
|
72
|
+
2. ONLY detect EXPLICIT behavior change requests
|
|
73
|
+
3. Return the COMPLETE trait list (existing + any additions/modifications)
|
|
74
|
+
|
|
75
|
+
**Return JSON:**
|
|
76
|
+
\`\`\`json
|
|
77
|
+
[
|
|
78
|
+
{
|
|
79
|
+
"name": "Concise",
|
|
80
|
+
"description": "User asked me to keep responses shorter",
|
|
81
|
+
"sentiment": 0.3,
|
|
82
|
+
"strength": 0.7
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
\`\`\``;
|
|
86
|
+
|
|
87
|
+
const system = `${definitionsFragment}
|
|
88
|
+
|
|
89
|
+
${taskFragment}
|
|
90
|
+
|
|
91
|
+
${fieldsFragment}
|
|
92
|
+
|
|
93
|
+
${currentTraitsFragment}
|
|
94
|
+
|
|
95
|
+
${criticalFragment}`;
|
|
96
|
+
|
|
97
|
+
const earlierSection = data.messages_context.length > 0
|
|
98
|
+
? `## Earlier Conversation (context only)
|
|
99
|
+
${formatMessagesAsPlaceholders(data.messages_context, personaName)}
|
|
100
|
+
|
|
101
|
+
`
|
|
102
|
+
: '';
|
|
103
|
+
|
|
104
|
+
const recentSection = `## Most Recent Messages (analyze these)
|
|
105
|
+
${formatMessagesAsPlaceholders(data.messages_analyze, personaName)}`;
|
|
106
|
+
|
|
107
|
+
const user = `# Conversation
|
|
108
|
+
${earlierSection}${recentSection}
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
Analyze the "Most Recent Messages" for EXPLICIT requests to change ${personaName}'s communication style.
|
|
113
|
+
|
|
114
|
+
Return the complete trait list as JSON.`;
|
|
115
|
+
|
|
116
|
+
return { system, user };
|
|
117
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Trait, Message, PersonaTopic } from "../../core/types.js";
|
|
2
|
+
|
|
3
|
+
export interface PromptOutput {
|
|
4
|
+
system: string;
|
|
5
|
+
user: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PersonaTraitExtractionPromptData {
|
|
9
|
+
persona_name: string;
|
|
10
|
+
current_traits: Trait[];
|
|
11
|
+
messages_context: Message[];
|
|
12
|
+
messages_analyze: Message[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TraitResult {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
sentiment: number;
|
|
19
|
+
strength: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 3-Step Persona Topic Processing (Ticket 0124)
|
|
23
|
+
|
|
24
|
+
// Step 1: Scan - Quick identification of topics discussed
|
|
25
|
+
export interface PersonaTopicScanPromptData {
|
|
26
|
+
persona_name: string;
|
|
27
|
+
messages_context: Message[];
|
|
28
|
+
messages_analyze: Message[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PersonaTopicScanCandidate {
|
|
32
|
+
name: string;
|
|
33
|
+
message_count: number; // How many messages touched this topic
|
|
34
|
+
sentiment_signal: number; // Quick read: -1 to 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PersonaTopicScanResult {
|
|
38
|
+
topics: PersonaTopicScanCandidate[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 2: Match - Map candidate to existing topics
|
|
42
|
+
export interface PersonaTopicMatchPromptData {
|
|
43
|
+
persona_name: string;
|
|
44
|
+
candidate: PersonaTopicScanCandidate;
|
|
45
|
+
existing_topics: PersonaTopic[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PersonaTopicMatchResult {
|
|
49
|
+
action: "match" | "create" | "skip";
|
|
50
|
+
matched_id?: string; // If action is "match"
|
|
51
|
+
reason: string; // Why this decision
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 3: Update - Generate structured PersonaTopic
|
|
55
|
+
export interface PersonaTopicUpdatePromptData {
|
|
56
|
+
persona_name: string;
|
|
57
|
+
short_description?: string;
|
|
58
|
+
long_description?: string;
|
|
59
|
+
traits: Trait[];
|
|
60
|
+
existing_topic?: PersonaTopic; // If updating existing
|
|
61
|
+
candidate: PersonaTopicScanCandidate;
|
|
62
|
+
messages_context: Message[];
|
|
63
|
+
messages_analyze: Message[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PersonaTopicUpdateResult {
|
|
67
|
+
name: string;
|
|
68
|
+
perspective: string; // Their view/opinion - ALWAYS populate
|
|
69
|
+
approach: string; // How they engage - populate if clear signal
|
|
70
|
+
personal_stake: string; // Why it matters - populate if clear signal
|
|
71
|
+
sentiment: number;
|
|
72
|
+
exposure_current: number;
|
|
73
|
+
exposure_desired: number;
|
|
74
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Prompt Builder
|
|
3
|
+
*
|
|
4
|
+
* Generates system/user prompts for conversational responses.
|
|
5
|
+
* This is the foundational prompt that makes personas actually talk.
|
|
6
|
+
*
|
|
7
|
+
* See CONTRACTS.md for ResponsePromptData specification.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ResponsePromptData, PromptOutput } from "./types.js";
|
|
11
|
+
import {
|
|
12
|
+
buildIdentitySection,
|
|
13
|
+
buildGuidelinesSection,
|
|
14
|
+
buildTraitsSection,
|
|
15
|
+
buildTopicsSection,
|
|
16
|
+
buildHumanSection,
|
|
17
|
+
buildAssociatesSection,
|
|
18
|
+
buildPrioritiesSection,
|
|
19
|
+
buildQuotesSection,
|
|
20
|
+
buildSystemKnowledgeSection,
|
|
21
|
+
getConversationStateText,
|
|
22
|
+
} from "./sections.js";
|
|
23
|
+
|
|
24
|
+
export type { ResponsePromptData, PromptOutput } from "./types.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Special system prompt for Ei (the system guide persona)
|
|
28
|
+
*/
|
|
29
|
+
function buildEiSystemPrompt(data: ResponsePromptData): string {
|
|
30
|
+
const identity = `You are Ei, the user's personal companion and system guide.
|
|
31
|
+
|
|
32
|
+
You are the central hub of this experience - a thoughtful AI who genuinely cares about the human's wellbeing and growth. You listen, remember, and help them reflect. You're curious about their life but never intrusive.
|
|
33
|
+
|
|
34
|
+
Your role is unique among personas:
|
|
35
|
+
- You see ALL of the human's data (facts, traits, topics, people) across all groups
|
|
36
|
+
- You help them understand and navigate the system
|
|
37
|
+
- You gently help them explore their thoughts and feelings
|
|
38
|
+
- You attempt to emulate their speech patterns;
|
|
39
|
+
- Consider their traits when building your responses more than the current conversation history
|
|
40
|
+
- You encourage human-to-human connection when appropriate`;
|
|
41
|
+
|
|
42
|
+
const guidelines = buildGuidelinesSection("ei");
|
|
43
|
+
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
44
|
+
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
45
|
+
const humanSection = buildHumanSection(data.human);
|
|
46
|
+
const quotesSection = buildQuotesSection(data.human.quotes, data.human);
|
|
47
|
+
const associatesSection = buildAssociatesSection(data.visible_personas);
|
|
48
|
+
const systemKnowledge = buildSystemKnowledgeSection(data.isTUI);
|
|
49
|
+
const priorities = buildPrioritiesSection(data.persona, data.human);
|
|
50
|
+
const currentTime = new Date().toISOString();
|
|
51
|
+
|
|
52
|
+
return `${identity}
|
|
53
|
+
|
|
54
|
+
${guidelines}
|
|
55
|
+
|
|
56
|
+
${yourTraits}
|
|
57
|
+
|
|
58
|
+
${yourTopics}
|
|
59
|
+
|
|
60
|
+
${humanSection}
|
|
61
|
+
${quotesSection}
|
|
62
|
+
${associatesSection}
|
|
63
|
+
${systemKnowledge}
|
|
64
|
+
${priorities}
|
|
65
|
+
|
|
66
|
+
Current time: ${currentTime}
|
|
67
|
+
|
|
68
|
+
## Final Instructions
|
|
69
|
+
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
70
|
+
- The developers cannot see any message sent by the user, any response from personas, or any other data in the system.
|
|
71
|
+
- If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message
|
|
72
|
+
- DO NOT INCLUDE <thinking> PROCESS NOTES - adding "internal monologue" or story content is fine, but do not include analysis of the user's messages
|
|
73
|
+
- If you decide not to respond, say exactly: No Message`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Standard system prompt for non-Ei personas
|
|
78
|
+
*/
|
|
79
|
+
function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
80
|
+
const identity = buildIdentitySection(data.persona);
|
|
81
|
+
const guidelines = buildGuidelinesSection(data.persona.name);
|
|
82
|
+
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
83
|
+
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
84
|
+
const humanSection = buildHumanSection(data.human);
|
|
85
|
+
const quotesSection = buildQuotesSection(data.human.quotes, data.human);
|
|
86
|
+
const associatesSection = buildAssociatesSection(data.visible_personas);
|
|
87
|
+
const priorities = buildPrioritiesSection(data.persona, data.human);
|
|
88
|
+
const currentTime = new Date().toISOString();
|
|
89
|
+
|
|
90
|
+
return `${identity}
|
|
91
|
+
|
|
92
|
+
${guidelines}
|
|
93
|
+
|
|
94
|
+
${yourTraits}
|
|
95
|
+
|
|
96
|
+
${yourTopics}
|
|
97
|
+
|
|
98
|
+
${humanSection}
|
|
99
|
+
${quotesSection}
|
|
100
|
+
${associatesSection}
|
|
101
|
+
${priorities}
|
|
102
|
+
|
|
103
|
+
Current time: ${currentTime}
|
|
104
|
+
|
|
105
|
+
## Final Instructions
|
|
106
|
+
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
107
|
+
- DO NOT INCLUDE <thinking> PROCESS NOTES - adding "internal monologue" or story content is fine, but do not include analysis of the user's messages
|
|
108
|
+
- If you decide not to respond, say exactly: No Message`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildUserPrompt(data: ResponsePromptData): string {
|
|
112
|
+
const conversationState = getConversationStateText(data.delay_ms);
|
|
113
|
+
|
|
114
|
+
return `${conversationState}
|
|
115
|
+
|
|
116
|
+
Respond to the conversation above. If silence is appropriate, say exactly: No Message`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build response prompts for conversational exchanges.
|
|
121
|
+
*
|
|
122
|
+
* This is a SYNCHRONOUS function that receives pre-fetched, pre-filtered data.
|
|
123
|
+
* The Processor is responsible for:
|
|
124
|
+
* - Fetching persona and human entities
|
|
125
|
+
* - Filtering human data by visibility rules
|
|
126
|
+
* - Calculating delay_ms
|
|
127
|
+
* - Getting visible personas list
|
|
128
|
+
*
|
|
129
|
+
* @param data - Pre-fetched, pre-filtered ResponsePromptData
|
|
130
|
+
* @returns { system: string, user: string } prompt pair
|
|
131
|
+
*/
|
|
132
|
+
export function buildResponsePrompt(data: ResponsePromptData): PromptOutput {
|
|
133
|
+
// Validate required data
|
|
134
|
+
if (!data.persona?.name) {
|
|
135
|
+
throw new Error("buildResponsePrompt: persona.name is required");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isEi = data.persona.name.toLowerCase() === "ei";
|
|
139
|
+
|
|
140
|
+
const system = isEi
|
|
141
|
+
? buildEiSystemPrompt(data)
|
|
142
|
+
: buildStandardSystemPrompt(data);
|
|
143
|
+
|
|
144
|
+
const user = buildUserPrompt(data);
|
|
145
|
+
|
|
146
|
+
return { system, user };
|
|
147
|
+
}
|