@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.
- package/LICENSE +191 -0
- package/dist/extractor.d.ts +40 -0
- package/dist/extractor.d.ts.map +1 -0
- package/dist/extractor.js +130 -0
- package/dist/extractor.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/partition.d.ts +25 -0
- package/dist/partition.d.ts.map +1 -0
- package/dist/partition.js +104 -0
- package/dist/partition.js.map +1 -0
- package/dist/pattern-detector.d.ts +27 -0
- package/dist/pattern-detector.d.ts.map +1 -0
- package/dist/pattern-detector.js +260 -0
- package/dist/pattern-detector.js.map +1 -0
- package/dist/personality-adapter.d.ts +10 -0
- package/dist/personality-adapter.d.ts.map +1 -0
- package/dist/personality-adapter.js +72 -0
- package/dist/personality-adapter.js.map +1 -0
- package/dist/retriever.d.ts +15 -0
- package/dist/retriever.d.ts.map +1 -0
- package/dist/retriever.js +179 -0
- package/dist/retriever.js.map +1 -0
- package/dist/sentiment.d.ts +7 -0
- package/dist/sentiment.d.ts.map +1 -0
- package/dist/sentiment.js +118 -0
- package/dist/sentiment.js.map +1 -0
- package/dist/store.d.ts +41 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +352 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +26 -0
- package/src/extractor.ts +212 -0
- package/src/index.ts +22 -0
- package/src/partition.ts +123 -0
- package/src/pattern-detector.ts +304 -0
- package/src/personality-adapter.ts +82 -0
- package/src/retriever.ts +213 -0
- package/src/sentiment.ts +129 -0
- package/src/store.ts +384 -0
- package/src/types.ts +92 -0
- package/tests/extractor.test.ts +247 -0
- package/tests/partition.test.ts +168 -0
- package/tests/pattern-detector.test.ts +150 -0
- package/tests/personality-adapter.test.ts +155 -0
- package/tests/retriever.test.ts +240 -0
- package/tests/sentiment.test.ts +207 -0
- package/tests/store.test.ts +390 -0
- package/tsconfig.json +13 -0
- 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
|
+
}
|
package/src/retriever.ts
ADDED
|
@@ -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
|
+
}
|
package/src/sentiment.ts
ADDED
|
@@ -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
|
+
}
|