Expand request review tooling and KB

This commit is contained in:
Andraxion 2026-06-27 01:12:35 -04:00
parent ab1dfbf029
commit cae21b61b7
16 changed files with 1258 additions and 241 deletions

View file

@ -6,6 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const kbRoot = path.join(repoRoot, "docs", "kb");
const requestTagCatalogPath = path.join(kbRoot, "tags.json");
const DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com";
const DEFAULT_DEEPSEEK_MODEL = "deepseek-v4-flash";
const DEFAULT_PROVIDER = process.env.REQUEST_ANALYZER_PROVIDER
@ -231,6 +232,48 @@ function uniqueStrings(values) {
return next;
}
function normalizeRequestTagLookupValue(value) {
return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
}
async function loadRequestTagCatalog() {
const payload = JSON.parse(await fs.readFile(requestTagCatalogPath, "utf8"));
const tags = Array.isArray(payload?.tags)
? payload.tags
.map((entry) => ({
id: String(entry?.id || "").trim(),
label: String(entry?.label || "").trim(),
aliases: Array.isArray(entry?.aliases) ? entry.aliases.map((alias) => String(alias || "").trim()).filter(Boolean) : [],
}))
.filter((entry) => entry.id && entry.label)
: [];
if (tags.length === 0) {
throw new Error("Request tag catalog did not contain any valid tags.");
}
return tags;
}
function buildRequestTagLookup(tagDefinitions) {
return new Map(
(Array.isArray(tagDefinitions) ? tagDefinitions : []).flatMap((entry) => [
[normalizeRequestTagLookupValue(entry.label), entry.label],
...(Array.isArray(entry.aliases) ? entry.aliases.map((alias) => [normalizeRequestTagLookupValue(alias), entry.label]) : []),
]),
);
}
function normalizeRequestTags(values, tagLookup, fallback = []) {
const normalized = uniqueStrings(Array.isArray(values) ? values : [])
.map((entry) => tagLookup.get(normalizeRequestTagLookupValue(entry)) || "")
.filter(Boolean);
if (normalized.length > 0) {
return uniqueStrings(normalized);
}
return uniqueStrings(Array.isArray(fallback) ? fallback : [])
.map((entry) => tagLookup.get(normalizeRequestTagLookupValue(entry)) || "")
.filter(Boolean);
}
function clampConfidence(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
@ -264,7 +307,7 @@ function buildFallbackTitle(text, fallback = "Pending request") {
return firstSentence.length > 72 ? `${firstSentence.slice(0, 69).trim()}...` : firstSentence;
}
function normalizeAnalysisItem(rawItem, index = 0, request = null, relevantSystems = []) {
function normalizeAnalysisItem(rawItem, index = 0, request = null, relevantSystems = [], kb = null) {
const source = rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)
? rawItem
: null;
@ -281,15 +324,21 @@ function normalizeAnalysisItem(rawItem, index = 0, request = null, relevantSyste
const primaryCategory = String(source.primaryCategory || source.category || "").trim()
|| String(relevantSystems[0]?.name || "Unsorted");
const affectedSystems = uniqueStrings(source.affectedSystems);
const defaultTags = uniqueStrings([
const defaultTags = [
primaryCategory,
...affectedSystems,
]);
const tags = uniqueStrings(Array.isArray(source.tags) ? source.tags : defaultTags);
];
const tags = normalizeRequestTags(
Array.isArray(source.tags) ? source.tags : defaultTags,
kb?.requestTagLookup || new Map(),
defaultTags,
);
const reviewRationale = String(source.reviewRationale || source.reviewReason || source.notes || "").trim();
const reviewOptions = uniqueStrings(source.reviewOptions);
return {
title,
primaryCategory,
tags: tags.length > 0 ? tags : ["Unsorted"],
tags: tags.length > 0 ? tags : ["General"],
statusRecommendation: normalizeItemStatusRecommendation(source.statusRecommendation || source.status),
parsedInterpretation,
implementationApproach,
@ -298,11 +347,13 @@ function normalizeAnalysisItem(rawItem, index = 0, request = null, relevantSyste
problemType: normalizeProblemType(source.problemType),
rawExcerpt: String(source.rawExcerpt || "").trim(),
confidence: clampConfidence(source.confidence),
reviewRationale,
reviewOptions,
notes: String(source.notes || "").trim(),
};
}
function normalizeAnalysisResult(rawResult, request, relevantSystems) {
function normalizeAnalysisResult(rawResult, request, relevantSystems, kb = null) {
const source = rawResult && typeof rawResult === "object" && !Array.isArray(rawResult)
? rawResult
: (Array.isArray(rawResult) ? { items: rawResult } : null);
@ -310,7 +361,7 @@ function normalizeAnalysisResult(rawResult, request, relevantSystems) {
throw new Error("Model response was not a JSON object.");
}
const items = (Array.isArray(source.items) ? source.items : [])
.map((item, index) => normalizeAnalysisItem(item, index, request, relevantSystems))
.map((item, index) => normalizeAnalysisItem(item, index, request, relevantSystems, kb))
.filter(Boolean);
if (items.length === 0) {
throw new Error("Model response did not contain any valid request items.");
@ -336,6 +387,7 @@ async function loadKnowledgeBase() {
const requestSchemaPath = path.join(kbRoot, "request-analysis-schema.json");
const systemsIndex = JSON.parse(await fs.readFile(systemsIndexPath, "utf8"));
const requestSchema = JSON.parse(await fs.readFile(requestSchemaPath, "utf8"));
const requestTagDefinitions = await loadRequestTagCatalog();
const docsById = new Map();
for (const system of Array.isArray(systemsIndex.systems) ? systemsIndex.systems : []) {
const docPath = path.join(repoRoot, String(system.docPath || "").replace(/\//g, path.sep));
@ -345,6 +397,8 @@ async function loadKnowledgeBase() {
return {
systemsIndex,
requestSchema,
requestTagDefinitions,
requestTagLookup: buildRequestTagLookup(requestTagDefinitions),
docsById,
};
}
@ -400,8 +454,11 @@ function pickRelevantSystems(kb, requestText, limit = 4) {
return ranked.slice(0, limit);
}
function buildPrompt(request, relevantSystems, schema) {
function buildPrompt(request, relevantSystems, schema, kb) {
const schemaSummary = JSON.stringify(schema, null, 2);
const tagCatalogSummary = Array.isArray(kb?.requestTagDefinitions)
? kb.requestTagDefinitions.map((entry) => `- ${entry.label}: ${entry.description || ""}`.trim()).join("\n")
: "";
const systemDocs = relevantSystems.map(({ system, docText }) => {
return [
`System: ${system.name}`,
@ -419,6 +476,8 @@ function buildPrompt(request, relevantSystems, schema) {
"You are processing Worldshaper editor requests.",
"Split a submission into one or more atomic requests.",
"Ground your decisions in the provided KB systems only.",
"Use only the standardized tags listed in the provided tag catalog.",
"Do not expose or simulate hidden chain-of-thought. Provide short structured review rationale instead.",
"Return only valid JSON.",
"Do not wrap the JSON in markdown fences.",
"If you are unsure, lower confidence and use statusRecommendation = \"needs_review\".",
@ -434,6 +493,9 @@ function buildPrompt(request, relevantSystems, schema) {
"Return JSON matching this schema:",
schemaSummary,
"",
"Standardized tags you may use:",
tagCatalogSummary,
"",
"Relevant KB systems:",
systemDocs,
].join("\n"),
@ -599,9 +661,9 @@ async function analyzeRequest(config, kb, request) {
if (!config.dryRun) {
await markRequestProcessing(config, request);
}
const prompt = buildPrompt(request, relevantSystems, kb.requestSchema);
const prompt = buildPrompt(request, relevantSystems, kb.requestSchema, kb);
const modelResult = await callModelApi(config, prompt);
const normalizedResult = normalizeAnalysisResult(modelResult, request, relevantSystems.map((entry) => entry.system));
const normalizedResult = normalizeAnalysisResult(modelResult, request, relevantSystems.map((entry) => entry.system), kb);
const action = shouldPromoteAnalysis(normalizedResult, config) ? "promote" : "review";
console.log(` Result: ${normalizedResult.items.length} item(s), action=${action}, confidence=${normalizedResult.confidence ?? "n/a"}`);
if (config.dryRun) {