Initial import NDC_1C

This commit is contained in:
2026-03-26 10:38:25 +03:00
commit a162d77ef7
2943 changed files with 3615871 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
const path_1 = __importDefault(require("path"));
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
function toBooleanFlag(value, defaultValue) {
if (!value || value.trim() === "") {
return defaultValue;
}
const lowered = value.trim().toLowerCase();
return !(lowered === "0" || lowered === "false" || lowered === "off" || lowered === "no");
}
exports.PORT = Number(process.env.PORT ?? 8787);
exports.TIMEZONE = process.env.TZ_FALLBACK ?? "Europe/Moscow";
exports.DEFAULT_OPENAI_BASE_URL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
exports.DEFAULT_MODEL = process.env.OPENAI_MODEL ?? "gpt-4o-mini";
exports.DEFAULT_TEMPERATURE = Number(process.env.OPENAI_TEMPERATURE ?? 0);
exports.DEFAULT_MAX_OUTPUT_TOKENS = Number(process.env.OPENAI_MAX_OUTPUT_TOKENS ?? 700);
exports.DEFAULT_PROMPT_VERSION = process.env.DEFAULT_PROMPT_VERSION ?? "normalizer_v2_0_2";
exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1, true);
exports.FEATURE_ASSISTANT_CONTRACTS_V11 = toBooleanFlag(process.env.FEATURE_ASSISTANT_CONTRACTS_V11, true);
exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1, true);
exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1, true);
exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_BROAD_GUARD_V1, true);
exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1, true);
exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1, true);
exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ANSWER_POLICY_V11, false);
exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1, true);
exports.DATA_DIR = process.env.DATA_DIR ?? path_1.default.resolve(exports.MODULE_ROOT, "data");
exports.TRACES_DIR = path_1.default.resolve(exports.DATA_DIR, "traces");
exports.PRESETS_DIR = path_1.default.resolve(exports.DATA_DIR, "presets");
exports.EVAL_CASES_DIR = path_1.default.resolve(exports.DATA_DIR, "eval_cases");
exports.ASSISTANT_SESSIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "assistant_sessions");
exports.PROMPTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "prompts");
exports.REPORTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "reports");
exports.EVAL_DATASETS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "eval_cases");
exports.SCHEMAS_DIR = path_1.default.resolve(exports.BACKEND_ROOT, "src", "schemas");
exports.ARCH_EXPORT_2020_DIR = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт");
+158
View File
@@ -0,0 +1,158 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAccountingAgentRouter = buildAccountingAgentRouter;
const express_1 = require("express");
const http_1 = require("../utils/http");
const PREFIX = "/api/accounting-agent/v1";
function buildAccountingAgentRouter(services) {
const router = (0, express_1.Router)();
const runtime = services.runtimeAdapter;
router.post(`${PREFIX}/runs/start`, (req, res, next) => {
try {
const body = (req.body ?? {});
const run = runtime.startRun({
sessionId: body.sessionId ? String(body.sessionId) : undefined,
initiator: body.initiator ? String(body.initiator) : "operator",
source: body.source ? String(body.source) : "gui",
metadata: (body.metadata ?? {}),
idempotencyKey: body.idempotencyKey ? String(body.idempotencyKey) : undefined
});
(0, http_1.created)(res, {
ok: true,
sessionId: run.sessionId,
runId: run.runId,
status: run.status,
run
});
}
catch (error) {
next(error);
}
});
router.post(`${PREFIX}/runs/finish`, (req, res, next) => {
try {
const body = (req.body ?? {});
const status = String(body.status ?? "DONE");
if (!["DONE", "ERROR", "CANCELLED"].includes(status)) {
throw new http_1.ApiError("INVALID_STATUS", `Invalid finish status: ${status}`, 400);
}
const run = runtime.finishRun({
runId: String(body.runId ?? ""),
status,
source: body.source ? String(body.source) : "gui",
reason: body.reason ? String(body.reason) : undefined,
metadata: (body.metadata ?? {}),
idempotencyKey: body.idempotencyKey ? String(body.idempotencyKey) : undefined
});
(0, http_1.ok)(res, {
ok: true,
run
});
}
catch (error) {
next(error);
}
});
router.get(`${PREFIX}/runs`, (_req, res) => {
(0, http_1.ok)(res, {
ok: true,
items: runtime.listRuns()
});
});
router.get(`${PREFIX}/runs/:runId`, (req, res, next) => {
try {
const run = runtime.getRun(String(req.params.runId));
if (!run) {
throw new http_1.ApiError("RUN_NOT_FOUND", `Run not found: ${req.params.runId}`, 404);
}
(0, http_1.ok)(res, {
ok: true,
run
});
}
catch (error) {
next(error);
}
});
router.post(`${PREFIX}/tasks/enqueue`, (req, res, next) => {
try {
const body = (req.body ?? {});
const task = runtime.enqueueTask({
runId: String(body.runId ?? ""),
payload: (body.payload ?? {}),
source: body.source ? String(body.source) : "gui",
idempotencyKey: body.idempotencyKey ? String(body.idempotencyKey) : undefined
});
(0, http_1.created)(res, {
ok: true,
task
});
}
catch (error) {
next(error);
}
});
router.post(`${PREFIX}/tasks/claim`, (_req, res) => {
const task = runtime.claimTask();
(0, http_1.ok)(res, {
ok: true,
task
});
});
router.post(`${PREFIX}/tasks/:taskId/complete`, (req, res, next) => {
try {
const body = (req.body ?? {});
const task = runtime.completeTask({
taskId: String(req.params.taskId),
result: (body.result ?? {}),
source: body.source ? String(body.source) : "worker",
idempotencyKey: body.idempotencyKey ? String(body.idempotencyKey) : undefined
});
(0, http_1.ok)(res, {
ok: true,
task
});
}
catch (error) {
next(error);
}
});
router.post(`${PREFIX}/tasks/:taskId/error`, (req, res, next) => {
try {
const body = (req.body ?? {});
const task = runtime.failTask({
taskId: String(req.params.taskId),
error: {
code: String(body.code ?? "TASK_ERROR"),
message: String(body.message ?? "Task failed"),
details: body.details
},
source: body.source ? String(body.source) : "worker",
idempotencyKey: body.idempotencyKey ? String(body.idempotencyKey) : undefined
});
(0, http_1.ok)(res, {
ok: true,
task
});
}
catch (error) {
next(error);
}
});
router.get(`${PREFIX}/results`, (_req, res) => {
(0, http_1.ok)(res, {
ok: true,
items: runtime.getResults()
});
});
router.get(`${PREFIX}/trace/run/:runId`, (req, res) => {
(0, http_1.ok)(res, {
ok: true,
items: runtime.getRunTrace(String(req.params.runId))
});
});
router.get(`${PREFIX}/health`, (_req, res) => {
(0, http_1.ok)(res, runtime.health());
});
return router;
}
+49
View File
@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantRouter = buildAssistantRouter;
const express_1 = require("express");
const http_1 = require("../utils/http");
function buildAssistantRouter(services) {
const router = (0, express_1.Router)();
router.post("/api/assistant/message", async (req, res, next) => {
try {
const payload = (req.body ?? {});
const userMessageSource = typeof payload.user_message === "string"
? payload.user_message
: typeof payload.message === "string"
? payload.message
: "";
const userMessage = userMessageSource.trim();
if (!userMessage) {
throw new http_1.ApiError("INVALID_ASSISTANT_MESSAGE", "Field `user_message` or `message` is required.", 400);
}
const response = await services.assistantService.handleMessage({
...payload,
user_message: userMessage,
message: userMessage,
mode: typeof payload.mode === "string" ? payload.mode : "assistant"
});
(0, http_1.ok)(res, response);
}
catch (error) {
next(error);
}
});
router.get("/api/assistant/session/:session_id", (req, res, next) => {
try {
const sessionId = String(req.params.session_id ?? "");
const session = services.assistantService.getSession(sessionId);
if (!session) {
throw new http_1.ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
}
(0, http_1.ok)(res, {
ok: true,
session
});
}
catch (error) {
next(error);
}
});
return router;
}
+35
View File
@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildEvalRouter = buildEvalRouter;
const express_1 = require("express");
const http_1 = require("../utils/http");
function buildEvalRouter(services) {
const router = (0, express_1.Router)();
router.post("/api/eval/run", async (req, res, next) => {
try {
const body = (req.body ?? {});
const report = await services.evalService.run({
normalizeConfig: (body.normalizeConfig ?? {}),
caseIds: Array.isArray(body.caseIds) ? body.caseIds : undefined,
useMock: Boolean(body.useMock),
mode: body.mode ?? "standard",
caseSetFile: typeof body.caseSetFile === "string" ? body.caseSetFile : undefined,
rawQuestions: typeof body.rawQuestions === "string" ? body.rawQuestions : undefined,
evalTarget: body.eval_target ?? "normalizer",
compareWithReportFile: typeof body.compare_with_report_file === "string"
? body.compare_with_report_file
: typeof body.comparisonBaselineReportFile === "string"
? body.comparisonBaselineReportFile
: undefined
});
(0, http_1.ok)(res, {
ok: true,
report
});
}
catch (error) {
next(error);
}
});
return router;
}
+32
View File
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildHistoryRouter = buildHistoryRouter;
const express_1 = require("express");
const traceLogger_1 = require("../services/traceLogger");
const http_1 = require("../utils/http");
function buildHistoryRouter() {
const router = (0, express_1.Router)();
router.get("/api/history", (_req, res) => {
(0, http_1.ok)(res, {
ok: true,
items: (0, traceLogger_1.listTraces)(200)
});
});
router.get("/api/history/:trace_id", (req, res, next) => {
try {
const traceId = String(req.params.trace_id);
const trace = (0, traceLogger_1.getTrace)(traceId);
if (!trace) {
throw new http_1.ApiError("TRACE_NOT_FOUND", `Trace not found: ${traceId}`, 404);
}
(0, http_1.ok)(res, {
ok: true,
trace
});
}
catch (error) {
next(error);
}
});
return router;
}
+19
View File
@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildNormalizeRouter = buildNormalizeRouter;
const express_1 = require("express");
const http_1 = require("../utils/http");
function buildNormalizeRouter(services) {
const router = (0, express_1.Router)();
router.post("/api/normalize", async (req, res, next) => {
try {
const payload = req.body;
const result = await services.normalizerService.normalize(payload);
(0, http_1.ok)(res, result);
}
catch (error) {
next(error);
}
});
return router;
}
+49
View File
@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildPresetsRouter = buildPresetsRouter;
const express_1 = require("express");
const nanoid_1 = require("nanoid");
const config_1 = require("../config");
const promptBuilder_1 = require("../services/promptBuilder");
const traceLogger_1 = require("../services/traceLogger");
const http_1 = require("../utils/http");
function buildPresetsRouter() {
const router = (0, express_1.Router)();
router.get("/api/presets", (_req, res) => {
const stored = (0, traceLogger_1.listPresets)();
const builtin = (0, promptBuilder_1.listBuiltinPromptPresets)();
const combined = [...builtin, ...stored];
(0, http_1.ok)(res, {
ok: true,
default_prompt_version: config_1.DEFAULT_PROMPT_VERSION,
presets: combined
});
});
router.post("/api/presets/save", (req, res, next) => {
try {
const body = (req.body ?? {});
const now = new Date().toISOString();
const preset = {
id: body.id ?? `preset-${(0, nanoid_1.nanoid)(8)}`,
name: body.name ?? "Пользовательский пресет",
createdAt: body.createdAt ?? now,
updatedAt: now,
prompt_version: body.prompt_version ?? config_1.DEFAULT_PROMPT_VERSION,
systemPrompt: body.systemPrompt ?? "",
developerPrompt: body.developerPrompt ?? "",
domainPrompt: body.domainPrompt ?? "",
schemaNotes: body.schemaNotes ?? "",
fewShotExamples: body.fewShotExamples ?? ""
};
(0, traceLogger_1.savePreset)(preset);
(0, http_1.created)(res, {
ok: true,
preset
});
}
catch (error) {
next(error);
}
});
return router;
}
+28
View File
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildTestConnectionRouter = buildTestConnectionRouter;
const express_1 = require("express");
const config_1 = require("../config");
const http_1 = require("../utils/http");
function buildTestConnectionRouter(client) {
const router = (0, express_1.Router)();
router.post("/api/openai/test-connection", async (req, res, next) => {
try {
const body = (req.body ?? {});
const result = await client.testConnection({
apiKey: String(body.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model: String(body.model ?? config_1.DEFAULT_MODEL),
baseUrl: String(body.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL)
});
(0, http_1.ok)(res, {
ok: true,
model: result.model,
timestamp: new Date().toISOString()
});
}
catch (error) {
next(error);
}
});
return router;
}
@@ -0,0 +1,213 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InMemoryRuntimeAdapter = void 0;
const nanoid_1 = require("nanoid");
const http_1 = require("../utils/http");
class InMemoryRuntimeAdapter {
runs = [];
tasks = [];
traces = [];
idempotencyCache = new Map();
now() {
return new Date().toISOString();
}
cacheKey(action, key) {
if (!key || !key.trim())
return null;
return `${action}:${key.trim()}`;
}
readIdempotency(action, idempotencyKey) {
const cache = this.cacheKey(action, idempotencyKey);
if (!cache)
return null;
return this.idempotencyCache.get(cache) ?? null;
}
writeIdempotency(action, idempotencyKey, value) {
const cache = this.cacheKey(action, idempotencyKey);
if (!cache)
return;
this.idempotencyCache.set(cache, value);
}
pushEvent(input) {
this.traces.push({
timestamp: this.now(),
level: input.level ?? "info",
service: "llm_normalizer_backend",
sessionId: input.sessionId,
runId: input.runId,
taskId: input.taskId ?? null,
eventType: input.eventType,
payload: input.payload
});
}
startRun(input) {
const cached = this.readIdempotency("startRun", input.idempotencyKey);
if (cached)
return cached;
const record = {
sessionId: input.sessionId ?? `session_${(0, nanoid_1.nanoid)(8)}`,
runId: `run_${(0, nanoid_1.nanoid)(10)}`,
status: "RUNNING",
initiator: input.initiator ?? "operator",
source: input.source ?? "gui",
createdAt: this.now(),
updatedAt: this.now(),
metadata: input.metadata ?? {}
};
this.runs.unshift(record);
this.pushEvent({
runId: record.runId,
sessionId: record.sessionId,
eventType: "RUN_STARTED",
payload: record.metadata
});
this.writeIdempotency("startRun", input.idempotencyKey, record);
return record;
}
finishRun(input) {
const cached = this.readIdempotency("finishRun", input.idempotencyKey);
if (cached)
return cached;
const record = this.runs.find((item) => item.runId === input.runId);
if (!record) {
throw new http_1.ApiError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, 404);
}
record.status = input.status;
record.updatedAt = this.now();
record.source = input.source ?? record.source;
record.metadata = {
...(record.metadata ?? {}),
...(input.metadata ?? {}),
reason: input.reason ?? null
};
this.pushEvent({
runId: record.runId,
sessionId: record.sessionId,
eventType: `RUN_FINISHED_${input.status}`,
level: input.status === "ERROR" ? "error" : "info",
payload: { reason: input.reason ?? null }
});
this.writeIdempotency("finishRun", input.idempotencyKey, record);
return record;
}
listRuns() {
return [...this.runs];
}
getRun(runId) {
return this.runs.find((item) => item.runId === runId) ?? null;
}
enqueueTask(input) {
const cached = this.readIdempotency("enqueueTask", input.idempotencyKey);
if (cached)
return cached;
const run = this.getRun(input.runId);
if (!run) {
throw new http_1.ApiError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, 404);
}
const task = {
taskId: `task_${(0, nanoid_1.nanoid)(10)}`,
runId: run.runId,
status: "QUEUED",
payload: input.payload,
source: input.source ?? "gui",
createdAt: this.now(),
updatedAt: this.now()
};
this.tasks.unshift(task);
this.pushEvent({
runId: run.runId,
sessionId: run.sessionId,
taskId: task.taskId,
eventType: "TASK_ENQUEUED",
payload: task.payload
});
this.writeIdempotency("enqueueTask", input.idempotencyKey, task);
return task;
}
claimTask() {
const task = this.tasks.find((item) => item.status === "QUEUED");
if (!task) {
return null;
}
task.status = "RUNNING";
task.updatedAt = this.now();
const run = this.getRun(task.runId);
if (run) {
this.pushEvent({
runId: run.runId,
sessionId: run.sessionId,
taskId: task.taskId,
eventType: "TASK_CLAIMED"
});
}
return task;
}
completeTask(input) {
const cached = this.readIdempotency("completeTask", input.idempotencyKey);
if (cached)
return cached;
const task = this.tasks.find((item) => item.taskId === input.taskId);
if (!task) {
throw new http_1.ApiError("TASK_NOT_FOUND", `Task not found: ${input.taskId}`, 404);
}
task.status = "DONE";
task.updatedAt = this.now();
task.result = input.result;
task.source = input.source ?? task.source;
const run = this.getRun(task.runId);
if (run) {
this.pushEvent({
runId: run.runId,
sessionId: run.sessionId,
taskId: task.taskId,
eventType: "TASK_DONE",
payload: input.result
});
}
this.writeIdempotency("completeTask", input.idempotencyKey, task);
return task;
}
failTask(input) {
const cached = this.readIdempotency("failTask", input.idempotencyKey);
if (cached)
return cached;
const task = this.tasks.find((item) => item.taskId === input.taskId);
if (!task) {
throw new http_1.ApiError("TASK_NOT_FOUND", `Task not found: ${input.taskId}`, 404);
}
task.status = "ERROR";
task.updatedAt = this.now();
task.error = input.error;
task.source = input.source ?? task.source;
const run = this.getRun(task.runId);
if (run) {
this.pushEvent({
runId: run.runId,
sessionId: run.sessionId,
taskId: task.taskId,
eventType: "TASK_ERROR",
level: "error",
payload: {
errorCode: input.error.code,
errorMessage: input.error.message
}
});
}
this.writeIdempotency("failTask", input.idempotencyKey, task);
return task;
}
getResults() {
return this.tasks.filter((item) => item.status === "DONE" || item.status === "ERROR");
}
getRunTrace(runId) {
return this.traces.filter((item) => item.runId === runId);
}
health() {
return {
ok: true,
queueDepth: this.tasks.filter((item) => item.status === "QUEUED").length,
runsTotal: this.runs.length
};
}
}
exports.InMemoryRuntimeAdapter = InMemoryRuntimeAdapter;
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+77
View File
@@ -0,0 +1,77 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createApp = createApp;
require("dotenv/config");
const cors_1 = __importDefault(require("cors"));
const express_1 = __importDefault(require("express"));
const config_1 = require("./config");
const accountingAgent_1 = require("./routes/accountingAgent");
const assistant_1 = require("./routes/assistant");
const eval_1 = require("./routes/eval");
const history_1 = require("./routes/history");
const normalize_1 = require("./routes/normalize");
const presets_1 = require("./routes/presets");
const testConnection_1 = require("./routes/testConnection");
const inMemoryRuntimeAdapter_1 = require("./runtime/inMemoryRuntimeAdapter");
const assistantService_1 = require("./services/assistantService");
const assistantSessionStore_1 = require("./services/assistantSessionStore");
const evalService_1 = require("./services/evalService");
const normalizerService_1 = require("./services/normalizerService");
const openaiResponsesClient_1 = require("./services/openaiResponsesClient");
const files_1 = require("./utils/files");
const http_1 = require("./utils/http");
const log_1 = require("./utils/log");
function createApp() {
(0, files_1.ensureDir)(config_1.TRACES_DIR);
(0, files_1.ensureDir)(config_1.PRESETS_DIR);
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
(0, files_1.ensureDir)(config_1.ASSISTANT_SESSIONS_DIR);
const app = (0, express_1.default)();
app.use((0, cors_1.default)());
app.use(express_1.default.json({ type: ["application/json", "application/*+json"], limit: "2mb" }));
const openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient();
const normalizerService = new normalizerService_1.NormalizerService(openaiClient);
const evalService = new evalService_1.EvalService(normalizerService);
const assistantSessionStore = new assistantSessionStore_1.AssistantSessionStore();
const assistantService = new assistantService_1.AssistantService(normalizerService, assistantSessionStore);
const runtimeAdapter = new inMemoryRuntimeAdapter_1.InMemoryRuntimeAdapter();
const services = {
normalizerService,
evalService,
assistantService,
runtimeAdapter
};
app.get("/api/health", (_req, res) => {
(0, http_1.ok)(res, {
ok: true,
service: "llm-normalizer-backend",
status: "RUNNING",
timezone: config_1.TIMEZONE,
now: new Date().toISOString()
});
});
app.use((0, testConnection_1.buildTestConnectionRouter)(openaiClient));
app.use((0, normalize_1.buildNormalizeRouter)(services));
app.use((0, eval_1.buildEvalRouter)(services));
app.use((0, assistant_1.buildAssistantRouter)(services));
app.use((0, history_1.buildHistoryRouter)());
app.use((0, presets_1.buildPresetsRouter)());
app.use((0, accountingAgent_1.buildAccountingAgentRouter)(services));
app.use(http_1.errorMiddleware);
return app;
}
if (require.main === module) {
const app = createApp();
app.listen(config_1.PORT, () => {
(0, log_1.logJson)({
timestamp: new Date().toISOString(),
level: "info",
service: "llm_normalizer_backend",
message: `Backend started on http://localhost:${config_1.PORT}`
});
});
}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+704
View File
@@ -0,0 +1,704 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.composeAssistantAnswer = composeAssistantAnswer;
function fallbackFromSummary(routeSummary) {
if (!routeSummary || routeSummary.mode !== "deterministic_v2") {
return "none";
}
return routeSummary.fallback.type;
}
function uniqueStrings(values, limit = 6) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
}
function formatList(items) {
if (items.length === 0) {
return "";
}
return items.map((item) => `- ${item}`).join("\n");
}
function extractTopFacts(results) {
const lines = [];
for (const result of results.filter((item) => item.status === "ok").slice(0, 3)) {
if (result.result_type === "chain") {
const top = result.items.slice(0, 3).map((item) => {
const counterparty = String(item.counterparty_id ?? "не указан");
const operations = String(item.operations_count ?? "0");
const docs = String(item.document_refs_count ?? "0");
return `Контрагент ${counterparty}: операций ${operations}, документов в связке ${docs}.`;
});
lines.push(...top);
continue;
}
if (result.result_type === "ranking") {
const top = result.items
.slice(0, 5)
.map((item) => `${item.rank ?? "•"}. ${String(item.entity ?? "Сущность")} — ${String(item.records_count ?? 0)}.`);
lines.push(...top);
continue;
}
if (result.result_type === "list") {
const top = result.items.slice(0, 5).map((item) => {
if (item.risk_score !== undefined) {
return `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}) — риск ${String(item.risk_score)}.`;
}
return `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}).`;
});
lines.push(...top);
continue;
}
const top = result.items
.slice(0, 3)
.map((item) => `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}).`);
lines.push(...top);
}
return lines;
}
function extractWhyIncluded(results) {
return uniqueStrings(results.flatMap((item) => item.why_included));
}
function extractSelectionReasons(results) {
return uniqueStrings(results.flatMap((item) => item.selection_reason));
}
function extractRiskFactors(results) {
return uniqueStrings(results.flatMap((item) => item.risk_factors));
}
function extractBusinessInterpretation(results) {
return uniqueStrings(results.flatMap((item) => item.business_interpretation));
}
function extractLimitations(results) {
return uniqueStrings(results.flatMap((item) => item.limitations));
}
function summaryValue(result, key) {
const summary = result.summary ?? {};
return Object.prototype.hasOwnProperty.call(summary, key) ? summary[key] : undefined;
}
function summaryBoolean(result, key) {
return summaryValue(result, key) === true;
}
function summaryString(result, key) {
const value = summaryValue(result, key);
return typeof value === "string" ? value : null;
}
function suggestNextStep(requirements, coverage) {
const next = [];
if (coverage.clarification_needed_for.length > 0) {
next.push("Уточните период, счет, документ или контрагента для требований: " + coverage.clarification_needed_for.join(", ") + ".");
}
if (coverage.requirements_uncovered.length > 0) {
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
}
if (coverage.out_of_scope_requirements.length > 0) {
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
}
if (next.length === 0 && requirements.length > 0) {
next.push("Следующим шагом можно открыть технический разбор и углубить проверку по выбранным объектам.");
}
return next;
}
function flattenEvidence(results) {
return results.flatMap((item) => item.evidence);
}
function buildClaimEvidenceLinks(results) {
const byClaim = new Map();
for (const evidence of flattenEvidence(results)) {
const claimRef = String(evidence.claim_ref ?? "").trim();
const evidenceId = String(evidence.evidence_id ?? "").trim();
if (!claimRef || !evidenceId) {
continue;
}
const current = byClaim.get(claimRef) ?? [];
current.push(evidenceId);
byClaim.set(claimRef, current);
}
return Array.from(byClaim.entries())
.slice(0, 10)
.map(([claim_ref, evidenceIds]) => ({
claim_ref,
evidence_ids: uniqueStrings(evidenceIds, 10)
}));
}
function aggregatePolicySignals(results) {
const broad_query_detected = results.some((item) => summaryBoolean(item, "broad_query_detected"));
const broad_result_flag = results.some((item) => summaryBoolean(item, "broad_result_flag"));
const minimum_evidence_failed = results.some((item) => summaryBoolean(item, "minimum_evidence_failed"));
let degraded_to = null;
for (const result of results) {
const degraded = summaryString(result, "degraded_to");
if (degraded === "clarification") {
degraded_to = "clarification";
break;
}
if (degraded === "partial") {
degraded_to = "partial";
}
}
const narrowingOrder = {
weak: 0,
medium: 1,
strong: 2
};
let narrowing_strength = null;
for (const result of results) {
const value = summaryString(result, "narrowing_strength");
if (value !== "weak" && value !== "medium" && value !== "strong") {
continue;
}
if (!narrowing_strength || narrowingOrder[value] < narrowingOrder[narrowing_strength]) {
narrowing_strength = value;
}
}
return {
broad_query_detected,
broad_result_flag,
minimum_evidence_failed,
degraded_to,
narrowing_strength
};
}
function confidenceToScore(value) {
if (value === "high")
return 3;
if (value === "medium")
return 2;
return 1;
}
function aggregateConfidence(results, evidenceItems) {
const scores = [];
for (const evidence of evidenceItems) {
scores.push(confidenceToScore(evidence.confidence));
}
for (const result of results) {
if (result.status === "error") {
continue;
}
scores.push(confidenceToScore(result.confidence));
}
if (scores.length === 0) {
return "low";
}
const average = scores.reduce((acc, item) => acc + item, 0) / scores.length;
if (average >= 2.6)
return "high";
if (average >= 1.8)
return "medium";
return "low";
}
function collectLimitationReasonCodes(evidenceItems) {
const codes = evidenceItems
.map((item) => item.limitation?.reason_code ?? null)
.filter((item) => Boolean(item));
return uniqueStrings(codes, 8);
}
function limitationReasonToText(code) {
if (code === "snapshot_only")
return "Evidence is snapshot-only and may lag source-of-record.";
if (code === "heuristic_inference")
return "Part of the conclusion relies on heuristic inference.";
if (code === "missing_mechanism")
return "Mechanism is unresolved for part of the evidence.";
if (code === "weak_source_mapping")
return "Source mapping is weak for part of the evidence.";
if (code === "insufficient_detail")
return "Evidence lacks detail for a strong factual claim.";
return "Some evidence limitations remain unresolved.";
}
function detectMissingAnchors(userMessage) {
const lower = String(userMessage ?? "").toLowerCase();
const hasPeriod = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/.test(lower);
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b\d{2}(?:\.\d{2})?\b)/i.test(lower);
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
return {
period: !hasPeriod,
account: !hasAccount,
documentOrObject: !hasDocumentOrObject,
counterparty: !hasCounterparty,
anomalyType: !hasAnomalyType
};
}
function buildClarificationQuestions(input) {
const questions = [];
const shouldAsk = input.mode === "clarification_required" || input.coverageReport.clarification_needed_for.length > 0;
if (!shouldAsk) {
return questions;
}
if (input.missingAnchors.period) {
questions.push("Уточните период проверки (например, 2020-06).");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
}
if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
}
if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или группу контрагентов.");
}
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(questions, 6);
}
function buildRecommendedActions(input) {
const actions = [];
if (input.mode === "focused_grounded") {
actions.push("Проверьте 1-2 ключевые записи по source_ref и зафиксируйте итог в рабочем файле проверки.");
}
if (input.mode === "broad_partial") {
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
}
if (input.mode === "clarification_required") {
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
}
if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
}
if (input.coverageReport.requirements_partially_covered.length > 0) {
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
}
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
}
if (input.limitationReasonCodes.includes("snapshot_only")) {
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
}
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
}
if (input.sourceRefs.length > 0) {
actions.push(`Начните проверку с source_ref: ${input.sourceRefs.slice(0, 2).join(", ")}.`);
}
return uniqueStrings(actions, 6);
}
function firstMeaningfulFact(results) {
const facts = extractTopFacts(results);
return facts.length > 0 ? facts[0] : null;
}
function buildPolicyDecision(input) {
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
if (input.fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
return {
mode: "out_of_scope",
fallback_type: "out_of_scope",
reply_type: "out_of_scope"
};
}
if (input.groundingCheck.status === "route_mismatch_blocked") {
return {
mode: "route_mismatch",
fallback_type: "partial",
reply_type: "route_mismatch_blocked"
};
}
if ((input.policySignals.degraded_to === "clarification" && input.policySignals.minimum_evidence_failed) ||
(input.fallbackType === "clarification" && !input.hasSupport) ||
(input.groundingCheck.status === "no_grounded_answer" && !input.hasSupport)) {
return {
mode: "clarification_required",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (input.errorResults.length > 0 && input.okResults.length === 0 && input.partialResults.length === 0) {
return {
mode: "backend_error",
fallback_type: input.fallbackType,
reply_type: "backend_error"
};
}
if (input.okResults.length === 0 && input.partialResults.length === 0 && input.emptyResults.length > 0) {
return {
mode: "empty",
fallback_type: input.fallbackType,
reply_type: "empty_but_valid"
};
}
if (input.groundingCheck.status === "no_grounded_answer" && input.okResults.length === 0 && input.partialResults.length === 0) {
return {
mode: "no_grounded",
fallback_type: input.fallbackType,
reply_type: "no_grounded_answer"
};
}
if (input.focusedStrong &&
!input.policySignals.broad_query_detected &&
!input.policySignals.minimum_evidence_failed &&
!hasCoverageGaps) {
return {
mode: "focused_grounded",
fallback_type: "none",
reply_type: "factual_with_explanation"
};
}
if (input.okResults.length > 0 ||
input.partialResults.length > 0 ||
hasCoverageGaps ||
input.policySignals.minimum_evidence_failed ||
input.policySignals.broad_result_flag ||
input.groundingCheck.status === "partial") {
return {
mode: "broad_partial",
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
return {
mode: "backend_error",
fallback_type: "unknown",
reply_type: "backend_error"
};
}
function buildAnswerSummary(mode) {
if (mode === "focused_grounded")
return "Сформирован прямой ответ на основе подтвержденной опоры.";
if (mode === "broad_partial")
return "Вывод ограничен: есть частичная опора, но не полный coverage.";
if (mode === "clarification_required")
return "Нужны уточнения: без сужения strong factual вывод ненадежен.";
if (mode === "out_of_scope")
return "Запрос вне доступного учетного контура.";
if (mode === "route_mismatch")
return "Результат маршрута не совпал с предметом вопроса.";
if (mode === "empty")
return "В текущем срезе данных релевантные записи не обнаружены.";
if (mode === "no_grounded")
return "Недостаточно опоры для обоснованного ответа.";
return "Не удалось собрать обоснованный ответ по текущему запросу.";
}
function buildDirectAnswer(input) {
const topFact = firstMeaningfulFact(input.retrievalResults);
if (input.mode === "focused_grounded") {
return topFact ?? "Подтвержденный результат получен; можно продолжать предметную проверку без деградации.";
}
if (input.mode === "broad_partial") {
if (topFact) {
return `Доступен ограниченный подтвержденный фрагмент: ${topFact}`;
}
return "Есть только ограниченная опора; вывод дан в частичном режиме без ложной точности.";
}
if (input.mode === "clarification_required") {
return "Текущий запрос слишком широкий или недоопределен; надежный factual вывод пока невозможен.";
}
if (input.mode === "out_of_scope") {
return "Могу отвечать только в пределах данных доступного учетного контура.";
}
if (input.mode === "route_mismatch") {
return "Предмет результата не совпал с предметом вопроса; требуется уточнение фокуса.";
}
if (input.mode === "empty") {
return "В текущем срезе данных проблемные записи по заданному условию не найдены.";
}
if (input.mode === "no_grounded") {
return "Недостаточно подтвержденной опоры для ответа в требуемой точности.";
}
if (input.policySignals.minimum_evidence_failed) {
return "Маршрут отработал, но минимальная evidence-опора не пройдена.";
}
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
}
function renderPolicyReply(structure) {
const mechanismLines = [`status=${structure.mechanism_block.status}`];
if (structure.mechanism_block.mechanism_notes.length > 0) {
mechanismLines.push(...structure.mechanism_block.mechanism_notes.map((item) => `note: ${item}`));
}
if (structure.mechanism_block.limitation_reason_codes.length > 0) {
mechanismLines.push(`limitation_codes: ${structure.mechanism_block.limitation_reason_codes.join(", ")}`);
}
if (structure.mechanism_block.status === "unresolved" && structure.mechanism_block.mechanism_notes.length === 0) {
mechanismLines.push("mechanism_note is intentionally omitted due to weak or missing mechanism evidence");
}
const evidenceLines = [
`coverage=${structure.evidence_block.coverage_note}`,
`evidence_ids=${structure.evidence_block.evidence_ids.length > 0 ? structure.evidence_block.evidence_ids.join(", ") : "none"}`
];
if (Array.isArray(structure.evidence_block.source_refs) && structure.evidence_block.source_refs.length > 0) {
evidenceLines.push(`source_refs=${structure.evidence_block.source_refs.join(", ")}`);
}
if (Array.isArray(structure.evidence_block.claim_evidence_links) && structure.evidence_block.claim_evidence_links.length > 0) {
const compactLinks = structure.evidence_block.claim_evidence_links
.slice(0, 4)
.map((item) => `${item.claim_ref}:${item.evidence_ids.join("|")}`);
evidenceLines.push(`claim_evidence_links=${compactLinks.join("; ")}`);
}
const uncertaintyLines = [
...structure.uncertainty_block.open_uncertainties.map((item) => `open: ${item}`),
...structure.uncertainty_block.limitations.map((item) => `limit: ${item}`)
];
if (uncertaintyLines.length === 0) {
uncertaintyLines.push("No material uncertainty detected in current scoped answer.");
}
const nextStepLines = [
...structure.next_step_block.recommended_actions.map((item) => `action: ${item}`),
...structure.next_step_block.clarification_questions.map((item) => `clarify: ${item}`)
];
if (nextStepLines.length === 0) {
nextStepLines.push("No additional action is required for this scoped answer.");
}
return [
`Answer summary: ${structure.answer_summary}`,
`Direct answer:\n${structure.direct_answer}`,
`Mechanism block:\n${formatList(mechanismLines)}`,
`Evidence block:\n${formatList(evidenceLines)}`,
`Uncertainty block:\n${formatList(uncertaintyLines)}`,
`Next step block:\n${formatList(nextStepLines)}`
]
.filter(Boolean)
.join("\n\n");
}
function composeAssistantAnswerV11(input) {
const fallbackType = fallbackFromSummary(input.routeSummary);
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
const evidenceItems = flattenEvidence(input.retrievalResults);
const policySignals = aggregatePolicySignals(input.retrievalResults);
const limitationReasonCodes = collectLimitationReasonCodes(evidenceItems);
const sourceRefs = uniqueStrings(evidenceItems
.map((item) => item.source_ref?.canonical_ref)
.filter((item) => typeof item === "string" && item.trim().length > 0), 8);
const mechanismNotes = uniqueStrings(evidenceItems
.map((item) => item.mechanism_note)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
const hasSupport = okResults.length > 0 ||
partialResults.length > 0 ||
evidenceItems.length > 0 ||
input.retrievalResults.some((item) => item.items.length > 0);
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
const hasCriticalEvidenceLimitation = limitationReasonCodes.includes("weak_source_mapping") ||
limitationReasonCodes.includes("insufficient_detail");
const hasNonLowRouteConfidence = input.retrievalResults.some((item) => item.status === "ok" && item.confidence !== "low");
const focusedStrong = okResults.length > 0 &&
input.groundingCheck.status === "grounded" &&
!hasCoverageGaps &&
!policySignals.broad_query_detected &&
!policySignals.broad_result_flag &&
!policySignals.minimum_evidence_failed &&
!hasCriticalEvidenceLimitation &&
(aggregateEvidenceConfidence !== "low" || hasNonLowRouteConfidence);
const decision = buildPolicyDecision({
fallbackType,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
okResults,
partialResults,
emptyResults,
errorResults,
hasSupport,
focusedStrong,
policySignals
});
const missingAnchors = detectMissingAnchors(input.userMessage);
const clarificationQuestions = buildClarificationQuestions({
mode: decision.mode,
missingAnchors,
coverageReport: input.coverageReport,
policySignals
});
const recommendedActions = buildRecommendedActions({
mode: decision.mode,
coverageReport: input.coverageReport,
policySignals,
limitationReasonCodes,
sourceRefs
});
const limitations = uniqueStrings([
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
...extractLimitations(input.retrievalResults),
...input.groundingCheck.reasons,
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
...(policySignals.broad_query_detected && policySignals.narrowing_strength === "weak"
? ["Broad query remains weakly narrowed; precision is intentionally limited."]
: [])
], 10);
const openUncertainties = uniqueStrings([
...input.groundingCheck.missing_requirements,
...(decision.mode === "clarification_required" && missingAnchors.period ? ["missing_anchor:period"] : []),
...(decision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
...(decision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
...(decision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : [])
], 8);
const mechanismStatus = mechanismNotes.length === 0
? "unresolved"
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
? "limited"
: "grounded";
const answerStructure = {
schema_version: "answer_structure_v1_1",
answer_summary: buildAnswerSummary(decision.mode),
direct_answer: buildDirectAnswer({
mode: decision.mode,
retrievalResults: input.retrievalResults,
policySignals
}),
mechanism_block: {
status: mechanismStatus,
mechanism_notes: mechanismNotes,
limitation_reason_codes: limitationReasonCodes
},
evidence_block: {
evidence_ids: uniqueStrings(evidenceItems.map((item) => item.evidence_id), 10),
source_refs: sourceRefs,
mechanism_notes: mechanismNotes,
coverage_note: input.coverageReport.requirements_total > 0 &&
input.coverageReport.requirements_total === input.coverageReport.requirements_covered &&
input.coverageReport.requirements_uncovered.length === 0 &&
input.coverageReport.requirements_partially_covered.length === 0
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(claimEvidenceLinks.length > 0
? {
claim_evidence_links: claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: openUncertainties,
limitations
},
next_step_block: {
recommended_actions: recommendedActions,
clarification_questions: clarificationQuestions
}
};
return {
assistant_reply: renderPolicyReply(answerStructure),
fallback_type: decision.fallback_type,
reply_type: decision.reply_type,
answer_structure_v11: answerStructure
};
}
function composeExplainableAnswer(input, scopeLabel) {
const facts = extractTopFacts(input.retrievalResults);
const whyIncluded = extractWhyIncluded(input.retrievalResults);
const selectionReasons = extractSelectionReasons(input.retrievalResults);
const riskFactors = extractRiskFactors(input.retrievalResults);
const interpretation = extractBusinessInterpretation(input.retrievalResults);
const limitations = uniqueStrings([...extractLimitations(input.retrievalResults), ...input.groundingCheck.reasons]);
const nextSteps = suggestNextStep(input.requirements, input.coverageReport);
const lead = scopeLabel === "full"
? "Итог: запрос обработан по предмету, найденные объекты подтверждены данными контура."
: "Итог: запрос обработан частично, ниже подтвержденная часть и ограничения.";
return [
lead,
facts.length > 0 ? "Подтвержденные результаты:\n" + formatList(facts) : "",
whyIncluded.length > 0 ? "Почему это попало в ответ:\n" + formatList(whyIncluded) : "",
selectionReasons.length > 0 ? "Основание отбора:\n" + formatList(selectionReasons) : "",
riskFactors.length > 0 ? "Подтверждающие признаки:\n" + formatList(riskFactors) : "",
interpretation.length > 0 ? "Практический смысл:\n" + formatList(interpretation) : "",
limitations.length > 0 ? "Ограничения:\n" + formatList(limitations) : "",
nextSteps.length > 0 ? "Что проверить дальше:\n" + formatList(nextSteps) : ""
]
.filter(Boolean)
.join("\n\n");
}
function composeAssistantAnswer(input) {
if (input.enableAnswerPolicyV11) {
return composeAssistantAnswerV11(input);
}
const fallbackType = fallbackFromSummary(input.routeSummary);
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
const hasBroadMinimumEvidenceSignal = input.retrievalResults.some((item) => summaryBoolean(item, "broad_guard_applied") && summaryBoolean(item, "minimum_evidence_failed"));
const hasBroadClarificationSignal = input.retrievalResults.some((item) => summaryBoolean(item, "broad_guard_applied") &&
summaryBoolean(item, "minimum_evidence_failed") &&
summaryString(item, "degraded_to") === "clarification");
if (fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
return {
assistant_reply: "Я могу отвечать только по данным вашей учетной базы. Этот запрос выходит за рамки доступного контура.",
fallback_type: "out_of_scope",
reply_type: "out_of_scope"
};
}
if (input.groundingCheck.status === "route_mismatch_blocked") {
return {
assistant_reply: [
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
"Уточните формулировку (например, нужный счет/участок учета), и я выполню повторный проход."
].join("\n\n"),
fallback_type: "partial",
reply_type: "route_mismatch_blocked"
};
}
if (input.groundingCheck.status === "no_grounded_answer" && okResults.length === 0 && !hasBroadMinimumEvidenceSignal) {
return {
assistant_reply: "Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
fallback_type: fallbackType,
reply_type: "no_grounded_answer"
};
}
if (hasBroadClarificationSignal && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (fallbackType === "clarification" && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы закрыть все части вопроса корректно.",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (errorResults.length > 0 && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
fallback_type: fallbackType,
reply_type: "backend_error"
};
}
if (partialResults.length > 0 && okResults.length === 0) {
return {
assistant_reply: composeExplainableAnswer(input, "partial"),
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
if (okResults.length === 0 && partialResults.length === 0 && emptyResults.length > 0) {
return {
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
fallback_type: fallbackType,
reply_type: "empty_but_valid"
};
}
const hasPartialCoverage = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0 ||
input.groundingCheck.status === "partial" ||
errorResults.length > 0;
if (okResults.length > 0 && hasPartialCoverage) {
return {
assistant_reply: composeExplainableAnswer(input, "partial"),
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
if (okResults.length > 0) {
return {
assistant_reply: composeExplainableAnswer(input, "full"),
fallback_type: "none",
reply_type: "factual_with_explanation"
};
}
return {
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
fallback_type: "unknown",
reply_type: "backend_error"
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,198 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AssistantSessionLogger = void 0;
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const files_1 = require("../utils/files");
function unique(values) {
return Array.from(new Set(values.filter((item) => typeof item === "string" && item.length > 0)));
}
function toObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toStringOrNull(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function extractFragments(assistantItem) {
if (!assistantItem.debug || !Array.isArray(assistantItem.debug.fragments)) {
return [];
}
return assistantItem.debug.fragments
.map((item) => toObject(item))
.filter((item) => item !== null);
}
function extractNormalizedQuestion(userText, assistantItem) {
const normalized = toObject(assistantItem.debug?.normalized);
if (normalized) {
const fromUserMessageRaw = toStringOrNull(normalized.user_message_raw);
if (fromUserMessageRaw)
return fromUserMessageRaw;
const fromUserQuestionRaw = toStringOrNull(normalized.user_question_raw);
if (fromUserQuestionRaw)
return fromUserQuestionRaw;
const fromNormalizedQuestion = toStringOrNull(normalized.normalized_question);
if (fromNormalizedQuestion)
return fromNormalizedQuestion;
}
const fragments = extractFragments(assistantItem);
if (fragments.length > 0) {
const joined = fragments
.map((fragment) => toStringOrNull(fragment.normalized_fragment_text) ?? toStringOrNull(fragment.raw_fragment_text))
.filter((item) => Boolean(item))
.join(" | ");
if (joined) {
return joined;
}
}
return userText;
}
function buildRouteLookup(assistantItem) {
const output = new Map();
if (!assistantItem.debug || !Array.isArray(assistantItem.debug.routes)) {
return output;
}
for (const route of assistantItem.debug.routes) {
const routeObject = toObject(route);
if (!routeObject)
continue;
const fragmentId = toStringOrNull(routeObject.fragment_id);
if (!fragmentId)
continue;
output.set(fragmentId, routeObject);
}
return output;
}
function buildDecompositionLines(assistantItem) {
const fragments = extractFragments(assistantItem);
if (fragments.length === 0) {
return ["Фрагменты декомпозиции не выделены."];
}
const routeLookup = buildRouteLookup(assistantItem);
return fragments.map((fragment, index) => {
const fragmentId = toStringOrNull(fragment.fragment_id) ?? `F${index + 1}`;
const fragmentText = toStringOrNull(fragment.normalized_fragment_text) ??
toStringOrNull(fragment.raw_fragment_text) ??
"текст фрагмента отсутствует";
const executionReadiness = toStringOrNull(fragment.execution_readiness);
const routeStatus = toStringOrNull(fragment.route_status);
const routeObject = routeLookup.get(fragmentId);
const route = toStringOrNull(routeObject?.route);
const noRouteReason = toStringOrNull(fragment.no_route_reason) ?? toStringOrNull(routeObject?.no_route_reason);
const parts = [`${fragmentId}: ${fragmentText}`];
if (executionReadiness)
parts.push(`execution_readiness=${executionReadiness}`);
if (routeStatus)
parts.push(`route_status=${routeStatus}`);
if (route)
parts.push(`route=${route}`);
if (noRouteReason)
parts.push(`no_route_reason=${noRouteReason}`);
return parts.join("; ");
});
}
function toHumanBlock(input) {
const lines = [];
lines.push(`Вопрос: ${input.questionRaw}`);
lines.push(`Понято как: ${input.questionUnderstood}`);
lines.push("Декомпозиция:");
lines.push(...input.decomposition.map((item) => `- ${item}`));
lines.push(`Ответ: ${input.answer}`);
return lines.join("\n");
}
function buildTurns(items) {
const turns = [];
const pendingUsers = [];
for (const item of items) {
if (item.role === "user") {
pendingUsers.push(item);
continue;
}
const pairedUser = pendingUsers.shift();
if (!pairedUser) {
continue;
}
const questionRaw = pairedUser.text;
const questionUnderstood = extractNormalizedQuestion(questionRaw, item);
const decomposition = buildDecompositionLines(item);
const answer = item.text;
turns.push({
turn_id: `turn-${turns.length + 1}`,
started_at: pairedUser.created_at ?? null,
completed_at: item.created_at ?? null,
human_block: toHumanBlock({
questionRaw,
questionUnderstood,
decomposition,
answer
}),
human_readable: {
question_raw: questionRaw,
question_understood: questionUnderstood,
decomposition,
answer,
reply_type: item.reply_type
},
technical_json: {
trace_id: item.trace_id,
user_message: pairedUser,
assistant_message: item,
debug: item.debug
}
});
}
return turns;
}
class AssistantSessionLogger {
rootDir;
constructor(rootDir = config_1.ASSISTANT_SESSIONS_DIR) {
this.rootDir = rootDir;
}
persistSession(session) {
(0, files_1.ensureDir)(this.rootDir);
const filePath = path_1.default.resolve(this.rootDir, `${session.session_id}.json`);
const startedAt = session.items[0]?.created_at ?? session.updated_at;
const userMessages = session.items.filter((item) => item.role === "user").length;
const assistantMessages = session.items.filter((item) => item.role === "assistant").length;
const assistantItems = session.items.filter((item) => item.role === "assistant");
const lastAssistant = assistantItems.length > 0 ? assistantItems[assistantItems.length - 1] : null;
const traceIds = unique(session.items.map((item) => item.trace_id));
const replyTypes = Array.from(new Set(session.items
.map((item) => item.reply_type)
.filter((item) => typeof item === "string" && item.length > 0)));
const turns = buildTurns(session.items);
const record = {
schema_version: "assistant_session_log_v1",
session_id: session.session_id,
started_at: startedAt,
updated_at: session.updated_at,
counters: {
total_messages: session.items.length,
user_messages: userMessages,
assistant_messages: assistantMessages
},
trace_ids: traceIds,
reply_types: replyTypes,
investigation_state: session.investigation_state,
turns,
conversation: session.items,
last_assistant: {
message_id: lastAssistant?.message_id ?? null,
reply_type: lastAssistant?.reply_type ?? null,
trace_id: lastAssistant?.trace_id ?? null,
created_at: lastAssistant?.created_at ?? null
}
};
(0, files_1.writeJsonFile)(filePath, record);
}
}
exports.AssistantSessionLogger = AssistantSessionLogger;
@@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AssistantSessionStore = void 0;
const nanoid_1 = require("nanoid");
const config_1 = require("../config");
const investigationState_1 = require("./investigationState");
const MAX_ITEMS_PER_SESSION = 200;
function cloneItem(item) {
return {
...item,
debug: item.debug ? { ...item.debug } : null
};
}
function cloneSession(state) {
return {
session_id: state.session_id,
updated_at: state.updated_at,
items: state.items.map(cloneItem),
investigation_state: (0, investigationState_1.cloneInvestigationState)(state.investigation_state)
};
}
function normalizeSessionShape(state) {
const legacy = state;
const normalizedItems = Array.isArray(legacy.items) ? legacy.items : [];
const investigationState = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1
? legacy.investigation_state ?? (0, investigationState_1.createEmptyInvestigationState)(state.session_id)
: legacy.investigation_state ?? null;
state.items = normalizedItems;
state.updated_at = typeof legacy.updated_at === "string" && legacy.updated_at.trim() ? legacy.updated_at : new Date().toISOString();
state.investigation_state = investigationState;
return state;
}
class AssistantSessionStore {
sessions = new Map();
ensureSession(sessionId) {
const resolvedId = (sessionId ?? "").trim() || `asst-${(0, nanoid_1.nanoid)(10)}`;
const existing = this.sessions.get(resolvedId);
if (existing) {
return cloneSession(normalizeSessionShape(existing));
}
const created = {
session_id: resolvedId,
updated_at: new Date().toISOString(),
items: [],
investigation_state: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? (0, investigationState_1.createEmptyInvestigationState)(resolvedId) : null
};
this.sessions.set(resolvedId, created);
return cloneSession(created);
}
appendItem(sessionId, item) {
const session = this.ensureMutableSession(sessionId);
session.items.push(item);
if (session.items.length > MAX_ITEMS_PER_SESSION) {
session.items = session.items.slice(session.items.length - MAX_ITEMS_PER_SESSION);
}
session.updated_at = new Date().toISOString();
return cloneItem(item);
}
getSession(sessionId) {
const found = this.sessions.get(sessionId);
return found ? cloneSession(normalizeSessionShape(found)) : null;
}
setInvestigationState(sessionId, state) {
const session = this.ensureMutableSession(sessionId);
session.investigation_state = (0, investigationState_1.cloneInvestigationState)(state);
session.updated_at = new Date().toISOString();
return (0, investigationState_1.cloneInvestigationState)(session.investigation_state);
}
ensureMutableSession(sessionId) {
const existing = this.sessions.get(sessionId);
if (existing) {
return normalizeSessionShape(existing);
}
const created = {
session_id: sessionId,
updated_at: new Date().toISOString(),
items: [],
investigation_state: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? (0, investigationState_1.createEmptyInvestigationState)(sessionId) : null
};
this.sessions.set(sessionId, created);
return created;
}
}
exports.AssistantSessionStore = AssistantSessionStore;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cloneInvestigationState = cloneInvestigationState;
exports.createEmptyInvestigationState = createEmptyInvestigationState;
exports.updateInvestigationState = updateInvestigationState;
const stage1Contracts_1 = require("../types/stage1Contracts");
function uniqueStrings(values) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
}
function capStrings(values, max) {
return uniqueStrings(values).slice(0, max);
}
function detectAccounts(text) {
return capStrings(text.match(/\b\d{2}(?:\.\d{2})?\b/g) ?? [], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
}
function detectPeriod(text) {
const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
if (monthly)
return `${monthly[1]}-${monthly[2]}`;
const yearly = text.match(/\b(20\d{2})\b/);
if (yearly)
return yearly[1];
return null;
}
function deriveDomain(routeSummary) {
if (!routeSummary)
return null;
if (routeSummary.mode === "legacy_v1") {
return routeSummary.route_hint;
}
const routes = routeSummary.decisions.map((item) => item.route).filter((route) => route !== "no_route");
const uniqueRoutes = uniqueStrings(routes);
if (uniqueRoutes.length === 0) {
return "no_route";
}
return uniqueRoutes.join(",");
}
function deriveNarrowingStatus(routeSummary, coverageReport) {
if (!routeSummary) {
return "unknown";
}
if (routeSummary.mode === "legacy_v1") {
return "not_needed";
}
if (routeSummary.fallback.type === "clarification" || coverageReport.clarification_needed_for.length > 0) {
return "needs_clarification";
}
const hasNoRoute = routeSummary.decisions.some((item) => item.route === "no_route");
if (hasNoRoute) {
return "broad_guarded";
}
return routeSummary.decisions.length > 1 ? "applied" : "not_needed";
}
function deriveQueryModeHint(routeSummary) {
if (!routeSummary) {
return "investigation_candidate";
}
if (routeSummary.mode === "legacy_v1") {
return "direct_answer";
}
return routeSummary.fallback.type === "none" ? "direct_answer" : "investigation_candidate";
}
function collectEvidenceRefs(retrievalResults) {
const refs = retrievalResults.flatMap((result) => result.evidence.map((item) => item.evidence_id));
return capStrings(refs, stage1Contracts_1.INVESTIGATION_MAX_EVIDENCE_REFS);
}
function collectOpenUncertainties(coverageReport, retrievalResults) {
const requirementNotes = [
...coverageReport.requirements_uncovered.map((item) => `uncovered:${item}`),
...coverageReport.requirements_partially_covered.map((item) => `partial:${item}`),
...coverageReport.clarification_needed_for.map((item) => `clarify:${item}`),
...coverageReport.out_of_scope_requirements.map((item) => `out_of_scope:${item}`)
];
const limitationNotes = retrievalResults.flatMap((result) => result.limitations).slice(0, 6);
return capStrings([...requirementNotes, ...limitationNotes], stage1Contracts_1.INVESTIGATION_MAX_UNCERTAINTIES);
}
function cloneInvestigationState(state) {
if (!state)
return null;
return {
...state,
focus: {
...state.focus,
primary_accounts: [...state.focus.primary_accounts]
},
evidence_refs: [...state.evidence_refs],
open_uncertainties: [...state.open_uncertainties],
followup_context: state.followup_context
? {
...state.followup_context,
referenced_requirement_ids: [...state.followup_context.referenced_requirement_ids]
}
: null
};
}
function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOString()) {
return {
schema_version: stage1Contracts_1.INVESTIGATION_STATE_SCHEMA_VERSION,
session_id: sessionId,
status: "idle",
turn_index: 0,
updated_at: timestamp,
question_id: null,
focus: {
domain: null,
period: null,
primary_accounts: [],
active_query_subject: null
},
narrowing_status: "unknown",
evidence_refs: [],
open_uncertainties: [],
last_answer_mode: null,
followup_context: null,
query_mode_hint: "direct_answer"
};
}
function updateInvestigationState(input) {
const previous = input.previous;
const focusFromMessage = capStrings(detectAccounts(input.userMessage), stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
const requirementIds = capStrings(input.requirements.map((item) => item.requirement_id), stage1Contracts_1.INVESTIGATION_MAX_REQUIREMENT_LINKS);
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
return {
schema_version: stage1Contracts_1.INVESTIGATION_STATE_SCHEMA_VERSION,
session_id: previous.session_id,
status: "active",
turn_index: previous.turn_index + 1,
updated_at: input.timestamp,
question_id: input.questionId,
focus: {
domain: deriveDomain(input.routeSummary) ?? previous.focus.domain,
period: detectPeriod(input.userMessage) ?? previous.focus.period,
primary_accounts: capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS),
active_query_subject: mainRequirement.slice(0, 180)
},
narrowing_status: deriveNarrowingStatus(input.routeSummary, input.coverageReport),
evidence_refs: capStrings([...collectEvidenceRefs(input.retrievalResults), ...previous.evidence_refs], stage1Contracts_1.INVESTIGATION_MAX_EVIDENCE_REFS),
open_uncertainties: collectOpenUncertainties(input.coverageReport, input.retrievalResults),
last_answer_mode: input.replyType,
followup_context: {
previous_question_id: previous.question_id,
last_user_message: input.userMessage.slice(0, 240),
referenced_requirement_ids: requirementIds
},
query_mode_hint: deriveQueryModeHint(input.routeSummary)
};
}
@@ -0,0 +1,944 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NormalizerService = void 0;
const nanoid_1 = require("nanoid");
const config_1 = require("../config");
const promptBuilder_1 = require("./promptBuilder");
const routeHintAdapter_1 = require("./routeHintAdapter");
const schemaValidator_1 = require("./schemaValidator");
const traceLogger_1 = require("./traceLogger");
const RETRY_INSTRUCTION_V1 = "IMPORTANT: return valid JSON strictly matching schema normalized_query_v1. No markdown.";
const RETRY_INSTRUCTION_V2 = "IMPORTANT: return valid JSON strictly matching schema normalized_query_v2. No markdown.";
const RETRY_INSTRUCTION_V2_0_1 = "IMPORTANT: return valid JSON strictly matching schema normalized_query_v2_0_1. No markdown.";
const RETRY_INSTRUCTION_V2_0_2 = "IMPORTANT: return valid JSON strictly matching schema normalized_query_v2_0_2. No markdown.";
function safeJsonParse(text) {
const cleaned = text.trim().replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/```$/i, "").trim();
return JSON.parse(cleaned);
}
function resolveSchemaVersion(payload) {
const explicit = String(payload.schemaVersion ?? "").toLowerCase().trim();
if (explicit === "v2_0_2" || explicit === "normalized_query_v2_0_2") {
return "v2_0_2";
}
if (explicit === "v2_0_1" || explicit === "normalized_query_v2_0_1") {
return "v2_0_1";
}
if (explicit === "v2" || explicit === "normalized_query_v2") {
return "v2";
}
if (explicit === "v1" || explicit === "normalized_query_v1") {
return "v1";
}
const promptVersion = String(payload.promptVersion ?? config_1.DEFAULT_PROMPT_VERSION).toLowerCase().trim();
if (promptVersion === "normalizer_v2" || promptVersion.startsWith("normalizer_v2")) {
if (promptVersion === "normalizer_v2_0_2") {
return "v2_0_2";
}
if (promptVersion === "normalizer_v2_0_1") {
return "v2_0_1";
}
return "v2";
}
return "v1";
}
function shouldEscalateOutputBudget(rawModelResponse) {
if (!rawModelResponse || typeof rawModelResponse !== "object") {
return false;
}
const root = rawModelResponse;
const status = String(root.status ?? "").toLowerCase();
const details = (root.incomplete_details ?? {});
const reason = String(details.reason ?? "").toLowerCase();
return status === "incomplete" && reason === "max_output_tokens";
}
function computeRetryMaxOutputTokens(current, rawModelResponse) {
if (!shouldEscalateOutputBudget(rawModelResponse)) {
return current;
}
const escalated = Math.max(current + 400, Math.ceil(current * 1.6));
return Math.min(escalated, 2400);
}
function collectDateSpans(text) {
const spans = [];
const datePattern = /\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g;
let match = null;
while ((match = datePattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
return spans;
}
function intersectsAnySpan(start, end, spans) {
return spans.some((span) => start < span.end && end > span.start);
}
function extractAccounts(text) {
const lower = String(text ?? "").toLowerCase();
const explicitAccounts = new Set();
const contextualPattern = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
let contextual = null;
while ((contextual = contextualPattern.exec(lower)) !== null) {
if (contextual[1]) {
explicitAccounts.add(contextual[1]);
}
}
if (explicitAccounts.size > 0) {
return Array.from(explicitAccounts);
}
const spans = collectDateSpans(lower);
const extracted = [];
const genericPattern = /\b\d{2}(?:\.\d{2})?\b/g;
let generic = null;
while ((generic = genericPattern.exec(lower)) !== null) {
const value = generic[0];
const start = generic.index;
const end = start + value.length;
if (intersectsAnySpan(start, end, spans)) {
continue;
}
extracted.push(value);
}
return Array.from(new Set(extracted));
}
function detectRouteByHeuristicsV1(question) {
const q = question.toLowerCase();
const hasExactTrace = /(документ\s*(№|#)|\bref\b|\bid\b|строк[аи].*проводк|конкретн(ый|ого|ая).*документ|точн(ый|ого).*источник|trx-\d+|inv-\d+)/i.test(q);
const hasCrossChain = /(разлож|цепоч|чем подтверж|связк|документ.*оплат|закрывающ|взаиморасчет|хвост.*(документ|оплат|проводк))/i.test(q);
const hasPeriodCloseRisk = /(предзакры|закрыти[ея].*период|перед сдачей отчетност|последн(ий|его).*(день|дня)|срыв.*закрыт|может взорвать)/i.test(q);
const hasHeavyOverview = /(рейтинг|топ|в целом|обзор|приоритиз|company|самых|концентрац|срез)/i.test(q);
const hasRiskProbe = /(аномал|подозр|зоны риска|ручной ошиб|подозрительн|риск|хвост)/i.test(q);
const hasRuleControl = /(контрол|правил|ошибк.*дат|срок.*амортиз|настройк|\b97\b|\bос\b|68\.02|ндс)/i.test(q);
if (hasExactTrace) {
return "live_mcp_drilldown";
}
if (hasCrossChain) {
return "hybrid_store_plus_live";
}
if (hasPeriodCloseRisk || hasHeavyOverview) {
return "batch_refresh_then_store";
}
if (hasRiskProbe || hasRuleControl) {
return "store_feature_risk";
}
return "store_canonical";
}
function buildMockNormalizedV1(userQuestion, expectedRoute) {
const q = userQuestion.toLowerCase();
const routeHint = expectedRoute ?? detectRouteByHeuristicsV1(userQuestion);
const hasPeriod = /(январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр|квартал|период|конец месяца|20\d{2})/i.test(userQuestion);
const hasHeavyGoal = /(рейтинг|топ|обзор|приоритиз|срез|в целом|концентрац|самых)/i.test(q);
const hasCloseRisk = /(предзакры|закрыти[ея].*период|срыв.*закрыт|последн.*день)/i.test(q);
const hasRule = /(правил|контрол|ошибк.*дат|амортиз|настройк|\b97\b|ндс|\b01\b|\b02\b)/i.test(q);
const hasAnomaly = /(аномал|подозр|риск|хвост|не сход|завис|крив)/i.test(q);
const hasExactTrace = routeHint === "live_mcp_drilldown";
let intentClass = "simple_factual";
if (routeHint === "live_mcp_drilldown") {
intentClass = "drilldown_explain";
}
else if (routeHint === "hybrid_store_plus_live") {
intentClass = "cross_entity";
}
else if (routeHint === "batch_refresh_then_store") {
intentClass = hasCloseRisk && !hasHeavyGoal ? "period_close_risk" : "heavy_analytical";
}
else if (routeHint === "store_feature_risk") {
intentClass = hasRule ? "rule_based_account_control" : hasAnomaly ? "anomaly_probe" : "ambiguous_human_query";
}
const expectedOutputShape = intentClass === "period_close_risk"
? "prioritized_review_list"
: routeHint === "batch_refresh_then_store"
? "ranked_list"
: routeHint === "hybrid_store_plus_live"
? "reconciliation_report"
: routeHint === "live_mcp_drilldown"
? "evidence_chain"
: hasAnomaly
? "anomaly_summary"
: "point_answer";
return {
schema_version: "normalized_query_v1",
user_question_raw: userQuestion,
normalized_question: userQuestion.trim(),
intent_class: intentClass,
business_problem_type: "normalization_playground",
domain_entities: routeHint === "hybrid_store_plus_live" ? ["контрагент", "документ", "проводка"] : ["счет"],
accounts_mentioned: extractAccounts(userQuestion),
documents_mentioned: /документ|реализац|поступлен|выписк|платеж/i.test(userQuestion) ? ["документ"] : [],
registers_mentioned: /регистр|движен/i.test(userQuestion) ? ["регистр"] : [],
period_scope: {
type: hasPeriod ? "inferred" : "missing",
value: hasPeriod ? "2020-06" : null,
confidence: hasPeriod ? "medium" : "low"
},
requires: {
needs_cross_entity_join: routeHint === "hybrid_store_plus_live",
needs_causal_chain: routeHint === "hybrid_store_plus_live" || /почему|чем подтверж|где рвется/i.test(userQuestion),
needs_exact_object_trace: hasExactTrace,
needs_ranking: routeHint === "batch_refresh_then_store" && intentClass !== "period_close_risk",
needs_anomaly_summary: hasAnomaly && routeHint !== "hybrid_store_plus_live",
needs_runtime_truth: hasExactTrace,
needs_period_cut: hasPeriod,
needs_evidence: routeHint === "hybrid_store_plus_live" || hasExactTrace
},
expected_output_shape: expectedOutputShape,
route_hint: routeHint,
ambiguities: hasPeriod
? []
: [
{
field: "period_scope",
reason: "period is not explicitly provided",
severity: "medium"
}
],
confidence: {
overall: hasPeriod ? "medium" : "low",
intent_class: "medium",
route_hint: hasPeriod ? "medium" : "low"
}
};
}
function applyConfidenceGuardV1(item) {
const wordCount = item.user_question_raw.trim().split(/\s+/).filter(Boolean).length;
const hasAmbiguity = item.ambiguities.length > 0;
const longLayeredQuestion = wordCount >= 20;
const uncertainPeriod = item.period_scope.type !== "explicit";
const hasPeriodBoundaryLex = /(предзакры|закрыти[ея].*период|перед сдачей отчетност|перед закрытием)/i.test(item.user_question_raw) &&
/(рейтинг|топ|обзор|summary|срез|концентрац|в целом|приоритиз|самых)/i.test(item.user_question_raw);
const suspicious = hasAmbiguity || longLayeredQuestion || uncertainPeriod || hasPeriodBoundaryLex;
if (!suspicious) {
return item;
}
return {
...item,
confidence: {
...item.confidence,
overall: item.confidence.overall === "high" ? "medium" : item.confidence.overall,
route_hint: item.confidence.route_hint === "high" ? "medium" : item.confidence.route_hint
}
};
}
function splitIntoCandidateFragments(message) {
const primary = message
.split(/[\n;]+|(?<=[.!?])\s+/)
.map((item) => item.replace(/^\s*[-*•]\s*/, "").trim())
.filter(Boolean);
if (primary.length > 0) {
return primary;
}
const fallback = message.trim();
return fallback ? [fallback] : [];
}
function inferTimeScope(text) {
const explicit = text.match(/\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/);
if (explicit) {
return {
type: "explicit",
value: explicit[1],
confidence: "high"
};
}
const inferred = text.match(/(январ[ья]|феврал[ья]|март[ае]?|апрел[ья]|ма[йя]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|квартал|конец месяца|период)/i);
if (inferred) {
return {
type: "inferred",
value: inferred[1],
confidence: "medium"
};
}
return {
type: "missing",
value: null,
confidence: "low"
};
}
function pickCandidateLabels(flags, domainRelevance) {
if (domainRelevance !== "in_scope") {
return [];
}
const labels = [];
if (flags.asks_for_exact_object_trace)
labels.push("drilldown_explain");
if (flags.has_multi_entity_scope && flags.asks_for_chain_explanation)
labels.push("cross_entity");
if (flags.asks_for_rule_check)
labels.push("rule_based_account_control");
if (flags.asks_for_anomaly_scan)
labels.push("anomaly_probe");
if (flags.asks_for_ranking_or_top || flags.asks_for_period_summary)
labels.push("heavy_analytical");
if (flags.mentions_period_close_context && !flags.asks_for_ranking_or_top)
labels.push("period_close_risk");
if (labels.length === 0)
labels.push("simple_factual");
return Array.from(new Set(labels));
}
function buildFragmentV2(rawText, index) {
const text = rawText.trim();
if (text.length < 3) {
return null;
}
const lower = text.toLowerCase();
const noiseOnly = /^(ну|короче|типа|ладно|ага|ок(ей)?)$/i.test(lower);
if (noiseOnly) {
return null;
}
const inScopeTokens = /(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|ндс|амортиз|расходы будущих периодов|рбп|ос|контрагент|оплат|банк|выписк|склад|товар|материал)/i.test(lower);
const translitInScopeTokens = /\b(?:schet|scheta|schetu|schetom|postavsh|kontragent|dokument|doc|oplata|oplati|platezh|vypisk|provodk|realiz|postuplen|nds|os|saldo|hvost|tail|anomali|risk|zakryt)\b/i.test(lower);
const genericAccountingTokens = /(фсбу|налогов(ый|ого)|нк рф|закон|форма отчетности|как правильно в бухгалтерии)/i.test(lower);
const offTopicTokens = /(погода|анекдот|музык|фильм|игр[аы]|рецепт|курс валют в мире)/i.test(lower);
let domainRelevance = "unclear";
let businessScope = "unclear";
if (offTopicTokens) {
domainRelevance = "out_of_scope";
businessScope = "offtopic";
}
else if (genericAccountingTokens && !inScopeTokens && !translitInScopeTokens) {
domainRelevance = "out_of_scope";
businessScope = "generic_accounting";
}
else if (inScopeTokens || translitInScopeTokens) {
domainRelevance = "in_scope";
businessScope = "company_specific_accounting";
}
const entityTokenCount = (lower.match(/(документ|оплат|проводк|контрагент|договор|реализац|поступлен|выписк|закрыт|взаиморасчет|склад|товар|материал)/g) ?? [])
.length;
const translitEntityTokenCount = (lower.match(/\b(?:dokument|oplata|platezh|provodk|kontragent|realiz|postuplen|vypisk|zakryt|schet|sklad|tovar|material)\b/g) ?? []).length;
const entityTokenCountTotal = entityTokenCount + translitEntityTokenCount;
const flags = {
has_multi_entity_scope: entityTokenCountTotal >= 2,
asks_for_chain_explanation: /(цепоч|разлож|почему|чем подтверж|где рвет|связк|логик.*операц)/i.test(lower),
asks_for_ranking_or_top: /(топ|рейтинг|сам(ые|ых)|максимальн|сильнее всего|приоритиз)/i.test(lower),
asks_for_period_summary: /(срез|обзор|в целом|картина периода|summary|по периоду)/i.test(lower),
asks_for_rule_check: /(правил|контрол|корректн|ошибк.*дат|срок списан|амортиз|настройк|проверь)/i.test(lower),
asks_for_anomaly_scan: /(аномал|подозр|риск|хвост|не сход|завис|крив|искажа)/i.test(lower),
asks_for_exact_object_trace: /(документ\s*(№|#)|\bref\b|\bid\b|строк[аи]\s+проводк|операц.*№|trx-\d+|inv-\d+|doc-\d+)/i.test(lower),
asks_for_evidence: /(чем подтверж|документ|проводк|движен|акт сверк|доказат|evidence)/i.test(lower),
mentions_period_close_context: /(закрыти[ея]\s+период|предзакры|конец месяца|сдач[аи]\s+отчетност)/i.test(lower)
};
const translitHints = {
chain: /\b(?:razlozh|pochemu|chem podtver|gde rv|svyaz|razryv|chain)\b/i.test(lower),
rule: /\b(?:prover|check|rule|control|korrekt)\b/i.test(lower),
anomaly: /\b(?:anomal|risk|hvost|tail|mismatch)\b/i.test(lower),
evidence: /\b(?:dokument|provodk|evidence|doc)\b/i.test(lower)
};
if (translitHints.chain)
flags.asks_for_chain_explanation = true;
if (translitHints.rule)
flags.asks_for_rule_check = true;
if (translitHints.anomaly)
flags.asks_for_anomaly_scan = true;
if (translitHints.evidence)
flags.asks_for_evidence = true;
const candidateLabels = pickCandidateLabels(flags, domainRelevance);
let confidence = "medium";
if (domainRelevance === "out_of_scope" || domainRelevance === "unclear") {
confidence = "low";
}
else if (flags.asks_for_exact_object_trace || flags.asks_for_ranking_or_top) {
confidence = "high";
}
return {
fragment_id: `F${index + 1}`,
raw_fragment_text: text,
normalized_fragment_text: text.charAt(0).toUpperCase() + text.slice(1),
domain_relevance: domainRelevance,
business_scope: businessScope,
entity_hints: Array.from(new Set(Array.from(lower.matchAll(/(поставщик|покупател|контрагент|договор|банк|склад|товар|материал|ос|взаиморасчет|реализац|поступлен)/g)).map((item) => item[0]))),
account_hints: extractAccounts(text),
document_hints: Array.from(new Set(Array.from(lower.matchAll(/(документ|реализац|поступлен|платеж|выписк|акт сверк)/g)).map((item) => item[0]))),
register_hints: Array.from(new Set(Array.from(lower.matchAll(/(регистр|движен|остатк|сальдо)/g)).map((item) => item[0]))),
time_scope: inferTimeScope(text),
flags,
candidate_labels: candidateLabels,
confidence
};
}
function buildMockNormalizedV2(userMessage) {
const rawFragments = splitIntoCandidateFragments(userMessage);
const fragments = [];
const discarded = [];
rawFragments.forEach((raw, index) => {
const built = buildFragmentV2(raw, index);
if (!built) {
discarded.push({
raw_fragment_text: raw,
reason: "noise_or_too_short"
});
return;
}
fragments.push(built);
});
const inScopeCount = fragments.filter((item) => item.domain_relevance === "in_scope").length;
const unclearCount = fragments.filter((item) => item.domain_relevance === "unclear").length;
const messageInScope = inScopeCount > 0;
const scopeConfidence = messageInScope ? (unclearCount > 0 ? "medium" : "high") : "low";
const needsClarification = messageInScope && (unclearCount > 0 || fragments.some((item) => item.time_scope.type === "missing"));
return {
schema_version: "normalized_query_v2",
user_message_raw: userMessage,
message_in_scope: messageInScope,
scope_confidence: scopeConfidence,
contains_multiple_tasks: fragments.length > 1,
fragments,
discarded_fragments: discarded,
global_notes: {
needs_clarification: needsClarification,
clarification_reason: needsClarification ? "Недостаточно периода/контекста по части фрагментов." : null
}
};
}
function hasSessionPeriodContext(context) {
if (!context) {
return false;
}
const periodHint = String(context.period_hint ?? "").trim();
const businessContext = String(context.business_context ?? "").toLowerCase();
if (periodHint.length > 0) {
return true;
}
return (businessContext.includes("current_analysis_period") ||
businessContext.includes("active_period") ||
businessContext.includes("рабочий месяц") ||
businessContext.includes("активный период"));
}
function hasBusinessNodeSignals(fragment) {
if (fragment.domain_relevance !== "in_scope") {
return false;
}
return (fragment.entity_hints.length > 0 ||
fragment.account_hints.length > 0 ||
fragment.document_hints.length > 0 ||
fragment.register_hints.length > 0 ||
fragment.candidate_labels.length > 0 ||
Object.values(fragment.flags).some((value) => value));
}
function routeCanBeSelected(fragment) {
if (fragment.domain_relevance !== "in_scope") {
return false;
}
if (fragment.business_scope === "unclear") {
return false;
}
return hasBusinessNodeSignals(fragment);
}
function dedupeSoftAssumptions(input) {
return Array.from(new Set(input));
}
function decideFragmentExecutionPolicy(fragment, sessionContext) {
const softAssumptions = [];
const hasPeriodContext = hasSessionPeriodContext(sessionContext);
const periodIsCritical = fragment.flags.asks_for_period_summary || fragment.flags.mentions_period_close_context || fragment.flags.asks_for_ranking_or_top;
if (fragment.domain_relevance === "out_of_scope") {
return {
execution_readiness: "needs_clarification",
clarification_reason: "fragment_out_of_scope",
soft_assumption_used: []
};
}
if (fragment.domain_relevance === "unclear") {
return {
execution_readiness: "needs_clarification",
clarification_reason: "domain_or_scope_unclear",
soft_assumption_used: []
};
}
if (!hasBusinessNodeSignals(fragment)) {
return {
execution_readiness: "needs_clarification",
clarification_reason: "business_area_not_identified",
soft_assumption_used: []
};
}
if (!routeCanBeSelected(fragment)) {
return {
execution_readiness: "needs_clarification",
clarification_reason: "route_cannot_be_selected_reliably",
soft_assumption_used: []
};
}
if (fragment.time_scope.type === "missing") {
if (hasPeriodContext) {
softAssumptions.push("period_from_session_context");
}
else if (periodIsCritical) {
return {
execution_readiness: "needs_clarification",
clarification_reason: "critical_period_missing",
soft_assumption_used: []
};
}
}
if (fragment.flags.asks_for_anomaly_scan ||
fragment.flags.asks_for_rule_check ||
fragment.flags.asks_for_ranking_or_top ||
fragment.flags.asks_for_period_summary) {
softAssumptions.push("problem_scan_mode_enabled");
}
if (fragment.business_scope === "company_specific_accounting" && fragment.entity_hints.length === 0 && fragment.account_hints.length === 0) {
softAssumptions.push("company_scope_defaulted");
}
const assumptions = dedupeSoftAssumptions(softAssumptions);
if (assumptions.length > 0) {
return {
execution_readiness: "executable_with_soft_assumptions",
clarification_reason: null,
soft_assumption_used: assumptions
};
}
return {
execution_readiness: "executable",
clarification_reason: null,
soft_assumption_used: []
};
}
function toV201Fragment(fragment, sessionContext) {
const policy = decideFragmentExecutionPolicy(fragment, sessionContext);
return {
...fragment,
execution_readiness: policy.execution_readiness,
clarification_reason: policy.clarification_reason,
soft_assumption_used: policy.soft_assumption_used
};
}
function applyClarificationPolicyV201(candidate, userMessage, sessionContext) {
if (!candidate || typeof candidate !== "object") {
return null;
}
const source = candidate;
if (!Array.isArray(source.fragments)) {
return null;
}
const baseFragments = source.fragments
.map((item) => item)
.filter((item) => item && typeof item === "object" && typeof item.fragment_id === "string");
const fragments = baseFragments.map((fragment) => toV201Fragment(fragment, sessionContext));
const inScopeFragments = fragments.filter((fragment) => fragment.domain_relevance === "in_scope");
const blockingFragments = inScopeFragments.filter((fragment) => fragment.execution_readiness === "needs_clarification");
const needsClarification = inScopeFragments.length > 0 && blockingFragments.length === inScopeFragments.length;
return {
schema_version: "normalized_query_v2_0_1",
user_message_raw: String(source.user_message_raw ?? userMessage),
message_in_scope: inScopeFragments.length > 0,
scope_confidence: source.scope_confidence ?? (inScopeFragments.length > 0 ? "medium" : "low"),
contains_multiple_tasks: typeof source.contains_multiple_tasks === "boolean" ? source.contains_multiple_tasks : fragments.length > 1,
fragments,
discarded_fragments: Array.isArray(source.discarded_fragments)
? source.discarded_fragments
: [],
global_notes: {
needs_clarification: needsClarification,
clarification_reason: needsClarification ? blockingFragments[0]?.clarification_reason ?? "clarification_required" : null
}
};
}
function resolveFragmentExecutionStateV202(fragment, sessionContext) {
const v201 = decideFragmentExecutionPolicy(fragment, sessionContext);
if (fragment.domain_relevance === "out_of_scope") {
return {
execution_readiness: "no_route",
clarification_reason: "fragment_out_of_scope",
soft_assumption_used: [],
route_status: "no_route",
no_route_reason: "out_of_scope"
};
}
if (v201.execution_readiness === "needs_clarification") {
return {
execution_readiness: "needs_clarification",
clarification_reason: v201.clarification_reason ?? "insufficient_specificity",
soft_assumption_used: [],
route_status: "no_route",
no_route_reason: "insufficient_specificity"
};
}
if (!routeCanBeSelected(fragment)) {
return {
execution_readiness: "no_route",
clarification_reason: "route_mapping_missing",
soft_assumption_used: [],
route_status: "no_route",
no_route_reason: "missing_mapping"
};
}
// Deterministic no-route guard:
// routable in-scope fragments cannot remain unresolved.
return {
execution_readiness: v201.execution_readiness,
clarification_reason: null,
soft_assumption_used: v201.soft_assumption_used,
route_status: "routed",
no_route_reason: null
};
}
function toV202Fragment(fragment, sessionContext) {
const policy = resolveFragmentExecutionStateV202(fragment, sessionContext);
return {
...fragment,
execution_readiness: policy.execution_readiness,
clarification_reason: policy.clarification_reason,
soft_assumption_used: policy.soft_assumption_used,
route_status: policy.route_status,
no_route_reason: policy.no_route_reason
};
}
function applyExecutionStatePolicyV202(candidate, userMessage, sessionContext) {
if (!candidate || typeof candidate !== "object") {
return null;
}
const source = candidate;
if (!Array.isArray(source.fragments)) {
return null;
}
const baseFragments = source.fragments
.map((item) => item)
.filter((item) => item && typeof item === "object" && typeof item.fragment_id === "string");
const fragments = baseFragments.map((fragment) => toV202Fragment(fragment, sessionContext));
const inScopeFragments = fragments.filter((fragment) => fragment.domain_relevance === "in_scope");
const clarificationBlocks = inScopeFragments.filter((fragment) => fragment.execution_readiness === "needs_clarification");
const needsClarification = inScopeFragments.length > 0 && clarificationBlocks.length === inScopeFragments.length;
return {
schema_version: "normalized_query_v2_0_2",
user_message_raw: String(source.user_message_raw ?? userMessage),
message_in_scope: inScopeFragments.length > 0,
scope_confidence: source.scope_confidence ?? (inScopeFragments.length > 0 ? "medium" : "low"),
contains_multiple_tasks: typeof source.contains_multiple_tasks === "boolean" ? source.contains_multiple_tasks : fragments.length > 1,
fragments,
discarded_fragments: Array.isArray(source.discarded_fragments)
? source.discarded_fragments
: [],
global_notes: {
needs_clarification: needsClarification,
clarification_reason: needsClarification ? clarificationBlocks[0]?.clarification_reason ?? "clarification_required" : null
}
};
}
function buildMockNormalizedV2_0_1(userMessage, sessionContext) {
const v2 = buildMockNormalizedV2(userMessage);
const adjusted = applyClarificationPolicyV201(v2, userMessage, sessionContext);
if (adjusted) {
return adjusted;
}
return {
schema_version: "normalized_query_v2_0_1",
user_message_raw: userMessage,
message_in_scope: v2.message_in_scope,
scope_confidence: v2.scope_confidence,
contains_multiple_tasks: v2.contains_multiple_tasks,
fragments: v2.fragments.map((fragment) => ({
...fragment,
execution_readiness: "needs_clarification",
clarification_reason: "policy_fallback",
soft_assumption_used: []
})),
discarded_fragments: v2.discarded_fragments,
global_notes: {
needs_clarification: true,
clarification_reason: "policy_fallback"
}
};
}
function buildMockNormalizedV2_0_2(userMessage, sessionContext) {
const v2 = buildMockNormalizedV2(userMessage);
const adjusted = applyExecutionStatePolicyV202(v2, userMessage, sessionContext);
if (adjusted) {
return adjusted;
}
return {
schema_version: "normalized_query_v2_0_2",
user_message_raw: userMessage,
message_in_scope: v2.message_in_scope,
scope_confidence: v2.scope_confidence,
contains_multiple_tasks: v2.contains_multiple_tasks,
fragments: v2.fragments.map((fragment) => ({
...fragment,
execution_readiness: "needs_clarification",
clarification_reason: "policy_fallback",
soft_assumption_used: [],
route_status: "no_route",
no_route_reason: "unsupported_fragment_type"
})),
discarded_fragments: v2.discarded_fragments,
global_notes: {
needs_clarification: true,
clarification_reason: "policy_fallback"
}
};
}
function routeHintForHistory(normalized, routeSummary) {
if (!normalized || !routeSummary) {
return null;
}
if (normalized.schema_version === "normalized_query_v1") {
return normalized.route_hint;
}
const decision = routeSummary.mode === "deterministic_v2" ? routeSummary.decisions.find((item) => item.route !== "no_route") : null;
return decision?.route ?? null;
}
function confidenceForHistory(normalized, routeSummary) {
if (!normalized || !routeSummary) {
return null;
}
if (normalized.schema_version === "normalized_query_v1") {
return normalized.confidence.route_hint;
}
return normalized.scope_confidence;
}
function collectTraceCompletenessIssues(input) {
const issues = [];
if (!input.rawModelResponse) {
issues.push("missing_raw_model_output");
}
if (!input.normalized) {
issues.push("missing_parsed_normalized_json");
return issues;
}
if (input.normalized.schema_version === "normalized_query_v1") {
return issues;
}
if (!Array.isArray(input.normalized.fragments)) {
issues.push("missing_parsed_fragments");
return issues;
}
for (const fragment of input.normalized.fragments) {
const needsResolvedExecutionState = input.normalized.schema_version === "normalized_query_v2_0_1" || input.normalized.schema_version === "normalized_query_v2_0_2";
if (needsResolvedExecutionState && !("execution_readiness" in fragment)) {
issues.push(`fragment_${fragment.fragment_id}_missing_execution_readiness`);
}
if (input.normalized.schema_version === "normalized_query_v2_0_2") {
if (!("route_status" in fragment)) {
issues.push(`fragment_${fragment.fragment_id}_missing_route_status`);
}
if (!("no_route_reason" in fragment)) {
issues.push(`fragment_${fragment.fragment_id}_missing_no_route_reason`);
}
}
}
if (!input.routeHintSummary || input.routeHintSummary.mode !== "deterministic_v2") {
issues.push("missing_route_hint_summary_v2");
return issues;
}
const decisionCount = Array.isArray(input.routeHintSummary.decisions) ? input.routeHintSummary.decisions.length : 0;
if (decisionCount !== input.normalized.fragments.length) {
issues.push("route_decision_count_mismatch");
}
return issues;
}
class NormalizerService {
openaiClient;
constructor(openaiClient) {
this.openaiClient = openaiClient;
}
async normalize(payload) {
const traceId = (0, nanoid_1.nanoid)(14);
const startedAt = Date.now();
const model = payload.model ?? config_1.DEFAULT_MODEL;
const baseUrl = payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL;
const temperature = payload.temperature ?? config_1.DEFAULT_TEMPERATURE;
const maxOutputTokens = payload.maxOutputTokens ?? config_1.DEFAULT_MAX_OUTPUT_TOKENS;
const retryPolicy = payload.retryPolicy ?? "default";
const schemaVersion = resolveSchemaVersion(payload);
const promptBundle = (0, promptBuilder_1.buildPromptBundle)({
promptVersion: payload.promptVersion,
systemPrompt: payload.systemPrompt,
developerPrompt: payload.developerPrompt,
domainPrompt: payload.domainPrompt,
schemaNotes: undefined,
fewShotExamples: payload.fewShotExamples
});
let rawModelResponse = null;
let outputText = "";
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
let requestCountForCase = 0;
if (payload.useMock) {
const mock = schemaVersion === "v2"
? buildMockNormalizedV2(payload.userQuestion)
: schemaVersion === "v2_0_2"
? buildMockNormalizedV2_0_2(payload.userQuestion, payload.context)
: schemaVersion === "v2_0_1"
? buildMockNormalizedV2_0_1(payload.userQuestion, payload.context)
: buildMockNormalizedV1(payload.userQuestion, payload.context?.expected_route);
rawModelResponse = { mode: "mock", schema_version: schemaVersion };
outputText = JSON.stringify(mock, null, 2);
}
else {
const apiKey = payload.apiKey ?? process.env.OPENAI_API_KEY;
const firstTry = await this.openaiClient.normalize({
apiKey: String(apiKey ?? ""),
model,
baseUrl,
temperature,
maxOutputTokens
}, {
systemPrompt: promptBundle.systemPrompt,
developerPrompt: promptBundle.combinedDeveloperPrompt,
domainPrompt: promptBundle.domainPrompt,
userQuestion: payload.userQuestion,
schemaVersion
});
requestCountForCase += 1;
rawModelResponse = firstTry.raw;
outputText = firstTry.outputText;
usage = firstTry.usage;
}
let normalizedCandidate;
let validation = { passed: false, errors: ["NO_VALIDATION"] };
try {
normalizedCandidate = safeJsonParse(outputText);
if (schemaVersion === "v2_0_2") {
normalizedCandidate = applyExecutionStatePolicyV202(normalizedCandidate, payload.userQuestion, payload.context);
}
else if (schemaVersion === "v2_0_1") {
normalizedCandidate = applyClarificationPolicyV201(normalizedCandidate, payload.userQuestion, payload.context);
}
validation = (0, schemaValidator_1.validateNormalized)(normalizedCandidate, schemaVersion);
}
catch (error) {
normalizedCandidate = null;
validation = {
passed: false,
errors: [`JSON_PARSE_ERROR: ${error instanceof Error ? error.message : String(error)}`]
};
}
const canRetry = retryPolicy === "default" || retryPolicy === "single-pass-strict";
if (!payload.useMock && !validation.passed && canRetry) {
const retryMaxOutputTokens = computeRetryMaxOutputTokens(maxOutputTokens, rawModelResponse);
const retry = await this.openaiClient.normalize({
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model,
baseUrl,
temperature,
maxOutputTokens: retryMaxOutputTokens
}, {
systemPrompt: promptBundle.systemPrompt,
developerPrompt: promptBundle.combinedDeveloperPrompt,
domainPrompt: promptBundle.domainPrompt,
userQuestion: payload.userQuestion,
schemaVersion,
controlledRetryInstruction: schemaVersion === "v2"
? RETRY_INSTRUCTION_V2
: schemaVersion === "v2_0_2"
? RETRY_INSTRUCTION_V2_0_2
: schemaVersion === "v2_0_1"
? RETRY_INSTRUCTION_V2_0_1
: RETRY_INSTRUCTION_V1
});
requestCountForCase += 1;
rawModelResponse = retry.raw;
outputText = retry.outputText;
usage = retry.usage;
try {
normalizedCandidate = safeJsonParse(outputText);
if (schemaVersion === "v2_0_2") {
normalizedCandidate = applyExecutionStatePolicyV202(normalizedCandidate, payload.userQuestion, payload.context);
}
else if (schemaVersion === "v2_0_1") {
normalizedCandidate = applyClarificationPolicyV201(normalizedCandidate, payload.userQuestion, payload.context);
}
validation = (0, schemaValidator_1.validateNormalized)(normalizedCandidate, schemaVersion);
}
catch (error) {
normalizedCandidate = null;
validation = {
passed: false,
errors: [`JSON_PARSE_ERROR_AFTER_RETRY: ${error instanceof Error ? error.message : String(error)}`]
};
}
}
let normalized = null;
if (validation.passed) {
if (schemaVersion === "v1") {
normalized = applyConfidenceGuardV1(normalizedCandidate);
}
else if (schemaVersion === "v2_0_2") {
normalized = normalizedCandidate;
}
else if (schemaVersion === "v2_0_1") {
normalized = normalizedCandidate;
}
else {
normalized = normalizedCandidate;
}
}
const routeHintSummary = normalized ? (0, routeHintAdapter_1.toRouteHintSummary)(normalized) : null;
const latency = Date.now() - startedAt;
const traceCompletenessIssues = collectTraceCompletenessIssues({
traceId,
schemaVersion,
rawModelResponse: rawModelResponse ?? outputText,
normalized,
routeHintSummary
});
if (traceCompletenessIssues.length > 0) {
console.error(`[trace-completeness] trace_id=${traceId} schema=${schemaVersion} issues=${traceCompletenessIssues.join(",")}`);
}
const response = {
trace_id: traceId,
ok: validation.passed,
normalized,
route_hint_summary: routeHintSummary,
raw_model_output: rawModelResponse ?? outputText,
validation,
usage,
latency_ms: latency,
prompt_version: promptBundle.prompt_version,
schema_version: schemaVersion,
request_count_for_case: requestCountForCase
};
const traceRouteHint = routeHintForHistory(normalized, routeHintSummary);
const traceConfidence = confidenceForHistory(normalized, routeHintSummary);
const traceRecord = {
trace_id: traceId,
timestamp: new Date().toISOString(),
model,
prompt_version: promptBundle.prompt_version,
schema_version: schemaVersion,
case_id: payload.context?.case_id,
user_question_raw: payload.userQuestion,
context: {
period_hint: payload.context?.period_hint ?? null,
business_context: payload.context?.business_context ?? null,
expected_route: payload.context?.expected_route ?? null,
case_id: payload.context?.case_id ?? null,
eval_mode: payload.context?.eval_mode ?? null,
trace_completeness_issues: traceCompletenessIssues
},
request_payload_redacted: (0, traceLogger_1.redactRequestPayload)({
...payload,
apiKey: payload.apiKey ? "***REDACTED***" : undefined
}),
raw_model_response: rawModelResponse ?? outputText,
parsed_normalized_json: normalized,
validation_result: validation,
route_hint_summary: routeHintSummary,
route_hint: traceRouteHint,
confidence: traceConfidence,
usage,
latency_ms: latency,
expected_route: payload.context?.expected_route,
eval_label: payload.context?.eval_label,
eval_mode: payload.context?.eval_mode,
request_count_for_case: requestCountForCase
};
(0, traceLogger_1.saveTrace)(traceRecord);
if (payload.saveAsTestCase && normalized?.schema_version === "normalized_query_v1") {
(0, traceLogger_1.saveEvalCase)({
case_id: `NQ-${Date.now()}`,
raw_question: payload.userQuestion,
expected: {
intent_class: normalized.intent_class,
route_hint: normalized.route_hint,
requires: {
needs_cross_entity_join: normalized.requires.needs_cross_entity_join,
needs_causal_chain: normalized.requires.needs_causal_chain
},
accounts_mentioned: normalized.accounts_mentioned,
expected_output_shape: normalized.expected_output_shape
}
});
}
return response;
}
}
exports.NormalizerService = NormalizerService;
@@ -0,0 +1,166 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAIResponsesClient = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const http_1 = require("../utils/http");
function extractUsage(raw) {
const usage = (raw.usage ?? {});
const input = Number(usage.input_tokens ?? usage.prompt_tokens ?? 0);
const output = Number(usage.output_tokens ?? usage.completion_tokens ?? 0);
const total = Number(usage.total_tokens ?? input + output);
return {
input_tokens: Number.isFinite(input) ? input : 0,
output_tokens: Number.isFinite(output) ? output : 0,
total_tokens: Number.isFinite(total) ? total : 0
};
}
function extractOutputText(raw) {
if (typeof raw.output_text === "string" && raw.output_text.trim().length > 0) {
return raw.output_text;
}
const output = raw.output;
if (Array.isArray(output)) {
for (const item of output) {
if (!item || typeof item !== "object") {
continue;
}
const content = item.content;
if (!Array.isArray(content)) {
continue;
}
for (const c of content) {
if (!c || typeof c !== "object") {
continue;
}
const block = c;
if (typeof block.text === "string" && block.text.trim()) {
return block.text;
}
}
}
}
const response = raw.response;
if (response && typeof response === "object") {
const nested = response;
if (typeof nested.output_text === "string" && nested.output_text.trim().length > 0) {
return nested.output_text;
}
}
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Не удалось извлечь output_text из Responses API ответа.", 502, raw);
}
function loadSchemaForTransport(schemaVersion) {
const schemaFile = schemaVersion === "v1"
? "normalized_query_v1.json"
: schemaVersion === "v2_0_1"
? "normalized_query_v2_0_1.json"
: schemaVersion === "v2_0_2"
? "normalized_query_v2_0_2.json"
: "normalized_query_v2.json";
const schemaPath = path_1.default.resolve(config_1.SCHEMAS_DIR, schemaFile);
return JSON.parse(fs_1.default.readFileSync(schemaPath, "utf-8"));
}
class OpenAIResponsesClient {
async testConnection(config) {
const payload = {
model: config.model,
input: [
{
role: "user",
content: [{ type: "input_text", text: "ping" }]
}
],
max_output_tokens: 16
};
await this.post(config, payload);
return { ok: true, model: config.model };
}
async normalize(config, prompt) {
const schema = loadSchemaForTransport(prompt.schemaVersion);
const schemaName = prompt.schemaVersion === "v1"
? "normalized_query_v1"
: prompt.schemaVersion === "v2_0_1"
? "normalized_query_v2_0_1"
: prompt.schemaVersion === "v2_0_2"
? "normalized_query_v2_0_2"
: "normalized_query_v2";
const developerPrompt = prompt.controlledRetryInstruction
? `${prompt.developerPrompt}\n\n${prompt.controlledRetryInstruction}`
: prompt.developerPrompt;
const payload = {
model: config.model,
temperature: config.temperature ?? 0,
max_output_tokens: config.maxOutputTokens ?? 700,
input: [
{
role: "system",
content: [{ type: "input_text", text: prompt.systemPrompt }]
},
{
role: "developer",
content: [{ type: "input_text", text: developerPrompt }]
},
{
role: "user",
content: [
{
type: "input_text",
text: `${prompt.domainPrompt}\n\nПользовательский вопрос:\n${prompt.userQuestion}`
}
]
}
],
text: {
format: {
type: "json_schema",
name: schemaName,
strict: true,
schema
}
}
};
const raw = await this.post(config, payload);
const outputText = extractOutputText(raw);
return {
raw,
outputText,
usage: extractUsage(raw)
};
}
async post(config, payload) {
if (!config.apiKey || config.apiKey.trim().length < 10) {
throw new http_1.ApiError("OPENAI_API_KEY_MISSING", "API ключ OpenAI не задан или слишком короткий.", 400);
}
const url = `${(config.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL).replace(/\/$/, "")}/responses`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
}
catch {
throw new http_1.ApiError("OPENAI_NON_JSON_RESPONSE", "OpenAI вернул не-JSON ответ.", 502, { status: response.status, body: text.slice(0, 500) });
}
if (!response.ok) {
const errorObj = (data.error ?? {});
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", String(errorObj.message ?? `OpenAI request failed with status ${response.status}`), response.status, {
status: response.status,
type: errorObj.type ?? null,
code: errorObj.code ?? null
});
}
return data;
}
}
exports.OpenAIResponsesClient = OpenAIResponsesClient;
+180
View File
@@ -0,0 +1,180 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listBuiltinPromptPresets = listBuiltinPromptPresets;
exports.loadDefaultPrompts = loadDefaultPrompts;
exports.buildPromptBundle = buildPromptBundle;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
function readPromptFile(relativePath) {
const filePath = path_1.default.resolve(config_1.PROMPTS_DIR, relativePath);
if (!fs_1.default.existsSync(filePath)) {
throw new Error(`Prompt file not found: ${filePath}`);
}
return fs_1.default.readFileSync(filePath, "utf-8").trim();
}
const BUILTIN_PROMPT_PRESETS = {
normalizer_v1: {
id: "default-normalizer-v1",
name: "Стандартный пресет NDC v1",
promptVersion: "normalizer_v1",
schemaNotes: "Используется схема normalized_query_v1. Строго соблюдать enum/required поля.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "default.txt"),
domain: path_1.default.join("domain", "default.txt"),
fewshot: path_1.default.join("fewshot", "default.txt")
}
},
normalizer_v1_1: {
id: "default-normalizer-v1_1",
name: "Стандартный пресет NDC v1.1",
promptVersion: "normalizer_v1_1",
schemaNotes: "v1.1: усиленная taxonomy intent/route и confidence policy. Используется схема normalized_query_v1 без дополнительных полей.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v1_1.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_fewshot_v1_1.txt")
}
},
normalizer_v1_1_1: {
id: "default-normalizer-v1_1_1",
name: "Стандартный пресет NDC v1.1.1",
promptVersion: "normalizer_v1_1_1",
schemaNotes: "v1.1.1: surgical patch для period_close_risk, exact drilldown requires и anomaly route escalation. Схема normalized_query_v1 без изменений.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v1_1_1.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_fewshot_v1_1_1.txt")
}
},
normalizer_v1_1_2: {
id: "default-normalizer-v1_1_2",
name: "Стандартный пресет NDC v1.1.2",
promptVersion: "normalizer_v1_1_2",
schemaNotes: "v1.1.2: точечный patch границы heavy_analytical vs period_close_risk + confidence guard на boundary кейсах. Схема normalized_query_v1 без изменений.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v1_1_2.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_fewshot_v1_1_2.txt")
}
},
normalizer_v1_1_2_1: {
id: "default-normalizer-v1_1_2_1",
name: "Стандартный пресет NDC v1.1.2.1",
promptVersion: "normalizer_v1_1_2_1",
schemaNotes: "v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v1_1_2_1.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_fewshot_v1_1_2_1.txt")
}
},
normalizer_v2: {
id: "default-normalizer-v2",
name: "Стандартный пресет NDC v2",
promptVersion: "normalizer_v2",
schemaNotes: "v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v2.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_v2.txt")
}
},
normalizer_v2_0_1: {
id: "default-normalizer-v2_0_1",
name: "Стандартный пресет NDC v2.0.1",
promptVersion: "normalizer_v2_0_1",
schemaNotes: "v2.0.1: clarification-threshold policy. Вопросы в контуре и с понятным route должны исполняться без лишних уточнений. Схема normalized_query_v2_0_1.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v2_0_1.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_v2_0_1.txt")
}
},
normalizer_v2_0_2: {
id: "default-normalizer-v2_0_2",
name: "Стандартный пресет NDC v2.0.2",
promptVersion: "normalizer_v2_0_2",
schemaNotes: "v2.0.2: execution-state hardening + explicit route_status/no_route_reason. Схема normalized_query_v2_0_2.",
files: {
system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v2_0_2.txt"),
domain: path_1.default.join("domain", "normalizer_domain_v1_1.txt"),
fewshot: path_1.default.join("fewshot", "normalizer_v2_0_2.txt")
}
}
};
function isPromptVersion(value) {
return (value === "normalizer_v1" ||
value === "normalizer_v1_1" ||
value === "normalizer_v1_1_1" ||
value === "normalizer_v1_1_2" ||
value === "normalizer_v1_1_2_1" ||
value === "normalizer_v2" ||
value === "normalizer_v2_0_1" ||
value === "normalizer_v2_0_2");
}
function resolvePromptVersion(requested) {
if (isPromptVersion(requested)) {
return requested;
}
if (isPromptVersion(config_1.DEFAULT_PROMPT_VERSION)) {
return config_1.DEFAULT_PROMPT_VERSION;
}
return "normalizer_v2_0_2";
}
function loadBuiltinPreset(promptVersion) {
const now = new Date().toISOString();
const definition = BUILTIN_PROMPT_PRESETS[promptVersion];
return {
id: definition.id,
name: definition.name,
createdAt: now,
updatedAt: now,
prompt_version: definition.promptVersion,
systemPrompt: readPromptFile(definition.files.system),
developerPrompt: readPromptFile(definition.files.developer),
domainPrompt: readPromptFile(definition.files.domain),
schemaNotes: definition.schemaNotes,
fewShotExamples: readPromptFile(definition.files.fewshot)
};
}
function listBuiltinPromptPresets() {
return Object.keys(BUILTIN_PROMPT_PRESETS).map((version) => loadBuiltinPreset(version));
}
function loadDefaultPrompts(promptVersion) {
return loadBuiltinPreset(resolvePromptVersion(promptVersion));
}
function buildPromptBundle(input) {
const selectedPromptVersion = resolvePromptVersion(input.promptVersion);
const defaults = loadDefaultPrompts(selectedPromptVersion);
const systemPrompt = (input.systemPrompt ?? defaults.systemPrompt).trim();
const developerPrompt = (input.developerPrompt ?? defaults.developerPrompt).trim();
const domainPrompt = (input.domainPrompt ?? defaults.domainPrompt).trim();
const schemaNotes = (input.schemaNotes ?? defaults.schemaNotes ?? "").trim();
const fewShotExamples = (input.fewShotExamples ?? defaults.fewShotExamples ?? "").trim();
const prompt_version = (input.promptVersion ?? defaults.prompt_version).trim() || selectedPromptVersion;
const sections = [developerPrompt, `Schema notes:\n${schemaNotes}`];
if (fewShotExamples) {
sections.push(`Few-shot examples:\n${fewShotExamples}`);
}
return {
prompt_version,
systemPrompt,
developerPrompt,
domainPrompt,
schemaNotes,
fewShotExamples,
combinedDeveloperPrompt: sections.join("\n\n")
};
}
@@ -0,0 +1,379 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeRetrievalResult = normalizeRetrievalResult;
const config_1 = require("../config");
const stage1Contracts_1 = require("../types/stage1Contracts");
function toObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toStringOrNull(value) {
if (typeof value !== "string")
return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toNumberOrNull(value) {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
return null;
}
function normalizeStatus(value) {
if (value === "ok" || value === "empty" || value === "partial" || value === "error") {
return value;
}
return "error";
}
function normalizeResultType(value) {
if (value === "list" || value === "summary" || value === "object" || value === "chain" || value === "ranking") {
return value;
}
return "summary";
}
function normalizeObjectArray(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item) => (item && typeof item === "object" ? item : null))
.filter((item) => item !== null);
}
function normalizeSummary(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
return value;
}
function normalizeErrors(value) {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => String(item));
}
function normalizeStringArray(value) {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => String(item));
}
function normalizeConfidence(value) {
if (value === "high" || value === "medium" || value === "low") {
return value;
}
return "medium";
}
function parseEvidenceConfidence(value) {
if (value === "high" || value === "medium" || value === "low") {
return value;
}
return null;
}
function normalizeEvidenceNamespace(value) {
const normalized = toStringOrNull(value)?.toLowerCase();
if (!normalized)
return "unknown";
if (normalized === "snapshot_2020" || normalized === "snapshot")
return "snapshot_2020";
if (normalized === "assistant_derived" || normalized === "derived")
return "assistant_derived";
return "unknown";
}
function inferEvidenceKind(item) {
if (item.mechanism_of_failure !== undefined || item.failed_expected_edge !== undefined || item.expected_next_step !== undefined) {
return "mechanism_link";
}
if (item.risk_score !== undefined || item.zero_guid_values !== undefined || item.unknown_link_count !== undefined) {
return "anomaly_signal";
}
if (item.records_count !== undefined || item.operations_count !== undefined || item.document_refs_count !== undefined) {
return "aggregation";
}
if (item.limitation !== undefined || item.is_snapshot_limited !== undefined) {
return "limitation_note";
}
return "factual_anchor";
}
function inferMechanismNoteLegacy(kind, item) {
const explicit = toStringOrNull(item.mechanism_note);
if (explicit) {
return explicit;
}
if (kind === "mechanism_link") {
const failure = toStringOrNull(item.mechanism_of_failure);
if (failure)
return failure;
return "Mechanism link inferred from retrieval evidence.";
}
if (kind === "anomaly_signal") {
return "Anomaly signal inferred from risk-oriented fields.";
}
if (kind === "aggregation") {
return "Aggregated evidence item.";
}
if (kind === "limitation_note") {
return "Evidence includes explicit limitation hints.";
}
return "Factual evidence anchor.";
}
function resolveMechanismNote(kind, item) {
const explicit = toStringOrNull(item.mechanism_note);
if (explicit) {
return {
note: explicit,
reliable: true
};
}
if (kind === "mechanism_link") {
const failure = toStringOrNull(item.mechanism_of_failure);
if (failure) {
return {
note: failure,
reliable: true
};
}
const failedEdge = toStringOrNull(item.failed_expected_edge);
const expectedNext = toStringOrNull(item.expected_next_step);
const composed = [failedEdge ? `failed_edge=${failedEdge}` : null, expectedNext ? `expected_next_step=${expectedNext}` : null]
.filter((part) => Boolean(part))
.join("; ");
if (composed) {
return {
note: composed,
reliable: true
};
}
}
if (!config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1) {
return {
note: inferMechanismNoteLegacy(kind, item),
reliable: false
};
}
return {
note: null,
reliable: false
};
}
function normalizeEvidenceSourceType(value, record) {
const normalized = toStringOrNull(value);
if (normalized === "retrieval_item" || normalized === "retrieval_summary" || normalized === "derived") {
return normalized;
}
if (record.records_count !== undefined || record.operations_count !== undefined || record.document_refs_count !== undefined) {
return "retrieval_summary";
}
return "retrieval_item";
}
function readPointer(record) {
const pointer = toObject(record.pointer);
return pointer ?? {};
}
function normalizeEvidencePointer(fragmentId, route, record, index) {
const pointer = readPointer(record);
const source = toObject(pointer.source);
const locator = toObject(pointer.locator);
const sourceEntityCandidate = toStringOrNull(source?.entity) ?? toStringOrNull(record.source_entity);
const sourceEntity = sourceEntityCandidate ?? "unknown_entity";
const sourceIdCandidate = toStringOrNull(source?.id) ?? toStringOrNull(record.source_id);
const sourceId = sourceIdCandidate ?? `${route}:${fragmentId}:${index + 1}`;
const period = toStringOrNull(source?.period) ?? toStringOrNull(record.period);
const namespace = normalizeEvidenceNamespace(source?.namespace ?? record.source_namespace);
return {
pointer: {
fragment_id: toStringOrNull(pointer.fragment_id) ?? fragmentId,
route: toStringOrNull(pointer.route) ?? route,
source: {
namespace,
entity: sourceEntity,
id: sourceId,
period
},
locator: {
field_path: toStringOrNull(locator?.field_path) ?? toStringOrNull(record.field_path),
item_index: toNumberOrNull(locator?.item_index) ?? index
}
},
fallback_source_namespace: namespace === "unknown",
fallback_source_entity: sourceEntityCandidate === null,
fallback_source_id: sourceIdCandidate === null
};
}
function canonicalizeSourceRefPart(value) {
return encodeURIComponent((value ?? "none").trim().toLowerCase());
}
function buildSourceRef(pointer) {
return {
schema_version: stage1Contracts_1.EVIDENCE_SOURCE_REF_SCHEMA_VERSION,
namespace: pointer.source.namespace,
entity: pointer.source.entity,
id: pointer.source.id,
period: pointer.source.period,
canonical_ref: [
stage1Contracts_1.EVIDENCE_SOURCE_REF_SCHEMA_VERSION,
canonicalizeSourceRefPart(pointer.source.namespace),
canonicalizeSourceRefPart(pointer.source.entity),
canonicalizeSourceRefPart(pointer.source.id),
canonicalizeSourceRefPart(pointer.source.period)
].join("|")
};
}
function toBoolean(value) {
if (typeof value === "boolean")
return value;
if (typeof value === "number")
return value !== 0;
if (typeof value === "string") {
const lowered = value.trim().toLowerCase();
return lowered === "true" || lowered === "1" || lowered === "yes";
}
return false;
}
function limitationCodeFromText(text) {
const lower = text.toLowerCase();
if (/(snapshot|read-only|read only)/i.test(lower)) {
return "snapshot_only";
}
if (/heuristic/i.test(lower)) {
return "heuristic_inference";
}
if (/mechanism/i.test(lower)) {
return "missing_mechanism";
}
if (/(guid|detail|specific)/i.test(lower)) {
return "insufficient_detail";
}
return "unknown";
}
function resolveEvidenceLimitation(input) {
const explicitLimitation = toStringOrNull(input.record.limitation);
if (explicitLimitation) {
return {
reason_code: limitationCodeFromText(explicitLimitation),
note: explicitLimitation
};
}
if (toBoolean(input.record.is_snapshot_limited)) {
return {
reason_code: "snapshot_only",
note: null
};
}
if (!config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1) {
return null;
}
if (input.mechanismExpected && !input.mechanismReliable) {
return {
reason_code: "missing_mechanism",
note: null
};
}
if (input.pointerWeak) {
return {
reason_code: "weak_source_mapping",
note: null
};
}
if (input.sourceType === "derived") {
return {
reason_code: "heuristic_inference",
note: null
};
}
if (input.evidenceKind === "limitation_note") {
return {
reason_code: "unknown",
note: null
};
}
return null;
}
function downgradeConfidence(value) {
if (value === "high")
return "medium";
if (value === "medium")
return "low";
return "low";
}
function resolveEvidenceConfidence(input) {
if (!config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1) {
return input.explicitConfidence ?? "medium";
}
let confidence = input.explicitConfidence ?? (input.sourceType === "retrieval_item" ? "medium" : "low");
if (input.limitation?.reason_code === "missing_mechanism" || input.limitation?.reason_code === "weak_source_mapping") {
confidence = downgradeConfidence(confidence);
}
if (input.sourceType === "derived" && !input.explicitConfidence) {
confidence = "low";
}
if (input.mechanismExpected && !input.mechanismReliable) {
confidence = "low";
}
if (input.pointerWeak) {
confidence = "low";
}
return confidence;
}
function normalizeEvidenceItems(fragmentId, requirementIds, route, value) {
const records = normalizeObjectArray(value);
return records.map((record, index) => {
const evidenceId = toStringOrNull(record.evidence_id) ?? `ev-${fragmentId}-${index + 1}`;
const claimRef = toStringOrNull(record.claim_ref) ??
(requirementIds[0] ? `requirement:${requirementIds[0]}` : `fragment:${fragmentId}`);
const evidenceKind = inferEvidenceKind(record);
const sourceType = normalizeEvidenceSourceType(record.source_type, record);
const pointerResult = normalizeEvidencePointer(fragmentId, route, record, index);
const mechanism = resolveMechanismNote(evidenceKind, record);
const mechanismExpected = evidenceKind === "mechanism_link" || evidenceKind === "anomaly_signal" || evidenceKind === "aggregation";
const pointerWeak = pointerResult.fallback_source_namespace || pointerResult.fallback_source_entity || pointerResult.fallback_source_id;
const limitation = resolveEvidenceLimitation({
record,
sourceType,
evidenceKind,
mechanismReliable: mechanism.reliable,
mechanismExpected,
pointerWeak
});
const confidence = resolveEvidenceConfidence({
explicitConfidence: parseEvidenceConfidence(record.confidence),
sourceType,
mechanismReliable: mechanism.reliable,
mechanismExpected,
limitation,
pointerWeak
});
return {
evidence_id: evidenceId,
claim_ref: claimRef,
source_type: sourceType,
source_ref: buildSourceRef(pointerResult.pointer),
pointer: pointerResult.pointer,
evidence_kind: evidenceKind,
mechanism_note: mechanism.note,
confidence,
limitation,
payload: record
};
});
}
function normalizeRetrievalResult(fragmentId, requirementIds, route, raw) {
return {
fragment_id: fragmentId,
requirement_ids: requirementIds,
route,
status: normalizeStatus(raw.status),
result_type: normalizeResultType(raw.result_type),
items: normalizeObjectArray(raw.items),
summary: normalizeSummary(raw.summary),
evidence: normalizeEvidenceItems(fragmentId, requirementIds, route, raw.evidence),
why_included: normalizeStringArray(raw.why_included),
selection_reason: normalizeStringArray(raw.selection_reason),
risk_factors: normalizeStringArray(raw.risk_factors),
business_interpretation: normalizeStringArray(raw.business_interpretation),
confidence: normalizeConfidence(raw.confidence),
limitations: normalizeStringArray(raw.limitations),
errors: normalizeErrors(raw.errors)
};
}
+292
View File
@@ -0,0 +1,292 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.simulateDeterministicRouting = simulateDeterministicRouting;
exports.toRouteHintSummary = toRouteHintSummary;
exports.toRouterInput = toRouterInput;
function toRouteHintSummaryV1(normalized) {
return {
mode: "legacy_v1",
intent_class: normalized.intent_class,
route_hint: normalized.route_hint,
confidence: normalized.confidence.route_hint,
decision_flags: {
needs_cross_entity_join: normalized.requires.needs_cross_entity_join,
needs_causal_chain: normalized.requires.needs_causal_chain,
needs_exact_object_trace: normalized.requires.needs_exact_object_trace,
needs_ranking: normalized.requires.needs_ranking,
needs_anomaly_summary: normalized.requires.needs_anomaly_summary,
needs_runtime_truth: normalized.requires.needs_runtime_truth,
needs_period_cut: normalized.requires.needs_period_cut,
needs_evidence: normalized.requires.needs_evidence
},
period_scope: normalized.period_scope,
entities: {
domain_entities: normalized.domain_entities,
accounts_mentioned: normalized.accounts_mentioned,
documents_mentioned: normalized.documents_mentioned,
registers_mentioned: normalized.registers_mentioned
}
};
}
function reasonForNoRoute(noRouteReason) {
if (noRouteReason === "out_of_scope") {
return "Fragment is out-of-scope for company-specific accounting contour.";
}
if (noRouteReason === "missing_mapping") {
return "Fragment is in-scope but route mapping is currently missing.";
}
if (noRouteReason === "unsupported_fragment_type") {
return "Fragment type is not supported by the current deterministic route map.";
}
return "Fragment requires clarification or is too underspecified for safe routing.";
}
function explicitRouteStatus(fragment) {
return "route_status" in fragment ? fragment.route_status : null;
}
function explicitNoRouteReason(fragment) {
return "no_route_reason" in fragment ? fragment.no_route_reason : null;
}
function executionReadiness(fragment) {
return "execution_readiness" in fragment ? fragment.execution_readiness : null;
}
function clarificationReason(fragment) {
return "clarification_reason" in fragment ? fragment.clarification_reason : null;
}
function softAssumptions(fragment) {
return "soft_assumption_used" in fragment ? fragment.soft_assumption_used : [];
}
function buildNoRouteDecision(fragment, noRouteReason) {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: executionReadiness(fragment),
clarification_reason: clarificationReason(fragment),
soft_assumption_used: softAssumptions(fragment),
route_status: "no_route",
no_route_reason: noRouteReason ?? "insufficient_specificity",
route: "no_route",
reason: reasonForNoRoute(noRouteReason)
};
}
function decideRouteForFragment(fragment) {
const status = explicitRouteStatus(fragment);
const noRouteReason = explicitNoRouteReason(fragment);
const readiness = executionReadiness(fragment);
const clarification = clarificationReason(fragment);
const soft = softAssumptions(fragment);
if (status === "no_route") {
return buildNoRouteDecision(fragment, noRouteReason);
}
if (readiness === "needs_clarification" || readiness === "no_route") {
return buildNoRouteDecision(fragment, noRouteReason ?? "insufficient_specificity");
}
if (fragment.domain_relevance !== "in_scope") {
return buildNoRouteDecision(fragment, "out_of_scope");
}
if (fragment.flags.asks_for_exact_object_trace) {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: readiness,
clarification_reason: clarification,
soft_assumption_used: soft,
route_status: "routed",
no_route_reason: null,
route: "live_mcp_drilldown",
reason: "Exact object trace requested."
};
}
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: readiness,
clarification_reason: clarification,
soft_assumption_used: soft,
route_status: "routed",
no_route_reason: null,
route: "batch_refresh_then_store",
reason: "Ranking/summary semantics require batch analytical route."
};
}
if (fragment.flags.has_multi_entity_scope && fragment.flags.asks_for_chain_explanation) {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: readiness,
clarification_reason: clarification,
soft_assumption_used: soft,
route_status: "routed",
no_route_reason: null,
route: "hybrid_store_plus_live",
reason: "Multi-entity causal chain requested."
};
}
if (fragment.flags.asks_for_rule_check && !fragment.flags.asks_for_chain_explanation) {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: readiness,
clarification_reason: clarification,
soft_assumption_used: soft,
route_status: "routed",
no_route_reason: null,
route: "store_feature_risk",
reason: "Rule-control check without causal decomposition."
};
}
if (fragment.flags.asks_for_anomaly_scan &&
!fragment.flags.asks_for_ranking_or_top &&
!(fragment.flags.has_multi_entity_scope && fragment.flags.asks_for_chain_explanation)) {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: readiness,
clarification_reason: clarification,
soft_assumption_used: soft,
route_status: "routed",
no_route_reason: null,
route: "store_feature_risk",
reason: "Anomaly scan without heavy ranking or causal chain."
};
}
if (status === "routed") {
return {
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
candidate_labels: fragment.candidate_labels,
decision_flags: fragment.flags,
execution_readiness: readiness,
clarification_reason: clarification,
soft_assumption_used: soft,
route_status: "routed",
no_route_reason: null,
route: "store_canonical",
reason: "Routed fragment without deep analytical or causal signals."
};
}
return buildNoRouteDecision(fragment, "missing_mapping");
}
function fallbackMessageFor(type) {
if (type === "out_of_scope") {
return "Я работаю только с данными и бухгалтерским контуром текущей компании. Запрос вне доступной предметной области.";
}
if (type === "clarification") {
return "Могу проверить это в контуре компании, но нужно уточнить период, документ, счет или участок учета.";
}
if (type === "partial") {
return "Обработаю только часть запроса, которая относится к данным компании. Остальное выходит за пределы доступного контура.";
}
return null;
}
function simulateDeterministicRouting(normalized) {
const decisions = normalized.fragments.map((fragment) => decideRouteForFragment(fragment));
const inScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope").length;
const outOfScopeCount = decisions.filter((item) => item.domain_relevance === "out_of_scope").length;
const routedInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.route !== "no_route").length;
const clarificationInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.execution_readiness === "needs_clarification").length;
const noRouteInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.route === "no_route").length;
let fallbackType = "none";
if (!normalized.message_in_scope || inScopeCount === 0) {
fallbackType = "out_of_scope";
}
else if (routedInScopeCount === 0 && clarificationInScopeCount > 0) {
fallbackType = "clarification";
}
else if (routedInScopeCount === 0 && noRouteInScopeCount > 0) {
fallbackType = "clarification";
}
else if ((inScopeCount > 0 && outOfScopeCount > 0) || (routedInScopeCount > 0 && noRouteInScopeCount > 0)) {
fallbackType = "partial";
}
return {
mode: "deterministic_v2",
message_in_scope: normalized.message_in_scope,
scope_confidence: normalized.scope_confidence,
planner: {
total_fragments: normalized.fragments.length,
in_scope_fragments: inScopeCount,
out_of_scope_fragments: outOfScopeCount,
discarded_fragments: normalized.discarded_fragments.length,
contains_multiple_tasks: normalized.contains_multiple_tasks
},
decisions,
fallback: {
type: fallbackType,
message: fallbackMessageFor(fallbackType)
}
};
}
function toRouteHintSummary(normalized) {
if (normalized.schema_version === "normalized_query_v2" ||
normalized.schema_version === "normalized_query_v2_0_1" ||
normalized.schema_version === "normalized_query_v2_0_2") {
return simulateDeterministicRouting(normalized);
}
return toRouteHintSummaryV1(normalized);
}
function toRouterInput(normalized) {
if (normalized.schema_version === "normalized_query_v2" ||
normalized.schema_version === "normalized_query_v2_0_1" ||
normalized.schema_version === "normalized_query_v2_0_2") {
return {
mode: "deterministic_v2",
message_in_scope: normalized.message_in_scope,
scope_confidence: normalized.scope_confidence,
contains_multiple_tasks: normalized.contains_multiple_tasks,
fragments: normalized.fragments.map((fragment) => ({
fragment_id: fragment.fragment_id,
domain_relevance: fragment.domain_relevance,
business_scope: fragment.business_scope,
execution_readiness: "execution_readiness" in fragment ? fragment.execution_readiness : null,
clarification_reason: "clarification_reason" in fragment ? fragment.clarification_reason : null,
soft_assumption_used: "soft_assumption_used" in fragment ? fragment.soft_assumption_used : [],
route_status: "route_status" in fragment ? fragment.route_status : null,
no_route_reason: "no_route_reason" in fragment ? fragment.no_route_reason : null,
flags: fragment.flags,
candidate_labels: fragment.candidate_labels,
confidence: fragment.confidence
}))
};
}
return {
mode: "legacy_v1",
intent_class: normalized.intent_class,
decision_flags: {
needs_cross_entity_join: normalized.requires.needs_cross_entity_join,
needs_causal_chain: normalized.requires.needs_causal_chain,
needs_exact_object_trace: normalized.requires.needs_exact_object_trace,
needs_ranking: normalized.requires.needs_ranking,
needs_anomaly_summary: normalized.requires.needs_anomaly_summary,
needs_runtime_truth: normalized.requires.needs_runtime_truth
},
route_hint: normalized.route_hint,
confidence: normalized.confidence.overall,
entities: {
domain_entities: normalized.domain_entities,
accounts_mentioned: normalized.accounts_mentioned,
documents_mentioned: normalized.documents_mentioned,
registers_mentioned: normalized.registers_mentioned
},
period_scope: normalized.period_scope
};
}
+57
View File
@@ -0,0 +1,57 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateNormalized = validateNormalized;
exports.assertNormalized = assertNormalized;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const _2020_1 = __importDefault(require("ajv/dist/2020"));
const config_1 = require("../config");
const validators = new Map();
function schemaPath(version) {
if (version === "v1") {
return path_1.default.resolve(config_1.SCHEMAS_DIR, "normalized_query_v1.json");
}
if (version === "v2_0_1") {
return path_1.default.resolve(config_1.SCHEMAS_DIR, "normalized_query_v2_0_1.json");
}
if (version === "v2_0_2") {
return path_1.default.resolve(config_1.SCHEMAS_DIR, "normalized_query_v2_0_2.json");
}
return path_1.default.resolve(config_1.SCHEMAS_DIR, "normalized_query_v2.json");
}
function loadValidator(version) {
const cached = validators.get(version);
if (cached) {
return cached;
}
const raw = fs_1.default.readFileSync(schemaPath(version), "utf-8");
const schema = JSON.parse(raw);
const ajv = new _2020_1.default({ allErrors: true, strict: false });
const compiled = ajv.compile(schema);
validators.set(version, compiled);
return compiled;
}
function normalizeAjvErrors(errors) {
if (!errors || errors.length === 0) {
return [];
}
return errors.map((item) => `${item.instancePath || "/"} ${item.message ?? "validation error"}`.trim());
}
function validateNormalized(payload, schemaVersion = "v1") {
const check = loadValidator(schemaVersion);
const passed = check(payload);
return {
passed: Boolean(passed),
errors: passed ? [] : normalizeAjvErrors(check.errors)
};
}
function assertNormalized(payload, schemaVersion = "v1") {
const validation = validateNormalized(payload, schemaVersion);
if (!validation.passed) {
throw new Error(`Invalid normalized JSON: ${validation.errors.join("; ")}`);
}
return payload;
}
+85
View File
@@ -0,0 +1,85 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveTrace = saveTrace;
exports.listTraces = listTraces;
exports.getTrace = getTrace;
exports.savePreset = savePreset;
exports.listPresets = listPresets;
exports.saveEvalCase = saveEvalCase;
exports.redactRequestPayload = redactRequestPayload;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const files_1 = require("../utils/files");
function redactSecrets(payload) {
const output = { ...payload };
delete output.apiKey;
return output;
}
function saveTrace(record) {
(0, files_1.ensureDir)(config_1.TRACES_DIR);
const target = path_1.default.resolve(config_1.TRACES_DIR, `${record.trace_id}.json`);
(0, files_1.writeJsonFile)(target, record);
}
function listTraces(limit = 100) {
(0, files_1.ensureDir)(config_1.TRACES_DIR);
const files = fs_1.default
.readdirSync(config_1.TRACES_DIR)
.filter((item) => item.endsWith(".json"))
.sort((a, b) => {
const pa = path_1.default.resolve(config_1.TRACES_DIR, a);
const pb = path_1.default.resolve(config_1.TRACES_DIR, b);
return fs_1.default.statSync(pb).mtimeMs - fs_1.default.statSync(pa).mtimeMs;
})
.slice(0, limit);
return files.map((fileName) => {
const raw = fs_1.default.readFileSync(path_1.default.resolve(config_1.TRACES_DIR, fileName), "utf-8");
const item = JSON.parse(raw);
return {
trace_id: item.trace_id,
timestamp: item.timestamp,
model: item.model,
question_short: item.user_question_raw.slice(0, 110),
confidence: item.confidence,
validation_passed: item.validation_result.passed,
route_hint: item.route_hint,
save_status: "saved"
};
});
}
function getTrace(traceId) {
(0, files_1.ensureDir)(config_1.TRACES_DIR);
const target = path_1.default.resolve(config_1.TRACES_DIR, `${traceId}.json`);
if (!fs_1.default.existsSync(target)) {
return null;
}
const raw = fs_1.default.readFileSync(target, "utf-8");
return JSON.parse(raw);
}
function savePreset(preset) {
(0, files_1.ensureDir)(config_1.PRESETS_DIR);
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.PRESETS_DIR, `${preset.id}.json`), preset);
}
function listPresets() {
(0, files_1.ensureDir)(config_1.PRESETS_DIR);
return fs_1.default
.readdirSync(config_1.PRESETS_DIR)
.filter((item) => item.endsWith(".json"))
.map((fileName) => {
const raw = fs_1.default.readFileSync(path_1.default.resolve(config_1.PRESETS_DIR, fileName), "utf-8");
return JSON.parse(raw);
})
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
function saveEvalCase(casePayload) {
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
const id = String(casePayload.case_id ?? `NQ-${Date.now()}`);
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${id}.json`), casePayload);
return id;
}
function redactRequestPayload(payload) {
return redactSecrets(payload);
}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+48
View File
@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ACCOUNTANT_SCORING_RUBRIC_V01 = exports.INVESTIGATION_MAX_REQUIREMENT_LINKS = exports.INVESTIGATION_MAX_PRIMARY_ACCOUNTS = exports.INVESTIGATION_MAX_UNCERTAINTIES = exports.INVESTIGATION_MAX_EVIDENCE_REFS = exports.EVIDENCE_SOURCE_REF_SCHEMA_VERSION = exports.ASSISTANT_EVAL_RECORD_SCHEMA_VERSION = exports.ANSWER_STRUCTURE_SCHEMA_VERSION = exports.INVESTIGATION_STATE_SCHEMA_VERSION = void 0;
exports.INVESTIGATION_STATE_SCHEMA_VERSION = "investigation_state_v1";
exports.ANSWER_STRUCTURE_SCHEMA_VERSION = "answer_structure_v1_1";
exports.ASSISTANT_EVAL_RECORD_SCHEMA_VERSION = "assistant_eval_record_v0_1";
exports.EVIDENCE_SOURCE_REF_SCHEMA_VERSION = "evidence_source_ref_v1";
exports.INVESTIGATION_MAX_EVIDENCE_REFS = 24;
exports.INVESTIGATION_MAX_UNCERTAINTIES = 12;
exports.INVESTIGATION_MAX_PRIMARY_ACCOUNTS = 8;
exports.INVESTIGATION_MAX_REQUIREMENT_LINKS = 8;
exports.ACCOUNTANT_SCORING_RUBRIC_V01 = {
retrieval_differentiation_rate: [
{ score: 0, label: "No Differentiation", description: "Ответы почти одинаковые для разных кейсов." },
{ score: 3, label: "Partial Differentiation", description: "Различия есть, но по механизмам недостаточно стабильны." },
{ score: 5, label: "Strong Differentiation", description: "Ответы устойчиво различаются по предмету и механизму." }
],
generic_explanation_rate: [
{ score: 0, label: "Mostly Generic", description: "Преобладают общие объяснения без локальной опоры." },
{ score: 3, label: "Mixed", description: "Есть и предметные, и общие блоки объяснения." },
{ score: 5, label: "Mostly Specific", description: "Объяснение в основном case-specific и операбельно." }
],
accountant_actionability_score: [
{ score: 0, label: "Not Actionable", description: "Бухгалтер не получает понятного следующего шага." },
{ score: 3, label: "Partially Actionable", description: "Следующий шаг есть, но недостаточно конкретен." },
{ score: 5, label: "Actionable", description: "Есть конкретные проверяемые действия и приоритет." }
],
false_confidence_rate: [
{ score: 0, label: "High False Confidence", description: "Часто дается уверенный тон при слабой опоре." },
{ score: 3, label: "Moderate False Confidence", description: "Периодически встречается избыточная уверенность." },
{ score: 5, label: "Low False Confidence", description: "Неопределенность обозначается честно и вовремя." }
],
broad_answer_rate: [
{ score: 0, label: "Broad by Default", description: "Часто даются широкие ответы без controlled narrowing." },
{ score: 3, label: "Partially Controlled", description: "Broad-ответы периодически сужаются, но не всегда." },
{ score: 5, label: "Controlled", description: "Broad-ответы редки и сопровождаются корректным сужением." }
],
mechanism_specificity_score: [
{ score: 0, label: "No Mechanism", description: "Есть только лейблы без механики поломки." },
{ score: 3, label: "Partial Mechanism", description: "Механизм описан частично, без полной связки." },
{ score: 5, label: "Mechanism-Aware", description: "Механизм поломки и опорные объекты связаны явно." }
],
followup_context_retention_score: [
{ score: 0, label: "Context Lost", description: "Follow-up теряет фокус текущего разбора." },
{ score: 3, label: "Context Partial", description: "Фокус удерживается частично, с дрейфом." },
{ score: 5, label: "Context Retained", description: "Follow-up устойчиво держит предмет и ограничения." }
]
};
+26
View File
@@ -0,0 +1,26 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureDir = ensureDir;
exports.readJsonFile = readJsonFile;
exports.writeJsonFile = writeJsonFile;
const fs_1 = __importDefault(require("fs"));
function ensureDir(path) {
if (!fs_1.default.existsSync(path)) {
fs_1.default.mkdirSync(path, { recursive: true });
}
}
function readJsonFile(path, fallback) {
try {
const raw = fs_1.default.readFileSync(path, "utf-8");
return JSON.parse(raw);
}
catch {
return fallback;
}
}
function writeJsonFile(path, value) {
fs_1.default.writeFileSync(path, JSON.stringify(value, null, 2), "utf-8");
}
+46
View File
@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiError = void 0;
exports.ok = ok;
exports.created = created;
exports.errorMiddleware = errorMiddleware;
class ApiError extends Error {
code;
status;
details;
constructor(code, message, status = 400, details) {
super(message);
this.code = code;
this.status = status;
this.details = details;
}
}
exports.ApiError = ApiError;
function ok(res, payload) {
return res.status(200).json(payload);
}
function created(res, payload) {
return res.status(201).json(payload);
}
function errorMiddleware(err, _req, res, _next) {
if (err instanceof ApiError) {
res.status(err.status).json({
ok: false,
error: {
code: err.code,
message: err.message,
details: err.details ?? null
}
});
return;
}
const fallback = err instanceof Error ? err.message : "Unknown error";
res.status(500).json({
ok: false,
error: {
code: "INTERNAL_ERROR",
message: fallback,
details: null
}
});
}
+31
View File
@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.logJson = logJson;
const REDACT_KEYS = new Set(["apiKey", "authorization", "Authorization", "openai_api_key", "OPENAI_API_KEY"]);
function redactObject(value) {
if (Array.isArray(value)) {
return value.map(redactObject);
}
if (value !== null && typeof value === "object") {
const source = value;
const out = {};
for (const [key, field] of Object.entries(source)) {
if (REDACT_KEYS.has(key)) {
out[key] = "***REDACTED***";
}
else {
out[key] = redactObject(field);
}
}
return out;
}
return value;
}
function logJson(entry) {
const safe = {
...entry,
details: redactObject(entry.details)
};
// Structured JSON logs for diagnostics/trace aggregation.
process.stdout.write(JSON.stringify(safe) + "\n");
}