@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.
- package/README.md +29 -13
- package/package.json +2 -4
- package/src/mama/hook-metrics.js +1 -1
- package/src/mama/link-expander.js +2 -2
- package/src/mama/response-formatter.js +6 -6
- package/src/mama/restart-metrics.js +1 -1
- package/src/mama/search-engine.js +3 -3
- package/src/mama/transparency-banner.js +1 -1
- package/src/server.js +72 -34
- package/src/tools/checkpoint-tools.js +2 -2
- package/src/tools/link-tools.js +1 -1
- package/src/tools/list-decisions.js +1 -1
- package/src/tools/quality-metrics-tools.js +13 -3
- package/src/tools/recall-decision.js +1 -1
- package/src/tools/save-decision.js +1 -1
- package/src/tools/search-narrative.js +1 -1
- package/src/tools/suggest-decision.js +16 -1
- package/src/tools/update-outcome.js +1 -1
- package/src/mama/config-loader.js +0 -218
- package/src/mama/db-manager.js +0 -626
- package/src/mama/debug-logger.js +0 -86
- package/src/mama/decision-formatter.js +0 -1262
- package/src/mama/decision-tracker.js +0 -621
- package/src/mama/embedding-cache.js +0 -221
- package/src/mama/embeddings.js +0 -304
- package/src/mama/errors.js +0 -326
- package/src/mama/mama-api.js +0 -2589
- package/src/mama/memory-inject.js +0 -248
- package/src/mama/memory-store.js +0 -89
- package/src/mama/outcome-tracker.js +0 -344
- package/src/mama/query-intent.js +0 -237
- package/src/mama/relevance-scorer.js +0 -284
- package/src/mama/time-formatter.js +0 -94
package/src/mama/query-intent.js
DELETED
|
@@ -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
|
-
};
|