@jungjaehoon/mama-server 1.7.2 → 1.7.5

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.
@@ -1,237 +0,0 @@
1
- /**
2
- * MAMA (Memory-Augmented MCP Architecture) - Query Intent Analysis
3
- *
4
- * Analyzes user queries to detect decision-related intent using EXAONE 3.5
5
- * Tasks: 2.1-2.8 (LLM intent analysis with fallback chain)
6
- * AC #1: Query intent analysis within 100ms
7
- * AC #5: LLM fallback (EXAONE → Gemma → Qwen)
8
- *
9
- * @module query-intent
10
- * @version 1.0
11
- * @date 2025-11-14
12
- */
13
-
14
- const { info, error: logError } = require('./debug-logger');
15
- const { generate, DEFAULT_MODEL, FALLBACK_MODEL } = require('@jungjaehoon/mama-core/ollama-client');
16
-
17
- /**
18
- * Analyze user message for decision-related intent
19
- *
20
- * Task 2.1-2.5: LLM intent analysis
21
- * AC #1: Detect if query involves decisions
22
- * AC #5: Fallback chain implemented
23
- *
24
- * @param {string} userMessage - User's message to analyze
25
- * @param {Object} options - Analysis options
26
- * @param {number} options.timeout - Timeout in ms (default: 100ms)
27
- * @param {number} options.threshold - Minimum confidence (default: 0.6)
28
- * @returns {Promise<Object>} Intent analysis result
29
- */
30
- async function analyzeIntent(userMessage, options = {}) {
31
- const {
32
- timeout = 5000, // Increased: LLM needs time, user accepts longer thinking
33
- threshold = 0.6,
34
- } = options;
35
-
36
- const startTime = Date.now();
37
-
38
- try {
39
- // Task 2.2: Build prompt for decision-making analysis
40
- const prompt = `
41
- Analyze if this query involves decision-making or past choices:
42
-
43
- User Message: "${userMessage}"
44
-
45
- Decision Indicators:
46
- 1. References to past decisions ("we chose X", "last time we did Y")
47
- 2. Questions about previous approaches ("why did we use X?")
48
- 3. Decision evolution queries ("should we change from X to Y?")
49
- 4. Architecture/strategy questions
50
- 5. Method/approach questions ("how do I...", "what's the way to...")
51
- 6. Best practice questions ("what should I use for...", "which one should I use...")
52
-
53
- Return JSON with "topic" as a short snake_case identifier (e.g., "mesh_structure", "database_choice", "auth_strategy", "coding_style", "error_handling"):
54
- {
55
- "involves_decision": boolean,
56
- "topic": string or null (extract main technical topic in snake_case),
57
- "confidence": 0.0-1.0,
58
- "reasoning": "brief explanation"
59
- }
60
-
61
- IMPORTANT: Generate "topic" freely based on the message content. Do NOT limit to predefined values.
62
-
63
- Examples:
64
- - "Why did we choose COMPLEX mesh structure?" → {"involves_decision": true, "topic": "mesh_structure", "confidence": 0.9}
65
- - "Let's use PostgreSQL for database" → {"involves_decision": true, "topic": "database_choice", "confidence": 0.9}
66
- - "How should we store workflow data?" → {"involves_decision": true, "topic": "workflow_storage", "confidence": 0.85}
67
- - "Read the file please" → {"involves_decision": false, "topic": null, "confidence": 0.1}
68
- `.trim();
69
-
70
- // Task 2.3: Call EXAONE 3.5 with Tier 1 fallback
71
- const result = await generateWithFallback(prompt, {
72
- format: 'json',
73
- temperature: 0.3,
74
- max_tokens: 200,
75
- timeout,
76
- });
77
-
78
- // eslint-disable-next-line no-unused-vars
79
- const latency = Date.now() - startTime;
80
-
81
- // Task 2.4: Parse response
82
- const parsed = typeof result === 'string' ? JSON.parse(result) : result;
83
-
84
- // Task 2.5: Threshold check
85
- const meetsThreshold = parsed.confidence >= threshold;
86
-
87
- if (!meetsThreshold) {
88
- info(`[MAMA] Intent confidence ${parsed.confidence} below threshold ${threshold}`);
89
- return {
90
- involves_decision: false,
91
- topic: null,
92
- confidence: parsed.confidence,
93
- reasoning: 'Confidence below threshold',
94
- };
95
- }
96
-
97
- return parsed;
98
- } catch (error) {
99
- // CLAUDE.md Rule #1: NO FALLBACK
100
- // Errors must be thrown for debugging
101
- logError(`[MAMA] Intent analysis FAILED: ${error.message}`);
102
- throw new Error(`Intent analysis failed: ${error.message}`);
103
- }
104
- }
105
-
106
- /**
107
- * Generate with tiered fallback chain
108
- *
109
- * Task 2.6-2.7: Implement fallback to Gemma 2B and Qwen 3B
110
- * AC #5: LLM fallback works
111
- *
112
- * @param {string} prompt - LLM prompt
113
- * @param {Object} options - Generation options
114
- * @returns {Promise<Object|string>} LLM response
115
- */
116
- async function generateWithFallback(prompt, options = {}) {
117
- const models = [
118
- DEFAULT_MODEL, // Tier 1: EXAONE 3.5 (2.4B)
119
- FALLBACK_MODEL, // Tier 2: Gemma 2B
120
- 'qwen:3b', // Tier 3: Qwen 3B
121
- ];
122
-
123
- for (let i = 0; i < models.length; i++) {
124
- const model = models[i];
125
-
126
- try {
127
- info(`[MAMA] Trying ${model}...`);
128
-
129
- const result = await generate(prompt, {
130
- ...options,
131
- model,
132
- });
133
-
134
- info(`[MAMA] ${model} succeeded`);
135
- return result;
136
- } catch (error) {
137
- console.warn(`[MAMA] ${model} failed: ${error.message}`);
138
-
139
- // Continue to next tier
140
- if (i === models.length - 1) {
141
- // All tiers failed
142
- throw new Error(`All LLM tiers failed. Last error: ${error.message}`);
143
- }
144
- }
145
- }
146
- }
147
-
148
- /**
149
- * Extract topic keywords from user message (fallback method)
150
- *
151
- * Task 2.8: Keyword-based fallback when all LLMs fail
152
- * Simple regex matching for common topics
153
- *
154
- * @param {string} userMessage - User's message
155
- * @returns {Object} Topic detection result
156
- */
157
- function extractTopicKeywords(userMessage) {
158
- const topicPatterns = {
159
- workflow_storage: /workflow|save|persist/i,
160
- mesh_structure: /mesh|structure/i,
161
- authentication: /auth|jwt|oauth|login/i,
162
- testing: /test|jest|spec/i,
163
- architecture: /architecture|design/i,
164
- coding_style: /style|format|coding/i,
165
- };
166
-
167
- for (const [topic, pattern] of Object.entries(topicPatterns)) {
168
- if (pattern.test(userMessage)) {
169
- return {
170
- involves_decision: true,
171
- topic,
172
- confidence: 0.5, // Lower confidence for keyword matching
173
- reasoning: 'Keyword-based detection (LLM fallback)',
174
- };
175
- }
176
- }
177
-
178
- return {
179
- involves_decision: false,
180
- topic: null,
181
- confidence: 0.0,
182
- reasoning: 'No topic keywords found',
183
- };
184
- }
185
-
186
- // Export API
187
- module.exports = {
188
- analyzeIntent,
189
- extractTopicKeywords,
190
- };
191
-
192
- // CLI execution for testing
193
- if (require.main === module) {
194
- info('🧠 MAMA Query Intent Analysis - Test\n');
195
-
196
- // Task 2.8: Test intent detection accuracy
197
- (async () => {
198
- const testQueries = [
199
- {
200
- message: 'Why did we choose COMPLEX mesh structure?',
201
- expected: { involves_decision: true, topic: 'mesh_structure' },
202
- },
203
- {
204
- message: 'Read the file please',
205
- expected: { involves_decision: false },
206
- },
207
- {
208
- message: 'We chose JWT for authentication, remember?',
209
- expected: { involves_decision: true, topic: 'authentication' },
210
- },
211
- ];
212
-
213
- for (const test of testQueries) {
214
- info(`📋 Testing: "${test.message}"`);
215
-
216
- try {
217
- const result = await analyzeIntent(test.message);
218
- info('✅ Result:', result);
219
-
220
- // Verify expectations
221
- if (result.involves_decision === test.expected.involves_decision) {
222
- info(' ✓ Decision detection matches');
223
- } else {
224
- info(' ✗ Decision detection MISMATCH');
225
- }
226
-
227
- info('');
228
- } catch (error) {
229
- logError(`❌ Error: ${error.message}\n`);
230
- }
231
- }
232
-
233
- info('═══════════════════════════');
234
- info('✅ Intent analysis tests complete');
235
- info('═══════════════════════════');
236
- })();
237
- }
@@ -1,284 +0,0 @@
1
- /**
2
- * MAMA (Memory-Augmented MCP Architecture) - Relevance Scorer
3
- *
4
- * Relevance scoring formula for decision ranking and top-N selection
5
- * Tasks: 1.1-1.4, 2.1-2.7 (Relevance scoring and top-N selection)
6
- * AC #1, #4, #5: Decision relevance, failure priority boost, top-N selection
7
- *
8
- * @module relevance-scorer
9
- * @version 1.0
10
- * @date 2025-11-14
11
- */
12
-
13
- const { cosineSimilarity } = require('./embeddings');
14
-
15
- /**
16
- * Calculate relevance score for a single decision
17
- *
18
- * Task 1.2: Implement calculateRelevance(decision, queryContext) function
19
- * AC #1, #4: Relevance scoring with failure priority boost
20
- *
21
- * Formula:
22
- * Relevance = (Recency × 0.2) + (Importance × 0.5) + (Semantic × 0.3)
23
- *
24
- * Where:
25
- * - Recency: exp(-days_since / 30) [30-day half-life]
26
- * - Importance: OUTCOME_WEIGHTS[outcome]
27
- * - FAILED: 1.0 (highest - failures are most valuable)
28
- * - PARTIAL: 0.7
29
- * - SUCCESS: 0.5
30
- * - null: 0.3 (ongoing, lowest)
31
- * - Semantic: cosineSimilarity(decision.embedding, query.embedding)
32
- *
33
- * @param {Object} decision - Decision object
34
- * @param {number} decision.created_at - Created timestamp
35
- * @param {string} decision.outcome - Outcome type
36
- * @param {Float32Array} decision.embedding - Decision embedding (384-dim)
37
- * @param {Object} queryContext - Query context
38
- * @param {Float32Array} queryContext.embedding - Query embedding (384-dim)
39
- * @returns {number} Relevance score (0.0-1.0)
40
- */
41
- function calculateRelevance(decision, queryContext) {
42
- // ═══════════════════════════════════════════════════════════
43
- // Recency Score (20%)
44
- // ═══════════════════════════════════════════════════════════
45
- // Exponential decay with 30-day half-life
46
- const daysSince = (Date.now() - decision.created_at) / (1000 * 60 * 60 * 24);
47
- const recencyScore = Math.exp(-daysSince / 30);
48
-
49
- // Decay curve:
50
- // 0 days = 1.0
51
- // 30 days = 0.5
52
- // 60 days = 0.25
53
- // 90 days = 0.125
54
-
55
- // ═══════════════════════════════════════════════════════════
56
- // Importance Score (50%) - AC #4: Failure Priority Boost
57
- // ═══════════════════════════════════════════════════════════
58
- const OUTCOME_WEIGHTS = {
59
- FAILED: 1.0, // Highest - failures are most valuable (AC #4)
60
- PARTIAL: 0.7,
61
- SUCCESS: 0.5,
62
- null: 0.3, // Ongoing, lowest
63
- };
64
-
65
- const importanceScore = OUTCOME_WEIGHTS[decision.outcome] || OUTCOME_WEIGHTS['null'];
66
-
67
- // ═══════════════════════════════════════════════════════════
68
- // Semantic Score (30%)
69
- // ═══════════════════════════════════════════════════════════
70
- let semanticScore = 0;
71
-
72
- if (decision.embedding && queryContext.embedding) {
73
- // Task 1.3: Use cosine similarity function
74
- semanticScore = cosineSimilarity(decision.embedding, queryContext.embedding);
75
- } else {
76
- // Fallback: no semantic match if embeddings missing
77
- semanticScore = 0;
78
- }
79
-
80
- // ═══════════════════════════════════════════════════════════
81
- // Weighted Sum (Total: 100%)
82
- // ═══════════════════════════════════════════════════════════
83
- const relevance = recencyScore * 0.2 + importanceScore * 0.5 + semanticScore * 0.3;
84
-
85
- return relevance;
86
- }
87
-
88
- /**
89
- * Select top N most relevant decisions
90
- *
91
- * Task 2.1: Add selectTopDecisions(decisions, queryContext, n=3) function
92
- * AC #1, #5: Top-N selection with threshold filtering
93
- *
94
- * @param {Array<Object>} decisions - Array of decision objects
95
- * @param {Object} queryContext - Query context with embedding
96
- * @param {number} n - Number of top decisions to return (default: 3)
97
- * @returns {Array<Object>} Top N decisions with relevance scores
98
- */
99
- function selectTopDecisions(decisions, queryContext, n = 3) {
100
- if (!Array.isArray(decisions) || decisions.length === 0) {
101
- return [];
102
- }
103
-
104
- // Task 2.3: Score all results by relevance
105
- const decisionsWithScores = decisions.map((decision) => ({
106
- ...decision,
107
- relevanceScore: calculateRelevance(decision, queryContext),
108
- }));
109
-
110
- // Task 2.4: Sort descending (highest relevance first)
111
- decisionsWithScores.sort((a, b) => b.relevanceScore - a.relevanceScore);
112
-
113
- // Task 2.6: Filter out < 0.5 relevance (AC #1)
114
- const filtered = decisionsWithScores.filter((d) => d.relevanceScore >= 0.5);
115
-
116
- // Task 2.5: Return top 3 (or top N)
117
- const topN = filtered.slice(0, n);
118
-
119
- return topN;
120
- }
121
-
122
- /**
123
- * Cosine similarity helper (re-exported from embeddings.js)
124
- *
125
- * Task 1.3: Implement cosine similarity function
126
- * AC #1: Semantic similarity calculation
127
- *
128
- * Note: This is re-exported from embeddings.js for convenience
129
- *
130
- * @param {Float32Array} vec1 - First embedding vector
131
- * @param {Float32Array} vec2 - Second embedding vector
132
- * @returns {number} Cosine similarity (0.0-1.0)
133
- */
134
- // Already available from embeddings.js - no need to reimplement
135
-
136
- /**
137
- * Format decisions with top-N selection and summary
138
- *
139
- * Task 8.2-8.3: Format top 3 in full detail, rest as summary
140
- * AC #5: Top-N selection with summary
141
- *
142
- * @param {Array<Object>} decisions - All decisions (sorted by relevance)
143
- * @param {number} topN - Number of decisions to show in full detail (default: 3)
144
- * @returns {Object} Formatted context {full: Array, summary: Object}
145
- */
146
- function formatTopNContext(decisions, topN = 3) {
147
- if (!Array.isArray(decisions) || decisions.length === 0) {
148
- return { full: [], summary: null };
149
- }
150
-
151
- // Split into top N and rest
152
- const fullDetailDecisions = decisions.slice(0, topN);
153
- const summaryDecisions = decisions.slice(topN);
154
-
155
- // Full detail for top N
156
- const full = fullDetailDecisions.map((d) => ({
157
- decision_id: d.id,
158
- topic: d.topic,
159
- decision: d.decision,
160
- reasoning: d.reasoning,
161
- outcome: d.outcome,
162
- failure_reason: d.failure_reason,
163
- user_involvement: d.user_involvement,
164
- confidence: d.confidence,
165
- relevanceScore: d.relevanceScore,
166
- created_at: d.created_at,
167
- }));
168
-
169
- // Summary for rest (count, duration, key failures only)
170
- let summary = null;
171
-
172
- if (summaryDecisions.length > 0) {
173
- // Calculate duration (oldest to newest)
174
- const oldestTimestamp = Math.min(...summaryDecisions.map((d) => d.created_at));
175
- const newestTimestamp = Math.max(...summaryDecisions.map((d) => d.created_at));
176
- const durationDays = Math.floor((newestTimestamp - oldestTimestamp) / (1000 * 60 * 60 * 24));
177
-
178
- // Extract key failures
179
- const failures = summaryDecisions
180
- .filter((d) => d.outcome === 'FAILED')
181
- .map((d) => ({ decision: d.decision, reason: d.failure_reason }));
182
-
183
- summary = {
184
- count: summaryDecisions.length,
185
- duration_days: durationDays,
186
- failures: failures.slice(0, 3), // Show max 3 failures
187
- };
188
- }
189
-
190
- return { full, summary };
191
- }
192
-
193
- /**
194
- * Test relevance scoring with sample decisions
195
- *
196
- * Task 1.4: Test relevance scoring with sample decisions
197
- * AC #1, #4: Verify scoring formula and failure priority
198
- *
199
- * @returns {Object} Test results
200
- */
201
- function testRelevanceScoring() {
202
- const now = Date.now();
203
-
204
- // Mock embeddings (dummy for testing)
205
- const queryEmbedding = new Float32Array(384).fill(0.5);
206
- const decisionEmbedding1 = new Float32Array(384).fill(0.5); // Identical (similarity = 1.0)
207
- // eslint-disable-next-line no-unused-vars
208
- const decisionEmbedding2 = new Float32Array(384).fill(0.3); // Different (similarity < 1.0)
209
-
210
- const scenarios = [
211
- // Scenario 1: Recent FAILED decision (should have highest relevance)
212
- {
213
- name: 'Recent FAILED decision',
214
- decision: {
215
- created_at: now - 5 * 24 * 60 * 60 * 1000, // 5 days ago
216
- outcome: 'FAILED',
217
- embedding: decisionEmbedding1,
218
- },
219
- queryContext: { embedding: queryEmbedding },
220
- expected: {
221
- recency: 0.85, // exp(-5/30) ≈ 0.85
222
- importance: 1.0, // FAILED = 1.0 (AC #4)
223
- semantic: 1.0, // Identical embeddings
224
- relevance: 0.87, // (0.85×0.2) + (1.0×0.5) + (1.0×0.3)
225
- },
226
- },
227
-
228
- // Scenario 2: Recent SUCCESS decision (lower importance)
229
- {
230
- name: 'Recent SUCCESS decision',
231
- decision: {
232
- created_at: now - 5 * 24 * 60 * 60 * 1000, // 5 days ago
233
- outcome: 'SUCCESS',
234
- embedding: decisionEmbedding1,
235
- },
236
- queryContext: { embedding: queryEmbedding },
237
- expected: {
238
- recency: 0.85,
239
- importance: 0.5, // SUCCESS = 0.5
240
- semantic: 1.0,
241
- relevance: 0.62, // (0.85×0.2) + (0.5×0.5) + (1.0×0.3)
242
- },
243
- },
244
-
245
- // Scenario 3: Old FAILED decision (recency decay)
246
- {
247
- name: 'Old FAILED decision',
248
- decision: {
249
- created_at: now - 60 * 24 * 60 * 60 * 1000, // 60 days ago
250
- outcome: 'FAILED',
251
- embedding: decisionEmbedding1,
252
- },
253
- queryContext: { embedding: queryEmbedding },
254
- expected: {
255
- recency: 0.25, // exp(-60/30) ≈ 0.25
256
- importance: 1.0,
257
- semantic: 1.0,
258
- relevance: 0.85, // (0.25×0.2) + (1.0×0.5) + (1.0×0.3)
259
- },
260
- },
261
- ];
262
-
263
- const results = scenarios.map((scenario) => {
264
- const calculated = calculateRelevance(scenario.decision, scenario.queryContext);
265
- const pass = Math.abs(calculated - scenario.expected.relevance) < 0.05;
266
-
267
- return {
268
- name: scenario.name,
269
- expected: scenario.expected.relevance.toFixed(2),
270
- calculated: calculated.toFixed(2),
271
- pass,
272
- };
273
- });
274
-
275
- return results;
276
- }
277
-
278
- // Export API
279
- module.exports = {
280
- calculateRelevance,
281
- selectTopDecisions,
282
- formatTopNContext,
283
- testRelevanceScoring,
284
- };
@@ -1,94 +0,0 @@
1
- /**
2
- * Time Formatter - Human-Readable Time Formatting
3
- *
4
- * Converts Unix timestamps to human-readable relative time format
5
- * Examples: "2d ago", "3h ago", "just now"
6
- *
7
- * Used by list_decisions and recall_decision tools
8
- *
9
- * @module time-formatter
10
- * @date 2025-11-20
11
- */
12
-
13
- const { warn } = require('./debug-logger');
14
-
15
- /**
16
- * Format Unix timestamp (milliseconds) to human-readable relative time
17
- *
18
- * AC #2: Format created_at as human-readable ("2d ago", "3h ago", etc.)
19
- *
20
- * @param {number|string} timestamp - Unix timestamp in milliseconds OR ISO 8601 string
21
- * @returns {string} Human-readable time string
22
- *
23
- * @example
24
- * formatTimeAgo(Date.now() - 3600000) // "1h ago"
25
- * formatTimeAgo(Date.now() - 172800000) // "2d ago"
26
- * formatTimeAgo("2025-11-20T10:30:00Z") // "2d ago" (if today is 2025-11-22)
27
- */
28
- function formatTimeAgo(timestamp) {
29
- try {
30
- // Handle null/undefined
31
- if (!timestamp) {
32
- warn('[time-formatter] Timestamp is null or undefined, returning "unknown"');
33
- return 'unknown';
34
- }
35
-
36
- // Parse ISO 8601 string to timestamp (if string provided)
37
- let timestampMs;
38
- if (typeof timestamp === 'string') {
39
- timestampMs = new Date(timestamp).getTime();
40
- if (isNaN(timestampMs)) {
41
- warn(`[time-formatter] Invalid ISO 8601 string: ${timestamp}`);
42
- return 'unknown';
43
- }
44
- } else {
45
- timestampMs = timestamp;
46
- }
47
-
48
- const now = Date.now();
49
- const diff = now - timestampMs;
50
-
51
- // Handle future timestamps (shouldn't happen, but be defensive)
52
- if (diff < 0) {
53
- warn(`[time-formatter] Future timestamp detected: ${timestamp}`);
54
- return 'just now';
55
- }
56
-
57
- // Calculate time units
58
- const seconds = Math.floor(diff / 1000);
59
- const minutes = Math.floor(seconds / 60);
60
- const hours = Math.floor(minutes / 60);
61
- const days = Math.floor(hours / 24);
62
- const weeks = Math.floor(days / 7);
63
- const months = Math.floor(days / 30);
64
- const years = Math.floor(days / 365);
65
-
66
- // Return human-readable format
67
- if (seconds < 60) {
68
- return 'just now';
69
- }
70
- if (minutes < 60) {
71
- return `${minutes}m ago`;
72
- }
73
- if (hours < 24) {
74
- return `${hours}h ago`;
75
- }
76
- if (days < 7) {
77
- return `${days}d ago`;
78
- }
79
- if (weeks < 4) {
80
- return `${weeks}w ago`;
81
- }
82
- if (months < 12) {
83
- return `${months}mo ago`;
84
- }
85
- return `${years}y ago`;
86
- } catch (error) {
87
- warn(`[time-formatter] Error formatting timestamp ${timestamp}: ${error.message}`);
88
- return 'unknown';
89
- }
90
- }
91
-
92
- module.exports = {
93
- formatTimeAgo,
94
- };