@auxiora/memory 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +191 -0
  2. package/dist/extractor.d.ts +40 -0
  3. package/dist/extractor.d.ts.map +1 -0
  4. package/dist/extractor.js +130 -0
  5. package/dist/extractor.js.map +1 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +8 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/partition.d.ts +25 -0
  11. package/dist/partition.d.ts.map +1 -0
  12. package/dist/partition.js +104 -0
  13. package/dist/partition.js.map +1 -0
  14. package/dist/pattern-detector.d.ts +27 -0
  15. package/dist/pattern-detector.d.ts.map +1 -0
  16. package/dist/pattern-detector.js +260 -0
  17. package/dist/pattern-detector.js.map +1 -0
  18. package/dist/personality-adapter.d.ts +10 -0
  19. package/dist/personality-adapter.d.ts.map +1 -0
  20. package/dist/personality-adapter.js +72 -0
  21. package/dist/personality-adapter.js.map +1 -0
  22. package/dist/retriever.d.ts +15 -0
  23. package/dist/retriever.d.ts.map +1 -0
  24. package/dist/retriever.js +179 -0
  25. package/dist/retriever.js.map +1 -0
  26. package/dist/sentiment.d.ts +7 -0
  27. package/dist/sentiment.d.ts.map +1 -0
  28. package/dist/sentiment.js +118 -0
  29. package/dist/sentiment.js.map +1 -0
  30. package/dist/store.d.ts +41 -0
  31. package/dist/store.d.ts.map +1 -0
  32. package/dist/store.js +352 -0
  33. package/dist/store.js.map +1 -0
  34. package/dist/types.d.ts +80 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +26 -0
  39. package/src/extractor.ts +212 -0
  40. package/src/index.ts +22 -0
  41. package/src/partition.ts +123 -0
  42. package/src/pattern-detector.ts +304 -0
  43. package/src/personality-adapter.ts +82 -0
  44. package/src/retriever.ts +213 -0
  45. package/src/sentiment.ts +129 -0
  46. package/src/store.ts +384 -0
  47. package/src/types.ts +92 -0
  48. package/tests/extractor.test.ts +247 -0
  49. package/tests/partition.test.ts +168 -0
  50. package/tests/pattern-detector.test.ts +150 -0
  51. package/tests/personality-adapter.test.ts +155 -0
  52. package/tests/retriever.test.ts +240 -0
  53. package/tests/sentiment.test.ts +207 -0
  54. package/tests/store.test.ts +390 -0
  55. package/tsconfig.json +13 -0
  56. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,82 @@
