Upgrade request analysis routing
This commit is contained in:
parent
1cd446bae8
commit
db3e080640
19 changed files with 1520 additions and 66 deletions
263
server.js
263
server.js
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue