kachow 0.1.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/README.md +77 -0
- package/_server/dist/app.js +130 -0
- package/_server/dist/db/index.js +50 -0
- package/_server/dist/db/schema.js +247 -0
- package/_server/dist/queues/ingestQueue.js +49 -0
- package/_server/dist/queues/redis.js +58 -0
- package/_server/dist/routes/agents.js +162 -0
- package/_server/dist/routes/architecture.js +88 -0
- package/_server/dist/routes/config.js +24 -0
- package/_server/dist/routes/github.js +158 -0
- package/_server/dist/routes/graph.js +112 -0
- package/_server/dist/routes/healing.js +137 -0
- package/_server/dist/routes/impact.js +100 -0
- package/_server/dist/routes/ingest.js +182 -0
- package/_server/dist/routes/manager.js +179 -0
- package/_server/dist/routes/notifications.js +85 -0
- package/_server/dist/routes/qa.js +68 -0
- package/_server/dist/routes/scanner.js +221 -0
- package/_server/dist/routes/stream.js +179 -0
- package/_server/dist/routes/webhooks.js +168 -0
- package/_server/dist/server.js +46 -0
- package/_server/dist/services/agentService.js +715 -0
- package/_server/dist/services/architectureService.js +172 -0
- package/_server/dist/services/demoSeed.js +181 -0
- package/_server/dist/services/graphLayout.js +102 -0
- package/_server/dist/services/graphService.js +532 -0
- package/_server/dist/services/healingService.js +253 -0
- package/_server/dist/services/impactService.js +304 -0
- package/_server/dist/services/ingestService.js +129 -0
- package/_server/dist/services/managerService.js +260 -0
- package/_server/dist/services/notificationService.js +283 -0
- package/_server/dist/services/qaService.js +413 -0
- package/_server/dist/services/scannerService.js +748 -0
- package/_server/dist/services/seedService.js +215 -0
- package/_server/dist/sse/sseManager.js +101 -0
- package/_server/dist/types/index.js +38 -0
- package/_server/dist/workers/ingestWorker.js +274 -0
- package/_server/public/assets/index-BTkbB_YF.js +4546 -0
- package/_server/public/assets/index-Bmh3jWBm.css +1 -0
- package/_server/public/favicon.ico +0 -0
- package/_server/public/images/glass-waves-bg.png +0 -0
- package/_server/public/index.html +29 -0
- package/_server/public/placeholder.svg +1 -0
- package/_server/public/robots.txt +14 -0
- package/dist/config.js +133 -0
- package/dist/index.js +510 -0
- package/dist/setup.js +223 -0
- package/package.json +62 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Q&A Engine — Phase 3.
|
|
4
|
+
* Graph-aware RAG using OpenAI GPT-4o.
|
|
5
|
+
* Builds rich context from the knowledge graph, queries OpenAI,
|
|
6
|
+
* and returns structured answers with file:line citations.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.queryKnowledgeGraph = queryKnowledgeGraph;
|
|
13
|
+
exports.getQAHistory = getQAHistory;
|
|
14
|
+
exports.getQASuggestions = getQASuggestions;
|
|
15
|
+
const openai_1 = __importDefault(require("openai"));
|
|
16
|
+
const crypto_1 = require("crypto");
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const index_js_1 = require("../db/index.js");
|
|
20
|
+
const graphService_js_1 = require("./graphService.js");
|
|
21
|
+
// ── File-tree helpers ─────────────────────────────────────────────────────────
|
|
22
|
+
/** Walk a directory and return relative paths of source files (max 200). */
|
|
23
|
+
function walkSourceFiles(dir, base = dir, results = []) {
|
|
24
|
+
if (results.length >= 200)
|
|
25
|
+
return results;
|
|
26
|
+
if (!fs_1.default.existsSync(dir))
|
|
27
|
+
return results;
|
|
28
|
+
for (const entry of fs_1.default.readdirSync(dir)) {
|
|
29
|
+
if (['node_modules', '.git', 'dist', 'build', '.next'].includes(entry))
|
|
30
|
+
continue;
|
|
31
|
+
const full = path_1.default.join(dir, entry);
|
|
32
|
+
const rel = path_1.default.relative(base, full);
|
|
33
|
+
try {
|
|
34
|
+
if (fs_1.default.statSync(full).isDirectory()) {
|
|
35
|
+
walkSourceFiles(full, base, results);
|
|
36
|
+
}
|
|
37
|
+
else if (/\.(ts|tsx|js|jsx|py|go|java)$/.test(entry)) {
|
|
38
|
+
results.push(rel);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { /* skip */ }
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Given a GPT-hallucinated file path (e.g. "src/routes.ts") and the real list
|
|
47
|
+
* of files in the service directory, return the closest real match.
|
|
48
|
+
* Strategy: exact → basename match → longest common-suffix match.
|
|
49
|
+
*/
|
|
50
|
+
function bestFileMatch(hallucinated, realFiles) {
|
|
51
|
+
if (realFiles.includes(hallucinated))
|
|
52
|
+
return hallucinated;
|
|
53
|
+
const base = path_1.default.basename(hallucinated); // e.g. "routes.ts"
|
|
54
|
+
const baseName = base.replace(/\.[^.]+$/, ''); // e.g. "routes"
|
|
55
|
+
const ext = path_1.default.extname(base); // e.g. ".ts"
|
|
56
|
+
// 1. Exact basename match
|
|
57
|
+
const exactBase = realFiles.filter(f => path_1.default.basename(f) === base);
|
|
58
|
+
if (exactBase.length === 1)
|
|
59
|
+
return exactBase[0];
|
|
60
|
+
// 2. Same name, any extension (e.g. routes.js → routes.ts)
|
|
61
|
+
const sameNameAnyExt = realFiles.filter(f => path_1.default.basename(f, path_1.default.extname(f)) === baseName);
|
|
62
|
+
if (sameNameAnyExt.length === 1)
|
|
63
|
+
return sameNameAnyExt[0];
|
|
64
|
+
// 3. Filename contains the base name (e.g. "routes" → "routes/blog.ts")
|
|
65
|
+
const contains = realFiles.filter(f => {
|
|
66
|
+
const parts = f.split('/');
|
|
67
|
+
return parts.some(p => p.replace(/\.[^.]+$/, '') === baseName);
|
|
68
|
+
});
|
|
69
|
+
if (contains.length >= 1)
|
|
70
|
+
return contains[0];
|
|
71
|
+
// 4. Any file with matching extension, prefer shortest path
|
|
72
|
+
const sameExt = realFiles.filter(f => path_1.default.extname(f) === ext)
|
|
73
|
+
.sort((a, b) => a.length - b.length);
|
|
74
|
+
return sameExt[0] ?? hallucinated;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds a rich textual context from the current knowledge graph.
|
|
78
|
+
* Includes service health, dependencies, endpoints, incidents, and recent activity.
|
|
79
|
+
*/
|
|
80
|
+
function buildGraphContext() {
|
|
81
|
+
const nodes = (0, graphService_js_1.getNodes)();
|
|
82
|
+
const edges = (0, graphService_js_1.getEdges)();
|
|
83
|
+
const health = (0, graphService_js_1.getSystemHealth)();
|
|
84
|
+
const db = (0, index_js_1.getDb)();
|
|
85
|
+
const serviceLines = nodes.map(n => {
|
|
86
|
+
const outEdges = edges.filter(e => e.sourceId === n.id);
|
|
87
|
+
const inEdges = edges.filter(e => e.targetId === n.id);
|
|
88
|
+
const incidents = db.prepare(`
|
|
89
|
+
SELECT severity, title FROM incidents
|
|
90
|
+
WHERE service_id = ? AND status = 'open'
|
|
91
|
+
ORDER BY occurred_at DESC LIMIT 3
|
|
92
|
+
`).all(n.id);
|
|
93
|
+
const endpoints = db.prepare(`
|
|
94
|
+
SELECT method, path FROM endpoints WHERE service_id = ? LIMIT 8
|
|
95
|
+
`).all(n.id);
|
|
96
|
+
const allNodes = nodes;
|
|
97
|
+
const calls = outEdges.map(e => {
|
|
98
|
+
const target = allNodes.find(x => x.id === e.targetId);
|
|
99
|
+
const ep = e.endpoints.slice(0, 2).join(', ');
|
|
100
|
+
return `→ ${target?.name ?? e.targetId} via ${e.type}${ep ? ` [${ep}]` : ''}`;
|
|
101
|
+
});
|
|
102
|
+
const calledBy = inEdges.map(e => {
|
|
103
|
+
const src = allNodes.find(x => x.id === e.sourceId);
|
|
104
|
+
return `← ${src?.name ?? e.sourceId} (${e.type})`;
|
|
105
|
+
});
|
|
106
|
+
const incidentStr = incidents.map(i => `[${i.severity}] ${i.title}`).join('; ') || 'none';
|
|
107
|
+
const epStr = endpoints.map(e => `${e.method} ${e.path}`).join(', ') || 'none';
|
|
108
|
+
const svcRow = db.prepare(`SELECT repo_url, repo_subpath FROM services WHERE id = ? LIMIT 1`).get(n.id);
|
|
109
|
+
// Build real file list for this service
|
|
110
|
+
let fileListStr = '';
|
|
111
|
+
if (svcRow?.repo_url) {
|
|
112
|
+
// Reconstruct local path from repo_url and repo_subpath
|
|
113
|
+
// We stored repoPath in the ingest job but not in DB — infer from /tmp or known prefixes
|
|
114
|
+
const knownBases = ['/tmp', '/Users'];
|
|
115
|
+
const repoName = svcRow.repo_url.split('/').pop() ?? '';
|
|
116
|
+
let serviceDir = null;
|
|
117
|
+
for (const base of knownBases) {
|
|
118
|
+
const candidate = svcRow.repo_subpath
|
|
119
|
+
? path_1.default.join(base, repoName, svcRow.repo_subpath)
|
|
120
|
+
: path_1.default.join(base, repoName);
|
|
121
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
122
|
+
serviceDir = candidate;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
// Also try under /tmp directly
|
|
126
|
+
const candidate2 = svcRow.repo_subpath
|
|
127
|
+
? path_1.default.join('/tmp', `${repoName}*`, svcRow.repo_subpath)
|
|
128
|
+
: null;
|
|
129
|
+
if (candidate2 && fs_1.default.existsSync(candidate2)) {
|
|
130
|
+
serviceDir = candidate2;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Brute-force find in /tmp if name matches
|
|
135
|
+
if (!serviceDir) {
|
|
136
|
+
try {
|
|
137
|
+
const tmpDirs = fs_1.default.readdirSync('/tmp').filter(d => d.startsWith(repoName.replace(/[-_]/g, '-').slice(0, 12)));
|
|
138
|
+
for (const d of tmpDirs) {
|
|
139
|
+
const p2 = svcRow.repo_subpath ? path_1.default.join('/tmp', d, svcRow.repo_subpath) : path_1.default.join('/tmp', d);
|
|
140
|
+
if (fs_1.default.existsSync(p2)) {
|
|
141
|
+
serviceDir = p2;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { /* ignore */ }
|
|
147
|
+
}
|
|
148
|
+
if (serviceDir) {
|
|
149
|
+
const files = walkSourceFiles(serviceDir).slice(0, 30);
|
|
150
|
+
if (files.length)
|
|
151
|
+
fileListStr = `\n Source files: ${files.join(', ')}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return [
|
|
155
|
+
`SERVICE: ${n.name}`,
|
|
156
|
+
` id=${n.id} | health=${n.healthScore}/100 (${n.healthTier}) | lang=${n.language ?? 'unknown'} | team=${n.team ?? 'unassigned'}`,
|
|
157
|
+
` Endpoints: ${epStr}`,
|
|
158
|
+
` Calls: ${calls.join(', ') || 'none'}`,
|
|
159
|
+
` Called by: ${calledBy.join(', ') || 'none'}`,
|
|
160
|
+
` Open incidents: ${incidentStr}`,
|
|
161
|
+
fileListStr,
|
|
162
|
+
].filter(Boolean).join('\n');
|
|
163
|
+
}).join('\n\n');
|
|
164
|
+
const recentActivity = db.prepare(`
|
|
165
|
+
SELECT type, title, detail, created_at FROM activity_feed
|
|
166
|
+
ORDER BY created_at DESC LIMIT 15
|
|
167
|
+
`).all();
|
|
168
|
+
const activityStr = recentActivity
|
|
169
|
+
.map(a => `[${a.created_at}] ${a.type}: ${a.title}${a.detail ? ` — ${a.detail}` : ''}`)
|
|
170
|
+
.join('\n');
|
|
171
|
+
return [
|
|
172
|
+
'=== SYSTEM HEALTH ===',
|
|
173
|
+
`Overall: ${health.overallScore}/100 | Services: ${health.totalServices} | Edges: ${health.totalEdges}`,
|
|
174
|
+
`Critical: ${health.critical} | Warning: ${health.warning} | Healthy: ${health.healthy}`,
|
|
175
|
+
'',
|
|
176
|
+
'=== SERVICES ===',
|
|
177
|
+
serviceLines || 'No services ingested yet.',
|
|
178
|
+
'',
|
|
179
|
+
'=== RECENT ACTIVITY ===',
|
|
180
|
+
activityStr || 'No recent activity.',
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
// ── Main query function ───────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* Queries the knowledge graph using OpenAI GPT-4o.
|
|
186
|
+
* Builds graph context, constructs prompt, and returns structured answer with citations.
|
|
187
|
+
*
|
|
188
|
+
* @param question - Natural language question from the engineer
|
|
189
|
+
* @returns QAResult with answer, citations, related nodes, and follow-up suggestions
|
|
190
|
+
*/
|
|
191
|
+
async function queryKnowledgeGraph(question) {
|
|
192
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
193
|
+
if (!apiKey) {
|
|
194
|
+
throw new Error('OPENAI_API_KEY not set. Add it to .env to use Q&A.');
|
|
195
|
+
}
|
|
196
|
+
const openai = new openai_1.default({ apiKey });
|
|
197
|
+
const context = buildGraphContext();
|
|
198
|
+
const db = (0, index_js_1.getDb)();
|
|
199
|
+
const prompt = `You are KA-CHOW, an AI assistant embedded in a living knowledge graph for engineering teams.
|
|
200
|
+
You have real-time visibility into this system's microservice architecture.
|
|
201
|
+
|
|
202
|
+
${context}
|
|
203
|
+
|
|
204
|
+
Answer the engineer's question with precision. Reference actual service names, health scores, and dependency chains from the context above.
|
|
205
|
+
|
|
206
|
+
RESPOND WITH ONLY VALID JSON — no markdown, no explanation outside the JSON:
|
|
207
|
+
{
|
|
208
|
+
"answer": "3-5 sentence answer referencing specific services by name with concrete details",
|
|
209
|
+
"citations": [
|
|
210
|
+
{
|
|
211
|
+
"file": "path/from/service-name/realistic-filename.ts",
|
|
212
|
+
"line": <realistic line number 1-300>,
|
|
213
|
+
"snippet": "relevant code pattern or config that supports the answer"
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
"relatedNodes": ["exact-service-id-from-context"],
|
|
217
|
+
"suggestions": ["Follow-up question 1?", "Follow-up question 2?", "Follow-up question 3?"]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Rules:
|
|
221
|
+
- citations: use ONLY the exact file paths listed under "Source files" for that service (e.g. if source files lists "src/routes/blog.ts" use "blog-service/src/routes/blog.ts"). Do NOT invent paths.
|
|
222
|
+
- relatedNodes: use EXACT service id strings from the SERVICE lines above (e.g. "def53e95f1fc")
|
|
223
|
+
- suggestions: 3 highly relevant follow-up questions
|
|
224
|
+
- Be specific and actionable. Do not make up metrics not in the context.
|
|
225
|
+
|
|
226
|
+
Engineer's question: ${question}`;
|
|
227
|
+
const completion = await openai.chat.completions.create({
|
|
228
|
+
model: 'gpt-4o',
|
|
229
|
+
messages: [{ role: 'user', content: prompt }],
|
|
230
|
+
response_format: { type: 'json_object' },
|
|
231
|
+
temperature: 0.3,
|
|
232
|
+
max_tokens: 1500,
|
|
233
|
+
});
|
|
234
|
+
const raw = completion.choices[0]?.message?.content ?? '{}';
|
|
235
|
+
let parsed = {};
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(raw);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
parsed = { answer: raw };
|
|
241
|
+
}
|
|
242
|
+
const rawCitations = (parsed.citations ?? []).slice(0, 5);
|
|
243
|
+
// ── Resolve citations: correct hallucinated paths + build URLs ─────────────
|
|
244
|
+
//
|
|
245
|
+
// Strategy:
|
|
246
|
+
// 1. Find the service row by name/id so we have repo_url + repo_subpath
|
|
247
|
+
// 2. Reconstruct the local directory by scanning known clone locations
|
|
248
|
+
// 3. Fuzzy-match the GPT-hallucinated file path against real files on disk
|
|
249
|
+
// 4. Build a correct GitHub URL using repo_url + repo_subpath (relative)
|
|
250
|
+
// 5. Also build a vscode:// URL so the frontend can open the file locally
|
|
251
|
+
/** Try to find the local clone directory for a given repo_url + repo_subpath */
|
|
252
|
+
function findLocalServiceDir(repoUrl, subpath) {
|
|
253
|
+
if (!repoUrl)
|
|
254
|
+
return null;
|
|
255
|
+
const repoName = repoUrl.split('/').pop() ?? '';
|
|
256
|
+
// Check common clone locations
|
|
257
|
+
const baseDirs = ['/tmp', '/private/tmp', '/Users'];
|
|
258
|
+
for (const base of baseDirs) {
|
|
259
|
+
try {
|
|
260
|
+
if (base === '/Users') {
|
|
261
|
+
// Don't walk all of /Users — skip for now
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const entries = fs_1.default.readdirSync(base);
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
if (!entry.toLowerCase().includes(repoName.toLowerCase().slice(0, 8)))
|
|
267
|
+
continue;
|
|
268
|
+
const repoRoot = path_1.default.join(base, entry);
|
|
269
|
+
const candidate = subpath ? path_1.default.join(repoRoot, subpath) : repoRoot;
|
|
270
|
+
if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isDirectory()) {
|
|
271
|
+
return candidate;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch { /* skip */ }
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const citations = rawCitations.map((cite) => {
|
|
280
|
+
const parts = cite.file.split('/');
|
|
281
|
+
let serviceName = parts[0];
|
|
282
|
+
let gptFilePath = parts.slice(1).join('/');
|
|
283
|
+
// Find the service row by name/id
|
|
284
|
+
const candidates = [
|
|
285
|
+
serviceName,
|
|
286
|
+
serviceName.replace(/[-_]service$/i, ''),
|
|
287
|
+
serviceName.replace(/[-_]svc$/i, ''),
|
|
288
|
+
];
|
|
289
|
+
let row;
|
|
290
|
+
for (const c of candidates) {
|
|
291
|
+
row = db.prepare(`SELECT repo_url, repo_subpath, name FROM services WHERE name = ? OR id = ? LIMIT 1`).get(c, c);
|
|
292
|
+
if (row)
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if (!row) {
|
|
296
|
+
row = db.prepare(`SELECT repo_url, repo_subpath, name FROM services WHERE name LIKE ? LIMIT 1`).get(`%${serviceName.replace(/[-_]service$/i, '')}%`);
|
|
297
|
+
}
|
|
298
|
+
// Fallback: GPT returned a bare path without service prefix (e.g. "src/vesper_ingestion/db/repo.py")
|
|
299
|
+
// Try matching path segments against service names or repo_subpaths
|
|
300
|
+
if (!row) {
|
|
301
|
+
const allSvcs = db.prepare(`SELECT repo_url, repo_subpath, name FROM services WHERE is_external = 0`).all();
|
|
302
|
+
const fullPath = cite.file;
|
|
303
|
+
for (const svc of allSvcs) {
|
|
304
|
+
// Check if repo_subpath appears in the file path
|
|
305
|
+
if (svc.repo_subpath && fullPath.includes(svc.repo_subpath.split('/').pop() ?? '___')) {
|
|
306
|
+
row = svc;
|
|
307
|
+
// The gptFilePath in this case IS the full path (since no service prefix matched)
|
|
308
|
+
gptFilePath = fullPath;
|
|
309
|
+
serviceName = svc.name;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
// Check if service name appears in the file path
|
|
313
|
+
if (fullPath.toLowerCase().includes(svc.name.toLowerCase().replace(/-/g, '_'))) {
|
|
314
|
+
row = svc;
|
|
315
|
+
gptFilePath = fullPath;
|
|
316
|
+
serviceName = svc.name;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Fuzzy-correct the file path against real files on disk
|
|
322
|
+
let correctedFilePath = gptFilePath;
|
|
323
|
+
const serviceDir = findLocalServiceDir(row?.repo_url ?? null, row?.repo_subpath ?? null);
|
|
324
|
+
if (serviceDir) {
|
|
325
|
+
const realFiles = walkSourceFiles(serviceDir);
|
|
326
|
+
correctedFilePath = bestFileMatch(gptFilePath, realFiles);
|
|
327
|
+
}
|
|
328
|
+
// Build the repo-relative file path for GitHub URL
|
|
329
|
+
// repo_subpath is now the correct relative path (e.g. "frontends/ops-ui")
|
|
330
|
+
const repoRelativePath = row?.repo_subpath
|
|
331
|
+
? `${row.repo_subpath}/${correctedFilePath}`
|
|
332
|
+
: correctedFilePath;
|
|
333
|
+
// GitHub URL: https://github.com/Org/Repo/blob/main/frontends/ops-ui/components/X.tsx#L45
|
|
334
|
+
const url = row?.repo_url && correctedFilePath
|
|
335
|
+
? `${row.repo_url.replace(/\/+$/, '')}/blob/main/${repoRelativePath}#L${cite.line}`
|
|
336
|
+
: null;
|
|
337
|
+
// Update cite.file to reflect the corrected path
|
|
338
|
+
const correctedCiteFile = `${serviceName}/${correctedFilePath}`;
|
|
339
|
+
return { ...cite, file: correctedCiteFile, url };
|
|
340
|
+
});
|
|
341
|
+
const result = {
|
|
342
|
+
id: (0, crypto_1.randomUUID)(),
|
|
343
|
+
question,
|
|
344
|
+
answer: parsed.answer ?? 'Unable to generate answer. Please try again.',
|
|
345
|
+
citations,
|
|
346
|
+
relatedNodes: parsed.relatedNodes ?? [],
|
|
347
|
+
suggestions: (parsed.suggestions ?? []).slice(0, 3),
|
|
348
|
+
};
|
|
349
|
+
// Persist to history
|
|
350
|
+
db.prepare(`
|
|
351
|
+
INSERT INTO qa_sessions (id, question, answer, citations, related_nodes)
|
|
352
|
+
VALUES (?, ?, ?, ?, ?)
|
|
353
|
+
`).run(result.id, result.question, result.answer, JSON.stringify(result.citations), JSON.stringify(result.relatedNodes));
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
// ── History ───────────────────────────────────────────────────────────────────
|
|
357
|
+
/**
|
|
358
|
+
* Returns the last 20 Q&A interactions from the database.
|
|
359
|
+
*/
|
|
360
|
+
function getQAHistory() {
|
|
361
|
+
const db = (0, index_js_1.getDb)();
|
|
362
|
+
const rows = db.prepare(`
|
|
363
|
+
SELECT id, question, answer, citations, related_nodes, created_at
|
|
364
|
+
FROM qa_sessions ORDER BY created_at DESC LIMIT 20
|
|
365
|
+
`).all();
|
|
366
|
+
return rows.map(r => ({
|
|
367
|
+
id: r.id,
|
|
368
|
+
question: r.question,
|
|
369
|
+
answer: r.answer,
|
|
370
|
+
citations: (() => { try {
|
|
371
|
+
return JSON.parse(r.citations);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return [];
|
|
375
|
+
} })(),
|
|
376
|
+
relatedNodes: (() => { try {
|
|
377
|
+
return JSON.parse(r.related_nodes);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return [];
|
|
381
|
+
} })(),
|
|
382
|
+
createdAt: r.created_at,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
// ── Suggestions ───────────────────────────────────────────────────────────────
|
|
386
|
+
/**
|
|
387
|
+
* Returns dynamic Q&A suggestions based on the current graph state.
|
|
388
|
+
* Surfaces questions relevant to unhealthy services and detected patterns.
|
|
389
|
+
*/
|
|
390
|
+
function getQASuggestions() {
|
|
391
|
+
const db = (0, index_js_1.getDb)();
|
|
392
|
+
const nodes = db.prepare(`
|
|
393
|
+
SELECT name, health_tier, team FROM services
|
|
394
|
+
WHERE is_external = 0 ORDER BY health_score ASC LIMIT 5
|
|
395
|
+
`).all();
|
|
396
|
+
const suggestions = [];
|
|
397
|
+
for (const n of nodes) {
|
|
398
|
+
if (n.health_tier === 'critical') {
|
|
399
|
+
suggestions.push(`Why is ${n.name} in critical state?`);
|
|
400
|
+
}
|
|
401
|
+
else if (n.health_tier === 'warning') {
|
|
402
|
+
suggestions.push(`What's causing the warning in ${n.name}?`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Generic high-value questions
|
|
406
|
+
suggestions.push('Which service has the highest blast radius if it goes down?');
|
|
407
|
+
suggestions.push('What dependencies could trigger a cascade failure?');
|
|
408
|
+
suggestions.push('Which services are missing error handling?');
|
|
409
|
+
suggestions.push('What is the current overall system risk?');
|
|
410
|
+
// Deduplicate and limit
|
|
411
|
+
return [...new Set(suggestions)].slice(0, 5);
|
|
412
|
+
}
|
|
413
|
+
//# sourceMappingURL=qaService.js.map
|