@jungjaehoon/mama-core 1.0.1
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 +171 -0
- package/package.json +68 -0
- package/src/config-loader.js +218 -0
- package/src/db-adapter/README.md +110 -0
- package/src/db-adapter/base-adapter.js +91 -0
- package/src/db-adapter/index.js +31 -0
- package/src/db-adapter/sqlite-adapter.js +364 -0
- package/src/db-adapter/statement.js +127 -0
- package/src/db-manager.js +671 -0
- package/src/debug-logger.js +86 -0
- package/src/decision-formatter.js +1276 -0
- package/src/decision-tracker.js +621 -0
- package/src/embedding-cache.js +222 -0
- package/src/embedding-client.js +141 -0
- package/src/embedding-server/index.js +424 -0
- package/src/embedding-server/mobile/auth.js +160 -0
- package/src/embedding-server/mobile/daemon.js +313 -0
- package/src/embedding-server/mobile/output-parser.js +281 -0
- package/src/embedding-server/mobile/session-api.js +279 -0
- package/src/embedding-server/mobile/session-manager.js +377 -0
- package/src/embedding-server/mobile/websocket-handler.js +389 -0
- package/src/embeddings.js +305 -0
- package/src/errors.js +326 -0
- package/src/index.js +41 -0
- package/src/mama-api.js +2614 -0
- package/src/memory-inject.js +174 -0
- package/src/memory-store.js +89 -0
- package/src/notification-manager.js +3 -0
- package/src/ollama-client.js +391 -0
- package/src/outcome-tracker.js +351 -0
- package/src/progress-indicator.js +110 -0
- package/src/query-intent.js +237 -0
- package/src/relevance-scorer.js +286 -0
- package/src/tier-validator.js +269 -0
- package/src/time-formatter.js +98 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA (Memory-Augmented MCP Architecture) - Memory Injection Hook
|
|
3
|
+
*
|
|
4
|
+
* UserPromptSubmit hook that injects decision history into Claude's context
|
|
5
|
+
* Tasks: 1.1-1.9 (Hook setup, timeout handling, context injection)
|
|
6
|
+
* AC #1: Query intent → Graph query → Format → Inject (5s timeout for LLM latency)
|
|
7
|
+
* AC #2: No history → null (graceful fallback)
|
|
8
|
+
* AC #3: Timeout → graceful fallback
|
|
9
|
+
*
|
|
10
|
+
* @module memory-inject
|
|
11
|
+
* @version 1.0
|
|
12
|
+
* @date 2025-11-14
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Error handling policy:
|
|
16
|
+
// - Timeout errors: thrown (caller handles retry/fallback)
|
|
17
|
+
// - Vector search unavailable: returns empty array (recoverable, not critical)
|
|
18
|
+
const { info, error: logError } = require('./debug-logger');
|
|
19
|
+
const { vectorSearch } = require('./memory-store');
|
|
20
|
+
const { formatContext } = require('./decision-formatter');
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
const TIMEOUT_MS = 5000; // LLM-based intent detection, user accepts longer thinking
|
|
24
|
+
const TOKEN_BUDGET = 500; // AC #1: Max 500 tokens per injection
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* UserPromptSubmit Hook Handler
|
|
28
|
+
*
|
|
29
|
+
* Task 1.1-1.9: Main entry point for memory injection
|
|
30
|
+
* AC #1, #2, #3: Intent analysis → Query → Format → Inject
|
|
31
|
+
*
|
|
32
|
+
* @param {string} userMessage - User's message from prompt
|
|
33
|
+
* @returns {Promise<string|null>} Injected context or null
|
|
34
|
+
*/
|
|
35
|
+
async function injectDecisionContext(userMessage) {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
let timeoutId = null;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Task 1.3: Implement timeout wrapper (Promise.race with timeout)
|
|
41
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
42
|
+
timeoutId = setTimeout(() => {
|
|
43
|
+
reject(new Error(`Memory injection timeout (${TIMEOUT_MS}ms)`));
|
|
44
|
+
}, TIMEOUT_MS);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const context = await Promise.race([
|
|
48
|
+
performMemoryInjection(userMessage, startTime),
|
|
49
|
+
timeoutPromise,
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// Clear timeout on success to prevent timer leak
|
|
53
|
+
if (timeoutId) {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return context;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Clear timeout on error to prevent timer leak
|
|
60
|
+
if (timeoutId) {
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
}
|
|
63
|
+
// CLAUDE.md Rule #1: NO FALLBACK
|
|
64
|
+
// Errors must be thrown for debugging (including timeout)
|
|
65
|
+
logError(`[MAMA] Memory injection FAILED: ${error.message}`);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Perform memory injection with all steps
|
|
72
|
+
*
|
|
73
|
+
* Simplified: Direct vector search without LLM intent analysis
|
|
74
|
+
* Faster, more reliable, works with all query types
|
|
75
|
+
*
|
|
76
|
+
* @param {string} userMessage - User's message
|
|
77
|
+
* @param {number} startTime - Start timestamp for latency tracking
|
|
78
|
+
* @returns {Promise<string|null>} Formatted context or null
|
|
79
|
+
*/
|
|
80
|
+
async function performMemoryInjection(userMessage, startTime) {
|
|
81
|
+
// 1. Generate query embedding
|
|
82
|
+
const { generateEmbedding } = require('./embeddings');
|
|
83
|
+
const queryEmbedding = await generateEmbedding(userMessage);
|
|
84
|
+
|
|
85
|
+
const embeddingLatency = Date.now() - startTime;
|
|
86
|
+
info(`[MAMA] Embedding generation: ${embeddingLatency}ms`);
|
|
87
|
+
|
|
88
|
+
// 2. Adaptive threshold (shorter queries need higher confidence)
|
|
89
|
+
const wordCount = userMessage.split(/\s+/).length;
|
|
90
|
+
const adaptiveThreshold = wordCount < 3 ? 0.7 : 0.6;
|
|
91
|
+
|
|
92
|
+
// 3. Vector search (returns [] on error for graceful degradation)
|
|
93
|
+
let results;
|
|
94
|
+
try {
|
|
95
|
+
results = await vectorSearch(queryEmbedding, 10, 0.5); // Get more candidates
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Vector search unavailability should not block the main conversation flow
|
|
98
|
+
logError(`[MAMA] Vector search failed: ${error.message}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 4. Filter by adaptive threshold
|
|
103
|
+
results = results.filter((r) => r.similarity >= adaptiveThreshold);
|
|
104
|
+
|
|
105
|
+
const searchLatency = Date.now() - startTime;
|
|
106
|
+
info(
|
|
107
|
+
`[MAMA] Vector search: ${searchLatency - embeddingLatency}ms (${results.length} results, threshold: ${adaptiveThreshold})`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// 5. Check if we have any decisions
|
|
111
|
+
if (results.length === 0) {
|
|
112
|
+
// Redact user content for privacy - only log length, not content
|
|
113
|
+
info(`[MAMA] No relevant decisions found (query length: ${userMessage.length} chars)`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 6. Format context summary
|
|
118
|
+
const formattedContext = formatContext(results, {
|
|
119
|
+
maxTokens: TOKEN_BUDGET,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const formatLatency = Date.now() - startTime;
|
|
123
|
+
info(`[MAMA] Format context: ${formatLatency - searchLatency}ms (total: ${formatLatency}ms)`);
|
|
124
|
+
|
|
125
|
+
// 7. Return formatted context (Claude Code will inject it)
|
|
126
|
+
return formattedContext;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Export API
|
|
130
|
+
module.exports = {
|
|
131
|
+
injectDecisionContext,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// CLI execution for testing
|
|
135
|
+
if (require.main === module) {
|
|
136
|
+
info('🧠 MAMA Memory Injection - Test\n');
|
|
137
|
+
|
|
138
|
+
// Test memory injection flow
|
|
139
|
+
(async () => {
|
|
140
|
+
const testMessages = [
|
|
141
|
+
'Why did we choose COMPLEX mesh structure?',
|
|
142
|
+
'Read the file please',
|
|
143
|
+
'We chose JWT authentication, why?',
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
for (const message of testMessages) {
|
|
147
|
+
info(`═══════════════════════════`);
|
|
148
|
+
info(`📋 Testing: "${message}"\n`);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
const context = await injectDecisionContext(message);
|
|
153
|
+
const latency = Date.now() - startTime;
|
|
154
|
+
|
|
155
|
+
if (context) {
|
|
156
|
+
info('\n✅ Context injected:');
|
|
157
|
+
info(context);
|
|
158
|
+
info(`\n⏱️ Latency: ${latency}ms (target: <${TIMEOUT_MS}ms)`);
|
|
159
|
+
} else {
|
|
160
|
+
info('\n⚠️ No context injected (null returned)');
|
|
161
|
+
info(`⏱️ Latency: ${latency}ms`);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logError(`\n❌ Error: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
info('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
info('═══════════════════════════');
|
|
171
|
+
info('✅ Memory injection tests complete');
|
|
172
|
+
info('═══════════════════════════');
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA Memory Store - Compatibility Wrapper
|
|
3
|
+
*
|
|
4
|
+
* This file now serves as a compatibility layer that re-exports db-manager functions.
|
|
5
|
+
* The actual database logic has been moved to db-manager.js which supports both
|
|
6
|
+
* SQLite (local development) and PostgreSQL (Railway production).
|
|
7
|
+
*
|
|
8
|
+
* Migration Note:
|
|
9
|
+
* - Old: memory-store.js directly used better-sqlite3 + sqlite-vss
|
|
10
|
+
* - New: memory-store.js → db-manager.js → db-adapter (SQLite or PostgreSQL)
|
|
11
|
+
*
|
|
12
|
+
* All MAMA modules can continue to require('memory-store') without changes.
|
|
13
|
+
*
|
|
14
|
+
* @module memory-store
|
|
15
|
+
* @version 2.0
|
|
16
|
+
* @date 2025-11-17
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const dbManager = require('./db-manager');
|
|
20
|
+
|
|
21
|
+
// Re-export all db-manager functions for backward compatibility
|
|
22
|
+
// These maintain the same interface as the original memory-store.js
|
|
23
|
+
module.exports = {
|
|
24
|
+
// Core database functions
|
|
25
|
+
initDB: dbManager.initDB, // Now async, returns Promise<connection>
|
|
26
|
+
getDB: dbManager.getDB, // Sync, throws if not initialized
|
|
27
|
+
getAdapter: dbManager.getAdapter, // Get database adapter (PostgreSQL or SQLite)
|
|
28
|
+
closeDB: dbManager.closeDB, // Async
|
|
29
|
+
|
|
30
|
+
// Vector search functions
|
|
31
|
+
insertEmbedding: dbManager.insertEmbedding, // Async
|
|
32
|
+
vectorSearch: dbManager.vectorSearch, // Async (returns null if unavailable)
|
|
33
|
+
queryVectorSearch: dbManager.queryVectorSearch, // Async - Story 014.14
|
|
34
|
+
|
|
35
|
+
// Decision functions
|
|
36
|
+
insertDecisionWithEmbedding: dbManager.insertDecisionWithEmbedding, // Async
|
|
37
|
+
queryDecisionGraph: dbManager.queryDecisionGraph, // Async
|
|
38
|
+
querySemanticEdges: dbManager.querySemanticEdges, // Async - Graph traversal
|
|
39
|
+
updateDecisionOutcome: dbManager.updateDecisionOutcome, // Async
|
|
40
|
+
|
|
41
|
+
// Compatibility functions
|
|
42
|
+
getPreparedStmt: dbManager.getPreparedStmt, // Deprecated
|
|
43
|
+
getDbPath: dbManager.getDbPath, // Returns adapter name
|
|
44
|
+
|
|
45
|
+
// Legacy exports (for backward compatibility with old code)
|
|
46
|
+
traverseDecisionChain: dbManager.queryDecisionGraph, // Alias
|
|
47
|
+
getSessionDecisions: async (sessionId) => {
|
|
48
|
+
// Fallback implementation
|
|
49
|
+
const adapter = dbManager.getAdapter();
|
|
50
|
+
const stmt = adapter.prepare(`
|
|
51
|
+
SELECT * FROM decisions
|
|
52
|
+
WHERE session_id = ?
|
|
53
|
+
ORDER BY created_at DESC
|
|
54
|
+
`);
|
|
55
|
+
return await stmt.all(sessionId);
|
|
56
|
+
},
|
|
57
|
+
incrementUsageSuccess: async (decisionId, timeSaved = 0) => {
|
|
58
|
+
const adapter = dbManager.getAdapter();
|
|
59
|
+
const stmt = adapter.prepare(`
|
|
60
|
+
UPDATE decisions
|
|
61
|
+
SET usage_success = usage_success + 1,
|
|
62
|
+
time_saved = time_saved + ?,
|
|
63
|
+
updated_at = ?
|
|
64
|
+
WHERE id = ?
|
|
65
|
+
`);
|
|
66
|
+
await stmt.run(timeSaved, Date.now(), decisionId);
|
|
67
|
+
},
|
|
68
|
+
incrementUsageFailure: async (decisionId) => {
|
|
69
|
+
const adapter = dbManager.getAdapter();
|
|
70
|
+
const stmt = adapter.prepare(`
|
|
71
|
+
UPDATE decisions
|
|
72
|
+
SET usage_failure = usage_failure + 1,
|
|
73
|
+
updated_at = ?
|
|
74
|
+
WHERE id = ?
|
|
75
|
+
`);
|
|
76
|
+
await stmt.run(Date.now(), decisionId);
|
|
77
|
+
},
|
|
78
|
+
getDecisionById: async (decisionId) => {
|
|
79
|
+
const adapter = dbManager.getAdapter();
|
|
80
|
+
const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ?');
|
|
81
|
+
return await stmt.get(decisionId);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Path exports (for compatibility)
|
|
85
|
+
DB_PATH: process.env.MAMA_DATABASE_URL ? 'PostgreSQL' : 'SQLite',
|
|
86
|
+
DB_DIR: process.env.MAMA_DATABASE_URL ? 'PostgreSQL' : '~/.mama',
|
|
87
|
+
LEGACY_DB_PATH: '~/.spinelift/memories.db',
|
|
88
|
+
DEFAULT_DB_PATH: '~/.mama/memories.db',
|
|
89
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA (Memory-Augmented MCP Architecture) - Ollama Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Simple wrapper for Ollama API with EXAONE 3.5 support
|
|
5
|
+
* Tasks: 9.2-9.5 (HTTP client, EXAONE wrapper, Error handling, Testing)
|
|
6
|
+
* AC #1: LLM integration ready
|
|
7
|
+
*
|
|
8
|
+
* @module ollama-client
|
|
9
|
+
* @version 1.0
|
|
10
|
+
* @date 2025-11-14
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { info, error: logError } = require('./debug-logger');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
|
|
16
|
+
// Ollama configuration
|
|
17
|
+
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'localhost';
|
|
18
|
+
const OLLAMA_PORT = process.env.OLLAMA_PORT || 11434;
|
|
19
|
+
const DEFAULT_MODEL = 'exaone3.5:2.4b';
|
|
20
|
+
const FALLBACK_MODEL = 'gemma:2b';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Call Ollama API
|
|
24
|
+
*
|
|
25
|
+
* Task 9.2: Implement HTTP client for Ollama API
|
|
26
|
+
* AC #1: LLM API callable
|
|
27
|
+
*
|
|
28
|
+
* @param {string} endpoint - API endpoint (e.g., '/api/generate')
|
|
29
|
+
* @param {Object} payload - Request payload
|
|
30
|
+
* @param {number} timeout - Timeout in milliseconds (default: 30000)
|
|
31
|
+
* @returns {Promise<Object>} API response
|
|
32
|
+
* @throws {Error} If request fails
|
|
33
|
+
*/
|
|
34
|
+
function callOllamaAPI(endpoint, payload, timeout = 30000) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const postData = JSON.stringify(payload);
|
|
37
|
+
|
|
38
|
+
const options = {
|
|
39
|
+
hostname: OLLAMA_HOST,
|
|
40
|
+
port: OLLAMA_PORT,
|
|
41
|
+
path: endpoint,
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const req = http.request(options, (res) => {
|
|
50
|
+
let data = '';
|
|
51
|
+
|
|
52
|
+
res.on('data', (chunk) => {
|
|
53
|
+
data += chunk;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
58
|
+
try {
|
|
59
|
+
// Ollama returns NDJSON (newline-delimited JSON)
|
|
60
|
+
// For non-streaming, we only get one line
|
|
61
|
+
const lines = data.trim().split('\n');
|
|
62
|
+
const response = JSON.parse(lines[lines.length - 1]);
|
|
63
|
+
resolve(response);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
reject(new Error(`Failed to parse Ollama response: ${error.message}`));
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
reject(new Error(`Ollama API error: ${res.statusCode} - ${data}`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
req.on('error', (error) => {
|
|
74
|
+
reject(new Error(`Ollama connection failed: ${error.message}`));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.setTimeout(timeout, () => {
|
|
78
|
+
req.destroy();
|
|
79
|
+
reject(new Error(`Ollama request timeout (${timeout}ms)`));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
req.write(postData);
|
|
83
|
+
req.end();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate text with EXAONE 3.5
|
|
89
|
+
*
|
|
90
|
+
* Task 9.3: Implement EXAONE wrapper (with fallback to Gemma)
|
|
91
|
+
* AC #1: Decision detection LLM ready
|
|
92
|
+
*
|
|
93
|
+
* @param {string} prompt - Input prompt
|
|
94
|
+
* @param {Object} options - Generation options
|
|
95
|
+
* @param {string} options.model - Model name (default: EXAONE 3.5)
|
|
96
|
+
* @param {string} options.format - Response format ('json' or null)
|
|
97
|
+
* @param {number} options.temperature - Temperature (default: 0.7)
|
|
98
|
+
* @param {number} options.max_tokens - Max tokens (default: 500)
|
|
99
|
+
* @returns {Promise<string|Object>} Generated text or JSON object
|
|
100
|
+
* @throws {Error} If generation fails
|
|
101
|
+
*/
|
|
102
|
+
async function generate(prompt, options = {}) {
|
|
103
|
+
const { model = DEFAULT_MODEL, format = null, temperature = 0.7, max_tokens = 500 } = options;
|
|
104
|
+
|
|
105
|
+
const payload = {
|
|
106
|
+
model,
|
|
107
|
+
prompt,
|
|
108
|
+
stream: false,
|
|
109
|
+
options: {
|
|
110
|
+
temperature,
|
|
111
|
+
num_predict: max_tokens,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (format === 'json') {
|
|
116
|
+
payload.format = 'json';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await callOllamaAPI('/api/generate', payload);
|
|
121
|
+
|
|
122
|
+
// Extract response text
|
|
123
|
+
const responseText = response.response;
|
|
124
|
+
|
|
125
|
+
// Parse JSON if requested
|
|
126
|
+
if (format === 'json') {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(responseText);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`Failed to parse JSON response: ${responseText}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return responseText;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Task 9.4: Try fallback model if EXAONE fails
|
|
137
|
+
// Match Ollama's specific "model 'xxx' not found" error pattern
|
|
138
|
+
const isModelNotFound =
|
|
139
|
+
/model ['"].*['"] not found/i.test(error.message) ||
|
|
140
|
+
(error.message.includes('404') && error.message.toLowerCase().includes('not found'));
|
|
141
|
+
if (model === DEFAULT_MODEL && isModelNotFound) {
|
|
142
|
+
console.warn(`[MAMA] EXAONE not found, trying fallback (${FALLBACK_MODEL})...`);
|
|
143
|
+
|
|
144
|
+
return generate(prompt, {
|
|
145
|
+
...options,
|
|
146
|
+
model: FALLBACK_MODEL,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Analyze decision from tool execution
|
|
156
|
+
*
|
|
157
|
+
* Wrapper for decision detection (used in Story 014.7.3)
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} toolExecution - Tool execution data
|
|
160
|
+
* @param {Object} sessionContext - Session context
|
|
161
|
+
* @returns {Promise<Object>} Decision analysis result
|
|
162
|
+
*/
|
|
163
|
+
async function analyzeDecision(toolExecution, sessionContext) {
|
|
164
|
+
const prompt = `
|
|
165
|
+
Analyze if this represents a DECISION (not just an action):
|
|
166
|
+
|
|
167
|
+
Session Context:
|
|
168
|
+
- Latest User Message: ${sessionContext.latest_user_message || 'N/A'}
|
|
169
|
+
- Recent Exchange: ${sessionContext.recent_exchange || 'N/A'}
|
|
170
|
+
|
|
171
|
+
Tool Execution:
|
|
172
|
+
- Tool: ${toolExecution.tool_name}
|
|
173
|
+
- Input: ${JSON.stringify(toolExecution.tool_input)}
|
|
174
|
+
- Result: ${toolExecution.exit_code === 0 ? 'SUCCESS' : 'FAILED'}
|
|
175
|
+
|
|
176
|
+
Decision Indicators:
|
|
177
|
+
1. User explicitly chose between alternatives?
|
|
178
|
+
Example: "Let's use JWT" (not "Use JWT" - that's just action)
|
|
179
|
+
|
|
180
|
+
2. User changed previous approach?
|
|
181
|
+
Example: "Complex → Simple approach"
|
|
182
|
+
|
|
183
|
+
3. User expressed preference?
|
|
184
|
+
Example: "Let's do it this way from now", "This approach is better"
|
|
185
|
+
|
|
186
|
+
4. Significant architectural choice?
|
|
187
|
+
Example: "Mesh structure: COMPLEX", "Authentication: JWT"
|
|
188
|
+
|
|
189
|
+
Is this a DECISION? Return JSON with "topic" as a short snake_case identifier:
|
|
190
|
+
{
|
|
191
|
+
"is_decision": boolean,
|
|
192
|
+
"topic": string or null (extract main technical topic in snake_case, e.g., "mesh_structure", "database_choice", "auth_strategy"),
|
|
193
|
+
"decision": string or null (the actual choice made, e.g., "COMPLEX", "PostgreSQL", "JWT"),
|
|
194
|
+
"reasoning": "Why this is/isn't a decision",
|
|
195
|
+
"confidence": 0.0-1.0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
IMPORTANT: Generate "topic" freely based on context. Do NOT limit to predefined values.
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const response = await generate(prompt, {
|
|
203
|
+
format: 'json',
|
|
204
|
+
temperature: 0.3, // Lower temperature for structured output
|
|
205
|
+
max_tokens: 300,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return response;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// CLAUDE.md Rule #1: NO FALLBACK
|
|
211
|
+
// Errors must be thrown for debugging
|
|
212
|
+
logError(`[MAMA] Decision analysis FAILED: ${error.message}`);
|
|
213
|
+
throw new Error(`Decision analysis failed: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Analyze query intent
|
|
219
|
+
*
|
|
220
|
+
* Wrapper for query intent detection (used in Story 014.7.2)
|
|
221
|
+
*
|
|
222
|
+
* @param {string} userMessage - User's message
|
|
223
|
+
* @returns {Promise<Object>} Query intent analysis
|
|
224
|
+
*/
|
|
225
|
+
async function analyzeQueryIntent(userMessage) {
|
|
226
|
+
const prompt = `
|
|
227
|
+
Analyze this user message to determine if it involves past decisions:
|
|
228
|
+
|
|
229
|
+
User Message: "${userMessage}"
|
|
230
|
+
|
|
231
|
+
Questions to answer:
|
|
232
|
+
1. Does this query reference past decisions or choices?
|
|
233
|
+
2. Is the user asking about previous approaches?
|
|
234
|
+
3. What topic is being discussed? (e.g., "mesh_structure", "authentication", "testing")
|
|
235
|
+
|
|
236
|
+
Return JSON:
|
|
237
|
+
{
|
|
238
|
+
"involves_decision": boolean,
|
|
239
|
+
"topic": "topic_name" | null,
|
|
240
|
+
"query_type": "recall" | "evolution" | "none",
|
|
241
|
+
"reasoning": "Why this involves/doesn't involve decisions"
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await generate(prompt, {
|
|
247
|
+
format: 'json',
|
|
248
|
+
temperature: 0.3,
|
|
249
|
+
max_tokens: 200,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return response;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// CLAUDE.md Rule #1: NO FALLBACK
|
|
255
|
+
// Errors must be thrown for debugging
|
|
256
|
+
logError(`[MAMA] Query intent analysis FAILED: ${error.message}`);
|
|
257
|
+
throw new Error(`Query intent analysis failed: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if Ollama is available
|
|
263
|
+
*
|
|
264
|
+
* Utility for health checks
|
|
265
|
+
*
|
|
266
|
+
* @returns {Promise<boolean>} True if Ollama is accessible
|
|
267
|
+
*/
|
|
268
|
+
async function isAvailable() {
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
const options = {
|
|
271
|
+
hostname: OLLAMA_HOST,
|
|
272
|
+
port: OLLAMA_PORT,
|
|
273
|
+
path: '/api/tags',
|
|
274
|
+
method: 'GET', // Use GET for health check
|
|
275
|
+
timeout: 2000,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const req = http.request(options, (res) => {
|
|
279
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
req.on('error', () => resolve(false));
|
|
283
|
+
req.on('timeout', () => {
|
|
284
|
+
req.destroy();
|
|
285
|
+
resolve(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
req.end();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* List available models
|
|
294
|
+
*
|
|
295
|
+
* Utility for setup script
|
|
296
|
+
*
|
|
297
|
+
* @returns {Promise<Array<string>>} Array of model names
|
|
298
|
+
*/
|
|
299
|
+
async function listModels() {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const options = {
|
|
302
|
+
hostname: OLLAMA_HOST,
|
|
303
|
+
port: OLLAMA_PORT,
|
|
304
|
+
path: '/api/tags',
|
|
305
|
+
method: 'GET', // Use GET for listing models
|
|
306
|
+
timeout: 5000,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const req = http.request(options, (res) => {
|
|
310
|
+
let data = '';
|
|
311
|
+
|
|
312
|
+
res.on('data', (chunk) => {
|
|
313
|
+
data += chunk;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
res.on('end', () => {
|
|
317
|
+
try {
|
|
318
|
+
const response = JSON.parse(data);
|
|
319
|
+
resolve(response.models?.map((m) => m.name) || []);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
reject(new Error(`Failed to parse models response: ${error.message}`));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
req.on('error', (error) => {
|
|
327
|
+
reject(new Error(`Failed to list models: ${error.message}`));
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
req.on('timeout', () => {
|
|
331
|
+
req.destroy();
|
|
332
|
+
reject(new Error('List models timeout'));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
req.end();
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Export API
|
|
340
|
+
module.exports = {
|
|
341
|
+
generate,
|
|
342
|
+
analyzeDecision,
|
|
343
|
+
analyzeQueryIntent,
|
|
344
|
+
isAvailable,
|
|
345
|
+
listModels,
|
|
346
|
+
DEFAULT_MODEL,
|
|
347
|
+
FALLBACK_MODEL,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// CLI execution for testing
|
|
351
|
+
if (require.main === module) {
|
|
352
|
+
info('🧠 MAMA Ollama Client - Test\n');
|
|
353
|
+
|
|
354
|
+
// Task 9.5: Test Ollama connection and generation
|
|
355
|
+
(async () => {
|
|
356
|
+
try {
|
|
357
|
+
info('📋 Test 1: Check Ollama availability...');
|
|
358
|
+
const available = await isAvailable();
|
|
359
|
+
if (!available) {
|
|
360
|
+
throw new Error('Ollama is not available');
|
|
361
|
+
}
|
|
362
|
+
info('✅ Ollama is available\n');
|
|
363
|
+
|
|
364
|
+
info('📋 Test 2: List available models...');
|
|
365
|
+
const models = await listModels();
|
|
366
|
+
info(`✅ Found ${models.length} models:`, models.join(', '), '\n');
|
|
367
|
+
|
|
368
|
+
info('📋 Test 3: Generate text...');
|
|
369
|
+
const text = await generate('What is 2+2?', {
|
|
370
|
+
temperature: 0.1,
|
|
371
|
+
max_tokens: 50,
|
|
372
|
+
});
|
|
373
|
+
info(`✅ Generated: ${text.trim()}\n`);
|
|
374
|
+
|
|
375
|
+
info('📋 Test 4: Generate JSON...');
|
|
376
|
+
const json = await generate('Return {"test": true, "value": 42}', {
|
|
377
|
+
format: 'json',
|
|
378
|
+
temperature: 0.1,
|
|
379
|
+
max_tokens: 50,
|
|
380
|
+
});
|
|
381
|
+
info('✅ Generated JSON:', json, '\n');
|
|
382
|
+
|
|
383
|
+
info('═══════════════════════════');
|
|
384
|
+
info('✅ All tests passed!');
|
|
385
|
+
info('═══════════════════════════');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
logError(`❌ Test failed: ${error.message}`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
})();
|
|
391
|
+
}
|