Expand request review tooling and KB
This commit is contained in:
parent
ab1dfbf029
commit
cae21b61b7
16 changed files with 1258 additions and 241 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue