@auxiora/memory 1.0.0 → 1.3.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/package.json +10 -4
- package/src/extractor.ts +0 -212
- package/src/index.ts +0 -22
- package/src/partition.ts +0 -123
- package/src/pattern-detector.ts +0 -304
- package/src/personality-adapter.ts +0 -82
- package/src/retriever.ts +0 -213
- package/src/sentiment.ts +0 -129
- package/src/store.ts +0 -384
- package/src/types.ts +0 -92
- package/tests/extractor.test.ts +0 -247
- package/tests/partition.test.ts +0 -168
- package/tests/pattern-detector.test.ts +0 -150
- package/tests/personality-adapter.test.ts +0 -155
- package/tests/retriever.test.ts +0 -240
- package/tests/sentiment.test.ts +0 -207
- package/tests/store.test.ts +0 -390
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@auxiora/memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
"node": ">=22.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@auxiora/logger": "1.
|
|
18
|
-
"@auxiora/
|
|
19
|
-
"@auxiora/
|
|
17
|
+
"@auxiora/logger": "1.3.0",
|
|
18
|
+
"@auxiora/core": "1.3.0",
|
|
19
|
+
"@auxiora/audit": "1.3.0"
|
|
20
20
|
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/"
|
|
26
|
+
],
|
|
21
27
|
"scripts": {
|
|
22
28
|
"build": "tsc",
|
|
23
29
|
"clean": "rm -rf dist",
|
package/src/extractor.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { getLogger } from '@auxiora/logger';
|
|
2
|
-
import type { MemoryEntry, PersonalityAdaptation, SentimentResult } from './types.js';
|
|
3
|
-
import type { MemoryStore } from './store.js';
|
|
4
|
-
import { SentimentAnalyzer } from './sentiment.js';
|
|
5
|
-
|
|
6
|
-
const logger = getLogger('memory:extractor');
|
|
7
|
-
|
|
8
|
-
export interface ExtractionResult {
|
|
9
|
-
factsExtracted: MemoryEntry[];
|
|
10
|
-
patternsDetected: MemoryEntry[];
|
|
11
|
-
relationshipsFound: MemoryEntry[];
|
|
12
|
-
contradictionsFound: Array<{ existing: MemoryEntry; new: string; resolution: string }>;
|
|
13
|
-
personalitySignals: PersonalityAdaptation[];
|
|
14
|
-
sentiment?: SentimentResult;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface AIProvider {
|
|
18
|
-
complete(messages: Array<{ role: string; content: string }>, options?: { systemPrompt?: string; temperature?: number }): Promise<{ content: string }>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const EXTRACTION_PROMPT = `Analyze this conversation exchange and extract structured information.
|
|
22
|
-
|
|
23
|
-
User: {userMessage}
|
|
24
|
-
Assistant: {assistantResponse}
|
|
25
|
-
|
|
26
|
-
Respond ONLY with valid JSON (no markdown fences) with these sections:
|
|
27
|
-
{
|
|
28
|
-
"facts": [{"content": "...", "category": "preference|fact|context", "importance": 0.0}],
|
|
29
|
-
"relationships": [{"content": "...", "type": "inside_joke|shared_experience|milestone|callback"}],
|
|
30
|
-
"patterns": [{"pattern": "...", "type": "communication|schedule|topic|mood"}],
|
|
31
|
-
"contradictions": [{"existingFact": "...", "newFact": "...", "resolution": "update|keep_both|ignore"}],
|
|
32
|
-
"personalitySignals": [{"trait": "humor|formality|verbosity|directness", "direction": "increase|decrease", "reason": "..."}]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
Only extract if there's clear signal. Empty arrays are fine. Be conservative.`;
|
|
36
|
-
|
|
37
|
-
interface RawFact {
|
|
38
|
-
content: string;
|
|
39
|
-
category: 'preference' | 'fact' | 'context';
|
|
40
|
-
importance: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface RawRelationship {
|
|
44
|
-
content: string;
|
|
45
|
-
type: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface RawPattern {
|
|
49
|
-
pattern: string;
|
|
50
|
-
type: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface RawContradiction {
|
|
54
|
-
existingFact: string;
|
|
55
|
-
newFact: string;
|
|
56
|
-
resolution: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface RawPersonalitySignal {
|
|
60
|
-
trait: string;
|
|
61
|
-
direction: 'increase' | 'decrease';
|
|
62
|
-
reason: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface RawExtraction {
|
|
66
|
-
facts?: RawFact[];
|
|
67
|
-
relationships?: RawRelationship[];
|
|
68
|
-
patterns?: RawPattern[];
|
|
69
|
-
contradictions?: RawContradiction[];
|
|
70
|
-
personalitySignals?: RawPersonalitySignal[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export class MemoryExtractor {
|
|
74
|
-
private sentimentAnalyzer = new SentimentAnalyzer();
|
|
75
|
-
|
|
76
|
-
constructor(
|
|
77
|
-
private store: MemoryStore,
|
|
78
|
-
private provider: AIProvider,
|
|
79
|
-
) {}
|
|
80
|
-
|
|
81
|
-
async extract(
|
|
82
|
-
userMessage: string,
|
|
83
|
-
assistantResponse: string,
|
|
84
|
-
_sessionContext?: { messageCount: number; sessionAge: number },
|
|
85
|
-
options?: { sourceUserId?: string; partitionId?: string },
|
|
86
|
-
): Promise<ExtractionResult> {
|
|
87
|
-
const result: ExtractionResult = {
|
|
88
|
-
factsExtracted: [],
|
|
89
|
-
patternsDetected: [],
|
|
90
|
-
relationshipsFound: [],
|
|
91
|
-
contradictionsFound: [],
|
|
92
|
-
personalitySignals: [],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Run heuristic sentiment analysis on user message
|
|
96
|
-
result.sentiment = this.sentimentAnalyzer.analyzeSentiment(userMessage);
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const prompt = EXTRACTION_PROMPT
|
|
100
|
-
.replace('{userMessage}', userMessage)
|
|
101
|
-
.replace('{assistantResponse}', assistantResponse);
|
|
102
|
-
|
|
103
|
-
const response = await this.provider.complete(
|
|
104
|
-
[{ role: 'user', content: prompt }],
|
|
105
|
-
{ temperature: 0.1 },
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
const parsed = this.parseResponse(response.content);
|
|
109
|
-
if (!parsed) return result;
|
|
110
|
-
|
|
111
|
-
const extraFields = {
|
|
112
|
-
...(options?.sourceUserId ? { sourceUserId: options.sourceUserId } : {}),
|
|
113
|
-
...(options?.partitionId ? { partitionId: options.partitionId } : {}),
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Process facts
|
|
117
|
-
if (parsed.facts) {
|
|
118
|
-
for (const fact of parsed.facts) {
|
|
119
|
-
if (!fact.content || typeof fact.content !== 'string') continue;
|
|
120
|
-
const entry = await this.store.add(
|
|
121
|
-
fact.content,
|
|
122
|
-
fact.category ?? 'fact',
|
|
123
|
-
'extracted',
|
|
124
|
-
{ importance: clampNumber(fact.importance ?? 0.5, 0, 1), ...extraFields },
|
|
125
|
-
);
|
|
126
|
-
result.factsExtracted.push(entry);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Process relationships
|
|
131
|
-
if (parsed.relationships) {
|
|
132
|
-
for (const rel of parsed.relationships) {
|
|
133
|
-
if (!rel.content || typeof rel.content !== 'string') continue;
|
|
134
|
-
const entry = await this.store.add(
|
|
135
|
-
rel.content,
|
|
136
|
-
'relationship',
|
|
137
|
-
'extracted',
|
|
138
|
-
{ importance: 0.7, ...extraFields },
|
|
139
|
-
);
|
|
140
|
-
result.relationshipsFound.push(entry);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Process patterns
|
|
145
|
-
if (parsed.patterns) {
|
|
146
|
-
for (const pat of parsed.patterns) {
|
|
147
|
-
if (!pat.pattern || typeof pat.pattern !== 'string') continue;
|
|
148
|
-
const entry = await this.store.add(
|
|
149
|
-
pat.pattern,
|
|
150
|
-
'pattern',
|
|
151
|
-
'observed',
|
|
152
|
-
{ importance: 0.5, confidence: 0.4, ...extraFields },
|
|
153
|
-
);
|
|
154
|
-
result.patternsDetected.push(entry);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Process contradictions
|
|
159
|
-
if (parsed.contradictions) {
|
|
160
|
-
for (const contra of parsed.contradictions) {
|
|
161
|
-
if (!contra.existingFact || !contra.newFact) continue;
|
|
162
|
-
const existingMemories = await this.store.search(contra.existingFact);
|
|
163
|
-
if (existingMemories.length > 0) {
|
|
164
|
-
result.contradictionsFound.push({
|
|
165
|
-
existing: existingMemories[0],
|
|
166
|
-
new: contra.newFact,
|
|
167
|
-
resolution: contra.resolution ?? 'keep_both',
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (contra.resolution === 'update' && existingMemories[0]) {
|
|
171
|
-
await this.store.update(existingMemories[0].id, {
|
|
172
|
-
content: contra.newFact,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Process personality signals
|
|
180
|
-
if (parsed.personalitySignals) {
|
|
181
|
-
for (const sig of parsed.personalitySignals) {
|
|
182
|
-
if (!sig.trait || !sig.direction) continue;
|
|
183
|
-
result.personalitySignals.push({
|
|
184
|
-
trait: sig.trait,
|
|
185
|
-
adjustment: sig.direction === 'increase' ? 0.1 : -0.1,
|
|
186
|
-
reason: sig.reason ?? '',
|
|
187
|
-
signalCount: 1,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
logger.debug('Memory extraction failed', { error: error as Error });
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return result;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private parseResponse(content: string): RawExtraction | undefined {
|
|
199
|
-
try {
|
|
200
|
-
// Strip markdown code fences if present
|
|
201
|
-
const cleaned = content.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
|
|
202
|
-
return JSON.parse(cleaned) as RawExtraction;
|
|
203
|
-
} catch {
|
|
204
|
-
logger.debug('Failed to parse extraction response', {});
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function clampNumber(value: number, min: number, max: number): number {
|
|
211
|
-
return Math.max(min, Math.min(max, value));
|
|
212
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
MemoryEntry,
|
|
3
|
-
MemoryCategory,
|
|
4
|
-
MemorySource,
|
|
5
|
-
MemoryPartition,
|
|
6
|
-
RelationshipMemory,
|
|
7
|
-
PatternMemory,
|
|
8
|
-
PersonalityAdaptation,
|
|
9
|
-
LivingMemoryState,
|
|
10
|
-
SentimentLabel,
|
|
11
|
-
SentimentResult,
|
|
12
|
-
SentimentSnapshot,
|
|
13
|
-
} from './types.js';
|
|
14
|
-
export { MemoryStore } from './store.js';
|
|
15
|
-
export { MemoryRetriever } from './retriever.js';
|
|
16
|
-
export { MemoryExtractor } from './extractor.js';
|
|
17
|
-
export type { ExtractionResult, AIProvider } from './extractor.js';
|
|
18
|
-
export { MemoryPartitionManager } from './partition.js';
|
|
19
|
-
export { PatternDetector } from './pattern-detector.js';
|
|
20
|
-
export type { PatternSignal } from './pattern-detector.js';
|
|
21
|
-
export { PersonalityAdapter } from './personality-adapter.js';
|
|
22
|
-
export { SentimentAnalyzer } from './sentiment.js';
|
package/src/partition.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import * as crypto from 'node:crypto';
|
|
4
|
-
import { getLogger } from '@auxiora/logger';
|
|
5
|
-
import { getMemoryDir } from '@auxiora/core';
|
|
6
|
-
import type { MemoryPartition } from './types.js';
|
|
7
|
-
|
|
8
|
-
const logger = getLogger('memory:partition');
|
|
9
|
-
|
|
10
|
-
export class MemoryPartitionManager {
|
|
11
|
-
private filePath: string;
|
|
12
|
-
|
|
13
|
-
constructor(options?: { dir?: string }) {
|
|
14
|
-
const dir = options?.dir ?? getMemoryDir();
|
|
15
|
-
this.filePath = path.join(dir, 'partitions.json');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async createPartition(
|
|
19
|
-
name: string,
|
|
20
|
-
type: MemoryPartition['type'],
|
|
21
|
-
options?: { ownerId?: string; memberIds?: string[] },
|
|
22
|
-
): Promise<MemoryPartition> {
|
|
23
|
-
const partitions = await this.readFile();
|
|
24
|
-
|
|
25
|
-
const partition: MemoryPartition = {
|
|
26
|
-
id: type === 'global' ? 'global' : `part-${crypto.randomUUID().slice(0, 8)}`,
|
|
27
|
-
name,
|
|
28
|
-
type,
|
|
29
|
-
ownerId: options?.ownerId,
|
|
30
|
-
memberIds: options?.memberIds ?? [],
|
|
31
|
-
createdAt: Date.now(),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
partitions.push(partition);
|
|
35
|
-
await this.writeFile(partitions);
|
|
36
|
-
logger.debug('Created partition', { id: partition.id, type });
|
|
37
|
-
return partition;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async getPartition(id: string): Promise<MemoryPartition | undefined> {
|
|
41
|
-
// Always have the implicit global partition
|
|
42
|
-
if (id === 'global') {
|
|
43
|
-
const partitions = await this.readFile();
|
|
44
|
-
const existing = partitions.find(p => p.id === 'global');
|
|
45
|
-
return existing ?? { id: 'global', name: 'Global', type: 'global', createdAt: 0 };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const partitions = await this.readFile();
|
|
49
|
-
return partitions.find(p => p.id === id);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async listPartitions(userId?: string): Promise<MemoryPartition[]> {
|
|
53
|
-
const partitions = await this.readFile();
|
|
54
|
-
|
|
55
|
-
// Always include the implicit global partition
|
|
56
|
-
if (!partitions.some(p => p.id === 'global')) {
|
|
57
|
-
partitions.unshift({ id: 'global', name: 'Global', type: 'global', createdAt: 0 });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!userId) return partitions;
|
|
61
|
-
|
|
62
|
-
// Return partitions accessible to this user
|
|
63
|
-
return partitions.filter(p =>
|
|
64
|
-
p.type === 'global' ||
|
|
65
|
-
p.ownerId === userId ||
|
|
66
|
-
p.memberIds?.includes(userId),
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async deletePartition(id: string): Promise<boolean> {
|
|
71
|
-
if (id === 'global') return false;
|
|
72
|
-
|
|
73
|
-
const partitions = await this.readFile();
|
|
74
|
-
const filtered = partitions.filter(p => p.id !== id);
|
|
75
|
-
if (filtered.length === partitions.length) return false;
|
|
76
|
-
|
|
77
|
-
await this.writeFile(filtered);
|
|
78
|
-
logger.debug('Deleted partition', { id });
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Check if a user has access to a partition.
|
|
84
|
-
*/
|
|
85
|
-
async hasAccess(partitionId: string, userId: string): Promise<boolean> {
|
|
86
|
-
if (partitionId === 'global') return true;
|
|
87
|
-
|
|
88
|
-
const partition = await this.getPartition(partitionId);
|
|
89
|
-
if (!partition) return false;
|
|
90
|
-
|
|
91
|
-
if (partition.type === 'global') return true;
|
|
92
|
-
if (partition.ownerId === userId) return true;
|
|
93
|
-
if (partition.memberIds?.includes(userId)) return true;
|
|
94
|
-
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get all partition IDs accessible to a user.
|
|
100
|
-
*/
|
|
101
|
-
async getAccessiblePartitionIds(userId: string): Promise<string[]> {
|
|
102
|
-
const accessible = await this.listPartitions(userId);
|
|
103
|
-
return accessible.map(p => p.id);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private async readFile(): Promise<MemoryPartition[]> {
|
|
107
|
-
try {
|
|
108
|
-
const content = await fs.readFile(this.filePath, 'utf-8');
|
|
109
|
-
return JSON.parse(content) as MemoryPartition[];
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private async writeFile(partitions: MemoryPartition[]): Promise<void> {
|
|
119
|
-
const dir = path.dirname(this.filePath);
|
|
120
|
-
await fs.mkdir(dir, { recursive: true });
|
|
121
|
-
await fs.writeFile(this.filePath, JSON.stringify(partitions, null, 2), 'utf-8');
|
|
122
|
-
}
|
|
123
|
-
}
|
package/src/pattern-detector.ts
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import type { SentimentSnapshot, SentimentLabel } from './types.js';
|
|
2
|
-
|
|
3
|
-
export interface PatternSignal {
|
|
4
|
-
type: 'communication' | 'schedule' | 'topic' | 'mood';
|
|
5
|
-
pattern: string;
|
|
6
|
-
confidence: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface MessageInfo {
|
|
10
|
-
content: string;
|
|
11
|
-
role: string;
|
|
12
|
-
timestamp: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
16
|
-
|
|
17
|
-
export class PatternDetector {
|
|
18
|
-
private sentimentHistory: SentimentSnapshot[] = [];
|
|
19
|
-
|
|
20
|
-
recordSentiment(snapshot: SentimentSnapshot): void {
|
|
21
|
-
this.sentimentHistory.push(snapshot);
|
|
22
|
-
// Keep only the last 200 snapshots
|
|
23
|
-
if (this.sentimentHistory.length > 200) {
|
|
24
|
-
this.sentimentHistory = this.sentimentHistory.slice(-200);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
getSentimentHistory(): SentimentSnapshot[] {
|
|
29
|
-
return [...this.sentimentHistory];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
detectMoodByTime(): PatternSignal[] {
|
|
33
|
-
if (this.sentimentHistory.length < 3) return [];
|
|
34
|
-
|
|
35
|
-
const signals: PatternSignal[] = [];
|
|
36
|
-
|
|
37
|
-
// Group by time period
|
|
38
|
-
const periodSentiments = new Map<string, SentimentLabel[]>();
|
|
39
|
-
for (const snap of this.sentimentHistory) {
|
|
40
|
-
const period = this.getTimePeriodName(snap.hour);
|
|
41
|
-
const list = periodSentiments.get(period) ?? [];
|
|
42
|
-
list.push(snap.sentiment);
|
|
43
|
-
periodSentiments.set(period, list);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
for (const [period, sentiments] of periodSentiments) {
|
|
47
|
-
if (sentiments.length < 3) continue;
|
|
48
|
-
const dominant = this.getDominantSentiment(sentiments);
|
|
49
|
-
if (dominant && dominant.label !== 'neutral' && dominant.ratio > 0.6) {
|
|
50
|
-
signals.push({
|
|
51
|
-
type: 'mood',
|
|
52
|
-
pattern: `User tends to be ${dominant.label} in the ${period}`,
|
|
53
|
-
confidence: Math.min(0.4 + dominant.ratio * 0.4, 0.85),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Group by day of week
|
|
59
|
-
const daySentiments = new Map<number, SentimentLabel[]>();
|
|
60
|
-
for (const snap of this.sentimentHistory) {
|
|
61
|
-
const list = daySentiments.get(snap.dayOfWeek) ?? [];
|
|
62
|
-
list.push(snap.sentiment);
|
|
63
|
-
daySentiments.set(snap.dayOfWeek, list);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
for (const [day, sentiments] of daySentiments) {
|
|
67
|
-
if (sentiments.length < 3) continue;
|
|
68
|
-
const dominant = this.getDominantSentiment(sentiments);
|
|
69
|
-
if (dominant && dominant.label !== 'neutral' && dominant.ratio > 0.7) {
|
|
70
|
-
signals.push({
|
|
71
|
-
type: 'mood',
|
|
72
|
-
pattern: `User tends to be ${dominant.label} on ${DAY_NAMES[day]}s`,
|
|
73
|
-
confidence: Math.min(0.4 + dominant.ratio * 0.4, 0.85),
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return signals;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private getDominantSentiment(sentiments: SentimentLabel[]): { label: SentimentLabel; ratio: number } | null {
|
|
82
|
-
const counts: Record<SentimentLabel, number> = { positive: 0, negative: 0, neutral: 0 };
|
|
83
|
-
for (const s of sentiments) {
|
|
84
|
-
counts[s]++;
|
|
85
|
-
}
|
|
86
|
-
const total = sentiments.length;
|
|
87
|
-
const entries: Array<[SentimentLabel, number]> = [
|
|
88
|
-
['positive', counts.positive],
|
|
89
|
-
['negative', counts.negative],
|
|
90
|
-
['neutral', counts.neutral],
|
|
91
|
-
];
|
|
92
|
-
entries.sort((a, b) => b[1] - a[1]);
|
|
93
|
-
return { label: entries[0][0], ratio: entries[0][1] / total };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private getTimePeriodName(hour: number): string {
|
|
97
|
-
if (hour >= 5 && hour < 12) return 'morning';
|
|
98
|
-
if (hour >= 12 && hour < 17) return 'afternoon';
|
|
99
|
-
if (hour >= 17 && hour < 21) return 'evening';
|
|
100
|
-
return 'night';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
detect(messages: MessageInfo[]): PatternSignal[] {
|
|
104
|
-
if (messages.length < 3) return [];
|
|
105
|
-
|
|
106
|
-
const userMessages = messages.filter(m => m.role === 'user');
|
|
107
|
-
if (userMessages.length === 0) return [];
|
|
108
|
-
|
|
109
|
-
const signals: PatternSignal[] = [];
|
|
110
|
-
|
|
111
|
-
signals.push(...this.detectCommunicationPatterns(userMessages));
|
|
112
|
-
signals.push(...this.detectSchedulePatterns(userMessages));
|
|
113
|
-
signals.push(...this.detectTopicPatterns(userMessages));
|
|
114
|
-
signals.push(...this.detectMoodPatterns(userMessages));
|
|
115
|
-
|
|
116
|
-
return signals;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private detectCommunicationPatterns(messages: MessageInfo[]): PatternSignal[] {
|
|
120
|
-
const signals: PatternSignal[] = [];
|
|
121
|
-
const lengths = messages.map(m => m.content.length);
|
|
122
|
-
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
123
|
-
|
|
124
|
-
if (avgLength < 50 && messages.length >= 5) {
|
|
125
|
-
signals.push({
|
|
126
|
-
type: 'communication',
|
|
127
|
-
pattern: 'User prefers brief messages',
|
|
128
|
-
confidence: Math.min(0.5 + messages.length * 0.05, 0.9),
|
|
129
|
-
});
|
|
130
|
-
} else if (avgLength > 200 && messages.length >= 5) {
|
|
131
|
-
signals.push({
|
|
132
|
-
type: 'communication',
|
|
133
|
-
pattern: 'User writes detailed messages',
|
|
134
|
-
confidence: Math.min(0.5 + messages.length * 0.05, 0.9),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Question frequency
|
|
139
|
-
const questionMessages = messages.filter(m => m.content.includes('?'));
|
|
140
|
-
const questionRatio = questionMessages.length / messages.length;
|
|
141
|
-
if (questionRatio > 0.6 && messages.length >= 5) {
|
|
142
|
-
signals.push({
|
|
143
|
-
type: 'communication',
|
|
144
|
-
pattern: 'User frequently asks questions',
|
|
145
|
-
confidence: Math.min(0.4 + questionRatio * 0.5, 0.9),
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Code-heavy detection
|
|
150
|
-
const codeMessages = messages.filter(m =>
|
|
151
|
-
m.content.includes('```') || m.content.includes('function ') || m.content.includes('const ') || m.content.includes('import '),
|
|
152
|
-
);
|
|
153
|
-
const codeRatio = codeMessages.length / messages.length;
|
|
154
|
-
if (codeRatio > 0.3 && messages.length >= 5) {
|
|
155
|
-
signals.push({
|
|
156
|
-
type: 'communication',
|
|
157
|
-
pattern: 'User frequently shares code snippets',
|
|
158
|
-
confidence: Math.min(0.4 + codeRatio * 0.5, 0.9),
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return signals;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private detectSchedulePatterns(messages: MessageInfo[]): PatternSignal[] {
|
|
166
|
-
const signals: PatternSignal[] = [];
|
|
167
|
-
if (messages.length < 5) return signals;
|
|
168
|
-
|
|
169
|
-
const hours = messages.map(m => new Date(m.timestamp).getHours());
|
|
170
|
-
const hourCounts = new Map<number, number>();
|
|
171
|
-
for (const h of hours) {
|
|
172
|
-
hourCounts.set(h, (hourCounts.get(h) ?? 0) + 1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Find the peak 3-hour window
|
|
176
|
-
let peakStart = 0;
|
|
177
|
-
let peakCount = 0;
|
|
178
|
-
for (let h = 0; h < 24; h++) {
|
|
179
|
-
const count =
|
|
180
|
-
(hourCounts.get(h) ?? 0) +
|
|
181
|
-
(hourCounts.get((h + 1) % 24) ?? 0) +
|
|
182
|
-
(hourCounts.get((h + 2) % 24) ?? 0);
|
|
183
|
-
if (count > peakCount) {
|
|
184
|
-
peakCount = count;
|
|
185
|
-
peakStart = h;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const peakRatio = peakCount / messages.length;
|
|
190
|
-
if (peakRatio > 0.5) {
|
|
191
|
-
const peakEnd = (peakStart + 2) % 24;
|
|
192
|
-
const period = this.getTimePeriod(peakStart);
|
|
193
|
-
signals.push({
|
|
194
|
-
type: 'schedule',
|
|
195
|
-
pattern: `User is most active in the ${period} (${peakStart}:00-${peakEnd}:59)`,
|
|
196
|
-
confidence: Math.min(0.4 + peakRatio * 0.5, 0.9),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return signals;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private detectTopicPatterns(messages: MessageInfo[]): PatternSignal[] {
|
|
204
|
-
const signals: PatternSignal[] = [];
|
|
205
|
-
if (messages.length < 5) return signals;
|
|
206
|
-
|
|
207
|
-
const wordCounts = new Map<string, number>();
|
|
208
|
-
const stopWords = new Set([
|
|
209
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
210
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
211
|
-
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
212
|
-
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
|
213
|
-
'and', 'but', 'or', 'not', 'so', 'yet', 'i', 'me', 'my', 'we', 'you',
|
|
214
|
-
'your', 'he', 'she', 'they', 'it', 'what', 'which', 'who', 'when',
|
|
215
|
-
'where', 'how', 'that', 'this', 'just', 'like', 'also', 'about',
|
|
216
|
-
'than', 'too', 'very', 'its', 'them', 'their', 'our',
|
|
217
|
-
]);
|
|
218
|
-
|
|
219
|
-
for (const msg of messages) {
|
|
220
|
-
const words = msg.content
|
|
221
|
-
.toLowerCase()
|
|
222
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
223
|
-
.split(/\s+/)
|
|
224
|
-
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
225
|
-
|
|
226
|
-
for (const word of words) {
|
|
227
|
-
wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const topWords = Array.from(wordCounts.entries())
|
|
232
|
-
.filter(([, count]) => count >= 3)
|
|
233
|
-
.sort((a, b) => b[1] - a[1])
|
|
234
|
-
.slice(0, 5);
|
|
235
|
-
|
|
236
|
-
if (topWords.length >= 2) {
|
|
237
|
-
const topics = topWords.map(([word]) => word).join(', ');
|
|
238
|
-
signals.push({
|
|
239
|
-
type: 'topic',
|
|
240
|
-
pattern: `User frequently discusses: ${topics}`,
|
|
241
|
-
confidence: Math.min(0.5 + topWords.length * 0.05, 0.85),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return signals;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private detectMoodPatterns(messages: MessageInfo[]): PatternSignal[] {
|
|
249
|
-
const signals: PatternSignal[] = [];
|
|
250
|
-
if (messages.length < 5) return signals;
|
|
251
|
-
|
|
252
|
-
let enthusiastic = 0;
|
|
253
|
-
let frustrated = 0;
|
|
254
|
-
let casual = 0;
|
|
255
|
-
|
|
256
|
-
for (const msg of messages) {
|
|
257
|
-
const text = msg.content;
|
|
258
|
-
if (text.includes('!') || /\b(great|awesome|love|amazing|excellent|perfect)\b/i.test(text)) {
|
|
259
|
-
enthusiastic++;
|
|
260
|
-
}
|
|
261
|
-
if (/\b(ugh|damn|broken|bug|error|fail|wrong|issue|problem)\b/i.test(text)) {
|
|
262
|
-
frustrated++;
|
|
263
|
-
}
|
|
264
|
-
if (/\b(lol|haha|heh|lmao|tbh|imo|btw|gonna|wanna)\b/i.test(text)) {
|
|
265
|
-
casual++;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const total = messages.length;
|
|
270
|
-
|
|
271
|
-
if (enthusiastic / total > 0.3) {
|
|
272
|
-
signals.push({
|
|
273
|
-
type: 'mood',
|
|
274
|
-
pattern: 'User tends to be enthusiastic and positive',
|
|
275
|
-
confidence: Math.min(0.4 + (enthusiastic / total) * 0.5, 0.85),
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (frustrated / total > 0.3) {
|
|
280
|
-
signals.push({
|
|
281
|
-
type: 'mood',
|
|
282
|
-
pattern: 'User may be experiencing frustration',
|
|
283
|
-
confidence: Math.min(0.4 + (frustrated / total) * 0.5, 0.85),
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (casual / total > 0.3) {
|
|
288
|
-
signals.push({
|
|
289
|
-
type: 'mood',
|
|
290
|
-
pattern: 'User uses casual, informal language',
|
|
291
|
-
confidence: Math.min(0.4 + (casual / total) * 0.5, 0.85),
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return signals;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private getTimePeriod(hour: number): string {
|
|
299
|
-
if (hour >= 5 && hour < 12) return 'morning';
|
|
300
|
-
if (hour >= 12 && hour < 17) return 'afternoon';
|
|
301
|
-
if (hour >= 17 && hour < 21) return 'evening';
|
|
302
|
-
return 'night';
|
|
303
|
-
}
|
|
304
|
-
}
|