Worldshaper/scripts/request-analysis-worker.mjs

757 lines
26 KiB
JavaScript
Raw Normal View History

import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const kbRoot = path.join(repoRoot, "docs", "kb");
2026-06-27 01:12:35 -04:00
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
|| (String(process.env.DEEPSEEK_API_KEY || "").trim() ? "deepseek" : "openai-compatible");
const DEFAULTS = {
apiBase: process.env.REQUEST_ANALYZER_API_BASE || `http://127.0.0.1:${process.env.PORT || "5180"}`,
provider: DEFAULT_PROVIDER,
modelBaseUrl: process.env.REQUEST_ANALYZER_MODEL_BASE_URL || "",
model: process.env.REQUEST_ANALYZER_MODEL || "",
apiKey: process.env.REQUEST_ANALYZER_API_KEY || process.env.DEEPSEEK_API_KEY || "",
2026-06-27 00:51:20 -04:00
adminPassword: process.env.REQUEST_ANALYZER_ADMIN_PASSWORD || process.env.LAUNCHER_ADMIN_PASSWORD || "",
limit: Number(process.env.REQUEST_ANALYZER_LIMIT || 5),
promoteThreshold: Number(process.env.REQUEST_ANALYZER_PROMOTE_THRESHOLD || 0.85),
maxTokens: Math.max(512, Number(process.env.REQUEST_ANALYZER_MAX_TOKENS || 4000)),
thinking: String(process.env.REQUEST_ANALYZER_THINKING || "disabled").trim().toLowerCase() === "enabled"
? "enabled"
: "disabled",
poll: false,
intervalMs: Number(process.env.REQUEST_ANALYZER_INTERVAL_MS || 30000),
dryRun: false,
requestId: "",
};
function printHelp() {
console.log(`Worldshaper request analysis worker
Usage:
npm run analyze:requests -- [options]
Options:
--provider <name> Model provider: deepseek or openai-compatible.
--request-id <id> Analyze one pending request only.
--limit <n> Maximum pending requests to process in one pass.
--poll Keep polling for new pending requests.
--interval-ms <ms> Poll interval when --poll is enabled.
--dry-run Do not write anything back to the API.
--api-base <url> Worldshaper API base URL.
--model-base-url <url> Model API base URL. Defaults to DeepSeek when provider=deepseek.
--model <id> Model id to use. Defaults to ${DEFAULT_DEEPSEEK_MODEL} for DeepSeek.
--api-key <key> API key for the model endpoint.
--max-tokens <n> Max output tokens for the model response.
--thinking <mode> DeepSeek thinking mode: enabled or disabled.
--promote-threshold <n> Minimum per-item confidence for auto-promotion.
--help Show this message.
Environment variables:
REQUEST_ANALYZER_API_BASE
REQUEST_ANALYZER_PROVIDER
REQUEST_ANALYZER_MODEL_BASE_URL
REQUEST_ANALYZER_MODEL
REQUEST_ANALYZER_API_KEY
2026-06-27 00:51:20 -04:00
REQUEST_ANALYZER_ADMIN_PASSWORD
REQUEST_ANALYZER_MAX_TOKENS
REQUEST_ANALYZER_THINKING
REQUEST_ANALYZER_LIMIT
REQUEST_ANALYZER_INTERVAL_MS
REQUEST_ANALYZER_PROMOTE_THRESHOLD
DEEPSEEK_API_KEY
2026-06-27 00:51:20 -04:00
LAUNCHER_ADMIN_PASSWORD
Notes:
- DeepSeek uses ${DEFAULT_DEEPSEEK_BASE_URL}/chat/completions.
- DeepSeek JSON mode is enabled automatically for this worker.
- Run the Worldshaper API server first so the worker can read and patch requests.
`);
}
function parseArgs(argv) {
const config = { ...DEFAULTS };
for (let index = 0; index < argv.length; index += 1) {
const arg = String(argv[index] || "").trim();
if (!arg) {
continue;
}
if (arg === "--help" || arg === "-h") {
config.help = true;
continue;
}
if (arg === "--provider") {
config.provider = String(argv[index + 1] || "").trim().toLowerCase() || config.provider;
index += 1;
continue;
}
if (arg === "--poll") {
config.poll = true;
continue;
}
if (arg === "--dry-run") {
config.dryRun = true;
continue;
}
if (arg === "--request-id") {
config.requestId = String(argv[index + 1] || "").trim();
index += 1;
continue;
}
if (arg === "--limit") {
config.limit = Math.max(1, Math.floor(Number(argv[index + 1]) || config.limit));
index += 1;
continue;
}
if (arg === "--interval-ms") {
config.intervalMs = Math.max(1000, Math.floor(Number(argv[index + 1]) || config.intervalMs));
index += 1;
continue;
}
if (arg === "--api-base") {
config.apiBase = String(argv[index + 1] || "").trim() || config.apiBase;
index += 1;
continue;
}
if (arg === "--model-base-url") {
config.modelBaseUrl = String(argv[index + 1] || "").trim() || config.modelBaseUrl;
index += 1;
continue;
}
if (arg === "--model") {
config.model = String(argv[index + 1] || "").trim() || config.model;
index += 1;
continue;
}
if (arg === "--api-key") {
config.apiKey = String(argv[index + 1] || "").trim() || config.apiKey;
index += 1;
continue;
}
if (arg === "--max-tokens") {
config.maxTokens = Math.max(512, Math.floor(Number(argv[index + 1]) || config.maxTokens));
index += 1;
continue;
}
if (arg === "--thinking") {
const thinkingValue = String(argv[index + 1] || "").trim().toLowerCase();
config.thinking = thinkingValue === "enabled" ? "enabled" : "disabled";
index += 1;
continue;
}
if (arg === "--promote-threshold") {
const parsed = Number(argv[index + 1]);
if (Number.isFinite(parsed)) {
config.promoteThreshold = Math.max(0, Math.min(1, parsed));
}
index += 1;
continue;
}
throw new Error(`Unknown option: ${arg}`);
}
return config;
}
function finalizeConfig(config) {
const provider = String(config.provider || "").trim().toLowerCase();
const nextConfig = {
...config,
provider: provider === "deepseek" ? "deepseek" : "openai-compatible",
};
if (!String(nextConfig.modelBaseUrl || "").trim()) {
nextConfig.modelBaseUrl = nextConfig.provider === "deepseek"
? DEFAULT_DEEPSEEK_BASE_URL
: "http://127.0.0.1:1234/v1";
}
if (!String(nextConfig.model || "").trim() && nextConfig.provider === "deepseek") {
nextConfig.model = DEFAULT_DEEPSEEK_MODEL;
}
return nextConfig;
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function withTrailingSlash(value) {
return String(value || "").endsWith("/") ? String(value || "") : `${String(value || "")}/`;
}
function buildUrl(base, pathname) {
return new URL(String(pathname || "").replace(/^\//, ""), withTrailingSlash(base)).toString();
}
async function fetchJson(url, init = {}) {
const response = await fetch(url, init);
if (!response.ok) {
const responseText = await response.text().catch(() => "");
throw new Error(`${response.status} ${response.statusText}${responseText ? `: ${responseText.slice(0, 240)}` : ""}`);
}
return response.json();
}
2026-06-27 00:51:20 -04:00
function buildAdminHeaders(config, baseHeaders = {}) {
const nextHeaders = {
...baseHeaders,
};
const adminPassword = String(config?.adminPassword || "").trim();
if (adminPassword) {
nextHeaders["x-worldshaper-admin-password"] = adminPassword;
}
return nextHeaders;
}
function tokenize(value) {
return String(value || "")
.toLowerCase()
.split(/[^a-z0-9_/-]+/i)
.map((entry) => entry.trim())
.filter((entry) => entry.length >= 2);
}
function uniqueStrings(values) {
const seen = new Set();
const next = [];
for (const value of Array.isArray(values) ? values : []) {
const normalized = String(value || "").replace(/\s+/g, " ").trim();
const key = normalized.toLowerCase();
if (!normalized || seen.has(key)) {
continue;
}
seen.add(key);
next.push(normalized);
}
return next;
}
2026-06-27 01:12:35 -04:00
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)) {
return null;
}
return Math.max(0, Math.min(1, parsed));
}
function normalizeItemStatusRecommendation(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "active" || normalized === "duplicate" || normalized === "blocked" || normalized === "needs_review") {
return normalized;
}
return "needs_review";
}
function normalizeProblemType(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "feature" || normalized === "bug" || normalized === "workflow" || normalized === "performance" || normalized === "ux" || normalized === "content") {
return normalized;
}
return "unknown";
}
function buildFallbackTitle(text, fallback = "Pending request") {
const normalized = String(text || "").replace(/\s+/g, " ").trim();
if (!normalized) {
return fallback;
}
const firstSentence = normalized.split(/[\r\n.!?]+/).map((entry) => entry.trim()).find(Boolean) || normalized;
return firstSentence.length > 72 ? `${firstSentence.slice(0, 69).trim()}...` : firstSentence;
}
2026-06-27 01:12:35 -04:00
function normalizeAnalysisItem(rawItem, index = 0, request = null, relevantSystems = [], kb = null) {
const source = rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)
? rawItem
: null;
if (!source) {
return null;
}
const fallbackText = String(source.rawExcerpt || request?.sourceText || "").trim();
const title = String(source.title || "").trim() || buildFallbackTitle(fallbackText, `Analyzed request ${index + 1}`);
const parsedInterpretation = String(source.parsedInterpretation || source.summary || "").trim();
const implementationApproach = String(source.implementationApproach || source.implementationNotes || "").trim();
if (!title || !parsedInterpretation || !implementationApproach) {
return null;
}
const primaryCategory = String(source.primaryCategory || source.category || "").trim()
|| String(relevantSystems[0]?.name || "Unsorted");
const affectedSystems = uniqueStrings(source.affectedSystems);
2026-06-27 01:12:35 -04:00
const defaultTags = [
primaryCategory,
...affectedSystems,
2026-06-27 01:12:35 -04:00
];
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,
2026-06-27 01:12:35 -04:00
tags: tags.length > 0 ? tags : ["General"],
statusRecommendation: normalizeItemStatusRecommendation(source.statusRecommendation || source.status),
parsedInterpretation,
implementationApproach,
affectedSystems,
affectedFiles: uniqueStrings(source.affectedFiles),
problemType: normalizeProblemType(source.problemType),
rawExcerpt: String(source.rawExcerpt || "").trim(),
confidence: clampConfidence(source.confidence),
2026-06-27 01:12:35 -04:00
reviewRationale,
reviewOptions,
notes: String(source.notes || "").trim(),
};
}
2026-06-27 01:12:35 -04:00
function normalizeAnalysisResult(rawResult, request, relevantSystems, kb = null) {
const source = rawResult && typeof rawResult === "object" && !Array.isArray(rawResult)
? rawResult
: (Array.isArray(rawResult) ? { items: rawResult } : null);
if (!source) {
throw new Error("Model response was not a JSON object.");
}
const items = (Array.isArray(source.items) ? source.items : [])
2026-06-27 01:12:35 -04:00
.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.");
}
const finiteConfidences = items.map((item) => item.confidence).filter((value) => Number.isFinite(value));
const averageConfidence = finiteConfidences.length > 0
? finiteConfidences.reduce((total, value) => total + value, 0) / finiteConfidences.length
: null;
const minimumConfidence = finiteConfidences.length > 0
? Math.min(...finiteConfidences)
: null;
return {
submissionId: String(source.submissionId || request.id || "").trim() || request.id,
sourceText: String(source.sourceText || request.sourceText || "").trim() || request.sourceText,
confidence: clampConfidence(source.confidence) ?? averageConfidence,
minimumConfidence,
items,
};
}
async function loadKnowledgeBase() {
const systemsIndexPath = path.join(kbRoot, "systems.json");
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"));
2026-06-27 01:12:35 -04:00
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));
const docText = await fs.readFile(docPath, "utf8").catch(() => "");
docsById.set(String(system.id || ""), docText);
}
return {
systemsIndex,
requestSchema,
2026-06-27 01:12:35 -04:00
requestTagDefinitions,
requestTagLookup: buildRequestTagLookup(requestTagDefinitions),
docsById,
};
}
function buildSystemSearchText(system, docText) {
const parts = [
system?.id,
system?.name,
...(Array.isArray(system?.aliases) ? system.aliases : []),
...(Array.isArray(system?.tags) ? system.tags : []),
...(Array.isArray(system?.uiSurfaces) ? system.uiSurfaces : []),
...(Array.isArray(system?.keyFiles) ? system.keyFiles : []),
...(Array.isArray(system?.apiEndpoints) ? system.apiEndpoints : []),
docText,
];
return parts.join(" ").toLowerCase();
}
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 ranked = systems
.map((system) => {
const docText = kb.docsById.get(String(system.id || "")) || "";
const corpus = buildSystemSearchText(system, docText);
let score = 0;
for (const token of queryTokens) {
if (corpus.includes(token)) {
score += 2;
}
}
for (const alias of Array.isArray(system.aliases) ? system.aliases : []) {
const normalizedAlias = String(alias || "").trim().toLowerCase();
if (normalizedAlias && requestLower.includes(normalizedAlias)) {
score += 5;
}
}
if (String(system.priorityForRequestTriage || "").trim().toLowerCase() === "high") {
score += 1;
}
return {
system,
docText,
score,
};
})
.sort((left, right) => right.score - left.score || String(left.system?.name || "").localeCompare(String(right.system?.name || "")));
const positive = ranked.filter((entry) => entry.score > 0).slice(0, limit);
if (positive.length > 0) {
return positive;
}
return ranked.slice(0, limit);
}
2026-06-27 01:12:35 -04:00
function buildPrompt(request, relevantSystems, schema, kb) {
const schemaSummary = JSON.stringify(schema, null, 2);
2026-06-27 01:12:35 -04:00
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");
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.",
2026-06-27 01:12:35 -04:00
"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\".",
].join("\n"),
},
{
role: "user",
content: [
`Submission id: ${request.id}`,
"Raw submission:",
request.sourceText,
"",
"Return JSON matching this schema:",
schemaSummary,
"",
2026-06-27 01:12:35 -04:00
"Standardized tags you may use:",
tagCatalogSummary,
"",
"Relevant KB systems:",
systemDocs,
].join("\n"),
},
];
}
function extractJsonString(text) {
const rawText = String(text || "").trim();
if (!rawText) {
throw new Error("Model returned empty content.");
}
const fencedMatch = rawText.match(/```(?:json)?\s*([\s\S]*?)```/i);
if (fencedMatch && fencedMatch[1]) {
return fencedMatch[1].trim();
}
const firstBrace = rawText.indexOf("{");
const lastBrace = rawText.lastIndexOf("}");
if (firstBrace >= 0 && lastBrace > firstBrace) {
return rawText.slice(firstBrace, lastBrace + 1);
}
throw new Error("Could not find JSON object in model response.");
}
function readMessageContent(messageContent) {
if (typeof messageContent === "string") {
return messageContent;
}
if (Array.isArray(messageContent)) {
return messageContent
.map((part) => {
if (typeof part === "string") {
return part;
}
if (part && typeof part === "object" && typeof part.text === "string") {
return part.text;
}
return "";
})
.filter(Boolean)
.join("\n");
}
return "";
}
async function callModelApi(config, messages) {
const headers = {
"Content-Type": "application/json",
};
if (config.apiKey) {
headers.Authorization = `Bearer ${config.apiKey}`;
}
const requestBody = {
model: config.model,
temperature: 0.2,
max_tokens: config.maxTokens,
messages,
};
if (config.provider === "deepseek") {
requestBody.response_format = { type: "json_object" };
requestBody.thinking = { type: config.thinking };
}
const payload = await fetchJson(buildUrl(config.modelBaseUrl, "chat/completions"), {
method: "POST",
headers,
body: JSON.stringify(requestBody),
});
const text = readMessageContent(payload?.choices?.[0]?.message?.content);
return JSON.parse(extractJsonString(text));
}
async function getLauncherRequests(config) {
return fetchJson(buildUrl(config.apiBase, "/api/launcher-requests"));
}
async function patchLauncherRequest(config, requestId, body) {
return fetchJson(buildUrl(config.apiBase, `/api/launcher-requests/${encodeURIComponent(requestId)}`), {
method: "PATCH",
2026-06-27 00:51:20 -04:00
headers: buildAdminHeaders(config, {
"Content-Type": "application/json",
2026-06-27 00:51:20 -04:00
}),
body: JSON.stringify(body),
});
}
async function processLauncherRequestAnalysis(config, requestId, body) {
return fetchJson(buildUrl(config.apiBase, `/api/launcher-requests/${encodeURIComponent(requestId)}/process-analysis`), {
method: "POST",
2026-06-27 00:51:20 -04:00
headers: buildAdminHeaders(config, {
"Content-Type": "application/json",
2026-06-27 00:51:20 -04:00
}),
body: JSON.stringify(body),
});
}
function shouldPromoteAnalysis(result, config) {
return result.items.length > 0 && result.items.every((item) => (
item.statusRecommendation === "active"
&& Number.isFinite(item.confidence)
&& item.confidence >= config.promoteThreshold
));
}
async function markRequestProcessing(config, request) {
const now = new Date().toISOString();
return patchLauncherRequest(config, request.id, {
analysis: {
...(request.analysis && typeof request.analysis === "object" && !Array.isArray(request.analysis) ? request.analysis : {}),
state: "processing",
model: config.model,
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
createdAt: request.analysis?.createdAt || now,
updatedAt: now,
error: "",
},
});
}
async function markRequestError(config, request, errorMessage) {
return processLauncherRequestAnalysis(config, request.id, {
action: "error",
model: config.model,
error: String(errorMessage || "Unknown analysis failure."),
analysis: {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
},
});
}
async function markRequestReview(config, request, result) {
return processLauncherRequestAnalysis(config, request.id, {
action: "review",
model: config.model,
analysis: {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
confidence: result.confidence,
items: result.items,
},
});
}
async function promoteRequest(config, request, result) {
return processLauncherRequestAnalysis(config, request.id, {
action: "promote",
model: config.model,
analysis: {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
confidence: result.confidence,
items: result.items,
},
});
}
async function analyzeRequest(config, kb, request) {
const relevantSystems = pickRelevantSystems(kb, request.sourceText, 4);
const systemNames = relevantSystems.map(({ system }) => system.name);
console.log(`Analyzing ${request.id}: ${request.title}`);
console.log(` Systems: ${systemNames.join(", ")}`);
if (!config.dryRun) {
await markRequestProcessing(config, request);
}
2026-06-27 01:12:35 -04:00
const prompt = buildPrompt(request, relevantSystems, kb.requestSchema, kb);
const modelResult = await callModelApi(config, prompt);
2026-06-27 01:12:35 -04:00
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) {
console.log(JSON.stringify({
requestId: request.id,
action,
result: normalizedResult,
}, null, 2));
return { action, result: normalizedResult };
}
if (action === "promote") {
await promoteRequest(config, request, normalizedResult);
} else {
await markRequestReview(config, request, normalizedResult);
}
return { action, result: normalizedResult };
}
function selectPendingRequests(payload, config) {
const requests = Array.isArray(payload?.requests) ? payload.requests : [];
const pending = requests.filter((request) => String(request?.status || "").trim().toLowerCase() === "pending");
if (config.requestId) {
return pending.filter((request) => String(request?.id || "").trim() === config.requestId);
}
return pending
.filter((request) => {
const analysisState = String(request?.analysis?.state || "").trim().toLowerCase();
return !analysisState || analysisState === "unprocessed";
})
.slice(0, Math.max(1, config.limit));
}
async function runSinglePass(config, kb) {
const payload = await getLauncherRequests(config);
const pendingRequests = selectPendingRequests(payload, config);
if (pendingRequests.length === 0) {
console.log("No eligible pending requests found.");
return 0;
}
let processedCount = 0;
for (const request of pendingRequests) {
try {
await analyzeRequest(config, kb, request);
processedCount += 1;
} catch (error) {
console.error(` Failed to analyze ${request.id}: ${String(error)}`);
if (!config.dryRun) {
try {
await markRequestError(config, request, String(error));
} catch (secondaryError) {
console.error(` Failed to record analysis error for ${request.id}: ${String(secondaryError)}`);
}
}
}
}
return processedCount;
}
async function main() {
const config = finalizeConfig(parseArgs(process.argv.slice(2)));
if (config.help) {
printHelp();
return;
}
if (!config.dryRun && !config.model) {
throw new Error("Missing local model id. Set REQUEST_ANALYZER_MODEL or pass --model.");
}
if (!config.dryRun && !config.apiKey) {
throw new Error(
config.provider === "deepseek"
? "Missing DeepSeek API key. Set DEEPSEEK_API_KEY or REQUEST_ANALYZER_API_KEY."
: "Missing model API key. Set REQUEST_ANALYZER_API_KEY or pass --api-key."
);
}
const kb = await loadKnowledgeBase();
if (config.poll) {
console.log(`Watching pending requests every ${config.intervalMs}ms`);
// eslint-disable-next-line no-constant-condition
while (true) {
await runSinglePass(config, kb);
await sleep(config.intervalMs);
}
}
await runSinglePass(config, kb);
}
main().catch((error) => {
console.error(String(error));
process.exitCode = 1;
});