ei-tui 0.1.24 → 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.
- package/README.md +42 -0
- package/package.json +1 -1
- package/src/README.md +4 -11
- package/src/cli/README.md +4 -5
- package/src/cli/retrieval.ts +3 -25
- package/src/cli.ts +3 -7
- package/src/core/AGENTS.md +1 -1
- package/src/core/constants/built-in-facts.ts +49 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/context-utils.ts +0 -1
- package/src/core/embedding-service.ts +8 -0
- package/src/core/handlers/dedup.ts +34 -14
- package/src/core/handlers/heartbeat.ts +2 -3
- package/src/core/handlers/human-extraction.ts +95 -30
- package/src/core/handlers/human-matching.ts +326 -248
- package/src/core/handlers/index.ts +8 -6
- package/src/core/handlers/persona-generation.ts +8 -8
- package/src/core/handlers/rewrite.ts +4 -29
- package/src/core/handlers/utils.ts +23 -1
- package/src/core/heartbeat-manager.ts +2 -4
- package/src/core/human-data-manager.ts +5 -27
- package/src/core/message-manager.ts +10 -10
- package/src/core/orchestrators/ceremony.ts +60 -46
- package/src/core/orchestrators/dedup-phase.ts +11 -5
- package/src/core/orchestrators/human-extraction.ts +351 -207
- package/src/core/orchestrators/index.ts +6 -4
- package/src/core/orchestrators/persona-generation.ts +3 -3
- package/src/core/processor.ts +113 -22
- package/src/core/prompt-context-builder.ts +4 -6
- package/src/core/state/human.ts +1 -26
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +107 -14
- package/src/core/tools/builtin/read-memory.ts +7 -8
- package/src/core/types/data-items.ts +2 -4
- package/src/core/types/entities.ts +6 -4
- package/src/core/types/enums.ts +6 -9
- package/src/core/types/llm.ts +2 -2
- package/src/core/utils/crossFind.ts +2 -5
- package/src/core/utils/event-windows.ts +31 -0
- package/src/integrations/claude-code/importer.ts +8 -4
- package/src/integrations/claude-code/types.ts +2 -0
- package/src/integrations/opencode/importer.ts +7 -3
- package/src/prompts/AGENTS.md +73 -1
- package/src/prompts/ceremony/dedup.ts +41 -7
- package/src/prompts/ceremony/rewrite.ts +3 -22
- package/src/prompts/ceremony/types.ts +3 -3
- package/src/prompts/generation/descriptions.ts +2 -2
- package/src/prompts/generation/types.ts +2 -2
- package/src/prompts/heartbeat/types.ts +2 -2
- package/src/prompts/human/event-scan.ts +122 -0
- package/src/prompts/human/fact-find.ts +106 -0
- package/src/prompts/human/fact-scan.ts +0 -2
- package/src/prompts/human/index.ts +17 -10
- package/src/prompts/human/person-match.ts +65 -0
- package/src/prompts/human/person-scan.ts +52 -59
- package/src/prompts/human/person-update.ts +241 -0
- package/src/prompts/human/topic-match.ts +65 -0
- package/src/prompts/human/topic-scan.ts +51 -71
- package/src/prompts/human/topic-update.ts +295 -0
- package/src/prompts/human/types.ts +63 -40
- package/src/prompts/index.ts +4 -8
- package/src/prompts/persona/topics-update.ts +2 -2
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/persona/types.ts +3 -3
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +9 -12
- package/src/prompts/response/types.ts +2 -3
- package/src/storage/embeddings.ts +1 -1
- package/src/storage/index.ts +1 -0
- package/src/storage/indexed.ts +174 -0
- package/src/storage/merge.ts +67 -2
- package/tui/src/app.tsx +7 -5
- package/tui/src/commands/archive.tsx +2 -2
- package/tui/src/commands/context.tsx +3 -4
- package/tui/src/commands/delete.tsx +4 -4
- package/tui/src/commands/dlq.ts +3 -4
- package/tui/src/commands/help.tsx +1 -1
- package/tui/src/commands/me.tsx +8 -18
- package/tui/src/commands/persona.tsx +2 -2
- package/tui/src/commands/provider.tsx +3 -5
- package/tui/src/commands/queue.ts +3 -4
- package/tui/src/commands/quotes.tsx +6 -8
- package/tui/src/commands/registry.ts +1 -1
- package/tui/src/commands/setsync.tsx +2 -2
- package/tui/src/commands/settings.tsx +18 -4
- package/tui/src/commands/spotify-auth.ts +0 -1
- package/tui/src/commands/tools.tsx +4 -5
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/context/overlay.tsx +17 -6
- package/tui/src/util/editor.ts +22 -11
- package/tui/src/util/persona-editor.tsx +6 -8
- package/tui/src/util/provider-editor.tsx +6 -8
- package/tui/src/util/toolkit-editor.tsx +3 -4
- package/tui/src/util/yaml-serializers.ts +48 -33
- package/src/cli/commands/traits.ts +0 -25
- package/src/prompts/human/item-match.ts +0 -74
- package/src/prompts/human/item-update.ts +0 -364
- package/src/prompts/human/trait-scan.ts +0 -115
package/README.md
CHANGED
|
@@ -116,6 +116,48 @@ Opencode saves all of its sessions locally, either in a JSON structure or, if yo
|
|
|
116
116
|
|
|
117
117
|
Then, Opencode can call into Ei and pull those details back out. That's why you always have a side-project or two going. See [TUI Readme](tui/README.md)
|
|
118
118
|
|
|
119
|
+
## Built-in Tool Integrations
|
|
120
|
+
|
|
121
|
+
Personas can use tools. Not just read-from-memory tools — *actual* tools. Web search. Your music. Your filesystem. Here's what ships with Ei out of the box:
|
|
122
|
+
|
|
123
|
+
### Ei Built-ins (always available, no setup)
|
|
124
|
+
|
|
125
|
+
| Tool | What it does |
|
|
126
|
+
|------|-------------|
|
|
127
|
+
| `read_memory` | Semantic search of your personal memory — facts, traits, topics, people, quotes. Personas call this automatically when the conversation touches something they might know about you. |
|
|
128
|
+
| `file_read` | Read a file from your local filesystem *(TUI only)* |
|
|
129
|
+
| `list_directory` | Explore folder structure *(TUI only)* |
|
|
130
|
+
| `directory_tree` | Recursive directory tree *(TUI only)* |
|
|
131
|
+
| `search_files` | Find files by name pattern *(TUI only)* |
|
|
132
|
+
| `grep` | Search file contents by regex *(TUI only)* |
|
|
133
|
+
| `get_file_info` | File/directory metadata *(TUI only)* |
|
|
134
|
+
|
|
135
|
+
The filesystem tools make Ei a legitimate coding assistant in the TUI. Ask a persona to review a file, understand a project structure, or track down where something is defined — it can actually look.
|
|
136
|
+
|
|
137
|
+
### Tavily Web Search (requires free API key)
|
|
138
|
+
|
|
139
|
+
| Tool | What it does |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `tavily_web_search` | Real-time web search — current events, fact-checking, anything that needs up-to-date information |
|
|
142
|
+
| `tavily_news_search` | Recent news articles |
|
|
143
|
+
|
|
144
|
+
Get a free key at [tavily.com](https://tavily.com) (1,000 requests/month free tier). Add it in **Settings → Tool Kits → Tavily Search**.
|
|
145
|
+
|
|
146
|
+
### Spotify (requires OAuth connection)
|
|
147
|
+
|
|
148
|
+
| Tool | What it does |
|
|
149
|
+
|------|-------------|
|
|
150
|
+
| `get_currently_playing` | What's playing right now — artist, title, album, progress |
|
|
151
|
+
| `get_liked_songs` | Your full liked songs library |
|
|
152
|
+
|
|
153
|
+
Connect in **Settings → Tool Kits → Spotify**. Once connected, personas can ask what you're listening to and actually know. Music-aware conversations.
|
|
154
|
+
|
|
155
|
+
### Assigning Tools to Personas
|
|
156
|
+
|
|
157
|
+
Tools aren't global — you choose which personas get access. Edit a persona and toggle the tools it can use. A focused work persona might only have filesystem tools. A general-purpose companion might have everything.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
119
161
|
## Technical Details
|
|
120
162
|
|
|
121
163
|
This project is separated into five (5) logical parts:
|
package/package.json
CHANGED
package/src/README.md
CHANGED
|
@@ -10,7 +10,7 @@ There are two distinct types of data: Human and Persona.
|
|
|
10
10
|
|
|
11
11
|
## Human
|
|
12
12
|
|
|
13
|
-
Human data is sort of the "Global" data - Each Persona can read and write elements to the humans Facts,
|
|
13
|
+
Human data is sort of the "Global" data - Each Persona can read and write elements to the humans Facts, People, and Topics. In addition, there are "Quotes" that can tie to those three types of data.
|
|
14
14
|
|
|
15
15
|
As the user uses the system, it tries to keep track of several data points for these elements:
|
|
16
16
|
|
|
@@ -24,12 +24,9 @@ As the user uses the system, it tries to keep track of several data points for t
|
|
|
24
24
|
* Current: How much the user has talked or heard about a subject, where:
|
|
25
25
|
+ 0.0: Obi-Wan Kenobi ...now that's a name I've not heard in a long time
|
|
26
26
|
+ 1.0: The user just spent 4 hours talking about Star Wars
|
|
27
|
-
-
|
|
28
|
-
* 1.0 on "Visual Learner" would mean that you've said or shown that it is the absolute best way for you to learn
|
|
29
|
-
* 0.0 on "Public Speaker" would mean you've said or shown that you have no desire, aptitude, or willingness to present
|
|
30
|
-
- Validated: "Facts" have proven almost as hard to get right as Traits, so I added a way for Ei and you to mark the ones that are true as "Validated"
|
|
27
|
+
- Validated: "Facts" have proven tricky, so I added a way for Ei and you to mark the ones that are true as "Validated"
|
|
31
28
|
|
|
32
|
-
Each of those types represents a piece of what the system "knows" about the person, and
|
|
29
|
+
Each of those types represents a piece of what the system "knows" about the person, and they're kept up-to-date as the person chats with Personas, but not always on every message. On each message to a Persona, a check is made:
|
|
33
30
|
|
|
34
31
|
```
|
|
35
32
|
if(Person.newMessages > count_of_human_[type]) {
|
|
@@ -37,9 +34,7 @@ if(Person.newMessages > count_of_human_[type]) {
|
|
|
37
34
|
}
|
|
38
35
|
```
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
> <sup>1</sup> Traits are unique because, after trying to extract them in the same way as the other pieces of data, I realized that it's sorta hard to understand a core aspect of someone in one message, or even 10. Even doing this analysis over a full 24 hours hasn't proven to be particularly effective, but it's the best we have so far.
|
|
37
|
+
This extracts quotes, description updates, title updates, etc. for the conversations the user is having, and keeps them feeling alive.
|
|
43
38
|
|
|
44
39
|
## Persona
|
|
45
40
|
|
|
@@ -72,8 +67,6 @@ I also frequently refer to this as "Extract," but this is the first step where w
|
|
|
72
67
|
|
|
73
68
|
Since we also pull out details during normal discourse (see above), this is the less-important step at this point, but still vital for catching up with the last few messages, or Personas that only received a few messages during the day and may not have hit the current limit for natural extraction.
|
|
74
69
|
|
|
75
|
-
Additionally, this is the ONLY time when Human Traits are created or updated - after (hopefully) enough messages have been exchanged for an agent to analyze it and say "Yup, Flare is _definitely_ verbose."
|
|
76
|
-
|
|
77
70
|
### Exposure Adjustment
|
|
78
71
|
|
|
79
72
|
Exposure is calculated by two metrics - `desired` and `current`. If an entity REALLY likes talking about a subject, their `desired` will be very high (1.0 max), ranging down to 0.0 for subjects which that entity does NOT wish to discuss. You may have guessed already, but `current` is how much they've recently talked about a topic.
|
package/src/cli/README.md
CHANGED
|
@@ -6,7 +6,6 @@ ei # Start the TUI
|
|
|
6
6
|
ei "query string" # Return up to 10 results across all types
|
|
7
7
|
ei -n 5 "query string" # Return up to 5 results
|
|
8
8
|
ei facts -n 5 "query string" # Return up to 5 facts
|
|
9
|
-
ei traits -n 5 "query string" # Return up to 5 traits
|
|
10
9
|
ei people -n 5 "query string" # Return up to 5 people
|
|
11
10
|
ei topics -n 5 "query string" # Return up to 5 topics
|
|
12
11
|
ei quotes -n 5 "query string" # Return up to 5 quotes
|
|
@@ -15,7 +14,7 @@ echo <id> | ei --id # Look up entity by ID from stdin
|
|
|
15
14
|
ei --install # Install the Ei tool for OpenCode
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
Type aliases: `fact`, `
|
|
17
|
+
Type aliases: `fact`, `person`, `topic`, `quote` all work (singular or plural).
|
|
19
18
|
|
|
20
19
|
# An Agentic Tool
|
|
21
20
|
|
|
@@ -37,12 +36,12 @@ This writes `~/.config/opencode/tools/ei.ts` with a complete tool definition. Re
|
|
|
37
36
|
|
|
38
37
|
## What the Tool Provides
|
|
39
38
|
|
|
40
|
-
The installed tool gives OpenCode agents access to all
|
|
39
|
+
The installed tool gives OpenCode agents access to all four data types with proper Zod-validated args:
|
|
41
40
|
|
|
42
41
|
| Arg | Type | Description |
|
|
43
42
|
|-----|------|-------------|
|
|
44
43
|
| `query` | string (required) | Search text, or entity ID when `lookup=true` |
|
|
45
|
-
| `type` | enum (optional) | `facts` \| `
|
|
44
|
+
| `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` — omit for balanced results |
|
|
46
45
|
| `limit` | number (optional) | Max results, default 10 |
|
|
47
46
|
| `lookup` | boolean (optional) | If true, fetch single entity by ID |
|
|
48
47
|
|
|
@@ -50,7 +49,7 @@ The installed tool gives OpenCode agents access to all five data types with prop
|
|
|
50
49
|
|
|
51
50
|
All search commands return arrays. Each result includes a `type` field.
|
|
52
51
|
|
|
53
|
-
**Fact /
|
|
52
|
+
**Fact / Person / Topic**: `{ type, id, name, description, sentiment, ...type-specific fields }`
|
|
54
53
|
|
|
55
54
|
**Quote**: `{ type, id, text, speaker, timestamp, linked_items[] }`
|
|
56
55
|
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageState, Quote, Fact,
|
|
1
|
+
import type { StorageState, Quote, Fact, Person, Topic } from "../core/types";
|
|
2
2
|
import { decodeAllEmbeddings } from "../storage/embeddings";
|
|
3
3
|
import { crossFind } from "../core/utils/index.ts";
|
|
4
4
|
import { join } from "path";
|
|
@@ -67,15 +67,6 @@ export interface FactResult {
|
|
|
67
67
|
name: string;
|
|
68
68
|
description: string;
|
|
69
69
|
sentiment: number;
|
|
70
|
-
validated: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface TraitResult {
|
|
74
|
-
id: string;
|
|
75
|
-
name: string;
|
|
76
|
-
description: string;
|
|
77
|
-
strength: number;
|
|
78
|
-
sentiment: number;
|
|
79
70
|
}
|
|
80
71
|
|
|
81
72
|
export interface PersonResult {
|
|
@@ -97,17 +88,16 @@ export interface TopicResult {
|
|
|
97
88
|
export type BalancedResult =
|
|
98
89
|
| ({ type: "quote" } & QuoteResult)
|
|
99
90
|
| ({ type: "fact" } & FactResult)
|
|
100
|
-
| ({ type: "trait" } & TraitResult)
|
|
101
91
|
| ({ type: "person" } & PersonResult)
|
|
102
92
|
| ({ type: "topic" } & TopicResult);
|
|
103
93
|
|
|
104
|
-
const DATA_TYPES = ["quote", "fact", "
|
|
94
|
+
const DATA_TYPES = ["quote", "fact", "person", "topic"] as const;
|
|
105
95
|
type DataType = typeof DATA_TYPES[number];
|
|
106
96
|
|
|
107
97
|
interface ScoredEntry {
|
|
108
98
|
type: DataType;
|
|
109
99
|
similarity: number;
|
|
110
|
-
mapped: QuoteResult | FactResult |
|
|
100
|
+
mapped: QuoteResult | FactResult | PersonResult | TopicResult;
|
|
111
101
|
itemId: string;
|
|
112
102
|
}
|
|
113
103
|
|
|
@@ -117,7 +107,6 @@ export function resolveLinkedItems(dataItemIds: string[], state: StorageState):
|
|
|
117
107
|
{ type: "topic", source: state.human.topics },
|
|
118
108
|
{ type: "person", source: state.human.people },
|
|
119
109
|
{ type: "fact", source: state.human.facts },
|
|
120
|
-
{ type: "trait", source: state.human.traits },
|
|
121
110
|
];
|
|
122
111
|
for (const { type, source } of collections) {
|
|
123
112
|
for (const entity of source) {
|
|
@@ -144,19 +133,9 @@ function mapFact(fact: Fact): FactResult {
|
|
|
144
133
|
name: fact.name,
|
|
145
134
|
description: fact.description,
|
|
146
135
|
sentiment: fact.sentiment,
|
|
147
|
-
validated: fact.validated,
|
|
148
136
|
};
|
|
149
137
|
}
|
|
150
138
|
|
|
151
|
-
function mapTrait(trait: Trait): TraitResult {
|
|
152
|
-
return {
|
|
153
|
-
id: trait.id,
|
|
154
|
-
name: trait.name,
|
|
155
|
-
description: trait.description,
|
|
156
|
-
strength: trait.strength ?? 0.5,
|
|
157
|
-
sentiment: trait.sentiment,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
139
|
|
|
161
140
|
function mapPerson(person: Person): PersonResult {
|
|
162
141
|
return {
|
|
@@ -200,7 +179,6 @@ export async function retrieveBalanced(
|
|
|
200
179
|
}> = [
|
|
201
180
|
{ type: "quote", items: state.human.quotes, mapper: (q: Quote) => mapQuote(q, state) },
|
|
202
181
|
{ type: "fact", items: state.human.facts, mapper: mapFact },
|
|
203
|
-
{ type: "trait", items: state.human.traits, mapper: mapTrait },
|
|
204
182
|
{ type: "person", items: state.human.people, mapper: mapPerson },
|
|
205
183
|
{ type: "topic", items: state.human.topics, mapper: mapTopic },
|
|
206
184
|
];
|
package/src/cli.ts
CHANGED
|
@@ -20,8 +20,6 @@ const TYPE_ALIASES: Record<string, string> = {
|
|
|
20
20
|
quotes: "quotes",
|
|
21
21
|
fact: "facts",
|
|
22
22
|
facts: "facts",
|
|
23
|
-
trait: "traits",
|
|
24
|
-
traits: "traits",
|
|
25
23
|
person: "people",
|
|
26
24
|
people: "people",
|
|
27
25
|
topic: "topics",
|
|
@@ -44,7 +42,6 @@ Usage:
|
|
|
44
42
|
Types:
|
|
45
43
|
quote / quotes Quotes from conversation history
|
|
46
44
|
fact / facts Facts about the user
|
|
47
|
-
trait / traits Personality traits
|
|
48
45
|
person / people People from the user's life
|
|
49
46
|
topic / topics Topics of interest
|
|
50
47
|
|
|
@@ -58,7 +55,6 @@ Examples:
|
|
|
58
55
|
ei "debugging" # Search everything
|
|
59
56
|
ei -n 5 "API design" # Top 5 across all types
|
|
60
57
|
ei quote "you guessed it" # Search quotes only
|
|
61
|
-
ei trait -n 3 "problem solving" # Top 3 matching traits
|
|
62
58
|
ei --id abc-123 # Look up entity by ID
|
|
63
59
|
ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
|
|
64
60
|
`);
|
|
@@ -71,7 +67,7 @@ function buildOpenCodeToolContent(): string {
|
|
|
71
67
|
'export default tool({',
|
|
72
68
|
' description: [',
|
|
73
69
|
' "Search the user\'s Ei knowledge base \u2014 a persistent memory store built from conversations.",',
|
|
74
|
-
' "Returns facts,
|
|
70
|
+
' "Returns facts, people, topics of interest, and quotes.",',
|
|
75
71
|
' "Use this to recall anything about the user: preferences, relationships, or past discussions.",',
|
|
76
72
|
' "Results include entity IDs that can be passed back with lookup=true to get full detail.",',
|
|
77
73
|
' ].join(" "),',
|
|
@@ -80,10 +76,10 @@ function buildOpenCodeToolContent(): string {
|
|
|
80
76
|
' "Search text, or an entity ID when lookup=true. Supports natural language."',
|
|
81
77
|
' ),',
|
|
82
78
|
' type: tool.schema',
|
|
83
|
-
' .enum(["facts", "
|
|
79
|
+
' .enum(["facts", "people", "topics", "quotes"])',
|
|
84
80
|
' .optional()',
|
|
85
81
|
' .describe(',
|
|
86
|
-
' "Filter to a specific data type. Omit to search all types (balanced across all
|
|
82
|
+
' "Filter to a specific data type. Omit to search all types (balanced across all 4).",',
|
|
87
83
|
' ),',
|
|
88
84
|
' limit: tool.schema',
|
|
89
85
|
' .number()',
|
package/src/core/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ core/
|
|
|
10
10
|
├── state-manager.ts # In-memory state + persistence
|
|
11
11
|
├── queue-processor.ts # LLM request queue with priorities
|
|
12
12
|
├── llm-client.ts # Multi-provider LLM abstraction
|
|
13
|
-
├── types.ts # All core types (source
|
|
13
|
+
├── types.ts # All core types (canonical source — CONTRACTS.md defers to these)
|
|
14
14
|
├── handlers/ # LLM response handlers
|
|
15
15
|
├── orchestrators/ # Multi-step workflows
|
|
16
16
|
├── personas/ # Persona loading logic
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in biographical fact categories that Ei tracks.
|
|
3
|
+
*
|
|
4
|
+
* BUILT_IN_FACTS: Array of fact objects (name field only) for iteration/display.
|
|
5
|
+
* BUILT_IN_FACT_NAMES: Set<string> for O(1) lookup (is this fact built-in?).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const BUILT_IN_FACTS: { name: string }[] = [
|
|
9
|
+
// Core Identity
|
|
10
|
+
{ name: "Full Name" },
|
|
11
|
+
{ name: "Nickname/Preferred Name" },
|
|
12
|
+
{ name: "Birthday" },
|
|
13
|
+
{ name: "Birthplace" },
|
|
14
|
+
{ name: "Hometown" },
|
|
15
|
+
{ name: "Current Location" },
|
|
16
|
+
|
|
17
|
+
// Professional
|
|
18
|
+
{ name: "Current Job Title" },
|
|
19
|
+
{ name: "Current Employer" },
|
|
20
|
+
{ name: "Industry/Field" },
|
|
21
|
+
{ name: "Years of Experience" },
|
|
22
|
+
|
|
23
|
+
// Personal
|
|
24
|
+
{ name: "Marital Status" },
|
|
25
|
+
{ name: "Spouse Name" },
|
|
26
|
+
{ name: "Spouse Birthday" },
|
|
27
|
+
{ name: "Date of Marriage" },
|
|
28
|
+
{ name: "Children" },
|
|
29
|
+
{ name: "Parents" },
|
|
30
|
+
{ name: "Gender" },
|
|
31
|
+
{ name: "Pronouns" },
|
|
32
|
+
{ name: "Eye Color" },
|
|
33
|
+
{ name: "Hair Color" },
|
|
34
|
+
{ name: "Height" },
|
|
35
|
+
{ name: "Weight" },
|
|
36
|
+
|
|
37
|
+
// Background
|
|
38
|
+
{ name: "Nationality/Citizenship" },
|
|
39
|
+
{ name: "Languages Spoken" },
|
|
40
|
+
{ name: "Education Level" },
|
|
41
|
+
{ name: "School/University" },
|
|
42
|
+
{ name: "Field of Study" },
|
|
43
|
+
{ name: "Military Service" },
|
|
44
|
+
{ name: "Religious Affiliation" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const BUILT_IN_FACT_NAMES: Set<string> = new Set(
|
|
48
|
+
BUILT_IN_FACTS.map((f) => f.name)
|
|
49
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./built-in-facts";
|
|
@@ -49,7 +49,6 @@ export function stripHumanEmbeddings(human: HumanEntity): HumanEntity {
|
|
|
49
49
|
return {
|
|
50
50
|
...human,
|
|
51
51
|
facts: (human.facts ?? []).map(stripDataItemEmbedding),
|
|
52
|
-
traits: (human.traits ?? []).map(stripDataItemEmbedding),
|
|
53
52
|
topics: (human.topics ?? []).map(stripDataItemEmbedding),
|
|
54
53
|
people: (human.people ?? []).map(stripDataItemEmbedding),
|
|
55
54
|
quotes: (human.quotes ?? []).map(stripQuoteEmbedding),
|
|
@@ -54,6 +54,14 @@ export function getItemEmbeddingText(item: { name: string; description?: string
|
|
|
54
54
|
return item.name;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export function getTopicEmbeddingText(topic: { name: string; category?: string; description?: string }): string {
|
|
58
|
+
return [topic.name, topic.category, topic.description].filter(Boolean).join(' - ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getPersonEmbeddingText(person: { name: string; relationship?: string; description?: string }): string {
|
|
62
|
+
return [person.name, person.relationship, person.description].filter(Boolean).join(' - ');
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export function needsEmbeddingUpdate(
|
|
58
66
|
existing: { name: string; description?: string } | undefined,
|
|
59
67
|
incoming: { name: string; description?: string }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StateManager } from "../state-manager.js";
|
|
2
2
|
import { LLMResponse } from "../types.js";
|
|
3
3
|
import type { DedupResult } from "../../prompts/ceremony/types.js";
|
|
4
|
-
import type { DataItemType, Fact,
|
|
4
|
+
import type { DataItemType, Fact, Topic, Person, Quote } from "../types/data-items.js";
|
|
5
5
|
import { getEmbeddingService } from "../embedding-service.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -23,6 +23,12 @@ export async function handleDedupCurate(
|
|
|
23
23
|
const entity_ids = response.request.data.entity_ids as string[];
|
|
24
24
|
const state = stateManager.getHuman();
|
|
25
25
|
|
|
26
|
+
// Validate entity_type
|
|
27
|
+
if (!entity_type || !['fact', 'topic', 'person'].includes(entity_type)) {
|
|
28
|
+
console.error(`[Dedup] Invalid entity_type: "${entity_type}" (from request data)`, response.request.data);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// Parse Opus response
|
|
27
33
|
let decisions: DedupResult;
|
|
28
34
|
try {
|
|
@@ -43,11 +49,29 @@ export async function handleDedupCurate(
|
|
|
43
49
|
|
|
44
50
|
console.log(`[Dedup] Processing cluster: ${decisions.update.length} updates, ${decisions.remove.length} removals, ${decisions.add.length} additions`);
|
|
45
51
|
|
|
46
|
-
//
|
|
47
|
-
const
|
|
52
|
+
// Map entity_type to pluralized state property name
|
|
53
|
+
const pluralMap: Record<string, 'facts' | 'topics' | 'people'> = {
|
|
54
|
+
fact: 'facts',
|
|
55
|
+
topic: 'topics',
|
|
56
|
+
person: 'people'
|
|
57
|
+
};
|
|
58
|
+
const entityList = state[pluralMap[entity_type]];
|
|
59
|
+
|
|
60
|
+
// Validate entityList exists
|
|
61
|
+
if (!entityList || !Array.isArray(entityList)) {
|
|
62
|
+
console.error(`[Dedup] entityList is ${entityList === undefined ? 'undefined' : 'not an array'} for entity_type="${entity_type}" (looking for state.${entity_type}s)`, {
|
|
63
|
+
entity_type,
|
|
64
|
+
entity_ids,
|
|
65
|
+
stateKeys: Object.keys(state),
|
|
66
|
+
factsExists: !!state.facts,
|
|
67
|
+
topicsExists: !!state.topics,
|
|
68
|
+
peopleExists: !!state.people
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
48
72
|
const entities = entity_ids
|
|
49
|
-
.map((id: string) => entityList.find((e: Fact |
|
|
50
|
-
.filter((e: Fact |
|
|
73
|
+
.map((id: string) => entityList.find((e: Fact | Topic | Person) => e.id === id))
|
|
74
|
+
.filter((e: Fact | Topic | Person | undefined): e is (Fact | Topic | Person) => e !== undefined);
|
|
51
75
|
|
|
52
76
|
if (entities.length === 0) {
|
|
53
77
|
console.warn(`[Dedup] No entities found for cluster (already merged?)`);
|
|
@@ -83,7 +107,7 @@ export async function handleDedupCurate(
|
|
|
83
107
|
// =========================================================================
|
|
84
108
|
|
|
85
109
|
for (const update of decisions.update) {
|
|
86
|
-
const entity = entityList.find((e: Fact |
|
|
110
|
+
const entity = entityList.find((e: Fact | Topic | Person) => e.id === update.id);
|
|
87
111
|
|
|
88
112
|
if (!entity) {
|
|
89
113
|
console.warn(`[Dedup] Entity ${update.id} not found (already merged?)`);
|
|
@@ -122,8 +146,6 @@ export async function handleDedupCurate(
|
|
|
122
146
|
// Type-safe cast based on entity_type
|
|
123
147
|
if (entity_type === 'fact') {
|
|
124
148
|
stateManager.human_fact_upsert(updatedEntity as Fact);
|
|
125
|
-
} else if (entity_type === 'trait') {
|
|
126
|
-
stateManager.human_trait_upsert(updatedEntity as Trait);
|
|
127
149
|
} else if (entity_type === 'topic') {
|
|
128
150
|
stateManager.human_topic_upsert(updatedEntity as Topic);
|
|
129
151
|
} else if (entity_type === 'person') {
|
|
@@ -137,7 +159,7 @@ export async function handleDedupCurate(
|
|
|
137
159
|
// =========================================================================
|
|
138
160
|
|
|
139
161
|
for (const removal of decisions.remove) {
|
|
140
|
-
const entity = entityList.find((e: Fact |
|
|
162
|
+
const entity = entityList.find((e: Fact | Topic | Person) => e.id === removal.to_be_removed);
|
|
141
163
|
|
|
142
164
|
if (!entity) {
|
|
143
165
|
console.warn(`[Dedup] Entity ${removal.to_be_removed} already deleted`);
|
|
@@ -146,7 +168,7 @@ export async function handleDedupCurate(
|
|
|
146
168
|
|
|
147
169
|
// Remove via StateManager (also cleans up quote references)
|
|
148
170
|
const removeMethod = `human_${entity_type}_remove` as
|
|
149
|
-
'human_fact_remove' | '
|
|
171
|
+
'human_fact_remove' | 'human_topic_remove' | 'human_person_remove';
|
|
150
172
|
|
|
151
173
|
const removed = stateManager[removeMethod](removal.to_be_removed);
|
|
152
174
|
if (removed) {
|
|
@@ -180,12 +202,12 @@ export async function handleDedupCurate(
|
|
|
180
202
|
description: addition.description,
|
|
181
203
|
sentiment: addition.sentiment ?? 0.0,
|
|
182
204
|
last_updated: new Date().toISOString(),
|
|
205
|
+
learned_by: "ei",
|
|
206
|
+
last_changed_by: "ei",
|
|
183
207
|
embedding,
|
|
184
208
|
// Type-specific fields with defaults
|
|
185
|
-
...(entity_type === 'trait' && { strength: addition.strength ?? 0.5 }),
|
|
186
209
|
...(entity_type === 'fact' && {
|
|
187
210
|
confidence: addition.confidence ?? 0.5,
|
|
188
|
-
validated: 'unknown' as import("../types/enums.js").ValidationLevel,
|
|
189
211
|
validated_date: ''
|
|
190
212
|
}),
|
|
191
213
|
...((entity_type === 'topic' || entity_type === 'person') && {
|
|
@@ -200,8 +222,6 @@ export async function handleDedupCurate(
|
|
|
200
222
|
// Type-safe cast based on entity_type
|
|
201
223
|
if (entity_type === 'fact') {
|
|
202
224
|
stateManager.human_fact_upsert(newEntity as Fact);
|
|
203
|
-
} else if (entity_type === 'trait') {
|
|
204
|
-
stateManager.human_trait_upsert(newEntity as Trait);
|
|
205
225
|
} else if (entity_type === 'topic') {
|
|
206
226
|
stateManager.human_topic_upsert(newEntity as Topic);
|
|
207
227
|
} else if (entity_type === 'person') {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ContextStatus,
|
|
3
3
|
LLMNextStep,
|
|
4
|
-
ValidationLevel,
|
|
5
4
|
type LLMResponse,
|
|
6
5
|
type Message,
|
|
7
6
|
} from "../types.js";
|
|
@@ -77,13 +76,13 @@ export function handleEiHeartbeat(response: LLMResponse, state: StateManager): v
|
|
|
77
76
|
timestamp: now,
|
|
78
77
|
read: false,
|
|
79
78
|
context_status: ContextStatus.Default,
|
|
80
|
-
f: true,
|
|
79
|
+
f: true, t: true, p: true,
|
|
81
80
|
});
|
|
82
81
|
|
|
83
82
|
if (found.type === "fact") {
|
|
84
83
|
const factsNav = isTUI ? "using /me facts" : "using \u2630 \u2192 My Data";
|
|
85
84
|
sendMessage(`Another persona updated a fact called "${found.name}" to "${found.description}". If that's right, you can lock it from further changes by ${factsNav}.`);
|
|
86
|
-
state.human_fact_upsert({ ...found,
|
|
85
|
+
state.human_fact_upsert({ ...found, validated_date: now });
|
|
87
86
|
console.log(`[handleEiHeartbeat] Notified about fact "${found.name}"`);
|
|
88
87
|
return;
|
|
89
88
|
}
|