Upgrade request analysis routing

This commit is contained in:
Andraxion 2026-06-27 01:44:11 -04:00
parent 1cd446bae8
commit db3e080640
19 changed files with 1520 additions and 66 deletions

263
server.js
View file

@ -101,6 +101,9 @@ const editorSettingsPath = path.join(dataRoot, "editor_settings.json");
const launcherRequestsPath = path.join(dataRoot, "launcher_requests.json");
const requestAnalysisWorkerScriptPath = path.join(__dirname, "scripts", "request-analysis-worker.mjs");
const requestTagCatalogPath = path.join(__dirname, "docs", "kb", "tags.json");
const kbSystemsIndexPath = path.join(__dirname, "docs", "kb", "systems.json");
const kbModulesIndexPath = path.join(__dirname, "docs", "kb", "modules.json");
const kbTerminologyPath = path.join(__dirname, "docs", "kb", "terminology.json");
const requestAnalysisRunState = {
child: null,
restartTimer: null,
@ -492,6 +495,41 @@ function normalizeLauncherRequestAnalysisTags(value, fallback = []) {
return normalizeLauncherRequestTags(Array.isArray(fallback) ? fallback : []);
}
function normalizeLauncherRequestAnalysisRouting(value) {
const source = value && typeof value === "object" && !Array.isArray(value)
? value
: null;
if (!source) {
return undefined;
}
const ambiguityRaw = String(source.ambiguity || "").trim().toLowerCase();
const ambiguity = ambiguityRaw === "low" || ambiguityRaw === "medium" || ambiguityRaw === "high"
? ambiguityRaw
: "medium";
const summary = String(source.summary || source.routingSummary || "").trim();
const rationale = String(source.rationale || "").trim();
const matchedTerms = normalizeLauncherRequestAnalysisStringList(source.matchedTerms || source.terms);
const suggestedTags = normalizeLauncherRequestAnalysisTags(source.suggestedTags || source.tags);
const suggestedSystems = normalizeLauncherRequestAnalysisStringList(source.suggestedSystems || source.suggestedSystemIds);
const suggestedModules = normalizeLauncherRequestAnalysisStringList(source.suggestedModules || source.suggestedModuleIds);
const possibleDirections = normalizeLauncherRequestAnalysisStringList(source.possibleDirections);
const kbSections = normalizeLauncherRequestAnalysisStringList(source.kbSections);
if (!summary && !rationale && matchedTerms.length === 0 && suggestedTags.length === 0 && suggestedSystems.length === 0 && suggestedModules.length === 0 && possibleDirections.length === 0 && kbSections.length === 0) {
return undefined;
}
return {
summary: summary || undefined,
ambiguity,
matchedTerms,
suggestedTags,
suggestedSystems,
suggestedModules,
rationale: rationale || undefined,
possibleDirections,
kbSections,
};
}
function normalizeLauncherRequestAnalysisItem(item, index = 0) {
const source = item && typeof item === "object" && !Array.isArray(item)
? item
@ -567,7 +605,8 @@ function normalizeLauncherRequestAnalysis(value) {
const updatedAt = String(source.updatedAt || "").trim() || createdAt;
const submissionId = String(source.submissionId || "").trim();
const sourceTextSnapshot = String(source.sourceTextSnapshot || source.sourceText || "").trim();
if (!model && !error && !submissionId && !sourceTextSnapshot && normalizedItems.length === 0 && state === "unprocessed") {
const routing = normalizeLauncherRequestAnalysisRouting(source.routing);
if (!model && !error && !submissionId && !sourceTextSnapshot && normalizedItems.length === 0 && state === "unprocessed" && !routing) {
return undefined;
}
return {
@ -579,6 +618,7 @@ function normalizeLauncherRequestAnalysis(value) {
error: error || undefined,
submissionId: submissionId || undefined,
sourceTextSnapshot: sourceTextSnapshot || undefined,
routing,
itemCount: normalizedItems.length,
items: normalizedItems,
};
@ -698,10 +738,16 @@ function isLauncherRequestAutoQueueEligible(entry) {
&& (!analysisState || analysisState === "unprocessed");
}
function getQueuedPendingLauncherRequestCount() {
function getQueuedPendingLauncherRequestCount(requestId = "") {
try {
const payload = readLauncherRequestsPayload();
return payload.requests.filter((entry) => isLauncherRequestAutoQueueEligible(entry)).length;
const normalizedRequestId = String(requestId || "").trim();
return payload.requests.filter((entry) => {
if (!isLauncherRequestAutoQueueEligible(entry)) {
return false;
}
return !normalizedRequestId || String(entry?.id || "").trim() === normalizedRequestId;
}).length;
} catch {
return 0;
}
@ -734,13 +780,14 @@ function scheduleQueuedRequestAnalysis(reason = "queued-pending-requests", delay
return true;
}
function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
function launchQueuedRequestAnalysis(reason = "queued-pending-requests", options = {}) {
const requestId = String(options?.requestId || "").trim();
if (!isRequestAnalysisConfigured()) {
return { launched: false, reason: "request-analysis-not-configured" };
}
const queuedPendingCount = getQueuedPendingLauncherRequestCount();
const queuedPendingCount = getQueuedPendingLauncherRequestCount(requestId);
if (queuedPendingCount <= 0) {
return { launched: false, reason: "no-pending-requests" };
return { launched: false, reason: requestId ? "request-not-queued" : "no-pending-requests" };
}
if (requestAnalysisRunState.child && !requestAnalysisRunState.child.killed) {
return { launched: false, reason: "request-analysis-already-running" };
@ -755,6 +802,9 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
"--limit",
String(Math.max(1, Math.floor(Number(process.env.REQUEST_ANALYZER_AUTORUN_LIMIT) || 5))),
];
if (requestId) {
args.push("--request-id", requestId);
}
const child = spawn(process.execPath, args, {
cwd: __dirname,
env: {
@ -770,6 +820,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
provider,
reason,
queuedPendingCount,
requestId,
pid: child.pid,
});
child.stdout?.on("data", (chunk) => {
@ -792,6 +843,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
provider,
reason,
queuedPendingCount: getQueuedPendingLauncherRequestCount(),
requestId,
code: Number.isFinite(Number(code)) ? Number(code) : null,
signal: signal || "",
});
@ -806,6 +858,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
type: "launcher-request-analysis-launch-error",
provider,
reason,
requestId,
error: String(error),
});
});
@ -814,6 +867,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
launched: true,
reason,
queuedPendingCount,
requestId,
pid: child.pid,
};
}
@ -2240,6 +2294,33 @@ function loadRequestTagCatalog() {
return DEFAULT_REQUEST_TAG_DEFINITIONS;
}
function readKbIndexFile(filePath, fallbackKey) {
try {
const payload = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
return payload;
}
} catch {
// Ignore KB read failures and fall back to empty payloads.
}
return {
schemaVersion: 1,
[fallbackKey]: [],
};
}
function readKbSystemsIndex() {
return readKbIndexFile(kbSystemsIndexPath, "systems");
}
function readKbModulesIndex() {
return readKbIndexFile(kbModulesIndexPath, "modules");
}
function readKbTerminologyIndex() {
return readKbIndexFile(kbTerminologyPath, "terms");
}
function normalizeRequestTag(value) {
const normalizedValue = normalizeRequestTagLookupValue(value);
if (!normalizedValue) {
@ -3029,6 +3110,76 @@ app.post("/api/launcher-requests/:requestId/process-analysis", (req, res) => {
}
});
app.post("/api/launcher-requests/:requestId/requeue-analysis", (req, res) => {
const requestId = String(req.params.requestId || "").trim();
if (!requireLauncherAdminAccess(req, res)) {
return;
}
try {
const payload = readLauncherRequestsPayload();
const index = payload.requests.findIndex((entry) => entry.id === requestId);
if (index < 0) {
res.status(404).json({ error: "Request not found." });
return;
}
const existing = payload.requests[index];
if (existing.status !== "pending") {
res.status(400).json({ error: "Only pending requests can be re-reviewed." });
return;
}
const mode = String(req.body?.mode || "saved").trim().toLowerCase() === "draft" ? "draft" : "saved";
const now = new Date().toISOString();
const draftSource = mode === "draft" && req.body?.request && typeof req.body.request === "object" && !Array.isArray(req.body.request)
? req.body.request
: {};
const nextRequest = normalizeLauncherRequestEntry({
...existing,
title: mode === "draft" ? draftSource.title ?? existing.title : existing.title,
category: mode === "draft" ? draftSource.category ?? existing.category : existing.category,
tags: mode === "draft" && Array.isArray(draftSource.tags) ? draftSource.tags : existing.tags,
sourceText: mode === "draft" ? draftSource.sourceText ?? existing.sourceText : existing.sourceText,
summary: mode === "draft" ? draftSource.summary ?? existing.summary : existing.summary,
implementationNotes: mode === "draft" ? draftSource.implementationNotes ?? existing.implementationNotes : existing.implementationNotes,
analysis: {
...(existing.analysis && typeof existing.analysis === "object" && !Array.isArray(existing.analysis) ? existing.analysis : {}),
state: "unprocessed",
confidence: null,
updatedAt: now,
createdAt: existing.analysis?.createdAt || now,
submissionId: requestId,
sourceTextSnapshot: mode === "draft"
? String((draftSource.sourceText ?? existing.sourceText) || "").trim()
: String(existing.sourceText || "").trim(),
error: "",
routing: undefined,
items: [],
},
updatedAt: now,
}, index);
const nextRequests = [...payload.requests];
nextRequests[index] = nextRequest;
writeLauncherRequestsPayload({
schemaVersion: payload.schemaVersion,
requests: nextRequests,
});
recordSaveEvent({
type: "launcher-request-analysis-requeue",
requestId,
reason: mode === "draft" ? "draft-resubmitted" : "saved-request-rerun",
textPreview: String(nextRequest.sourceText || "").slice(0, 80),
});
const launchResult = launchQueuedRequestAnalysis("manual-request-rerun", { requestId });
res.json({
ok: true,
request: nextRequest,
requests: nextRequests,
...launchResult,
});
} catch (err) {
res.status(500).json({ error: `Failed to requeue launcher request analysis: ${String(err)}` });
}
});
app.post("/api/launcher-requests/process-pending", (req, res) => {
if (!requireLauncherAdminAccess(req, res)) {
return;
@ -3047,6 +3198,106 @@ app.post("/api/launcher-requests/process-pending", (req, res) => {
}
});
app.post("/api/admin/kb/query", (req, res) => {
if (!requireLauncherAdminAccess(req, res)) {
return;
}
try {
const systemsIndex = readKbSystemsIndex();
const modulesIndex = readKbModulesIndex();
const terminologyIndex = readKbTerminologyIndex();
const requestedTags = normalizeLauncherRequestTags(req.body?.tags || []);
const requestedSystems = normalizeLauncherRequestAnalysisStringList(req.body?.systems || req.body?.systemIds);
const requestedModules = normalizeLauncherRequestAnalysisStringList(req.body?.modules || req.body?.moduleIds);
const searchTerms = normalizeLauncherRequestAnalysisStringList(req.body?.searchTerms || req.body?.terms);
const limit = Math.max(1, Math.min(12, Math.floor(Number(req.body?.limit) || 6)));
const searchNeedles = [
...requestedTags,
...requestedSystems,
...requestedModules,
...searchTerms,
].map((entry) => String(entry || "").trim().toLowerCase()).filter(Boolean);
const rankEntry = (entry, extraText = "") => {
const corpus = [
entry?.id,
entry?.name,
entry?.label,
...(Array.isArray(entry?.aliases) ? entry.aliases : []),
...(Array.isArray(entry?.tags) ? entry.tags : []),
...(Array.isArray(entry?.systemIds) ? entry.systemIds : []),
extraText,
].join(" ").toLowerCase();
return searchNeedles.reduce((score, needle) => (corpus.includes(needle) ? score + 2 : score), 0);
};
const matchedSystems = (Array.isArray(systemsIndex.systems) ? systemsIndex.systems : [])
.map((system) => ({
system,
score: rankEntry(system, [system.docPath, ...(Array.isArray(system.uiSurfaces) ? system.uiSurfaces : [])].join(" ")),
}))
.filter(({ system, score }) => {
if (requestedSystems.length > 0 && requestedSystems.includes(String(system.id || "").trim())) {
return true;
}
if (requestedTags.length > 0 && (Array.isArray(system.tags) ? system.tags : []).some((tag) => requestedTags.includes(String(tag || "").trim()))) {
return true;
}
return score > 0;
})
.sort((left, right) => right.score - left.score)
.slice(0, limit)
.map(({ system }) => ({
...system,
docText: fs.readFileSync(path.join(__dirname, String(system.docPath || "").replace(/\//g, path.sep)), "utf8"),
}));
const matchedModules = (Array.isArray(modulesIndex.modules) ? modulesIndex.modules : [])
.map((moduleEntry) => ({
moduleEntry,
score: rankEntry(moduleEntry, moduleEntry.docPath),
}))
.filter(({ moduleEntry, score }) => {
if (requestedModules.length > 0 && requestedModules.includes(String(moduleEntry.id || "").trim())) {
return true;
}
if (requestedSystems.length > 0 && (Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []).some((systemId) => requestedSystems.includes(String(systemId || "").trim()))) {
return true;
}
if (requestedTags.length > 0 && (Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []).some((tag) => requestedTags.includes(String(tag || "").trim()))) {
return true;
}
return score > 0;
})
.sort((left, right) => right.score - left.score)
.slice(0, limit)
.map(({ moduleEntry }) => ({
...moduleEntry,
docText: fs.readFileSync(path.join(__dirname, String(moduleEntry.docPath || "").replace(/\//g, path.sep)), "utf8"),
}));
const matchedTerms = (Array.isArray(terminologyIndex.terms) ? terminologyIndex.terms : [])
.filter((term) => {
const corpus = [
term?.canonical,
...(Array.isArray(term?.aliases) ? term.aliases : []),
...(Array.isArray(term?.tags) ? term.tags : []),
...(Array.isArray(term?.systemIds) ? term.systemIds : []),
].join(" ").toLowerCase();
return searchNeedles.length === 0 || searchNeedles.some((needle) => corpus.includes(needle));
})
.slice(0, limit);
res.json({
ok: true,
requestedTags,
requestedSystems,
requestedModules,
searchTerms,
systems: matchedSystems,
modules: matchedModules,
terminology: matchedTerms,
});
} catch (err) {
res.status(500).json({ error: `Failed to query KB: ${String(err)}` });
}
});
app.get("/api/world-default", (_req, res) => {
try {
const indexPayload = readWorldIndexPayload();