1
+ import type { PersonalityAdaptation } from './types.js';
2
+ import type { MemoryStore } from './store.js';
3
+
4
+ export class PersonalityAdapter {
5
+ constructor(private store: MemoryStore) {}
6
+
7
+ async recordSignal(signal: PersonalityAdaptation): Promise<void> {
8
+ const existing = await this.store.getByCategory('personality');
9
+
10
+ // Find existing adaptation for this trait
11
+ const match = existing.find(m => {
12
+ try {
13
+ const data = JSON.parse(m.content) as PersonalityAdaptation;
14
+ return data.trait === signal.trait;
15
+ } catch {
16
+ return false;
17
+ }
18
+ });
19
+
20
+ if (match) {
21
+ const current = JSON.parse(match.content) as PersonalityAdaptation;
22
+ // Accumulate signals: weighted average moving toward the new direction
23
+ const combined: PersonalityAdaptation = {
24
+ trait: signal.trait,
25
+ adjustment: clamp(current.adjustment + signal.adjustment * 0.2, -1, 1),
26
+ reason: signal.reason,
27
+ signalCount: current.signalCount + 1,
28
+ };
29
+ await this.store.update(match.id, {
30
+ content: JSON.stringify(combined),
31
+ confidence: Math.min(0.5 + combined.signalCount * 0.05, 0.95),
32
+ });
33
+ } else {
34
+ await this.store.add(
35
+ JSON.stringify(signal),
36
+ 'personality',
37
+ 'observed',
38
+ {
39
+ importance: 0.6,
40
+ confidence: 0.5,
41
+ },
42
+ );
43
+ }
44
+ }
45
+
46
+ async getAdjustments(): Promise<PersonalityAdaptation[]> {
47
+ const entries = await this.store.getByCategory('personality');
48
+ const results: PersonalityAdaptation[] = [];
49
+
50
+ for (const entry of entries) {
51
+ try {
52
+ const data = JSON.parse(entry.content) as PersonalityAdaptation;
53
+ results.push(data);
54
+ } catch {
55
+ // Skip malformed entries
56
+ }
57
+ }
58
+
59
+ return results;
60
+ }
61
+
62
+ async getPromptModifier(): Promise<string> {
63
+ const adjustments = await this.getAdjustments();
64
+ if (adjustments.length === 0) return '';
65
+
66
+ const lines = adjustments
67
+ .filter(a => Math.abs(a.adjustment) > 0.1)
68
+ .map(a => {
69
+ const direction = a.adjustment > 0 ? 'Increase' : 'Decrease';
70
+ const magnitude = Math.abs(a.adjustment) > 0.5 ? 'significantly' : 'slightly';
71
+ return `- ${direction} ${a.trait} ${magnitude} (${a.reason})`;
72
+ });
73
+
74
+ if (lines.length === 0) return '';
75
+
76
+ return `\n\n## Personality Adaptations (learned from interactions)\n${lines.join('\n')}`;
77
+ }
78
+ }
79
+
80
+ function clamp(value: number, min: number, max: number): number {
81
+ return Math.max(min, Math.min(max, value));
82
+ }
@@ -0,0 +1,213 @@
1
+ import type { MemoryEntry, MemoryCategory } from './types.js';
2
+
3
+ const TOKEN_BUDGET = 1000;
4
+ const CHARS_PER_TOKEN = 4;
5
+ const MAX_CHARS = TOKEN_BUDGET * CHARS_PER_TOKEN;
6
+ const MIN_SCORE = 0.1;
7
+
8
+ const SCORING_WEIGHTS = {
9
+ tagRelevance: 0.35,
10
+ importance: 0.25,
11
+ recency: 0.15,
12
+ access: 0.10,
13
+ confidence: 0.10,
14
+ relationship: 0.05,
15
+ };
16
+
17
+ const BUDGET_ALLOCATION: Record<MemoryCategory, number> = {
18
+ preference: 0.25,
19
+ fact: 0.25,
20
+ context: 0.15,
21
+ relationship: 0.15,
22
+ pattern: 0.10,
23
+ personality: 0.10,
24
+ };
25
+
26
+ const SECTION_HEADERS: Record<string, string> = {
27
+ fact: '### Key Facts',
28
+ preference: '### Preferences',
29
+ context: '### Context',
30
+ relationship: '### Your Relationship',
31
+ pattern: '### Communication Patterns',
32
+ personality: '### Personality Notes',
33
+ };
34
+
35
+ interface ScoredMemory {
36
+ entry: MemoryEntry;
37
+ score: number;
38
+ }
39
+
40
+ export class MemoryRetriever {
41
+ /**
42
+ * Retrieve relevant memories for a user message.
43
+ * If accessiblePartitionIds is provided, only memories from those partitions are included.
44
+ * This ensures private memories are never leaked to other users.
45
+ */
46
+ retrieve(memories: MemoryEntry[], userMessage: string, accessiblePartitionIds?: string[]): string {
47
+ if (memories.length === 0) return '';
48
+
49
+ // Filter by accessible partitions if specified
50
+ if (accessiblePartitionIds) {
51
+ const idSet = new Set(accessiblePartitionIds);
52
+ memories = memories.filter(m => idSet.has(m.partitionId ?? 'global'));
53
+ }
54
+
55
+ if (memories.length === 0) return '';
56
+
57
+ const now = Date.now();
58
+
59
+ // Filter out expired memories
60
+ const active = memories.filter(m => m.expiresAt === undefined || m.expiresAt > now);
61
+ if (active.length === 0) return '';
62
+
63
+ const queryTags = this.extractQueryTags(userMessage);
64
+
65
+ // Score each memory
66
+ const scored: ScoredMemory[] = active.map(m => ({
67
+ entry: m,
68
+ score: this.scoreMemory(m, queryTags, now),
69
+ }));
70
+
71
+ // Boost related memories
72
+ this.boostRelatedMemories(scored);
73
+
74
+ // Filter by minimum score
75
+ const relevant = scored
76
+ .filter(s => s.score >= MIN_SCORE)
77
+ .sort((a, b) => b.score - a.score);
78
+
79
+ if (relevant.length === 0) return '';
80
+
81
+ // Group by category and allocate budget
82
+ const sections = this.buildSections(relevant);
83
+ if (sections.length === 0) return '';
84
+
85
+ return `\n\n---\n\n## What you know about the user\n\n${sections.join('\n\n')}`;
86
+ }
87
+
88
+ private scoreMemory(memory: MemoryEntry, queryTags: string[], now: number): number {
89
+ // 1. Tag relevance (0-1)
90
+ let tagScore = 0;
91
+ if (queryTags.length > 0 && memory.tags.length > 0) {
92
+ const overlap = memory.tags.filter(t => queryTags.includes(t)).length;
93
+ tagScore = overlap / Math.max(queryTags.length, 1);
94
+ }
95
+
96
+ // 2. Importance (0-1)
97
+ const importanceScore = memory.importance;
98
+
99
+ // 3. Recency (0-1) — decays over 30 days
100
+ const ageMs = now - memory.updatedAt;
101
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
102
+ const recencyScore = Math.max(0, 1 - ageDays / 30);
103
+
104
+ // 4. Access frequency (0-1)
105
+ const accessScore = Math.min(memory.accessCount / 10, 1);
106
+
107
+ // 5. Confidence (0-1)
108
+ const confidenceScore = memory.confidence;
109
+
110
+ // 6. Relationship bonus
111
+ const relationshipBonus = memory.category === 'relationship' ? 1 : 0;
112
+
113
+ return (
114
+ tagScore * SCORING_WEIGHTS.tagRelevance +
115
+ importanceScore * SCORING_WEIGHTS.importance +
116
+ recencyScore * SCORING_WEIGHTS.recency +
117
+ accessScore * SCORING_WEIGHTS.access +
118
+ confidenceScore * SCORING_WEIGHTS.confidence +
119
+ relationshipBonus * SCORING_WEIGHTS.relationship
120
+ );
121
+ }
122
+
123
+ private boostRelatedMemories(scored: ScoredMemory[]): void {
124
+ const byId = new Map(scored.map(s => [s.entry.id, s]));
125
+
126
+ for (const item of scored) {
127
+ if (item.score >= MIN_SCORE && item.entry.relatedMemories) {
128
+ for (const relatedId of item.entry.relatedMemories) {
129
+ const related = byId.get(relatedId);
130
+ if (related) {
131
+ related.score += item.score * 0.15;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ private buildSections(scored: ScoredMemory[]): string[] {
139
+ // Group by category
140
+ const groups = new Map<MemoryCategory, ScoredMemory[]>();
141
+ for (const s of scored) {
142
+ const cat = s.entry.category;
143
+ if (!groups.has(cat)) groups.set(cat, []);
144
+ groups.get(cat)!.push(s);
145
+ }
146
+
147
+ const sections: string[] = [];
148
+ let totalChars = 0;
149
+
150
+ // Process categories in budget allocation order (highest allocation first)
151
+ const orderedCategories = Object.entries(BUDGET_ALLOCATION)
152
+ .sort(([, a], [, b]) => b - a)
153
+ .map(([cat]) => cat as MemoryCategory);
154
+
155
+ for (const category of orderedCategories) {
156
+ const items = groups.get(category);
157
+ if (!items || items.length === 0) continue;
158
+
159
+ const budgetChars = MAX_CHARS * BUDGET_ALLOCATION[category];
160
+ const header = SECTION_HEADERS[category] ?? `### ${category}`;
161
+ const lines: string[] = [header];
162
+ let sectionChars = header.length;
163
+
164
+ for (const { entry } of items) {
165
+ const line = this.formatLine(entry);
166
+ if (sectionChars + line.length > budgetChars) break;
167
+ if (totalChars + sectionChars + line.length > MAX_CHARS) break;
168
+ lines.push(line);
169
+ sectionChars += line.length;
170
+ }
171
+
172
+ if (lines.length > 1) {
173
+ sections.push(lines.join('\n'));
174
+ totalChars += sectionChars;
175
+ }
176
+
177
+ if (totalChars >= MAX_CHARS) break;
178
+ }
179
+
180
+ return sections;
181
+ }
182
+
183
+ private formatLine(entry: MemoryEntry): string {
184
+ const meta: string[] = [];
185
+ if (entry.category === 'fact' && entry.confidence >= 0.8) {
186
+ meta.push('high confidence');
187
+ }
188
+ if (entry.category === 'relationship') {
189
+ // Try to extract relationship type from tags or just show category
190
+ }
191
+ const suffix = meta.length > 0 ? ` (${meta.join(', ')})` : '';
192
+ return `- ${entry.content}${suffix}`;
193
+ }
194
+
195
+ private extractQueryTags(text: string): string[] {
196
+ const stopWords = new Set([
197
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
198
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
199
+ 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
200
+ 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
201
+ 'before', 'after', 'and', 'but', 'or', 'not', 'so', 'yet',
202
+ 'i', 'me', 'my', 'we', 'you', 'your', 'he', 'she', 'they', 'it',
203
+ 'what', 'which', 'who', 'when', 'where', 'how', 'that', 'this',
204
+ ]);
205
+
206
+ return text
207
+ .toLowerCase()
208
+ .replace(/[^a-z0-9\s-]/g, '')
209
+ .split(/\s+/)
210
+ .filter(w => w.length > 2 && !stopWords.has(w))
211
+ .filter((w, i, arr) => arr.indexOf(w) === i);
212
+ }
213
+ }
@@ -0,0 +1,129 @@
1
+ import type { SentimentLabel, SentimentResult } from './types.js';
2
+
3
+ const POSITIVE_WORDS = new Set([
4
+ 'good', 'great', 'awesome', 'amazing', 'excellent', 'fantastic', 'wonderful',
5
+ 'love', 'happy', 'glad', 'pleased', 'perfect', 'beautiful', 'brilliant',
6
+ 'thanks', 'thank', 'appreciate', 'helpful', 'nice', 'cool', 'best',
7
+ 'excited', 'enjoy', 'impressive', 'incredible', 'outstanding', 'superb',
8
+ 'delighted', 'thrilled', 'grateful', 'marvelous', 'terrific', 'splendid',
9
+ 'like', 'yes', 'agree', 'right', 'correct', 'exactly', 'absolutely',
10
+ 'well', 'fine', 'okay', 'sure', 'works', 'solved', 'fixed', 'done',
11
+ ]);
12
+
13
+ const NEGATIVE_WORDS = new Set([
14
+ 'bad', 'terrible', 'awful', 'horrible', 'wrong', 'broken', 'error',
15
+ 'fail', 'failed', 'failure', 'bug', 'crash', 'issue', 'problem',
16
+ 'hate', 'angry', 'frustrated', 'annoyed', 'disappointed', 'confused',
17
+ 'ugly', 'slow', 'stuck', 'impossible', 'useless', 'waste', 'stupid',
18
+ 'worse', 'worst', 'never', 'nothing', 'nobody', 'nowhere', 'ugh',
19
+ 'damn', 'crap', 'sucks', 'annoying', 'painful', 'difficult', 'hard',
20
+ 'unfortunately', 'sadly', 'regret', 'sorry', 'no', 'not', 'cannot',
21
+ ]);
22
+
23
+ const POSITIVE_EMOJI_PATTERNS = /[:\)]|:\)|:D|;\)|<3|❤|😊|😄|👍|🎉|✨|🙌|💯|🔥|⭐|😁|🥳|💪/g;
24
+ const NEGATIVE_EMOJI_PATTERNS = /[:\(]|:\(|:\/|😢|😡|😤|👎|💔|😞|😠|🤬|😭|😩|😫/g;
25
+
26
+ export class SentimentAnalyzer {
27
+ analyzeSentiment(text: string): SentimentResult {
28
+ const lower = text.toLowerCase();
29
+ const words = lower.replace(/[^a-z0-9\s'-]/g, '').split(/\s+/).filter(Boolean);
30
+
31
+ let positiveScore = 0;
32
+ let negativeScore = 0;
33
+ const matchedKeywords: string[] = [];
34
+
35
+ // Word-based scoring
36
+ for (const word of words) {
37
+ if (POSITIVE_WORDS.has(word)) {
38
+ positiveScore += 1;
39
+ matchedKeywords.push(word);
40
+ }
41
+ if (NEGATIVE_WORDS.has(word)) {
42
+ negativeScore += 1;
43
+ matchedKeywords.push(word);
44
+ }
45
+ }
46
+
47
+ // Emoji-based scoring
48
+ const positiveEmojis = text.match(POSITIVE_EMOJI_PATTERNS);
49
+ const negativeEmojis = text.match(NEGATIVE_EMOJI_PATTERNS);
50
+ if (positiveEmojis) positiveScore += positiveEmojis.length * 1.5;
51
+ if (negativeEmojis) negativeScore += negativeEmojis.length * 1.5;
52
+
53
+ // Punctuation patterns
54
+ const exclamationCount = (text.match(/!/g) || []).length;
55
+ const questionCount = (text.match(/\?/g) || []).length;
56
+ const capsRatio = this.getCapsRatio(text);
57
+
58
+ // Exclamation marks boost existing sentiment
59
+ if (exclamationCount > 0) {
60
+ if (positiveScore > negativeScore) {
61
+ positiveScore += exclamationCount * 0.3;
62
+ } else if (negativeScore > positiveScore) {
63
+ negativeScore += exclamationCount * 0.3;
64
+ }
65
+ }
66
+
67
+ // ALL CAPS can indicate strong sentiment (frustration or excitement)
68
+ if (capsRatio > 0.5 && words.length > 2) {
69
+ if (negativeScore > positiveScore) {
70
+ negativeScore += 1;
71
+ } else if (positiveScore > negativeScore) {
72
+ positiveScore += 1;
73
+ }
74
+ }
75
+
76
+ // Negation handling: "not good", "don't like", etc.
77
+ const negationCount = this.countNegations(lower);
78
+ if (negationCount > 0 && positiveScore > negativeScore) {
79
+ // Negation flips positive sentiment partially
80
+ const flip = Math.min(negationCount, positiveScore);
81
+ positiveScore -= flip;
82
+ negativeScore += flip * 0.5;
83
+ }
84
+
85
+ const totalScore = positiveScore + negativeScore;
86
+ let sentiment: SentimentLabel;
87
+ let confidence: number;
88
+
89
+ if (totalScore === 0) {
90
+ sentiment = 'neutral';
91
+ confidence = 0.5;
92
+ } else {
93
+ const ratio = positiveScore / totalScore;
94
+
95
+ if (ratio > 0.6) {
96
+ sentiment = 'positive';
97
+ confidence = Math.min(0.4 + (ratio - 0.5) * 1.2, 0.95);
98
+ } else if (ratio < 0.4) {
99
+ sentiment = 'negative';
100
+ confidence = Math.min(0.4 + (0.5 - ratio) * 1.2, 0.95);
101
+ } else {
102
+ sentiment = 'neutral';
103
+ confidence = Math.max(0.3, 0.5 - Math.abs(0.5 - ratio) * 2);
104
+ }
105
+
106
+ // Boost confidence with more evidence
107
+ if (totalScore >= 3) {
108
+ confidence = Math.min(confidence + 0.1, 0.95);
109
+ }
110
+ }
111
+
112
+ // Deduplicate keywords
113
+ const uniqueKeywords = [...new Set(matchedKeywords)];
114
+
115
+ return { sentiment, confidence, keywords: uniqueKeywords };
116
+ }
117
+
118
+ private getCapsRatio(text: string): number {
119
+ const letters = text.replace(/[^a-zA-Z]/g, '');
120
+ if (letters.length === 0) return 0;
121
+ const upperCount = (text.match(/[A-Z]/g) || []).length;
122
+ return upperCount / letters.length;
123
+ }
124
+
125
+ private countNegations(text: string): number {
126
+ const negations = text.match(/\b(not|no|never|don't|doesn't|didn't|won't|wouldn't|can't|cannot|isn't|aren't|wasn't|weren't)\b/g);
127
+ return negations ? negations.length : 0;
128
+ }
129
+ }