Upgrade request analysis routing
This commit is contained in:
parent
1cd446bae8
commit
db3e080640
19 changed files with 1520 additions and 66 deletions
|
|
@ -7,6 +7,10 @@ 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 terminologyCatalogPath = path.join(kbRoot, "terminology.json");
|
||||
const moduleIndexPath = path.join(kbRoot, "modules.json");
|
||||
const editorCapabilitiesPath = path.join(kbRoot, "editor-capabilities.md");
|
||||
const queueWorkflowPath = path.join(kbRoot, "queue-workflow.md");
|
||||
const DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
||||
const DEFAULT_DEEPSEEK_MODEL = "deepseek-v4-flash";
|
||||
const DEFAULT_PROVIDER = process.env.REQUEST_ANALYZER_PROVIDER
|
||||
|
|
@ -253,6 +257,45 @@ async function loadRequestTagCatalog() {
|
|||
return tags;
|
||||
}
|
||||
|
||||
async function loadTerminologyCatalog() {
|
||||
const payload = JSON.parse(await fs.readFile(terminologyCatalogPath, "utf8"));
|
||||
const terms = Array.isArray(payload?.terms)
|
||||
? payload.terms
|
||||
.map((entry) => ({
|
||||
canonical: String(entry?.canonical || "").trim(),
|
||||
aliases: Array.isArray(entry?.aliases) ? entry.aliases.map((alias) => String(alias || "").trim()).filter(Boolean) : [],
|
||||
tags: Array.isArray(entry?.tags) ? entry.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : [],
|
||||
systemIds: Array.isArray(entry?.systemIds) ? entry.systemIds.map((systemId) => String(systemId || "").trim()).filter(Boolean) : [],
|
||||
}))
|
||||
.filter((entry) => entry.canonical)
|
||||
: [];
|
||||
return {
|
||||
schemaVersion: Number(payload?.schemaVersion) || 1,
|
||||
terms,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadModuleIndex() {
|
||||
const payload = JSON.parse(await fs.readFile(moduleIndexPath, "utf8"));
|
||||
const modules = Array.isArray(payload?.modules)
|
||||
? payload.modules
|
||||
.map((entry) => ({
|
||||
id: String(entry?.id || "").trim(),
|
||||
name: String(entry?.name || "").trim(),
|
||||
docPath: String(entry?.docPath || "").trim(),
|
||||
tags: Array.isArray(entry?.tags) ? entry.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : [],
|
||||
systemIds: Array.isArray(entry?.systemIds) ? entry.systemIds.map((systemId) => String(systemId || "").trim()).filter(Boolean) : [],
|
||||
aliases: Array.isArray(entry?.aliases) ? entry.aliases.map((alias) => String(alias || "").trim()).filter(Boolean) : [],
|
||||
priority: String(entry?.priority || "medium").trim().toLowerCase(),
|
||||
}))
|
||||
.filter((entry) => entry.id && entry.name && entry.docPath)
|
||||
: [];
|
||||
return {
|
||||
schemaVersion: Number(payload?.schemaVersion) || 1,
|
||||
modules,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestTagLookup(tagDefinitions) {
|
||||
return new Map(
|
||||
(Array.isArray(tagDefinitions) ? tagDefinitions : []).flatMap((entry) => [
|
||||
|
|
@ -388,21 +431,68 @@ async function loadKnowledgeBase() {
|
|||
const systemsIndex = JSON.parse(await fs.readFile(systemsIndexPath, "utf8"));
|
||||
const requestSchema = JSON.parse(await fs.readFile(requestSchemaPath, "utf8"));
|
||||
const requestTagDefinitions = await loadRequestTagCatalog();
|
||||
const terminology = await loadTerminologyCatalog();
|
||||
const moduleIndex = await loadModuleIndex();
|
||||
const docsById = new Map();
|
||||
const moduleDocsById = new Map();
|
||||
for (const system of Array.isArray(systemsIndex.systems) ? systemsIndex.systems : []) {
|
||||
const docPath = path.join(repoRoot, String(system.docPath || "").replace(/\//g, path.sep));
|
||||
const docText = await fs.readFile(docPath, "utf8").catch(() => "");
|
||||
docsById.set(String(system.id || ""), docText);
|
||||
}
|
||||
for (const moduleEntry of Array.isArray(moduleIndex.modules) ? moduleIndex.modules : []) {
|
||||
const docPath = path.join(repoRoot, String(moduleEntry.docPath || "").replace(/\//g, path.sep));
|
||||
const docText = await fs.readFile(docPath, "utf8").catch(() => "");
|
||||
moduleDocsById.set(String(moduleEntry.id || ""), docText);
|
||||
}
|
||||
return {
|
||||
systemsIndex,
|
||||
requestSchema,
|
||||
requestTagDefinitions,
|
||||
requestTagLookup: buildRequestTagLookup(requestTagDefinitions),
|
||||
terminology,
|
||||
moduleIndex,
|
||||
docsById,
|
||||
moduleDocsById,
|
||||
editorCapabilitiesText: await fs.readFile(editorCapabilitiesPath, "utf8").catch(() => ""),
|
||||
queueWorkflowText: await fs.readFile(queueWorkflowPath, "utf8").catch(() => ""),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAmbiguityLevel(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
if (normalized === "low" || normalized === "medium" || normalized === "high") {
|
||||
return normalized;
|
||||
}
|
||||
return "medium";
|
||||
}
|
||||
|
||||
function normalizeSystemIds(values, kb, fallback = []) {
|
||||
const knownIds = new Set((Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).map((system) => String(system.id || "").trim()));
|
||||
const requested = uniqueStrings(Array.isArray(values) ? values : [])
|
||||
.map((entry) => String(entry || "").trim())
|
||||
.filter((entry) => knownIds.has(entry));
|
||||
if (requested.length > 0) {
|
||||
return requested;
|
||||
}
|
||||
return uniqueStrings(Array.isArray(fallback) ? fallback : [])
|
||||
.map((entry) => String(entry || "").trim())
|
||||
.filter((entry) => knownIds.has(entry));
|
||||
}
|
||||
|
||||
function normalizeModuleIds(values, kb, fallback = []) {
|
||||
const knownIds = new Set((Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : []).map((moduleEntry) => String(moduleEntry.id || "").trim()));
|
||||
const requested = uniqueStrings(Array.isArray(values) ? values : [])
|
||||
.map((entry) => String(entry || "").trim())
|
||||
.filter((entry) => knownIds.has(entry));
|
||||
if (requested.length > 0) {
|
||||
return requested;
|
||||
}
|
||||
return uniqueStrings(Array.isArray(fallback) ? fallback : [])
|
||||
.map((entry) => String(entry || "").trim())
|
||||
.filter((entry) => knownIds.has(entry));
|
||||
}
|
||||
|
||||
function buildSystemSearchText(system, docText) {
|
||||
const parts = [
|
||||
system?.id,
|
||||
|
|
@ -417,15 +507,52 @@ function buildSystemSearchText(system, docText) {
|
|||
return parts.join(" ").toLowerCase();
|
||||
}
|
||||
|
||||
function buildModuleSearchText(moduleEntry, docText) {
|
||||
const parts = [
|
||||
moduleEntry?.id,
|
||||
moduleEntry?.name,
|
||||
...(Array.isArray(moduleEntry?.aliases) ? moduleEntry.aliases : []),
|
||||
...(Array.isArray(moduleEntry?.tags) ? moduleEntry.tags : []),
|
||||
...(Array.isArray(moduleEntry?.systemIds) ? moduleEntry.systemIds : []),
|
||||
docText,
|
||||
];
|
||||
return parts.join(" ").toLowerCase();
|
||||
}
|
||||
|
||||
function collectTerminologyMatches(kb, requestText) {
|
||||
const requestLower = String(requestText || "").toLowerCase();
|
||||
return (Array.isArray(kb?.terminology?.terms) ? kb.terminology.terms : [])
|
||||
.map((term) => {
|
||||
const phrases = [term.canonical, ...(Array.isArray(term.aliases) ? term.aliases : [])]
|
||||
.map((entry) => String(entry || "").trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const matched = phrases.filter((phrase) => requestLower.includes(phrase));
|
||||
return matched.length > 0
|
||||
? {
|
||||
term,
|
||||
matched,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function pickRelevantSystems(kb, requestText, limit = 4) {
|
||||
const systems = Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : [];
|
||||
const queryTokens = tokenize(requestText);
|
||||
const requestLower = String(requestText || "").toLowerCase();
|
||||
const terminologyMatches = collectTerminologyMatches(kb, requestText);
|
||||
const terminologyBoostBySystemId = new Map();
|
||||
terminologyMatches.forEach(({ term, matched }) => {
|
||||
(Array.isArray(term.systemIds) ? term.systemIds : []).forEach((systemId) => {
|
||||
terminologyBoostBySystemId.set(systemId, (terminologyBoostBySystemId.get(systemId) || 0) + (matched.length * 4));
|
||||
});
|
||||
});
|
||||
const ranked = systems
|
||||
.map((system) => {
|
||||
const docText = kb.docsById.get(String(system.id || "")) || "";
|
||||
const corpus = buildSystemSearchText(system, docText);
|
||||
let score = 0;
|
||||
let score = terminologyBoostBySystemId.get(String(system.id || "").trim()) || 0;
|
||||
for (const token of queryTokens) {
|
||||
if (corpus.includes(token)) {
|
||||
score += 2;
|
||||
|
|
@ -454,30 +581,280 @@ function pickRelevantSystems(kb, requestText, limit = 4) {
|
|||
return ranked.slice(0, limit);
|
||||
}
|
||||
|
||||
function buildPrompt(request, relevantSystems, schema, kb) {
|
||||
function pickRelevantModules(kb, requestText, routing, limit = 4) {
|
||||
const modules = Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : [];
|
||||
const queryTokens = tokenize(requestText);
|
||||
const requestLower = String(requestText || "").toLowerCase();
|
||||
const suggestedTags = new Set(Array.isArray(routing?.suggestedTags) ? routing.suggestedTags : []);
|
||||
const suggestedSystems = new Set(Array.isArray(routing?.suggestedSystems) ? routing.suggestedSystems : []);
|
||||
const ranked = modules
|
||||
.map((moduleEntry) => {
|
||||
const docText = kb.moduleDocsById.get(String(moduleEntry.id || "")) || "";
|
||||
const corpus = buildModuleSearchText(moduleEntry, docText);
|
||||
let score = 0;
|
||||
for (const token of queryTokens) {
|
||||
if (corpus.includes(token)) {
|
||||
score += 2;
|
||||
}
|
||||
}
|
||||
for (const alias of Array.isArray(moduleEntry.aliases) ? moduleEntry.aliases : []) {
|
||||
const normalizedAlias = String(alias || "").trim().toLowerCase();
|
||||
if (normalizedAlias && requestLower.includes(normalizedAlias)) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
for (const tag of Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []) {
|
||||
if (suggestedTags.has(tag)) {
|
||||
score += 4;
|
||||
}
|
||||
}
|
||||
for (const systemId of Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []) {
|
||||
if (suggestedSystems.has(systemId)) {
|
||||
score += 4;
|
||||
}
|
||||
}
|
||||
if (String(moduleEntry.priority || "medium") === "high") {
|
||||
score += 1;
|
||||
}
|
||||
return {
|
||||
moduleEntry,
|
||||
docText,
|
||||
score,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => right.score - left.score || String(left.moduleEntry?.name || "").localeCompare(String(right.moduleEntry?.name || "")));
|
||||
const positive = ranked.filter((entry) => entry.score > 0).slice(0, limit);
|
||||
if (positive.length > 0) {
|
||||
return positive;
|
||||
}
|
||||
return ranked.slice(0, limit);
|
||||
}
|
||||
|
||||
function deriveHeuristicRouting(kb, requestText) {
|
||||
const terminologyMatches = collectTerminologyMatches(kb, requestText);
|
||||
const relevantSystems = pickRelevantSystems(kb, requestText, 4);
|
||||
const matchedTerms = uniqueStrings(
|
||||
terminologyMatches.flatMap(({ matched }) => matched),
|
||||
);
|
||||
const suggestedTags = uniqueStrings([
|
||||
...terminologyMatches.flatMap(({ term }) => Array.isArray(term.tags) ? term.tags : []),
|
||||
...relevantSystems.flatMap(({ system }) => Array.isArray(system.tags) ? system.tags : []),
|
||||
]);
|
||||
const suggestedSystems = uniqueStrings([
|
||||
...terminologyMatches.flatMap(({ term }) => Array.isArray(term.systemIds) ? term.systemIds : []),
|
||||
...relevantSystems.map(({ system }) => String(system.id || "").trim()),
|
||||
]);
|
||||
const suggestedModules = pickRelevantModules(kb, requestText, {
|
||||
suggestedTags,
|
||||
suggestedSystems,
|
||||
}, 4).map(({ moduleEntry }) => String(moduleEntry.id || "").trim());
|
||||
const normalizedText = String(requestText || "").replace(/\s+/g, " ").trim();
|
||||
const tokenCount = tokenize(normalizedText).length;
|
||||
const genericPatterns = [
|
||||
/\b(make|improve|fix|upgrade)\b.+\b(game|editor|worldshaper|site|launcher|it)\b/i,
|
||||
/^\s*(better|more|faster|cooler|good)\s*!?\s*$/i,
|
||||
/^\s*add\s+[a-z0-9_-]+\s*!?\s*$/i,
|
||||
];
|
||||
const isBroad = genericPatterns.some((pattern) => pattern.test(normalizedText)) || tokenCount <= 4;
|
||||
const ambiguity = isBroad
|
||||
? "high"
|
||||
: (matchedTerms.length === 0 || suggestedSystems.length > 3 ? "medium" : "low");
|
||||
const displaySystemNames = suggestedSystems
|
||||
.map((systemId) => (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).find((system) => String(system.id || "").trim() === systemId)?.name || systemId)
|
||||
.slice(0, 4);
|
||||
const possibleDirections = uniqueStrings([
|
||||
suggestedTags.includes("Graphics Painter") ? "Treat it as an asset-editing request inside the Graphics Painter." : "",
|
||||
suggestedTags.includes("Tiling") ? "Treat it as map-grid painting or placement behavior on the tile canvas." : "",
|
||||
suggestedTags.includes("Content") ? "Treat it as a new asset or content-record request that needs catalog support." : "",
|
||||
suggestedTags.includes("Rendering") || suggestedTags.includes("Worlds") ? "Treat it as a runtime-facing request that may need renderer or world metadata support." : "",
|
||||
suggestedTags.includes("Chunks") ? "Treat it as a chunk or world-scale placement workflow." : "",
|
||||
]).slice(0, 4);
|
||||
return {
|
||||
summary: displaySystemNames.length > 0
|
||||
? `Likely touches ${displaySystemNames.join(", ")}.`
|
||||
: "Broad request with no single obvious subsystem match yet.",
|
||||
ambiguity,
|
||||
matchedTerms,
|
||||
suggestedTags: suggestedTags.length > 0 ? suggestedTags.slice(0, 6) : ["General"],
|
||||
suggestedSystems: suggestedSystems.slice(0, 4),
|
||||
suggestedModules,
|
||||
rationale: matchedTerms.length > 0
|
||||
? `Matched terminology: ${matchedTerms.join(", ")}.`
|
||||
: "Used broad alias and system matching because the submission did not name an exact editor surface.",
|
||||
possibleDirections: possibleDirections.length > 0
|
||||
? possibleDirections
|
||||
: ["Clarify which editor surface or runtime behavior the request should target first."],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRoutingResult(rawResult, kb, requestText, fallbackRouting) {
|
||||
const source = rawResult && typeof rawResult === "object" && !Array.isArray(rawResult)
|
||||
? rawResult
|
||||
: {};
|
||||
const suggestedTags = normalizeRequestTags(
|
||||
Array.isArray(source.suggestedTags) ? source.suggestedTags : source.tags,
|
||||
kb?.requestTagLookup || new Map(),
|
||||
fallbackRouting?.suggestedTags || ["General"],
|
||||
);
|
||||
const suggestedSystems = normalizeSystemIds(
|
||||
Array.isArray(source.suggestedSystems) ? source.suggestedSystems : source.suggestedSystemIds,
|
||||
kb,
|
||||
fallbackRouting?.suggestedSystems || [],
|
||||
);
|
||||
const suggestedModules = normalizeModuleIds(
|
||||
Array.isArray(source.suggestedModules) ? source.suggestedModules : source.suggestedModuleIds,
|
||||
kb,
|
||||
fallbackRouting?.suggestedModules || [],
|
||||
);
|
||||
const matchedTerms = uniqueStrings(source.matchedTerms || source.terms || fallbackRouting?.matchedTerms || []);
|
||||
const possibleDirections = uniqueStrings(source.possibleDirections || fallbackRouting?.possibleDirections || []);
|
||||
const summary = String(source.summary || source.routingSummary || fallbackRouting?.summary || "").trim()
|
||||
|| `Likely touches ${suggestedTags.join(", ")}.`;
|
||||
const rationale = String(source.rationale || source.reviewRationale || fallbackRouting?.rationale || "").trim()
|
||||
|| "Routing used terminology and KB alias matching.";
|
||||
const ambiguity = normalizeAmbiguityLevel(source.ambiguity || fallbackRouting?.ambiguity || "");
|
||||
const kbSections = uniqueStrings([
|
||||
...suggestedSystems.map((systemId) => {
|
||||
const system = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).find((entry) => String(entry.id || "").trim() === systemId);
|
||||
return String(system?.docPath || "").trim();
|
||||
}),
|
||||
...suggestedModules.map((moduleId) => {
|
||||
const moduleEntry = (Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : []).find((entry) => String(entry.id || "").trim() === moduleId);
|
||||
return String(moduleEntry?.docPath || "").trim();
|
||||
}),
|
||||
].filter(Boolean));
|
||||
return {
|
||||
summary,
|
||||
ambiguity,
|
||||
matchedTerms,
|
||||
suggestedTags: suggestedTags.length > 0 ? suggestedTags : ["General"],
|
||||
suggestedSystems,
|
||||
suggestedModules,
|
||||
rationale,
|
||||
possibleDirections: possibleDirections.length > 0
|
||||
? possibleDirections
|
||||
: (fallbackRouting?.possibleDirections || ["Clarify which part of the editor or runtime should own this request."]),
|
||||
kbSections,
|
||||
sourceText: String(requestText || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildRoutingPrompt(request, kb, heuristicRouting) {
|
||||
const systemSummaries = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : [])
|
||||
.map((system) => `- ${system.id}: ${system.name} | tags=${(Array.isArray(system.tags) ? system.tags : []).join(", ")} | aliases=${(Array.isArray(system.aliases) ? system.aliases : []).join(", ")}`)
|
||||
.join("\n");
|
||||
const moduleSummaries = (Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : [])
|
||||
.map((moduleEntry) => `- ${moduleEntry.id}: ${moduleEntry.name} | tags=${(Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []).join(", ")} | systems=${(Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []).join(", ")}`)
|
||||
.join("\n");
|
||||
const terminologySummary = (Array.isArray(kb?.terminology?.terms) ? kb.terminology.terms : [])
|
||||
.map((term) => `- ${term.canonical}: aliases=${(Array.isArray(term.aliases) ? term.aliases : []).join(", ")} | tags=${(Array.isArray(term.tags) ? term.tags : []).join(", ")} | systems=${(Array.isArray(term.systemIds) ? term.systemIds : []).join(", ")}`)
|
||||
.join("\n");
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
"You are the routing pass for Worldshaper request analysis.",
|
||||
"Map the submission onto likely systems, modules, and standardized tags before the deeper analysis pass runs.",
|
||||
"Prefer Worldshaper terminology when the user uses adjacent language such as sprite editor, painting tool, recoloring, engine, runtime, map, grid, or chunk.",
|
||||
"Broad requests must still receive a useful interpretation and possible directions.",
|
||||
"Do not expose or simulate hidden chain-of-thought.",
|
||||
"Return only valid JSON with keys: summary, ambiguity, matchedTerms, suggestedTags, suggestedSystems, suggestedModules, rationale, possibleDirections.",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
`Submission id: ${request.id}`,
|
||||
"Raw submission:",
|
||||
request.sourceText,
|
||||
"",
|
||||
"Standardized tags:",
|
||||
(Array.isArray(kb?.requestTagDefinitions) ? kb.requestTagDefinitions : []).map((entry) => `- ${entry.label}: ${entry.description || ""}`).join("\n"),
|
||||
"",
|
||||
"System index:",
|
||||
systemSummaries,
|
||||
"",
|
||||
"Focused modules:",
|
||||
moduleSummaries,
|
||||
"",
|
||||
"Terminology map:",
|
||||
terminologySummary,
|
||||
"",
|
||||
"Heuristic seed:",
|
||||
JSON.stringify(heuristicRouting, null, 2),
|
||||
].join("\n"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildKbContext(kb, requestText, routing) {
|
||||
const relevantSystems = uniqueStrings([
|
||||
...normalizeSystemIds(routing?.suggestedSystems, kb, []),
|
||||
...pickRelevantSystems(kb, requestText, 4).map(({ system }) => String(system.id || "").trim()),
|
||||
])
|
||||
.slice(0, 4)
|
||||
.map((systemId) => {
|
||||
const system = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).find((entry) => String(entry.id || "").trim() === systemId);
|
||||
return system
|
||||
? {
|
||||
system,
|
||||
docText: kb.docsById.get(systemId) || "",
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
const relevantModules = uniqueStrings([
|
||||
...normalizeModuleIds(routing?.suggestedModules, kb, []),
|
||||
...pickRelevantModules(kb, requestText, routing, 5).map(({ moduleEntry }) => String(moduleEntry.id || "").trim()),
|
||||
])
|
||||
.slice(0, 5)
|
||||
.map((moduleId) => {
|
||||
const moduleEntry = (Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : []).find((entry) => String(entry.id || "").trim() === moduleId);
|
||||
return moduleEntry
|
||||
? {
|
||||
moduleEntry,
|
||||
docText: kb.moduleDocsById.get(moduleId) || "",
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return {
|
||||
systems: relevantSystems,
|
||||
modules: relevantModules,
|
||||
editorCapabilitiesText: String(kb?.editorCapabilitiesText || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAnalysisPrompt(request, routing, kbContext, 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}`,
|
||||
`System ID: ${system.id}`,
|
||||
`Tags: ${(Array.isArray(system.tags) ? system.tags : []).join(", ")}`,
|
||||
`Key files: ${(Array.isArray(system.keyFiles) ? system.keyFiles : []).join(", ")}`,
|
||||
`API endpoints: ${(Array.isArray(system.apiEndpoints) ? system.apiEndpoints : []).join(", ") || "(none)"}`,
|
||||
docText,
|
||||
].join("\n");
|
||||
}).join("\n\n---\n\n");
|
||||
const systemDocs = kbContext.systems.map(({ system, docText }) => [
|
||||
`System: ${system.name}`,
|
||||
`System ID: ${system.id}`,
|
||||
`Tags: ${(Array.isArray(system.tags) ? system.tags : []).join(", ")}`,
|
||||
`Key files: ${(Array.isArray(system.keyFiles) ? system.keyFiles : []).join(", ")}`,
|
||||
`API endpoints: ${(Array.isArray(system.apiEndpoints) ? system.apiEndpoints : []).join(", ") || "(none)"}`,
|
||||
docText,
|
||||
].join("\n")).join("\n\n---\n\n");
|
||||
const moduleDocs = kbContext.modules.map(({ moduleEntry, docText }) => [
|
||||
`Module: ${moduleEntry.name}`,
|
||||
`Module ID: ${moduleEntry.id}`,
|
||||
`Tags: ${(Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []).join(", ")}`,
|
||||
`System IDs: ${(Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []).join(", ")}`,
|
||||
docText,
|
||||
].join("\n")).join("\n\n---\n\n");
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
"You are processing Worldshaper editor requests.",
|
||||
"Split a submission into one or more atomic requests.",
|
||||
"Ground your decisions in the provided KB systems only.",
|
||||
"Ground your decisions in the provided KB systems and modules 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.",
|
||||
"If a request is broad or ambiguous, still provide the most useful likely interpretation and concrete implementation path you can.",
|
||||
"Return only valid JSON.",
|
||||
"Do not wrap the JSON in markdown fences.",
|
||||
"If you are unsure, lower confidence and use statusRecommendation = \"needs_review\".",
|
||||
|
|
@ -490,14 +867,23 @@ function buildPrompt(request, relevantSystems, schema, kb) {
|
|||
"Raw submission:",
|
||||
request.sourceText,
|
||||
"",
|
||||
"Routing summary:",
|
||||
JSON.stringify(routing, null, 2),
|
||||
"",
|
||||
"Return JSON matching this schema:",
|
||||
schemaSummary,
|
||||
"",
|
||||
"Standardized tags you may use:",
|
||||
tagCatalogSummary,
|
||||
"",
|
||||
"Capability shorthand:",
|
||||
kbContext.editorCapabilitiesText || "(none)",
|
||||
"",
|
||||
"Relevant KB systems:",
|
||||
systemDocs,
|
||||
systemDocs || "(none)",
|
||||
"",
|
||||
"Relevant KB modules:",
|
||||
moduleDocs || "(none)",
|
||||
].join("\n"),
|
||||
},
|
||||
];
|
||||
|
|
@ -541,7 +927,7 @@ function readMessageContent(messageContent) {
|
|||
return "";
|
||||
}
|
||||
|
||||
async function callModelApi(config, messages) {
|
||||
async function callModelApi(config, messages, options = {}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
|
@ -550,8 +936,8 @@ async function callModelApi(config, messages) {
|
|||
}
|
||||
const requestBody = {
|
||||
model: config.model,
|
||||
temperature: 0.2,
|
||||
max_tokens: config.maxTokens,
|
||||
temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.2,
|
||||
max_tokens: Math.max(256, Math.floor(Number(options.maxTokens) || config.maxTokens)),
|
||||
messages,
|
||||
};
|
||||
if (config.provider === "deepseek") {
|
||||
|
|
@ -592,11 +978,13 @@ async function processLauncherRequestAnalysis(config, requestId, body) {
|
|||
}
|
||||
|
||||
function shouldPromoteAnalysis(result, config) {
|
||||
return result.items.length > 0 && result.items.every((item) => (
|
||||
return result.items.length > 0
|
||||
&& String(result?.routing?.ambiguity || "").trim().toLowerCase() !== "high"
|
||||
&& result.items.every((item) => (
|
||||
item.statusRecommendation === "active"
|
||||
&& Number.isFinite(item.confidence)
|
||||
&& item.confidence >= config.promoteThreshold
|
||||
));
|
||||
));
|
||||
}
|
||||
|
||||
async function markRequestProcessing(config, request) {
|
||||
|
|
@ -635,6 +1023,7 @@ async function markRequestReview(config, request, result) {
|
|||
submissionId: request.id,
|
||||
sourceTextSnapshot: request.sourceText,
|
||||
confidence: result.confidence,
|
||||
routing: result.routing,
|
||||
items: result.items,
|
||||
},
|
||||
});
|
||||
|
|
@ -648,22 +1037,100 @@ async function promoteRequest(config, request, result) {
|
|||
submissionId: request.id,
|
||||
sourceTextSnapshot: request.sourceText,
|
||||
confidence: result.confidence,
|
||||
routing: result.routing,
|
||||
items: result.items,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildFallbackAnalysisResult(request, routing, kb) {
|
||||
const primaryTag = Array.isArray(routing?.suggestedTags) && routing.suggestedTags.length > 0
|
||||
? routing.suggestedTags[0]
|
||||
: "General";
|
||||
const primarySystemId = Array.isArray(routing?.suggestedSystems) && routing.suggestedSystems.length > 0
|
||||
? routing.suggestedSystems[0]
|
||||
: "";
|
||||
const primarySystem = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : [])
|
||||
.find((system) => String(system.id || "").trim() === primarySystemId);
|
||||
const title = buildFallbackTitle(
|
||||
request.sourceText,
|
||||
routing?.ambiguity === "high" ? "Broader editor improvement request" : "Pending request",
|
||||
);
|
||||
const parsedInterpretation = routing?.ambiguity === "high"
|
||||
? `This submission is broad, but it most likely points toward ${primaryTag} work${primarySystem?.name ? ` touching ${primarySystem.name}` : ""}. It needs a narrower decision before it can be promoted cleanly.`
|
||||
: `This request most likely targets ${primarySystem?.name || primaryTag}, based on the submission language and the matched KB terminology.`;
|
||||
const implementationApproach = routing?.ambiguity === "high"
|
||||
? `Turn the request into a smaller scoped follow-up by choosing one direction first: ${(Array.isArray(routing?.possibleDirections) && routing.possibleDirections.length > 0 ? routing.possibleDirections : ["clarify whether this should be content, map editing, or runtime work"]).join(" ")}`.trim()
|
||||
: `Start from ${primarySystem?.name || primaryTag}, audit the existing workflow there, and then scope the change so the request can be broken into editor-facing behavior, saved data changes, and any runtime handoff that may be required.`;
|
||||
const reviewOptions = Array.isArray(routing?.possibleDirections) && routing.possibleDirections.length > 0
|
||||
? routing.possibleDirections
|
||||
: ["Clarify which part of the editor or runtime should own this request first."];
|
||||
return {
|
||||
submissionId: request.id,
|
||||
sourceText: request.sourceText,
|
||||
confidence: 0.38,
|
||||
minimumConfidence: 0.38,
|
||||
routing,
|
||||
items: [
|
||||
{
|
||||
title,
|
||||
primaryCategory: primaryTag,
|
||||
tags: Array.isArray(routing?.suggestedTags) && routing.suggestedTags.length > 0 ? routing.suggestedTags : ["General"],
|
||||
statusRecommendation: "needs_review",
|
||||
parsedInterpretation,
|
||||
implementationApproach,
|
||||
affectedSystems: primarySystem?.name ? [primarySystem.name] : [],
|
||||
affectedFiles: [],
|
||||
problemType: "feature",
|
||||
rawExcerpt: String(request.sourceText || "").trim(),
|
||||
confidence: 0.38,
|
||||
reviewRationale: String(routing?.rationale || "").trim() || "The request is still too broad to auto-promote safely.",
|
||||
reviewOptions,
|
||||
notes: String(routing?.summary || "").trim(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function analyzeRequest(config, kb, request) {
|
||||
const relevantSystems = pickRelevantSystems(kb, request.sourceText, 4);
|
||||
const systemNames = relevantSystems.map(({ system }) => system.name);
|
||||
const heuristicRouting = deriveHeuristicRouting(kb, request.sourceText);
|
||||
const routingPrompt = buildRoutingPrompt(request, kb, heuristicRouting);
|
||||
let routing = heuristicRouting;
|
||||
try {
|
||||
const routingResult = await callModelApi(config, routingPrompt, {
|
||||
maxTokens: Math.min(config.maxTokens, 900),
|
||||
temperature: 0.1,
|
||||
});
|
||||
routing = normalizeRoutingResult(routingResult, kb, request.sourceText, heuristicRouting);
|
||||
} catch (error) {
|
||||
console.warn(` Routing pass fallback for ${request.id}: ${String(error)}`);
|
||||
routing = normalizeRoutingResult({}, kb, request.sourceText, heuristicRouting);
|
||||
}
|
||||
const kbContext = buildKbContext(kb, request.sourceText, routing);
|
||||
const systemNames = kbContext.systems.map(({ system }) => system.name);
|
||||
console.log(`Analyzing ${request.id}: ${request.title}`);
|
||||
console.log(` Systems: ${systemNames.join(", ")}`);
|
||||
if (!config.dryRun) {
|
||||
await markRequestProcessing(config, request);
|
||||
}
|
||||
const prompt = buildPrompt(request, relevantSystems, kb.requestSchema, kb);
|
||||
const modelResult = await callModelApi(config, prompt);
|
||||
const normalizedResult = normalizeAnalysisResult(modelResult, request, relevantSystems.map((entry) => entry.system), kb);
|
||||
const prompt = buildAnalysisPrompt(request, routing, kbContext, kb.requestSchema, kb);
|
||||
let normalizedResult;
|
||||
try {
|
||||
const modelResult = await callModelApi(config, prompt, {
|
||||
maxTokens: config.maxTokens,
|
||||
temperature: 0.2,
|
||||
});
|
||||
normalizedResult = normalizeAnalysisResult(
|
||||
modelResult,
|
||||
request,
|
||||
kbContext.systems.map((entry) => entry.system),
|
||||
kb,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(` Analysis pass fallback for ${request.id}: ${String(error)}`);
|
||||
normalizedResult = buildFallbackAnalysisResult(request, routing, kb);
|
||||
}
|
||||
normalizedResult.routing = routing;
|
||||
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