АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия + доп кля + конфиг дизапйна
This commit is contained in:
+9
-1
@@ -3,7 +3,8 @@ 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.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = 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;
|
||||
exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = 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;
|
||||
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = 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, "..");
|
||||
@@ -71,8 +72,15 @@ 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.AUTORUN_ANNOTATIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_annotations");
|
||||
exports.AUTORUN_ANNOTATIONS_FILE = path_1.default.resolve(exports.AUTORUN_ANNOTATIONS_DIR, "annotations.json");
|
||||
exports.AUTORUN_GENERATOR_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_generators");
|
||||
exports.AUTORUN_GENERATOR_HISTORY_FILE = path_1.default.resolve(exports.AUTORUN_GENERATOR_DIR, "history.json");
|
||||
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экспорт");
|
||||
exports.ASSISTANT_CANON_FILE = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "TECH", "assistant_canon.md");
|
||||
exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "TECH", "capabilities_registry.json");
|
||||
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "TECH", "manual_case_decision_schema.json");
|
||||
|
||||
+796
-3
@@ -9,6 +9,29 @@ const path_1 = __importDefault(require("path"));
|
||||
const express_1 = require("express");
|
||||
const config_1 = require("../config");
|
||||
const http_1 = require("../utils/http");
|
||||
const capabilitiesRegistry_1 = require("../services/capabilitiesRegistry");
|
||||
const MANUAL_CASE_DECISIONS = [
|
||||
"covered_ok",
|
||||
"covered_but_bad_answer",
|
||||
"candidate_for_implementation",
|
||||
"needs_routing_extension",
|
||||
"out_of_scope_but_answer_softly",
|
||||
"unsafe_question_limit_strictly",
|
||||
"needs_dialog_policy_fix",
|
||||
"needs_capability_registry_update",
|
||||
"bad_test_case"
|
||||
];
|
||||
const DECISION_QUEUE_MAP = {
|
||||
covered_ok: "none",
|
||||
covered_but_bad_answer: "policy_fix",
|
||||
candidate_for_implementation: "routing_extension",
|
||||
needs_routing_extension: "routing_extension",
|
||||
out_of_scope_but_answer_softly: "soft_boundary",
|
||||
unsafe_question_limit_strictly: "safety_policy",
|
||||
needs_dialog_policy_fix: "policy_fix",
|
||||
needs_capability_registry_update: "capability_registry",
|
||||
bad_test_case: "testset_hygiene"
|
||||
};
|
||||
function toRecord(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -67,6 +90,304 @@ function clampInt(value, min, max, fallback) {
|
||||
return max;
|
||||
return rounded;
|
||||
}
|
||||
function parseManualCaseDecision(value, fallback = "needs_dialog_policy_fix") {
|
||||
const normalized = toStringSafe(value);
|
||||
if (!normalized)
|
||||
return fallback;
|
||||
return (MANUAL_CASE_DECISIONS.includes(normalized) ? normalized : fallback);
|
||||
}
|
||||
function parseAnnotationAuthor(value) {
|
||||
const author = toStringSafe(value);
|
||||
if (!author)
|
||||
return null;
|
||||
return author.slice(0, 80);
|
||||
}
|
||||
function readManualDecisionSchema() {
|
||||
const fallback = {
|
||||
schema_version: "manual_case_decision_schema_v1_fallback",
|
||||
enum: MANUAL_CASE_DECISIONS,
|
||||
labels: {
|
||||
covered_ok: "Покрыто и ок",
|
||||
covered_but_bad_answer: "Покрыто, но ответ плохой",
|
||||
candidate_for_implementation: "Кандидат на внедрение",
|
||||
needs_routing_extension: "Нужно расширение маршрутизации",
|
||||
out_of_scope_but_answer_softly: "Вне скоупа, но нужен мягкий ответ",
|
||||
unsafe_question_limit_strictly: "Высокий риск, строгие ограничения",
|
||||
needs_dialog_policy_fix: "Нужен фикс диалоговой политики",
|
||||
needs_capability_registry_update: "Нужно обновить реестр возможностей",
|
||||
bad_test_case: "Плохой тест-кейс"
|
||||
},
|
||||
queue_mapping: DECISION_QUEUE_MAP
|
||||
};
|
||||
if (!fs_1.default.existsSync(config_1.MANUAL_CASE_DECISION_SCHEMA_FILE)) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(fs_1.default.readFileSync(config_1.MANUAL_CASE_DECISION_SCHEMA_FILE, "utf-8"));
|
||||
const record = toRecord(parsed);
|
||||
return record ?? fallback;
|
||||
}
|
||||
catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
function readAutoGenHistory() {
|
||||
if (!fs_1.default.existsSync(config_1.AUTORUN_GENERATOR_HISTORY_FILE))
|
||||
return [];
|
||||
try {
|
||||
const parsed = JSON.parse(fs_1.default.readFileSync(config_1.AUTORUN_GENERATOR_HISTORY_FILE, "utf-8"));
|
||||
if (!Array.isArray(parsed))
|
||||
return [];
|
||||
return parsed
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item) => item !== null)
|
||||
.map((item) => ({
|
||||
generation_id: toStringSafe(item.generation_id) ?? "",
|
||||
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
|
||||
mode: toStringSafe(item.mode) ?? "codex_creative",
|
||||
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
|
||||
domain: toStringSafe(item.domain),
|
||||
questions: toArray(item.questions)
|
||||
.map((q) => toStringSafe(q))
|
||||
.filter((q) => q !== null)
|
||||
.slice(0, 500),
|
||||
generated_by: toStringSafe(item.generated_by),
|
||||
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
||||
context: toRecord(item.context)
|
||||
? {
|
||||
llm_provider: toStringSafe(toRecord(item.context)?.llm_provider),
|
||||
model: toStringSafe(toRecord(item.context)?.model),
|
||||
assistant_prompt_version: toStringSafe(toRecord(item.context)?.assistant_prompt_version),
|
||||
decomposition_prompt_version: toStringSafe(toRecord(item.context)?.decomposition_prompt_version),
|
||||
prompt_fingerprint: toStringSafe(toRecord(item.context)?.prompt_fingerprint)
|
||||
}
|
||||
: null
|
||||
}))
|
||||
.filter((item) => item.generation_id.length > 0)
|
||||
.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function writeAutoGenHistory(records) {
|
||||
const dir = path_1.default.dirname(config_1.AUTORUN_GENERATOR_HISTORY_FILE);
|
||||
if (!fs_1.default.existsSync(dir)) {
|
||||
fs_1.default.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs_1.default.writeFileSync(config_1.AUTORUN_GENERATOR_HISTORY_FILE, JSON.stringify(records, null, 2), "utf-8");
|
||||
}
|
||||
function readEvalDatasetCases(filePath) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf-8"));
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((item) => toRecord(item)).filter((item) => item !== null);
|
||||
}
|
||||
const record = toRecord(parsed);
|
||||
if (!record)
|
||||
return [];
|
||||
const cases = toArray(record.cases).map((item) => toRecord(item)).filter((item) => item !== null);
|
||||
return cases;
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function collectCanonicalQuestions(limit = 300) {
|
||||
if (!fs_1.default.existsSync(config_1.EVAL_DATASETS_DIR)) {
|
||||
return [];
|
||||
}
|
||||
const entries = fs_1.default.readdirSync(config_1.EVAL_DATASETS_DIR, { withFileTypes: true });
|
||||
const questions = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
||||
continue;
|
||||
const fullPath = path_1.default.resolve(config_1.EVAL_DATASETS_DIR, entry.name);
|
||||
const cases = readEvalDatasetCases(fullPath);
|
||||
for (const testCase of cases) {
|
||||
const rawQuestion = toStringSafe(testCase.raw_question) ?? toStringSafe(testCase.user_message) ?? toStringSafe(testCase.query);
|
||||
if (rawQuestion) {
|
||||
questions.push(rawQuestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(new Set(questions)).slice(0, limit);
|
||||
}
|
||||
function normalizeDomainHint(value) {
|
||||
const domain = toStringSafe(value);
|
||||
if (!domain)
|
||||
return null;
|
||||
return domain.toLowerCase();
|
||||
}
|
||||
function fallbackDomainTemplates(domain) {
|
||||
if (domain?.includes("vat") || domain?.includes("ндс")) {
|
||||
return [
|
||||
"Сколько НДС к уплате на дату по организации?",
|
||||
"Покажи прогноз НДС за период по организации.",
|
||||
"Почему по НДС сейчас ноль и из чего сложился расчет?"
|
||||
];
|
||||
}
|
||||
if (domain?.includes("counter") || domain?.includes("контраг")) {
|
||||
return [
|
||||
"Покажи топ контрагентов по сумме платежей за период.",
|
||||
"Какой самый крупный договор у выбранной организации?",
|
||||
"Какие документы были по контрагенту за весь период?"
|
||||
];
|
||||
}
|
||||
if (domain?.includes("settlement") || domain?.includes("задолж") || domain?.includes("расчет")) {
|
||||
return [
|
||||
"Какие незакрытые расчеты висят на конец периода?",
|
||||
"Есть ли незакрытые авансы по поставщикам?",
|
||||
"Покажи цепочки закрытия по счетам 60/62."
|
||||
];
|
||||
}
|
||||
return [
|
||||
"С какой организацией сейчас можно работать в активном контуре?",
|
||||
"Покажи ключевые операции за выбранный период.",
|
||||
"Какие вопросы по этому домену ассистент поддерживает прямо сейчас?"
|
||||
];
|
||||
}
|
||||
function mutateIntoQwenStyle(base, index) {
|
||||
const wrappers = ["йо ", "слушай ", "подскажи плиз ", "короче ", "мож ", "а ну-ка "];
|
||||
const tails = ["", " без воды", " по факту", " и коротко", " прям сейчас", " за весь период"];
|
||||
const typoMap = [
|
||||
[/\bкомпания\b/gi, "компиния"],
|
||||
[/\bсейчас\b/gi, "щас"],
|
||||
[/\bпожалуйста\b/gi, "плиз"],
|
||||
[/\bкакая\b/gi, "кака"],
|
||||
[/\bчто\b/gi, "че"]
|
||||
];
|
||||
const prefix = wrappers[index % wrappers.length];
|
||||
const tail = tails[index % tails.length];
|
||||
let text = `${prefix}${base}${tail}`.trim();
|
||||
if (index % 2 === 0) {
|
||||
const [pattern, replacement] = typoMap[index % typoMap.length];
|
||||
text = text.replace(pattern, replacement);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function generateQwenSeedQuestions(count, domain) {
|
||||
const seed = collectCanonicalQuestions(450);
|
||||
const source = seed.length > 0 ? seed : fallbackDomainTemplates(domain);
|
||||
const filtered = domain
|
||||
? source.filter((item) => item.toLowerCase().includes(domain) || fallbackDomainTemplates(domain).includes(item))
|
||||
: source;
|
||||
const bag = filtered.length > 0 ? filtered : source;
|
||||
const out = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const base = bag[index % bag.length];
|
||||
out.push(mutateIntoQwenStyle(base, index));
|
||||
}
|
||||
return Array.from(new Set(out)).slice(0, count);
|
||||
}
|
||||
function generateCodexCreativeQuestions(count, domain) {
|
||||
const domainTemplates = fallbackDomainTemplates(domain);
|
||||
const patterns = [
|
||||
"Дай бизнес-срез по состоянию на дату: {q}",
|
||||
"Нужен аккуратный ответ как бухгалтеру: {q}",
|
||||
"Если данных не хватает, скажи что уточнить, но сначала попробуй: {q}",
|
||||
"Сформулируй результат без технички и с шагом дальше: {q}",
|
||||
"Проверь в read-only и скажи что видно: {q}"
|
||||
];
|
||||
const out = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const base = domainTemplates[index % domainTemplates.length];
|
||||
const pattern = patterns[index % patterns.length];
|
||||
out.push(pattern.replace("{q}", base));
|
||||
}
|
||||
return Array.from(new Set(out)).slice(0, count);
|
||||
}
|
||||
function generateAutogenId() {
|
||||
return `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
function readAnnotations() {
|
||||
if (!fs_1.default.existsSync(config_1.AUTORUN_ANNOTATIONS_FILE)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const raw = fs_1.default.readFileSync(config_1.AUTORUN_ANNOTATIONS_FILE, "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item) => item !== null)
|
||||
.map((item) => {
|
||||
const context = toRecord(item.context);
|
||||
return {
|
||||
annotation_id: toStringSafe(item.annotation_id) ?? "",
|
||||
run_id: toStringSafe(item.run_id) ?? "",
|
||||
case_id: toStringSafe(item.case_id) ?? "",
|
||||
session_id: toStringSafe(item.session_id) ?? "",
|
||||
message_index: clampInt(toNumberSafe(item.message_index), 0, 100_000, 0),
|
||||
rating: clampInt(toNumberSafe(item.rating), 1, 5, 1),
|
||||
comment: toStringSafe(item.comment) ?? "",
|
||||
manual_case_decision: parseManualCaseDecision(item.manual_case_decision),
|
||||
annotation_author: parseAnnotationAuthor(item.annotation_author),
|
||||
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
|
||||
updated_at: toStringSafe(item.updated_at) ?? new Date().toISOString(),
|
||||
context: {
|
||||
message_id: toStringSafe(context?.message_id),
|
||||
trace_id: toStringSafe(context?.trace_id),
|
||||
reply_type: toStringSafe(context?.reply_type),
|
||||
eval_target: toStringSafe(context?.eval_target) ?? "unknown",
|
||||
prompt_version: toStringSafe(context?.prompt_version),
|
||||
domain: toStringSafe(context?.domain),
|
||||
query_class: toStringSafe(context?.query_class)
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter((item) => item.annotation_id && item.run_id && item.case_id);
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function writeAnnotations(items) {
|
||||
fs_1.default.writeFileSync(config_1.AUTORUN_ANNOTATIONS_FILE, JSON.stringify(items, null, 2), "utf-8");
|
||||
}
|
||||
function annotationKey(runId, caseId, messageIndex) {
|
||||
return `${runId}::${caseId}::${messageIndex}`;
|
||||
}
|
||||
function buildAnnotationStatsMap(runId, annotations) {
|
||||
const scoped = annotations.filter((item) => item.run_id === runId);
|
||||
const buckets = new Map();
|
||||
for (const item of scoped) {
|
||||
const bucket = buckets.get(item.case_id) ?? { count: 0, ratings: [], latestMs: null };
|
||||
bucket.count += 1;
|
||||
bucket.ratings.push(item.rating);
|
||||
const ms = Date.parse(item.updated_at);
|
||||
if (Number.isFinite(ms) && (bucket.latestMs === null || ms > bucket.latestMs)) {
|
||||
bucket.latestMs = ms;
|
||||
}
|
||||
buckets.set(item.case_id, bucket);
|
||||
}
|
||||
const result = new Map();
|
||||
for (const [caseId, bucket] of buckets.entries()) {
|
||||
const avg = bucket.ratings.length > 0 ? Number((bucket.ratings.reduce((a, b) => a + b, 0) / bucket.ratings.length).toFixed(2)) : null;
|
||||
result.set(caseId, {
|
||||
count: bucket.count,
|
||||
latest_at: bucket.latestMs === null ? null : new Date(bucket.latestMs).toISOString(),
|
||||
avg_rating: avg
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function buildAnnotationsByMessageIndex(runId, caseId, annotations) {
|
||||
const map = new Map();
|
||||
for (const item of annotations) {
|
||||
if (item.run_id !== runId || item.case_id !== caseId)
|
||||
continue;
|
||||
const current = map.get(item.message_index);
|
||||
const currentMs = current ? Date.parse(current.updated_at) : null;
|
||||
const nextMs = Date.parse(item.updated_at);
|
||||
if (!current || (!Number.isNaN(nextMs) && (currentMs === null || nextMs >= currentMs))) {
|
||||
map.set(item.message_index, item);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
function resolveRunTarget(input) {
|
||||
const explicit = toStringSafe(input.report.eval_target);
|
||||
if (explicit === "assistant_stage1" || explicit === "assistant_stage2" || explicit === "assistant_p0" || explicit === "normalizer") {
|
||||
@@ -221,7 +542,7 @@ function getResultCases(report) {
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item) => item !== null);
|
||||
}
|
||||
function buildCaseSummaries(report, runId, checkDialogAvailability) {
|
||||
function buildCaseSummaries(report, runId, checkDialogAvailability, annotationStatsByCase) {
|
||||
const results = getResultCases(report);
|
||||
return results.map((item, index) => {
|
||||
const caseId = toStringSafe(item.case_id) ?? `case-${index + 1}`;
|
||||
@@ -235,6 +556,7 @@ function buildCaseSummaries(report, runId, checkDialogAvailability) {
|
||||
const dialogAvailable = checkDialogAvailability
|
||||
? fs_1.default.existsSync(path_1.default.resolve(config_1.ASSISTANT_SESSIONS_DIR, `${sessionId}.json`))
|
||||
: false;
|
||||
const annotationStats = annotationStatsByCase?.get(caseId);
|
||||
return {
|
||||
case_id: caseId,
|
||||
domain: toStringSafe(item.domain),
|
||||
@@ -245,6 +567,9 @@ function buildCaseSummaries(report, runId, checkDialogAvailability) {
|
||||
reply_type: toStringSafe(item.reply_type),
|
||||
session_id: sessionId,
|
||||
dialog_available: dialogAvailable,
|
||||
commented_count: annotationStats?.count ?? 0,
|
||||
latest_annotation_at: annotationStats?.latest_at ?? null,
|
||||
avg_rating: annotationStats?.avg_rating ?? null,
|
||||
checks,
|
||||
metric_subscores: metricSubscores
|
||||
};
|
||||
@@ -521,6 +846,7 @@ function loadSessionDialog(runId, caseId) {
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item) => item !== null);
|
||||
const messages = conversation.map((item) => ({
|
||||
message_id: toStringSafe(item.message_id),
|
||||
role: toStringSafe(item.role) ?? "unknown",
|
||||
text: toStringSafe(item.text) ?? "",
|
||||
created_at: toStringSafe(item.created_at),
|
||||
@@ -588,6 +914,7 @@ function buildFallbackDialog(run, caseId) {
|
||||
session_id: sessionId,
|
||||
messages: [
|
||||
{
|
||||
message_id: null,
|
||||
role: "user",
|
||||
text: userText,
|
||||
created_at: null,
|
||||
@@ -595,6 +922,7 @@ function buildFallbackDialog(run, caseId) {
|
||||
reply_type: null
|
||||
},
|
||||
{
|
||||
message_id: null,
|
||||
role: "assistant",
|
||||
text: assistantSummaryParts.join("\n"),
|
||||
created_at: null,
|
||||
@@ -606,6 +934,175 @@ function buildFallbackDialog(run, caseId) {
|
||||
assistant_mode: null
|
||||
};
|
||||
}
|
||||
function withMessageAnnotations(runId, caseId, messages, annotations) {
|
||||
const byIndex = buildAnnotationsByMessageIndex(runId, caseId, annotations);
|
||||
return messages.map((message, index) => {
|
||||
const annotation = byIndex.get(index) ?? null;
|
||||
return {
|
||||
...message,
|
||||
message_index: index,
|
||||
commented: annotation !== null,
|
||||
annotation
|
||||
};
|
||||
});
|
||||
}
|
||||
function generateAnnotationId() {
|
||||
return `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
function parseComment(value) {
|
||||
const text = toStringSafe(value) ?? "";
|
||||
return text.trim();
|
||||
}
|
||||
function parseDecisionFilter(value) {
|
||||
const normalized = toStringSafe(value);
|
||||
if (!normalized || normalized === "all")
|
||||
return "all";
|
||||
return parseManualCaseDecision(normalized);
|
||||
}
|
||||
function parseAutoGenMode(value) {
|
||||
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
|
||||
if (normalized === "qwen_seed" || normalized === "codex_creative") {
|
||||
return normalized;
|
||||
}
|
||||
return "codex_creative";
|
||||
}
|
||||
function parseAutogenCount(value) {
|
||||
return clampInt(toNumberSafe(value), 1, 200, 24);
|
||||
}
|
||||
function parseAutogenDomain(value) {
|
||||
const domain = normalizeDomainHint(value);
|
||||
if (!domain)
|
||||
return null;
|
||||
return domain.slice(0, 80);
|
||||
}
|
||||
function hasAnyRunFilterQuery(query) {
|
||||
return Boolean(toStringSafe(query.from) ??
|
||||
toStringSafe(query.to) ??
|
||||
toStringSafe(query.target) ??
|
||||
toStringSafe(query.mode) ??
|
||||
toStringSafe(query.use_mock) ??
|
||||
toStringSafe(query.prompt_contains));
|
||||
}
|
||||
function buildAutogenCaseSetFileName(mode, generationId) {
|
||||
const now = new Date();
|
||||
const stamp = [
|
||||
now.getUTCFullYear(),
|
||||
String(now.getUTCMonth() + 1).padStart(2, "0"),
|
||||
String(now.getUTCDate()).padStart(2, "0"),
|
||||
String(now.getUTCHours()).padStart(2, "0"),
|
||||
String(now.getUTCMinutes()).padStart(2, "0"),
|
||||
String(now.getUTCSeconds()).padStart(2, "0")
|
||||
].join("");
|
||||
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
|
||||
}
|
||||
function buildAutogenCaseSetPayload(input) {
|
||||
const cases = input.questions.map((question, index) => ({
|
||||
case_id: `AUTO-${String(index + 1).padStart(3, "0")}`,
|
||||
scenario_tag: `${input.mode}_${input.domain ?? "general"}`,
|
||||
question_type: "direct",
|
||||
broadness_level: "medium",
|
||||
turns: [{ user_message: question }],
|
||||
expected_hints: {
|
||||
expected_reply_type: null,
|
||||
expected_degraded_to: null
|
||||
}
|
||||
}));
|
||||
return {
|
||||
suite_id: `assistant_autogen_${input.generationId}`,
|
||||
suite_version: "0.1.0",
|
||||
schema_version: "assistant_autogen_suite_v0_1",
|
||||
generated_at: new Date().toISOString(),
|
||||
generation_id: input.generationId,
|
||||
mode: input.mode,
|
||||
domain: input.domain,
|
||||
scenario_count: cases.length,
|
||||
case_ids: cases.map((item) => item.case_id),
|
||||
cases
|
||||
};
|
||||
}
|
||||
function collectPostAnalysis(annotations, runMap, limitPerQueue) {
|
||||
const byDecision = {};
|
||||
const byQueue = {};
|
||||
const byDomain = new Map();
|
||||
const queues = {
|
||||
routing_extension: [],
|
||||
policy_fix: [],
|
||||
capability_registry: [],
|
||||
soft_boundary: [],
|
||||
safety_policy: [],
|
||||
testset_hygiene: [],
|
||||
covered_ok: []
|
||||
};
|
||||
const registry = (0, capabilitiesRegistry_1.loadCapabilitiesRegistry)();
|
||||
for (const item of annotations) {
|
||||
byDecision[item.manual_case_decision] = (byDecision[item.manual_case_decision] ?? 0) + 1;
|
||||
const queueKey = DECISION_QUEUE_MAP[item.manual_case_decision];
|
||||
byQueue[queueKey] = (byQueue[queueKey] ?? 0) + 1;
|
||||
const run = runMap.get(item.run_id) ?? null;
|
||||
const caseSummary = run
|
||||
? buildCaseSummaries(run.report, run.run_id, false).find((candidate) => candidate.case_id === item.case_id) ?? null
|
||||
: null;
|
||||
const nearestGroup = (0, capabilitiesRegistry_1.resolveNearestCapabilityGroup)({
|
||||
domain: caseSummary?.domain ?? item.context.domain,
|
||||
queryClass: caseSummary?.query_class ?? item.context.query_class
|
||||
}) ??
|
||||
registry.groups[0] ??
|
||||
null;
|
||||
const domainKey = caseSummary?.domain ?? item.context.domain ?? "unknown";
|
||||
byDomain.set(domainKey, (byDomain.get(domainKey) ?? 0) + 1);
|
||||
const view = {
|
||||
annotation_id: item.annotation_id,
|
||||
run_id: item.run_id,
|
||||
case_id: item.case_id,
|
||||
message_index: item.message_index,
|
||||
rating: item.rating,
|
||||
comment: item.comment,
|
||||
manual_case_decision: item.manual_case_decision,
|
||||
annotation_author: item.annotation_author,
|
||||
updated_at: item.updated_at,
|
||||
domain: caseSummary?.domain ?? item.context.domain ?? null,
|
||||
query_class: caseSummary?.query_class ?? item.context.query_class ?? null,
|
||||
trace_id: item.context.trace_id ?? caseSummary?.trace_id ?? null,
|
||||
reply_type: item.context.reply_type ?? caseSummary?.reply_type ?? null,
|
||||
nearest_capability_group: nearestGroup
|
||||
? {
|
||||
group_code: nearestGroup.group_code,
|
||||
group_title: nearestGroup.group_title,
|
||||
maturity_status: nearestGroup.maturity_status
|
||||
}
|
||||
: null
|
||||
};
|
||||
if (queueKey === "none") {
|
||||
if (queues.covered_ok.length < limitPerQueue)
|
||||
queues.covered_ok.push(view);
|
||||
continue;
|
||||
}
|
||||
if (!queues[queueKey]) {
|
||||
queues[queueKey] = [];
|
||||
}
|
||||
if (queues[queueKey].length < limitPerQueue) {
|
||||
queues[queueKey].push(view);
|
||||
}
|
||||
}
|
||||
const domainSummary = Array.from(byDomain.entries())
|
||||
.map(([domain, total]) => ({ domain, total }))
|
||||
.sort((a, b) => b.total - a.total);
|
||||
return {
|
||||
stats: {
|
||||
annotations_total: annotations.length,
|
||||
by_decision: byDecision,
|
||||
by_queue: byQueue,
|
||||
domains_total: domainSummary.length
|
||||
},
|
||||
domain_summary: domainSummary,
|
||||
queues,
|
||||
recommended_regression_candidates: [
|
||||
...queues.routing_extension.slice(0, 20),
|
||||
...queues.policy_fix.slice(0, 20),
|
||||
...queues.safety_policy.slice(0, 20)
|
||||
].slice(0, 60)
|
||||
};
|
||||
}
|
||||
function buildAutoRunsRouter() {
|
||||
const router = (0, express_1.Router)();
|
||||
router.get("/api/autoruns/history", (req, res) => {
|
||||
@@ -648,13 +1145,18 @@ function buildAutoRunsRouter() {
|
||||
if (!run) {
|
||||
throw new http_1.ApiError("AUTORUN_NOT_FOUND", `Run not found: ${runId}`, 404);
|
||||
}
|
||||
const cases = buildCaseSummaries(run.report, run.run_id, true);
|
||||
const annotations = readAnnotations();
|
||||
const annotationStatsByCase = buildAnnotationStatsMap(runId, annotations);
|
||||
const cases = buildCaseSummaries(run.report, run.run_id, true, annotationStatsByCase);
|
||||
const coverage = buildCoverageFromCases(cases);
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
run: buildRunSummary(run),
|
||||
coverage,
|
||||
cases,
|
||||
annotations_summary: {
|
||||
total: annotations.filter((item) => item.run_id === runId).length
|
||||
},
|
||||
report: run.report
|
||||
});
|
||||
}
|
||||
@@ -675,11 +1177,302 @@ function buildAutoRunsRouter() {
|
||||
}
|
||||
const sessionDialog = loadSessionDialog(runId, caseId);
|
||||
const dialog = sessionDialog ?? buildFallbackDialog(run, caseId);
|
||||
const annotations = readAnnotations();
|
||||
const messages = withMessageAnnotations(runId, caseId, dialog.messages, annotations);
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
run_id: runId,
|
||||
case_id: caseId,
|
||||
...dialog
|
||||
...dialog,
|
||||
messages,
|
||||
annotations: annotations
|
||||
.filter((item) => item.run_id === runId && item.case_id === caseId)
|
||||
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at))
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
router.get("/api/autoruns/annotations", (req, res, next) => {
|
||||
try {
|
||||
const runIdFilter = toStringSafe(req.query.run_id);
|
||||
const caseIdFilter = toStringSafe(req.query.case_id);
|
||||
const minRatingRaw = toNumberSafe(req.query.min_rating);
|
||||
const minRating = minRatingRaw === null ? null : clampInt(minRatingRaw, 1, 5, 1);
|
||||
const decisionFilter = parseDecisionFilter(req.query.manual_case_decision);
|
||||
const limit = clampInt(toNumberSafe(req.query.limit), 1, 2000, 400);
|
||||
const scanLimit = clampInt(toNumberSafe(req.query.scan_limit), 50, 5000, 2500);
|
||||
const annotations = readAnnotations()
|
||||
.filter((item) => (runIdFilter ? item.run_id === runIdFilter : true))
|
||||
.filter((item) => (caseIdFilter ? item.case_id === caseIdFilter : true))
|
||||
.filter((item) => (minRating === null ? true : item.rating >= minRating))
|
||||
.filter((item) => (decisionFilter === "all" ? true : item.manual_case_decision === decisionFilter))
|
||||
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at))
|
||||
.slice(0, limit);
|
||||
const runIndex = indexRuns(scanLimit);
|
||||
const runMap = new Map(runIndex.map((item) => [item.run_id, item]));
|
||||
const items = annotations.map((item) => {
|
||||
const run = runMap.get(item.run_id) ?? null;
|
||||
const runSummary = run ? buildRunSummary(run) : null;
|
||||
const cases = run ? buildCaseSummaries(run.report, run.run_id, false) : [];
|
||||
const caseSummary = cases.find((candidate) => candidate.case_id === item.case_id) ?? null;
|
||||
return {
|
||||
...item,
|
||||
run: runSummary,
|
||||
case_summary: caseSummary,
|
||||
technical_context: {
|
||||
report_path: run?.report_path ?? null,
|
||||
trace_id: item.context.trace_id,
|
||||
reply_type: item.context.reply_type,
|
||||
domain: item.context.domain,
|
||||
query_class: item.context.query_class,
|
||||
checks: caseSummary?.checks ?? null,
|
||||
metric_subscores: caseSummary?.metric_subscores ?? null
|
||||
}
|
||||
};
|
||||
});
|
||||
const avgRating = items.length > 0 ? Number((items.reduce((acc, item) => acc + item.rating, 0) / items.length).toFixed(2)) : null;
|
||||
const byDecision = items.reduce((acc, item) => {
|
||||
acc[item.manual_case_decision] = (acc[item.manual_case_decision] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
generated_at: new Date().toISOString(),
|
||||
filters_applied: {
|
||||
run_id: runIdFilter ?? null,
|
||||
case_id: caseIdFilter ?? null,
|
||||
min_rating: minRating,
|
||||
manual_case_decision: decisionFilter,
|
||||
limit
|
||||
},
|
||||
stats: {
|
||||
total: items.length,
|
||||
avg_rating: avgRating,
|
||||
by_decision: byDecision
|
||||
},
|
||||
available_manual_case_decisions: MANUAL_CASE_DECISIONS,
|
||||
manual_case_decision_schema: readManualDecisionSchema(),
|
||||
items
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
router.post("/api/autoruns/annotations", (req, res, next) => {
|
||||
try {
|
||||
const body = toRecord(req.body);
|
||||
if (!body) {
|
||||
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "JSON body is required", 400);
|
||||
}
|
||||
const runId = toStringSafe(body.run_id);
|
||||
const caseId = toStringSafe(body.case_id);
|
||||
const messageIndexRaw = toNumberSafe(body.message_index);
|
||||
const ratingRaw = toNumberSafe(body.rating);
|
||||
const comment = parseComment(body.comment);
|
||||
const manualCaseDecision = parseManualCaseDecision(body.manual_case_decision);
|
||||
const annotationAuthor = parseAnnotationAuthor(body.annotation_author);
|
||||
if (!runId || !caseId) {
|
||||
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "run_id and case_id are required", 400);
|
||||
}
|
||||
if (messageIndexRaw === null) {
|
||||
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "message_index is required", 400);
|
||||
}
|
||||
const messageIndex = clampInt(messageIndexRaw, 0, 100_000, 0);
|
||||
if (ratingRaw === null) {
|
||||
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "rating is required", 400);
|
||||
}
|
||||
const rating = clampInt(ratingRaw, 1, 5, 1);
|
||||
if (comment.length === 0) {
|
||||
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "comment is required", 400);
|
||||
}
|
||||
const run = findRunById(runId);
|
||||
if (!run) {
|
||||
throw new http_1.ApiError("AUTORUN_NOT_FOUND", `Run not found: ${runId}`, 404);
|
||||
}
|
||||
const cases = buildCaseSummaries(run.report, run.run_id, false);
|
||||
const caseSummary = cases.find((item) => item.case_id === caseId) ?? null;
|
||||
if (!caseSummary) {
|
||||
throw new http_1.ApiError("AUTORUN_CASE_NOT_FOUND", `Case not found: ${caseId} in run ${runId}`, 404);
|
||||
}
|
||||
const sessionDialog = loadSessionDialog(runId, caseId);
|
||||
const dialog = sessionDialog ?? buildFallbackDialog(run, caseId);
|
||||
if (messageIndex >= dialog.messages.length) {
|
||||
throw new http_1.ApiError("AUTORUN_MESSAGE_NOT_FOUND", `Message index ${messageIndex} out of range`, 400);
|
||||
}
|
||||
const targetMessage = dialog.messages[messageIndex];
|
||||
const targetRole = toStringSafe(targetMessage.role) ?? "unknown";
|
||||
if (targetRole !== "assistant") {
|
||||
throw new http_1.ApiError("AUTORUN_MESSAGE_NOT_ASSISTANT", "Only assistant answers can be annotated", 400);
|
||||
}
|
||||
const nowIso = new Date().toISOString();
|
||||
const annotations = readAnnotations();
|
||||
const key = annotationKey(runId, caseId, messageIndex);
|
||||
const existingIndex = annotations.findIndex((item) => annotationKey(item.run_id, item.case_id, item.message_index) === key);
|
||||
const existing = existingIndex >= 0 ? annotations[existingIndex] : null;
|
||||
const annotation = {
|
||||
annotation_id: existing?.annotation_id ?? generateAnnotationId(),
|
||||
run_id: runId,
|
||||
case_id: caseId,
|
||||
session_id: caseSummary.session_id,
|
||||
message_index: messageIndex,
|
||||
rating,
|
||||
comment,
|
||||
manual_case_decision: manualCaseDecision,
|
||||
annotation_author: annotationAuthor,
|
||||
created_at: existing?.created_at ?? nowIso,
|
||||
updated_at: nowIso,
|
||||
context: {
|
||||
message_id: toStringSafe(targetMessage.message_id),
|
||||
trace_id: toStringSafe(targetMessage.trace_id) ?? caseSummary.trace_id,
|
||||
reply_type: toStringSafe(targetMessage.reply_type) ?? caseSummary.reply_type,
|
||||
eval_target: run.eval_target,
|
||||
prompt_version: toStringSafe(run.report.prompt_version),
|
||||
domain: caseSummary.domain,
|
||||
query_class: caseSummary.query_class
|
||||
}
|
||||
};
|
||||
if (existingIndex >= 0) {
|
||||
annotations[existingIndex] = annotation;
|
||||
}
|
||||
else {
|
||||
annotations.push(annotation);
|
||||
}
|
||||
writeAnnotations(annotations);
|
||||
const annotationStatsByCase = buildAnnotationStatsMap(runId, annotations);
|
||||
const caseStats = annotationStatsByCase.get(caseId) ?? null;
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
annotation,
|
||||
case_annotation_stats: caseStats
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
router.get("/api/autoruns/manual-decision-schema", (_req, res) => {
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
schema: readManualDecisionSchema(),
|
||||
enum: MANUAL_CASE_DECISIONS
|
||||
});
|
||||
});
|
||||
router.get("/api/autoruns/post-analysis", (req, res, next) => {
|
||||
try {
|
||||
const query = req.query;
|
||||
const runIdFilter = toStringSafe(query.run_id);
|
||||
const limitPerQueue = clampInt(toNumberSafe(query.limit_per_queue), 5, 250, 40);
|
||||
const annotationLimit = clampInt(toNumberSafe(query.annotation_limit), 20, 5000, 1500);
|
||||
const scanLimit = clampInt(toNumberSafe(query.scan_limit), 50, 5000, 2500);
|
||||
const runFilters = parseFilters(query);
|
||||
const applyRunFilters = hasAnyRunFilterQuery(query);
|
||||
const runIndex = indexRuns(Math.max(scanLimit, runFilters.scan_limit));
|
||||
const filteredRuns = applyRunFilters ? runIndex.filter((run) => matchesFilters(run, runFilters)) : runIndex;
|
||||
const runMap = new Map(filteredRuns.map((run) => [run.run_id, run]));
|
||||
const scopedAnnotations = readAnnotations()
|
||||
.filter((item) => (runIdFilter ? item.run_id === runIdFilter : true))
|
||||
.filter((item) => (runMap.size > 0 ? runMap.has(item.run_id) : true))
|
||||
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at))
|
||||
.slice(0, annotationLimit);
|
||||
const analysis = collectPostAnalysis(scopedAnnotations, runMap, limitPerQueue);
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
generated_at: new Date().toISOString(),
|
||||
filters_applied: {
|
||||
run_id: runIdFilter ?? null,
|
||||
run_filters_applied: applyRunFilters,
|
||||
limit_per_queue: limitPerQueue,
|
||||
annotation_limit: annotationLimit,
|
||||
scan_limit: scanLimit
|
||||
},
|
||||
runs_considered: filteredRuns.slice(0, 500).map((item) => buildRunSummary(item)),
|
||||
manual_case_decision_schema: readManualDecisionSchema(),
|
||||
post_analysis: analysis
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
router.get("/api/autoruns/autogen/history", (req, res, next) => {
|
||||
try {
|
||||
const limit = clampInt(toNumberSafe(req.query.limit), 1, 500, 120);
|
||||
const rawMode = toStringSafe(req.query.mode);
|
||||
const includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
|
||||
const modeFilter = rawMode ?? "codex_creative";
|
||||
const items = readAutoGenHistory()
|
||||
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
|
||||
.slice(0, limit);
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
generated_at: new Date().toISOString(),
|
||||
items
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
router.post("/api/autoruns/autogen/generate", (req, res, next) => {
|
||||
try {
|
||||
const body = toRecord(req.body);
|
||||
if (!body) {
|
||||
throw new http_1.ApiError("INVALID_AUTOGEN_PAYLOAD", "JSON body is required", 400);
|
||||
}
|
||||
const mode = parseAutoGenMode(body.mode);
|
||||
const count = parseAutogenCount(body.count);
|
||||
const domain = parseAutogenDomain(body.domain);
|
||||
const persistCaseSet = toBooleanSafe(body.persist_to_eval_cases) ?? true;
|
||||
const generatedBy = parseAnnotationAuthor(body.generated_by);
|
||||
const context = toRecord(body.context);
|
||||
const questions = mode === "qwen_seed"
|
||||
? generateQwenSeedQuestions(count, domain)
|
||||
: generateCodexCreativeQuestions(count, domain);
|
||||
const generationId = generateAutogenId();
|
||||
let savedCaseSetFile = null;
|
||||
if (persistCaseSet) {
|
||||
if (!fs_1.default.existsSync(config_1.EVAL_CASES_DIR)) {
|
||||
fs_1.default.mkdirSync(config_1.EVAL_CASES_DIR, { recursive: true });
|
||||
}
|
||||
const fileName = buildAutogenCaseSetFileName(mode, generationId);
|
||||
const filePath = path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName);
|
||||
const payload = buildAutogenCaseSetPayload({
|
||||
generationId,
|
||||
mode,
|
||||
domain,
|
||||
questions
|
||||
});
|
||||
fs_1.default.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
||||
savedCaseSetFile = fileName;
|
||||
}
|
||||
const record = {
|
||||
generation_id: generationId,
|
||||
created_at: new Date().toISOString(),
|
||||
mode,
|
||||
count: questions.length,
|
||||
domain,
|
||||
questions,
|
||||
generated_by: generatedBy,
|
||||
saved_case_set_file: savedCaseSetFile,
|
||||
context: context
|
||||
? {
|
||||
llm_provider: toStringSafe(context.llm_provider),
|
||||
model: toStringSafe(context.model),
|
||||
assistant_prompt_version: toStringSafe(context.assistant_prompt_version),
|
||||
decomposition_prompt_version: toStringSafe(context.decomposition_prompt_version),
|
||||
prompt_fingerprint: toStringSafe(context.prompt_fingerprint)
|
||||
}
|
||||
: null
|
||||
};
|
||||
const history = readAutoGenHistory();
|
||||
history.unshift(record);
|
||||
writeAutoGenHistory(history.slice(0, 500));
|
||||
(0, http_1.ok)(res, {
|
||||
ok: true,
|
||||
generation: record
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
+2
@@ -31,6 +31,8 @@ function createApp() {
|
||||
(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);
|
||||
(0, files_1.ensureDir)(config_1.AUTORUN_ANNOTATIONS_DIR);
|
||||
(0, files_1.ensureDir)(config_1.AUTORUN_GENERATOR_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" }));
|
||||
|
||||
+38
-38
@@ -1158,29 +1158,29 @@ function buildProblemCentricActions(input) {
|
||||
actions.push("Проверьте зачет аванса или взаимозачет и связку платежа с закрытием расчета.");
|
||||
}
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
}
|
||||
if (unitTypes.has("cross_branch_inconsistency_cluster")) {
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
}
|
||||
if (unitTypes.has("lifecycle_anomaly_node")) {
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
}
|
||||
for (const unit of input.units) {
|
||||
if (unit.lifecycle_defect_type === "stale_active_state") {
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "misclosed_state") {
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
}
|
||||
}
|
||||
if (input.missingAnchors.period && input.mode !== "clarification_required") {
|
||||
@@ -1188,20 +1188,20 @@ function buildProblemCentricActions(input) {
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
if (input.missingAnchors.period) {
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
}
|
||||
}
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
return uniqueStrings(actions, 6);
|
||||
}
|
||||
@@ -1215,25 +1215,25 @@ function buildProblemCentricClarifications(input) {
|
||||
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
}
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
return uniqueStrings(questions, 6);
|
||||
}
|
||||
@@ -1402,10 +1402,10 @@ function detectMissingAnchors(userMessage, retrievalResults = [], options) {
|
||||
hasPeriodAnchorInRetrieval(retrievalResults) ||
|
||||
Boolean(options?.normalizationPeriodExplicit) ||
|
||||
hasPeriodAnchorInCompanyAnchors(options?.companyAnchors);
|
||||
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(lower) || hasAccountAnchorInRetrieval(retrievalResults);
|
||||
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
|
||||
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
|
||||
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
|
||||
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(lower) || hasAccountAnchorInRetrieval(retrievalResults);
|
||||
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
|
||||
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
|
||||
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
|
||||
return {
|
||||
period: !hasPeriod,
|
||||
account: !hasAccount,
|
||||
@@ -1424,50 +1424,50 @@ function buildClarificationQuestions(input) {
|
||||
questions.push("Уточните период проверки (например, июль 2020).");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
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 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
}
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
if (input.coverageReport.requirements_partially_covered.length > 0) {
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("snapshot_only")) {
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
}
|
||||
if (input.sourceRefs.length > 0) {
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
}
|
||||
return uniqueStrings(actions, 6);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.loadAssistantCanonExcerpt = loadAssistantCanonExcerpt;
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const config_1 = require("../config");
|
||||
const FALLBACK_CANON = [
|
||||
"Не выдумывай возможности.",
|
||||
"Не обещай настройку 1С и админ-действия.",
|
||||
"Не показывай внутренние технические термины пользователю.",
|
||||
"Говори по-человечески и предлагай ближайший полезный поддерживаемый шаг."
|
||||
].join(" ");
|
||||
let cache = null;
|
||||
function stripMarkdown(input) {
|
||||
return input
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.replace(/[`*_>\-\[\]\(\)]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function loadAssistantCanonExcerpt(maxChars = 900) {
|
||||
try {
|
||||
const mtimeMs = fs_1.default.existsSync(config_1.ASSISTANT_CANON_FILE) ? fs_1.default.statSync(config_1.ASSISTANT_CANON_FILE).mtimeMs : -1;
|
||||
if (cache && cache.mtimeMs === mtimeMs) {
|
||||
return cache.excerpt;
|
||||
}
|
||||
if (!fs_1.default.existsSync(config_1.ASSISTANT_CANON_FILE)) {
|
||||
return FALLBACK_CANON;
|
||||
}
|
||||
const raw = fs_1.default.readFileSync(config_1.ASSISTANT_CANON_FILE, "utf-8");
|
||||
const normalized = stripMarkdown(raw);
|
||||
const excerpt = normalized.length > maxChars ? `${normalized.slice(0, maxChars).trim()}...` : normalized;
|
||||
cache = { mtimeMs, excerpt: excerpt || FALLBACK_CANON };
|
||||
return cache.excerpt;
|
||||
}
|
||||
catch {
|
||||
return cache?.excerpt ?? FALLBACK_CANON;
|
||||
}
|
||||
}
|
||||
+91
-91
@@ -536,8 +536,8 @@ const P0_DOMAIN_CARDS = [
|
||||
/закрыт[а-яё]*\s+период/i,
|
||||
/close\s+operation/i,
|
||||
/allocation/i,
|
||||
/закр/i,
|
||||
/перио/i,
|
||||
/закр/i,
|
||||
/перио/i,
|
||||
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
|
||||
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
|
||||
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
|
||||
@@ -559,14 +559,14 @@ function parseDateCandidate(value) {
|
||||
}
|
||||
function extractDate(record) {
|
||||
const attrs = record.attributes ?? {};
|
||||
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
|
||||
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
|
||||
for (const key of directKeys) {
|
||||
if (attrs[key] !== undefined && attrs[key] !== null) {
|
||||
return String(attrs[key]);
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
|
||||
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -593,7 +593,7 @@ function countNavigationLinks(record) {
|
||||
return count;
|
||||
}
|
||||
function findCounterpartyLinks(record) {
|
||||
return record.links.filter((link) => link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field));
|
||||
return record.links.filter((link) => link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field));
|
||||
}
|
||||
function extractGuids(text) {
|
||||
const matches = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) ?? [];
|
||||
@@ -1305,14 +1305,14 @@ function inferPeriodScope(fragmentText) {
|
||||
granularity: "year"
|
||||
};
|
||||
}
|
||||
if (/квартал|quarter/i.test(fragmentText)) {
|
||||
if (/квартал|quarter/i.test(fragmentText)) {
|
||||
return {
|
||||
from: null,
|
||||
to: null,
|
||||
granularity: "quarter"
|
||||
};
|
||||
}
|
||||
if (/месяц|month|период/i.test(fragmentText)) {
|
||||
if (/месяц|month|период/i.test(fragmentText)) {
|
||||
return {
|
||||
from: null,
|
||||
to: null,
|
||||
@@ -1378,7 +1378,7 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
||||
const excludedInterpretations = [];
|
||||
const rankingBasis = ["closure_risk", "repeatability", "financial_impact"];
|
||||
const explanationFocus = ["why_selected", "where_chain_breaks", "what_business_risk"];
|
||||
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
|
||||
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
|
||||
pushMany(domainScope, ["bank", "settlements"]);
|
||||
pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]);
|
||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||
@@ -1390,51 +1390,51 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
||||
const hasDeferredExpenseAccountScope = accountScope.some((item) => item === "97");
|
||||
const hasMonthCloseCostsAccountScope = accountScope.some((item) => CLOSE_COST_ACCOUNTS.includes(item));
|
||||
const hasExplicitMonthCloseLexicalMarker = /(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(lower) ||
|
||||
(/закр/i.test(lower) && /перио/i.test(lower));
|
||||
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
|
||||
(/закр/i.test(lower) && /перио/i.test(lower));
|
||||
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
|
||||
pushMany(domainScope, ["suppliers", "settlements"]);
|
||||
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
|
||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||
}
|
||||
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
|
||||
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
|
||||
pushMany(domainScope, ["customers", "settlements"]);
|
||||
pushMany(documentTypes, ["sales_document", "settlement_document"]);
|
||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||
}
|
||||
if (/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(lower) ||
|
||||
if (/ндс|vat|книга\s+покупок|книга\s+продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(lower) ||
|
||||
hasVatAccountScope) {
|
||||
pushMany(domainScope, ["vat", "taxes"]);
|
||||
pushMany(documentTypes, ["invoice", "vat_document"]);
|
||||
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
||||
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
||||
}
|
||||
if (/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
|
||||
if (/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
|
||||
hasFixedAssetAccountScope) {
|
||||
pushMany(domainScope, ["fixed_assets"]);
|
||||
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
||||
pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
|
||||
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
|
||||
}
|
||||
if (/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
|
||||
if (/рбп|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
|
||||
hasDeferredExpenseAccountScope) {
|
||||
pushMany(domainScope, ["deferred_expense", "period_close"]);
|
||||
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
|
||||
pushMany(entityTypes, ["document", "posting"]);
|
||||
pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]);
|
||||
}
|
||||
if (/цепоч|разрыв|СЃРІСЏР·|документ.*РїСЂРѕРІРѕРґ|РіРґРµ рвет|Р¶РёРІСѓС‚ отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
|
||||
if (/цепоч|разрыв|связ|документ.*провод|где рвет|живут отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
|
||||
pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]);
|
||||
pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]);
|
||||
}
|
||||
if (/аномал|СЂРёСЃРє|С…РІРѕСЃС‚|РїРѕРґРѕР·СЂ|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
|
||||
if (/аномал|риск|хвост|подозр|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
|
||||
pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]);
|
||||
}
|
||||
if (WRONG_DOCUMENT_MARKERS.test(lower)) {
|
||||
pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]);
|
||||
}
|
||||
if (/Р¶РёРІСѓС‚ отдельно|РЅРµ СЃРІСЏР·|без СЃРІСЏР·Рё|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
|
||||
if (/живут отдельно|не связ|без связи|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
|
||||
pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]);
|
||||
}
|
||||
if (REPEATED_ANOMALY_MARKERS.test(lower)) {
|
||||
@@ -1446,10 +1446,10 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
||||
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
||||
pushMany(documentTypes, ["period_close_document"]);
|
||||
}
|
||||
if (/РЅРµ РІ платеже|не\s+в\s+платеже|not payment/i.test(lower)) {
|
||||
if (/не\s+в\s+платеже|not payment/i.test(lower)) {
|
||||
pushMany(excludedInterpretations, ["simple_payment_delay"]);
|
||||
}
|
||||
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|не\s+по\s+сумм|не\s+сумм|not by amount/i.test(lower)) {
|
||||
if (/не\s+по\s+сумм|не\s+сумм|не\s+сумма|not by amount/i.test(lower)) {
|
||||
pushMany(excludedInterpretations, ["amount_only_anomaly"]);
|
||||
pushMany(rankingBasis, ["amount_independent_risk"]);
|
||||
}
|
||||
@@ -1735,16 +1735,16 @@ function inferAccountsFromRecord(record, corpus) {
|
||||
accounts.push(token.split(".")[0]);
|
||||
}
|
||||
for (const key of Object.keys(record.attributes ?? {})) {
|
||||
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
|
||||
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
|
||||
accounts.push("51");
|
||||
}
|
||||
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
|
||||
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
|
||||
accounts.push("60");
|
||||
}
|
||||
if (/счетучетандс/i.test(key)) {
|
||||
if (/счетучетандс/i.test(key)) {
|
||||
accounts.push("19");
|
||||
}
|
||||
if (/субконтодт/i.test(key)) {
|
||||
if (/субконтодт/i.test(key)) {
|
||||
accounts.push("60");
|
||||
}
|
||||
}
|
||||
@@ -1752,28 +1752,28 @@ function inferAccountsFromRecord(record, corpus) {
|
||||
}
|
||||
function inferDocumentTypesFromRecord(record, corpus) {
|
||||
const items = [];
|
||||
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
|
||||
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
|
||||
pushMany(items, ["bank_statement", "payment_order"]);
|
||||
}
|
||||
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
|
||||
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
|
||||
items.push("supplier_receipt");
|
||||
}
|
||||
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
|
||||
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
|
||||
items.push("sales_document");
|
||||
}
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
pushMany(items, ["invoice", "vat_document"]);
|
||||
}
|
||||
if (/корректировк|ручн|manual/i.test(corpus)) {
|
||||
if (/корректировк|ручн|manual/i.test(corpus)) {
|
||||
items.push("manual_operation");
|
||||
}
|
||||
if (/закрытие|регламент/i.test(corpus)) {
|
||||
if (/закрытие|регламент/i.test(corpus)) {
|
||||
items.push("period_close_document");
|
||||
}
|
||||
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
|
||||
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
|
||||
pushMany(items, ["fixed_asset_card", "depreciation_document"]);
|
||||
}
|
||||
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
|
||||
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
|
||||
items.push("deferred_expense_document");
|
||||
}
|
||||
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
|
||||
@@ -1801,10 +1801,10 @@ function inferDomainsFromRecord(corpus, documentTypes, record) {
|
||||
if (documentTypes.some((item) => item === "deferred_expense_document")) {
|
||||
pushMany(domains, ["deferred_expense", "period_close"]);
|
||||
}
|
||||
if (/закрытие|регламент|period close/i.test(corpus)) {
|
||||
if (/закрытие|регламент|period close/i.test(corpus)) {
|
||||
domains.push("period_close");
|
||||
}
|
||||
const hasSettlementLexicalAnchor = /(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(corpus);
|
||||
const hasSettlementLexicalAnchor = /(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(corpus);
|
||||
const hasSettlementDocAnchor = documentTypes.some((item) => item === "bank_statement" || item === "payment_order" || item === "supplier_receipt" || item === "sales_document");
|
||||
const hasSettlementDomainAnchor = domains.includes("bank") || domains.includes("suppliers") || domains.includes("customers") || domains.includes("supplier_payments");
|
||||
if (findCounterpartyLinks(record).length > 0 && (hasSettlementLexicalAnchor || hasSettlementDocAnchor || hasSettlementDomainAnchor)) {
|
||||
@@ -1824,13 +1824,13 @@ function inferEntityTypes(record) {
|
||||
entities.push("counterparty");
|
||||
}
|
||||
const corpus = collectTextFromRecord(record);
|
||||
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus)) {
|
||||
if (/договор|contract/i.test(corpus)) {
|
||||
entities.push("contract");
|
||||
}
|
||||
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
|
||||
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
|
||||
entities.push("fixed_asset");
|
||||
}
|
||||
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
entities.push("tax_entry");
|
||||
}
|
||||
return uniqueStrings(entities);
|
||||
@@ -1842,25 +1842,25 @@ function inferRelationPatterns(record, corpus) {
|
||||
if (hasDocLinks) {
|
||||
patterns.push("document_to_posting");
|
||||
}
|
||||
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
|
||||
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
|
||||
patterns.push("payment_to_settlement");
|
||||
}
|
||||
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
|
||||
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
|
||||
patterns.push("statement_to_document");
|
||||
}
|
||||
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
|
||||
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
|
||||
patterns.push("asset_card_to_depreciation");
|
||||
}
|
||||
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
|
||||
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
|
||||
patterns.push("deferred_expense_to_writeoff");
|
||||
}
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
patterns.push("invoice_to_vat");
|
||||
}
|
||||
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus) && hasDocLinks) {
|
||||
if (/договор|contract/i.test(corpus) && hasDocLinks) {
|
||||
patterns.push("contract_to_documents");
|
||||
}
|
||||
if (/склад|товар|материал|receipt/i.test(corpus)) {
|
||||
if (/склад|товар|материал|receipt/i.test(corpus)) {
|
||||
patterns.push("receipt_to_stock_movement");
|
||||
}
|
||||
return uniqueStrings(patterns);
|
||||
@@ -1899,7 +1899,7 @@ function inferAnomalyPatterns(record, corpus, relationPatterns, lifecycleMarkers
|
||||
if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) {
|
||||
anomalies.push("posting_mismatch");
|
||||
}
|
||||
if (/ручн|manual|корректировк/.test(corpus)) {
|
||||
if (/ручн|manual|корректировк/.test(corpus)) {
|
||||
anomalies.push("manual_intervention_suspicion");
|
||||
}
|
||||
if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) {
|
||||
@@ -1911,7 +1911,7 @@ function inferAnomalyPatterns(record, corpus, relationPatterns, lifecycleMarkers
|
||||
if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) {
|
||||
anomalies.push("silent_orphan");
|
||||
}
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
if (!hasAmountSignal && anomalies.length > 0) {
|
||||
anomalies.push("amount_independent_risk");
|
||||
}
|
||||
@@ -1954,15 +1954,15 @@ function evaluateExcludedInterpretations(profile, signals, record) {
|
||||
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"];
|
||||
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
|
||||
if (!hasStructural) {
|
||||
reasons.push("Рсключено как simple_payment_delay без структурного дефекта.");
|
||||
reasons.push("Исключено как simple_payment_delay без структурного дефекта.");
|
||||
}
|
||||
}
|
||||
if (interpretationSet.has("amount_only_anomaly")) {
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"];
|
||||
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
|
||||
if (hasAmountSignal && !hasStructural) {
|
||||
reasons.push("Рсключено как amount-only аномалия без структурных признаков.");
|
||||
reasons.push("Исключено как amount-only аномалия без структурных признаков.");
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -2016,22 +2016,22 @@ function evaluateRecordAgainstProfile(record, profile) {
|
||||
const graphTraversal = evaluateGraphTraversalForRecord(profile, signals);
|
||||
const matchReasons = [];
|
||||
if (accountMatch && profile.account_scope.length > 0) {
|
||||
matchReasons.push("Совпал account_scope.");
|
||||
matchReasons.push("Совпал account_scope.");
|
||||
}
|
||||
if (domainMatch && profile.domain_scope.length > 0) {
|
||||
matchReasons.push("Совпал domain_scope.");
|
||||
matchReasons.push("Совпал domain_scope.");
|
||||
}
|
||||
if (documentMatch && profile.document_types.length > 0) {
|
||||
matchReasons.push("Совпал document_types.");
|
||||
matchReasons.push("Совпал document_types.");
|
||||
}
|
||||
if (relationMatch && profile.relation_patterns.length > 0) {
|
||||
matchReasons.push("Совпали relation_patterns.");
|
||||
matchReasons.push("Совпали relation_patterns.");
|
||||
}
|
||||
if (anomalyMatch && profile.anomaly_patterns.length > 0) {
|
||||
matchReasons.push("Совпали anomaly_patterns.");
|
||||
matchReasons.push("Совпали anomaly_patterns.");
|
||||
}
|
||||
if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) {
|
||||
matchReasons.push("Совпал lifecycle_stage_filters.");
|
||||
matchReasons.push("Совпал lifecycle_stage_filters.");
|
||||
}
|
||||
if (graphTraversal.domain_match) {
|
||||
matchReasons.push("Graph traversal domain matched.");
|
||||
@@ -2206,7 +2206,7 @@ class AssistantDataLayer {
|
||||
business_interpretation: [],
|
||||
confidence: "low",
|
||||
limitations: ["Snapshot data files could not be loaded."],
|
||||
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
|
||||
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
|
||||
};
|
||||
}
|
||||
let result = null;
|
||||
@@ -3037,8 +3037,8 @@ class AssistantDataLayer {
|
||||
? "medium"
|
||||
: "low",
|
||||
business_interpretation: group.risk_factors.size > 0
|
||||
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
|
||||
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
|
||||
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
|
||||
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
|
||||
relation_types: Array.from(group.relations.entries())
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.map((item) => item[0]),
|
||||
@@ -3086,24 +3086,24 @@ class AssistantDataLayer {
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [
|
||||
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
|
||||
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
|
||||
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
|
||||
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
|
||||
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
|
||||
guidFilter.length > 0
|
||||
? "GUID-фильтрация включена."
|
||||
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
|
||||
? "GUID-фильтрация включена."
|
||||
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
|
||||
`Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`,
|
||||
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
|
||||
],
|
||||
risk_factors: semanticProfile.anomaly_patterns,
|
||||
business_interpretation: [
|
||||
"По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
|
||||
"Для точечного drilldown добавьте GUID или уточните период/контрагента."
|
||||
"По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
|
||||
"Для точечного drilldown добавьте GUID или уточните период/контрагента."
|
||||
],
|
||||
confidence: "medium",
|
||||
limitations: [
|
||||
guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
|
||||
"Рсточник данных — snapshot 2020 (read-only), Р° РЅРµ live состояние базы 1РЎ.",
|
||||
guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
|
||||
"Источник данных — snapshot 2020 (read-only), а не live состояние базы 1С.",
|
||||
domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся."
|
||||
],
|
||||
errors: []
|
||||
@@ -3144,33 +3144,33 @@ class AssistantDataLayer {
|
||||
},
|
||||
evidence: evidence.slice(0, 12),
|
||||
why_included: [
|
||||
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
|
||||
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
|
||||
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
|
||||
semanticProfile.account_scope.length > 0
|
||||
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
|
||||
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
|
||||
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
|
||||
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
|
||||
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
|
||||
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
|
||||
`Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.`
|
||||
],
|
||||
selection_reason: [
|
||||
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
|
||||
"GUID-mode отключен: full scan без ограничителей не использовался.",
|
||||
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
|
||||
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
|
||||
"GUID-mode отключен: full scan без ограничителей не использовался.",
|
||||
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
|
||||
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.",
|
||||
`Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`,
|
||||
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
|
||||
],
|
||||
risk_factors: aggregatedRiskFactors.length > 0
|
||||
? aggregatedRiskFactors
|
||||
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
|
||||
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
|
||||
business_interpretation: [
|
||||
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
|
||||
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
|
||||
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
|
||||
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
|
||||
],
|
||||
confidence: "high",
|
||||
limitations: [
|
||||
guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
|
||||
"Рсточник данных — snapshot 2020 (read-only), РЅРµ live контур 1РЎ.",
|
||||
guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
|
||||
"Источник данных — snapshot 2020 (read-only), не live контур 1С.",
|
||||
domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся."
|
||||
],
|
||||
errors: []
|
||||
@@ -3455,12 +3455,12 @@ class AssistantDataLayer {
|
||||
})),
|
||||
why_included: items.length > 0
|
||||
? [
|
||||
"Показаны сущности с максимальным количеством записей.",
|
||||
"Показаны сущности с максимальным количеством записей.",
|
||||
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
|
||||
]
|
||||
: [],
|
||||
selection_reason: [
|
||||
"Ранжирование выполнено по records_count по убыванию.",
|
||||
"Ранжирование выполнено по records_count по убыванию.",
|
||||
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
|
||||
],
|
||||
risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]),
|
||||
@@ -3469,7 +3469,7 @@ class AssistantDataLayer {
|
||||
],
|
||||
confidence: "medium",
|
||||
limitations: [
|
||||
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
|
||||
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
|
||||
domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся."
|
||||
],
|
||||
errors: []
|
||||
@@ -3604,7 +3604,7 @@ class AssistantDataLayer {
|
||||
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
|
||||
],
|
||||
risk_factors: semanticProfile.anomaly_patterns,
|
||||
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
|
||||
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
|
||||
confidence: "high",
|
||||
limitations: [
|
||||
"Это read-only snapshot, а не онлайн-состояние 1С.",
|
||||
@@ -3634,11 +3634,11 @@ class AssistantDataLayer {
|
||||
},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
|
||||
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
|
||||
risk_factors: [],
|
||||
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
|
||||
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
|
||||
confidence: "low",
|
||||
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
|
||||
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
@@ -3686,14 +3686,14 @@ class AssistantDataLayer {
|
||||
},
|
||||
evidence: matches.slice(0, 10),
|
||||
why_included: matches.length > 0
|
||||
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
|
||||
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
|
||||
: [],
|
||||
selection_reason: [
|
||||
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
|
||||
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
|
||||
],
|
||||
risk_factors: [],
|
||||
business_interpretation: [
|
||||
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
|
||||
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
|
||||
],
|
||||
confidence: matches.length > 0 ? "medium" : "low",
|
||||
limitations: [
|
||||
@@ -3722,12 +3722,12 @@ class AssistantDataLayer {
|
||||
matched_records: matches.length
|
||||
},
|
||||
evidence: matches.slice(0, 10),
|
||||
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
|
||||
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
|
||||
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
|
||||
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
|
||||
risk_factors: [],
|
||||
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
|
||||
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
|
||||
confidence: matches.length > 0 ? "high" : "medium",
|
||||
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
|
||||
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
|
||||
+21
-21
@@ -378,7 +378,7 @@ function parseDateLike(raw) {
|
||||
if (dayMonthYear) {
|
||||
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
|
||||
}
|
||||
const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i);
|
||||
const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i);
|
||||
if (rusMonthYear) {
|
||||
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
|
||||
if (!month)
|
||||
@@ -467,7 +467,7 @@ function normalizedAnchorFromFragments(normalized) {
|
||||
source: `normalized_time_scope:${type || "unknown"}`
|
||||
};
|
||||
}
|
||||
if (/(?:июл|july|РёСЋР»)/i.test(value)) {
|
||||
if (/(?:июл|july|июл)/i.test(value)) {
|
||||
return {
|
||||
value: `${JULY_YEAR}-${JULY_MONTH}`,
|
||||
source: `normalized_time_scope:${type || "unknown"}`
|
||||
@@ -491,7 +491,7 @@ function resolveJulyAnchor(rawText) {
|
||||
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
|
||||
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
|
||||
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
|
||||
const monthByNamed = /(?:июл|july|июл)/i.test(lower);
|
||||
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
|
||||
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
|
||||
return {
|
||||
@@ -662,7 +662,7 @@ function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
|
||||
return item;
|
||||
}
|
||||
const text = String(item.fragment_text ?? "").trim();
|
||||
if (/2020-07|июл|РёСЋР»|july/i.test(text)) {
|
||||
if (/2020-07|июл|июл|july/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
@@ -681,7 +681,7 @@ function resolveDomainPolarityGuard(input) {
|
||||
prefixes.has("62") ||
|
||||
prefixes.has("51") ||
|
||||
prefixes.has("76") ||
|
||||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|С…РІРѕСЃС‚)/i.test(lower);
|
||||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|хвост)/i.test(lower);
|
||||
if (!settlementSignal) {
|
||||
return {
|
||||
applied: false,
|
||||
@@ -700,12 +700,12 @@ function resolveDomainPolarityGuard(input) {
|
||||
reason_codes: []
|
||||
};
|
||||
}
|
||||
const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
|
||||
const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
|
||||
(prefixes.has("60") ? 2 : 0) +
|
||||
(/(?:сч[её]т\s*60|по\s*60|счет\s*60|РїРѕ\s*60)/i.test(lower) ? 1 : 0);
|
||||
const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
|
||||
(/(?:сч[её]т\s*60|по\s*60|счет\s*60)/i.test(lower) ? 1 : 0);
|
||||
const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
|
||||
(prefixes.has("62") ? 2 : 0) +
|
||||
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0);
|
||||
(/(?:сч[её]т\s*62|по\s*62|счет\s*62)/i.test(lower) ? 1 : 0);
|
||||
let polarity = "mixed_or_unresolved";
|
||||
if (supplierScore > 0 || customerScore > 0) {
|
||||
if (supplierScore >= customerScore + 2) {
|
||||
@@ -758,10 +758,10 @@ function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
|
||||
return item;
|
||||
}
|
||||
const text = String(item.fragment_text ?? "").trim();
|
||||
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) {
|
||||
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60)/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62|РїРѕ\s*62)/i.test(text)) {
|
||||
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62)/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
@@ -771,10 +771,10 @@ function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
|
||||
});
|
||||
}
|
||||
function containsReceivableSignal(value) {
|
||||
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
|
||||
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
|
||||
}
|
||||
function containsPayableSignal(value) {
|
||||
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
|
||||
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
|
||||
}
|
||||
function problemUnitCorpus(unit) {
|
||||
return [
|
||||
@@ -1319,15 +1319,15 @@ function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
|
||||
? "no_grounded_answer"
|
||||
: "partial";
|
||||
const reasonMap = {
|
||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
||||
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
|
||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
|
||||
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
|
||||
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
|
||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
|
||||
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
|
||||
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
|
||||
};
|
||||
const reasons = [
|
||||
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
||||
|
||||
+6
-12
@@ -65,6 +65,8 @@ const addressFilterExtractor_1 = __importStar(require("./addressFilterExtractor"
|
||||
const predecomposeContract_1 = __importStar(require("./address_runtime/predecomposeContract"));
|
||||
const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient"));
|
||||
const addressMcpClient_1 = __importStar(require("./addressMcpClient"));
|
||||
const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry"));
|
||||
const assistantCanon_1 = __importStar(require("./assistantCanon"));
|
||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||
const dataScopeProbeCache = new Map();
|
||||
@@ -3903,17 +3905,7 @@ function buildLivingChatPrompt(userMessage, conversationWindow) {
|
||||
return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
|
||||
}
|
||||
function buildAssistantCapabilityContractReply() {
|
||||
return [
|
||||
"Я ассистент по анализу данных 1С в режиме чтения.",
|
||||
"Что умею сейчас:",
|
||||
"1. Находить документы, операции, договоры и остатки по контрагенту/договору/периоду.",
|
||||
"2. Делать агрегаты по базе: активность, роли контрагентов, top-срезы по суммам и операциям.",
|
||||
"3. Кратко объяснять результат и подсказывать следующий точный запрос.",
|
||||
"Что не умею:",
|
||||
"1. Не настраиваю 1С и не меняю конфигурацию.",
|
||||
"2. Не создаю и не провожу документы в базе.",
|
||||
"3. Не выполняю админские действия на сервере."
|
||||
].join("\n");
|
||||
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
||||
}
|
||||
function normalizeScopeLabel(value) {
|
||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||
@@ -5016,6 +5008,7 @@ class AssistantService {
|
||||
else {
|
||||
const conversationWindow = buildLivingChatContextWindow(session.items);
|
||||
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
||||
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
||||
const chatResponse = await this.chatClient.chat({
|
||||
llmProvider: payload.llmProvider,
|
||||
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
|
||||
@@ -5029,7 +5022,8 @@ class AssistantService {
|
||||
"Работай честно: не заявляй действия, которые недоступны в этом рантайме.",
|
||||
"Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.",
|
||||
"Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.",
|
||||
"Если пользователь спрашивает про возможности, отвечай только по этому контракту."
|
||||
"Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
|
||||
`Канон поведения: ${canonExcerpt}`
|
||||
].join(" "),
|
||||
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
|
||||
userMessage: userPrompt,
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.loadCapabilitiesRegistry = loadCapabilitiesRegistry;
|
||||
exports.buildCapabilityContractReplyFromRegistry = buildCapabilityContractReplyFromRegistry;
|
||||
exports.resolveNearestCapabilityGroup = resolveNearestCapabilityGroup;
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const config_1 = require("../config");
|
||||
const FALLBACK_REGISTRY = {
|
||||
schema_version: "capabilities_registry_fallback_v1",
|
||||
updated_at: "2026-04-09T00:00:00.000Z",
|
||||
assistant_mode: "read_only",
|
||||
groups: [
|
||||
{
|
||||
group_code: "vat",
|
||||
group_title: "НДС",
|
||||
description: "Срезы и расчеты НДС на базе данных 1С.",
|
||||
risk_level: "high",
|
||||
maturity_status: "partial",
|
||||
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
|
||||
unsupported_operations: ["submit_tax_declaration"],
|
||||
required_entities: ["period", "organization"],
|
||||
optional_entities: ["counterparty"],
|
||||
typical_queries: ["Сколько НДС к уплате за период?"],
|
||||
related_routes: [],
|
||||
safe_alternatives: ["Показать движения по 68/19 за период"],
|
||||
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
|
||||
},
|
||||
{
|
||||
group_code: "counterparties",
|
||||
group_title: "Контрагенты",
|
||||
description: "Документы, операции, договоры и срезы по контрагентам.",
|
||||
risk_level: "medium",
|
||||
maturity_status: "production_ready",
|
||||
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||||
unsupported_operations: ["edit_counterparty_card"],
|
||||
required_entities: ["counterparty_scope_or_contract"],
|
||||
optional_entities: ["period", "organization"],
|
||||
typical_queries: ["Покажи документы по контрагенту"],
|
||||
related_routes: [],
|
||||
safe_alternatives: ["Уточнить ИНН/наименование контрагента"],
|
||||
one_c_hints: ["Справочник.Контрагенты"]
|
||||
},
|
||||
{
|
||||
group_code: "boundaries",
|
||||
group_title: "Ограничения",
|
||||
description: "Операции, которые ассистент не выполняет.",
|
||||
risk_level: "high",
|
||||
maturity_status: "production_ready",
|
||||
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
|
||||
unsupported_operations: ["configure_1c", "admin_server_actions", "create_or_post_documents"],
|
||||
required_entities: [],
|
||||
optional_entities: [],
|
||||
typical_queries: ["Можешь настроить 1С?"],
|
||||
related_routes: [],
|
||||
safe_alternatives: ["Сформировать план диагностики для 1С/ИТ-админа"],
|
||||
one_c_hints: []
|
||||
}
|
||||
]
|
||||
};
|
||||
let cache = null;
|
||||
function toRecord(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value))
|
||||
return null;
|
||||
return value;
|
||||
}
|
||||
function toStringSafe(value) {
|
||||
if (typeof value !== "string")
|
||||
return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
function toArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
function readRegistryFromFile() {
|
||||
if (!fs_1.default.existsSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE))
|
||||
return null;
|
||||
try {
|
||||
const raw = fs_1.default.readFileSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE, "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
const root = toRecord(parsed);
|
||||
if (!root)
|
||||
return null;
|
||||
const groups = toArray(root.groups)
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item) => item !== null)
|
||||
.map((item) => ({
|
||||
group_code: toStringSafe(item.group_code) ?? "unknown_group",
|
||||
group_title: toStringSafe(item.group_title) ?? "Группа",
|
||||
description: toStringSafe(item.description) ?? "",
|
||||
risk_level: toStringSafe(item.risk_level) ?? "medium",
|
||||
maturity_status: toStringSafe(item.maturity_status) ??
|
||||
"partial",
|
||||
supported_operations: toArray(item.supported_operations)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
unsupported_operations: toArray(item.unsupported_operations)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
required_entities: toArray(item.required_entities)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
optional_entities: toArray(item.optional_entities)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
typical_queries: toArray(item.typical_queries)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
related_routes: toArray(item.related_routes)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
safe_alternatives: toArray(item.safe_alternatives)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null),
|
||||
one_c_hints: toArray(item.one_c_hints)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v) => v !== null)
|
||||
}));
|
||||
if (groups.length === 0)
|
||||
return null;
|
||||
return {
|
||||
schema_version: toStringSafe(root.schema_version) ?? "capabilities_registry_v1",
|
||||
updated_at: toStringSafe(root.updated_at) ?? new Date().toISOString(),
|
||||
assistant_mode: toStringSafe(root.assistant_mode) ?? "read_only",
|
||||
groups
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function loadCapabilitiesRegistry() {
|
||||
try {
|
||||
const mtimeMs = fs_1.default.existsSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE)
|
||||
? fs_1.default.statSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE).mtimeMs
|
||||
: -1;
|
||||
if (cache && cache.mtimeMs === mtimeMs) {
|
||||
return cache.value;
|
||||
}
|
||||
const value = readRegistryFromFile() ?? FALLBACK_REGISTRY;
|
||||
cache = { mtimeMs, value };
|
||||
return value;
|
||||
}
|
||||
catch {
|
||||
return cache?.value ?? FALLBACK_REGISTRY;
|
||||
}
|
||||
}
|
||||
function buildCapabilityContractReplyFromRegistry() {
|
||||
const registry = loadCapabilitiesRegistry();
|
||||
const topGroups = registry.groups.slice(0, 6);
|
||||
const groupLines = topGroups.map((group, index) => {
|
||||
const ops = group.supported_operations.slice(0, 3).join(", ");
|
||||
return `${index + 1}. ${group.group_title}: ${group.description}${ops ? ` (например: ${ops})` : ""}.`;
|
||||
});
|
||||
return [
|
||||
"Я ассистент по анализу данных 1С в режиме чтения.",
|
||||
"Что умею по группам:",
|
||||
...groupLines,
|
||||
"Если хотите, раскрою любую группу точечно и дам готовую формулировку запроса.",
|
||||
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
||||
].join("\n");
|
||||
}
|
||||
function resolveNearestCapabilityGroup(input) {
|
||||
const registry = loadCapabilitiesRegistry();
|
||||
const haystack = `${String(input.domain ?? "")} ${String(input.queryClass ?? "")}`.toLowerCase();
|
||||
if (!haystack.trim())
|
||||
return null;
|
||||
const scoring = registry.groups.map((group) => {
|
||||
let score = 0;
|
||||
const bucket = `${group.group_code} ${group.group_title} ${group.description} ${group.supported_operations.join(" ")}`.toLowerCase();
|
||||
for (const token of haystack.split(/[\s._/-]+/g).filter(Boolean)) {
|
||||
if (bucket.includes(token))
|
||||
score += 1;
|
||||
}
|
||||
return { group, score };
|
||||
});
|
||||
scoring.sort((a, b) => b.score - a.score);
|
||||
return scoring[0] && scoring[0].score > 0 ? scoring[0].group : null;
|
||||
}
|
||||
+73
-73
@@ -127,53 +127,53 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
states: [
|
||||
{
|
||||
state_code: "initiated_payment",
|
||||
state_label: "Платеж инициирован",
|
||||
state_label: "Платеж инициирован",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["payment_order_created"],
|
||||
exit_conditions: ["bank_recorded"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Есть инициирование платежа."
|
||||
business_meaning: "Есть инициирование платежа."
|
||||
},
|
||||
{
|
||||
state_code: "bank_recorded",
|
||||
state_label: "Платеж отражен банком",
|
||||
state_label: "Платеж отражен банком",
|
||||
state_class: "active",
|
||||
entry_conditions: ["bank_statement_recorded"],
|
||||
exit_conditions: ["settlement_linked"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
|
||||
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "settlement_closed",
|
||||
state_label: "Расчет закрыт",
|
||||
state_label: "Расчет закрыт",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["payment_to_settlement_linked"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Платеж доведен до расчетного результата."
|
||||
business_meaning: "Платеж доведен до расчетного результата."
|
||||
},
|
||||
{
|
||||
state_code: "stale_unlinked_payment",
|
||||
state_label: "Платеж завис без закрытия",
|
||||
state_label: "Платеж завис без закрытия",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["bank_recorded", "missing_link"],
|
||||
exit_conditions: ["settlement_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
|
||||
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
|
||||
},
|
||||
{
|
||||
state_code: "misclosed_payment",
|
||||
state_label: "Платеж закрыт некорректно",
|
||||
state_label: "Платеж закрыт некорректно",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["wrong_document_type_or_posting_mismatch"],
|
||||
exit_conditions: ["settlement_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
|
||||
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
|
||||
}
|
||||
],
|
||||
transitions: [
|
||||
@@ -184,7 +184,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
required_evidence: ["bank_statement_recorded"],
|
||||
optional_evidence: ["payment_order"],
|
||||
forbidden_conditions: [],
|
||||
business_meaning: "Платеж должен появиться во выписке."
|
||||
business_meaning: "Платеж должен появиться во выписке."
|
||||
},
|
||||
{
|
||||
from_state: "bank_recorded",
|
||||
@@ -193,7 +193,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
required_evidence: ["payment_to_settlement_link"],
|
||||
optional_evidence: ["document_to_posting"],
|
||||
forbidden_conditions: ["wrong_document_type"],
|
||||
business_meaning: "После выписки должен закрываться расчет."
|
||||
business_meaning: "После выписки должен закрываться расчет."
|
||||
}
|
||||
],
|
||||
defects: []
|
||||
@@ -205,43 +205,43 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
states: [
|
||||
{
|
||||
state_code: "invoice_issued",
|
||||
state_label: "Реализация отражена",
|
||||
state_label: "Реализация отражена",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["realization_document_exists"],
|
||||
exit_conditions: ["payment_recorded"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Возникла дебиторская позиция."
|
||||
business_meaning: "Возникла дебиторская позиция."
|
||||
},
|
||||
{
|
||||
state_code: "payment_recorded",
|
||||
state_label: "Оплата отражена",
|
||||
state_label: "Оплата отражена",
|
||||
state_class: "active",
|
||||
entry_conditions: ["payment_document_exists"],
|
||||
exit_conditions: ["receivable_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Оплата есть, ожидается корректное закрытие."
|
||||
business_meaning: "Оплата есть, ожидается корректное закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "receivable_closed",
|
||||
state_label: "Дебиторка закрыта",
|
||||
state_label: "Дебиторка закрыта",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["closing_document_linked"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Дебиторская позиция закрыта корректно."
|
||||
business_meaning: "Дебиторская позиция закрыта корректно."
|
||||
},
|
||||
{
|
||||
state_code: "stale_receivable",
|
||||
state_label: "Дебиторка зависла",
|
||||
state_label: "Дебиторка зависла",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["unresolved_settlement"],
|
||||
exit_conditions: ["receivable_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
|
||||
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
|
||||
}
|
||||
],
|
||||
transitions: [
|
||||
@@ -252,7 +252,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
required_evidence: ["payment_document_exists"],
|
||||
optional_evidence: [],
|
||||
forbidden_conditions: [],
|
||||
business_meaning: "После реализации ожидается оплата/зачет."
|
||||
business_meaning: "После реализации ожидается оплата/зачет."
|
||||
},
|
||||
{
|
||||
from_state: "payment_recorded",
|
||||
@@ -261,7 +261,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
required_evidence: ["closing_document_linked"],
|
||||
optional_evidence: ["register_movement_exists"],
|
||||
forbidden_conditions: ["cross_branch_inconsistency"],
|
||||
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
|
||||
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
|
||||
}
|
||||
],
|
||||
defects: []
|
||||
@@ -273,43 +273,43 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
states: [
|
||||
{
|
||||
state_code: "recognized",
|
||||
state_label: "РБП признан",
|
||||
state_label: "РБП признан",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["deferred_expense_created"],
|
||||
exit_conditions: ["writeoff_started"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "РБП поставлен на учет."
|
||||
business_meaning: "РБП поставлен на учет."
|
||||
},
|
||||
{
|
||||
state_code: "partially_written_off",
|
||||
state_label: "Частичное списание",
|
||||
state_label: "Частичное списание",
|
||||
state_class: "active",
|
||||
entry_conditions: ["partial_writeoff_exists"],
|
||||
exit_conditions: ["fully_written_off"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Списание идет по графику."
|
||||
business_meaning: "Списание идет по графику."
|
||||
},
|
||||
{
|
||||
state_code: "fully_written_off",
|
||||
state_label: "РБП полностью списан",
|
||||
state_label: "РБП полностью списан",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["full_writeoff_exists"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "РБП завершил lifecycle."
|
||||
business_meaning: "РБП завершил lifecycle."
|
||||
},
|
||||
{
|
||||
state_code: "overdue_writeoff",
|
||||
state_label: "Просроченное списание",
|
||||
state_label: "Просроченное списание",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["period_boundary", "missing_link"],
|
||||
exit_conditions: ["fully_written_off"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "РБП живет дольше допустимого окна."
|
||||
business_meaning: "РБП живет дольше допустимого окна."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -322,53 +322,53 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
states: [
|
||||
{
|
||||
state_code: "capitalized",
|
||||
state_label: "Капвложения отражены",
|
||||
state_label: "Капвложения отражены",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["capitalization_document_exists"],
|
||||
exit_conditions: ["accepted_for_accounting"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Объект зафиксирован как вложение."
|
||||
business_meaning: "Объект зафиксирован как вложение."
|
||||
},
|
||||
{
|
||||
state_code: "accepted_for_accounting",
|
||||
state_label: "Принят к учету",
|
||||
state_label: "Принят к учету",
|
||||
state_class: "active",
|
||||
entry_conditions: ["acceptance_document_exists"],
|
||||
exit_conditions: ["depreciation_active"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Объект переведен в основной контур учета."
|
||||
business_meaning: "Объект переведен в основной контур учета."
|
||||
},
|
||||
{
|
||||
state_code: "depreciation_active",
|
||||
state_label: "Амортизация активна",
|
||||
state_label: "Амортизация активна",
|
||||
state_class: "active",
|
||||
entry_conditions: ["depreciation_register_movement"],
|
||||
exit_conditions: ["disposed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Жизненный цикл ОС идет штатно."
|
||||
business_meaning: "Жизненный цикл ОС идет штатно."
|
||||
},
|
||||
{
|
||||
state_code: "contradictory_asset_state",
|
||||
state_label: "Противоречивый статус ОС",
|
||||
state_label: "Противоречивый статус ОС",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["posting_mismatch_or_wrong_path"],
|
||||
exit_conditions: ["depreciation_active"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
|
||||
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
|
||||
},
|
||||
{
|
||||
state_code: "disposed",
|
||||
state_label: "Выбыл",
|
||||
state_label: "Выбыл",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["disposal_document_exists"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Жизненный цикл ОС завершен."
|
||||
business_meaning: "Жизненный цикл ОС завершен."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -381,43 +381,43 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
states: [
|
||||
{
|
||||
state_code: "vat_registered",
|
||||
state_label: "НДС отражен документно",
|
||||
state_label: "НДС отражен документно",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["invoice_registered"],
|
||||
exit_conditions: ["vat_reflected"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Сформирован первичный документный слой НДС."
|
||||
business_meaning: "Сформирован первичный документный слой НДС."
|
||||
},
|
||||
{
|
||||
state_code: "vat_reflected",
|
||||
state_label: "НДС отражен в учете",
|
||||
state_label: "НДС отражен в учете",
|
||||
state_class: "active",
|
||||
entry_conditions: ["vat_register_movement"],
|
||||
exit_conditions: ["vat_deducted"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "НДС проходит штатную стадию отражения."
|
||||
business_meaning: "НДС проходит штатную стадию отражения."
|
||||
},
|
||||
{
|
||||
state_code: "vat_deducted",
|
||||
state_label: "НДС принят к вычету",
|
||||
state_label: "НДС принят к вычету",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["deduction_confirmed"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "НДС-цепочка завершена корректно."
|
||||
business_meaning: "НДС-цепочка завершена корректно."
|
||||
},
|
||||
{
|
||||
state_code: "vat_conflict",
|
||||
state_label: "Конфликт НДС-цепочки",
|
||||
state_label: "Конфликт НДС-цепочки",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["cross_branch_inconsistency"],
|
||||
exit_conditions: ["vat_reflected"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
|
||||
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -430,53 +430,53 @@ const LIFECYCLE_DOMAIN_MODELS = {
|
||||
states: [
|
||||
{
|
||||
state_code: "preclose_checks",
|
||||
state_label: "Предзакрытие",
|
||||
state_label: "Предзакрытие",
|
||||
state_class: "active",
|
||||
entry_conditions: ["period_scope_detected"],
|
||||
exit_conditions: ["close_ready"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Рдет проверка готовности периода."
|
||||
business_meaning: "Идет проверка готовности периода."
|
||||
},
|
||||
{
|
||||
state_code: "close_ready",
|
||||
state_label: "Готов к закрытию",
|
||||
state_label: "Готов к закрытию",
|
||||
state_class: "active",
|
||||
entry_conditions: ["no_blockers_detected"],
|
||||
exit_conditions: ["close_completed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Период может быть закрыт."
|
||||
business_meaning: "Период может быть закрыт."
|
||||
},
|
||||
{
|
||||
state_code: "close_completed",
|
||||
state_label: "Закрытие завершено",
|
||||
state_label: "Закрытие завершено",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["close_operation_done"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Период закрыт."
|
||||
business_meaning: "Период закрыт."
|
||||
},
|
||||
{
|
||||
state_code: "close_blocked",
|
||||
state_label: "Закрытие заблокировано",
|
||||
state_label: "Закрытие заблокировано",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["period_close_risk_or_stale_state"],
|
||||
exit_conditions: ["close_ready"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
|
||||
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "close_contradicted",
|
||||
state_label: "Закрыт формально, но с противоречием",
|
||||
state_label: "Закрыт формально, но с противоречием",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["misclosed_or_cross_branch_conflict"],
|
||||
exit_conditions: ["close_completed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
|
||||
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -488,7 +488,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "missing_expected_transition",
|
||||
defect_class: "path",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Ожидаемый переход не произошел.",
|
||||
business_meaning: "Ожидаемый переход не произошел.",
|
||||
evidence_requirements: ["expected_state", "missing_transition_signal"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -496,7 +496,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "invalid_transition",
|
||||
defect_class: "path",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Переход произошел по некорректному пути.",
|
||||
business_meaning: "Переход произошел по некорректному пути.",
|
||||
evidence_requirements: ["invalid_transition_signal"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -504,7 +504,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "stale_active_state",
|
||||
defect_class: "timing",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Объект завис в активном состоянии.",
|
||||
business_meaning: "Объект завис в активном состоянии.",
|
||||
evidence_requirements: ["stale_marker", "missing_transition_signal"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -512,7 +512,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "contradictory_state",
|
||||
defect_class: "consistency",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Статусы объекта противоречат друг другу.",
|
||||
business_meaning: "Статусы объекта противоречат друг другу.",
|
||||
evidence_requirements: ["contradiction_signal"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -520,7 +520,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "premature_terminal_state",
|
||||
defect_class: "closure",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Терминальное состояние наступило преждевременно.",
|
||||
business_meaning: "Терминальное состояние наступило преждевременно.",
|
||||
evidence_requirements: ["terminal_state", "missing_required_previous_state"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -528,7 +528,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "misclosed_state",
|
||||
defect_class: "closure",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Контур формально закрыт, но закрыт неверно.",
|
||||
business_meaning: "Контур формально закрыт, но закрыт неверно.",
|
||||
evidence_requirements: ["wrong_closure_path"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -536,7 +536,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "orphan_intermediate_state",
|
||||
defect_class: "path",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
|
||||
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
|
||||
evidence_requirements: ["intermediate_state_without_next"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -544,7 +544,7 @@ const SHARED_DEFECTS = [
|
||||
defect_code: "cross_branch_state_conflict",
|
||||
defect_class: "consistency",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
|
||||
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
|
||||
evidence_requirements: ["cross_branch_conflict_signal"],
|
||||
period_impact_potential: "direct"
|
||||
}
|
||||
@@ -845,23 +845,23 @@ function staleDurationHint(domain, defect, input) {
|
||||
return "unknown_snapshot_window";
|
||||
}
|
||||
function lifecycleInterpretation(input) {
|
||||
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
|
||||
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
|
||||
if (input.defect === "stale_active_state") {
|
||||
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
|
||||
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
|
||||
}
|
||||
if (input.defect === "misclosed_state") {
|
||||
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
|
||||
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
|
||||
}
|
||||
if (input.defect === "cross_branch_state_conflict") {
|
||||
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
|
||||
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
|
||||
}
|
||||
if (input.defect === "missing_expected_transition") {
|
||||
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
|
||||
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
|
||||
}
|
||||
if (input.defect === "invalid_transition") {
|
||||
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
|
||||
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
|
||||
}
|
||||
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||||
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||||
}
|
||||
function resolveLifecycle(input) {
|
||||
const lifecycle_domain = inferLifecycleDomain(input);
|
||||
|
||||
+4
-4
@@ -67,9 +67,9 @@ const BUILTIN_PROMPT_PRESETS = {
|
||||
},
|
||||
normalizer_v1_1_2_1: {
|
||||
id: "default-normalizer-v1_1_2_1",
|
||||
name: "Стандартный пресет NDC 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 без изменений.",
|
||||
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"),
|
||||
@@ -79,9 +79,9 @@ const BUILTIN_PROMPT_PRESETS = {
|
||||
},
|
||||
normalizer_v2: {
|
||||
id: "default-normalizer-v2",
|
||||
name: "Стандартный пресет NDC 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.",
|
||||
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"),
|
||||
|
||||
@@ -38,7 +38,7 @@ const PERIOD_IMPACT_PATTERN = /(?:period\s*close|month\s*close|month-end|residua
|
||||
const CAUSAL_PATTERN = /(?:\bwhy\b|\bbecause\b|\breason\b|explain\s+mechanism|почему|объясни|механизм|причин)/i;
|
||||
const AMBIGUITY_PATTERN = /(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
|
||||
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
|
||||
const DOMAIN_LEXICAL_ANCHOR_PATTERN = /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С‚.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i;
|
||||
const DOMAIN_LEXICAL_ANCHOR_PATTERN = /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|ндс|сч[её]С‚.?фактур|книг[аи]|затрат|закрыт|остатк)/i;
|
||||
exports.ROUTE_DISCIPLINE_RULE_TABLE = [
|
||||
{
|
||||
query_class: "exact_object_trace",
|
||||
|
||||
@@ -134,9 +134,28 @@ export const TRACES_DIR = path.resolve(DATA_DIR, "traces");
|
||||
export const PRESETS_DIR = path.resolve(DATA_DIR, "presets");
|
||||
export const EVAL_CASES_DIR = path.resolve(DATA_DIR, "eval_cases");
|
||||
export const ASSISTANT_SESSIONS_DIR = path.resolve(DATA_DIR, "assistant_sessions");
|
||||
export const AUTORUN_ANNOTATIONS_DIR = path.resolve(DATA_DIR, "autorun_annotations");
|
||||
export const AUTORUN_ANNOTATIONS_FILE = path.resolve(AUTORUN_ANNOTATIONS_DIR, "annotations.json");
|
||||
export const AUTORUN_GENERATOR_DIR = path.resolve(DATA_DIR, "autorun_generators");
|
||||
export const AUTORUN_GENERATOR_HISTORY_FILE = path.resolve(AUTORUN_GENERATOR_DIR, "history.json");
|
||||
|
||||
export const PROMPTS_DIR = path.resolve(MODULE_ROOT, "prompts");
|
||||
export const REPORTS_DIR = path.resolve(MODULE_ROOT, "reports");
|
||||
export const EVAL_DATASETS_DIR = path.resolve(MODULE_ROOT, "eval_cases");
|
||||
export const SCHEMAS_DIR = path.resolve(BACKEND_ROOT, "src", "schemas");
|
||||
export const ARCH_EXPORT_2020_DIR = path.resolve(MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт");
|
||||
export const ASSISTANT_CANON_FILE = path.resolve(MODULE_ROOT, "..", "docs", "TECH", "assistant_canon.md");
|
||||
export const ASSISTANT_CAPABILITIES_REGISTRY_FILE = path.resolve(
|
||||
MODULE_ROOT,
|
||||
"..",
|
||||
"docs",
|
||||
"TECH",
|
||||
"capabilities_registry.json"
|
||||
);
|
||||
export const MANUAL_CASE_DECISION_SCHEMA_FILE = path.resolve(
|
||||
MODULE_ROOT,
|
||||
"..",
|
||||
"docs",
|
||||
"TECH",
|
||||
"manual_case_decision_schema.json"
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,17 @@
|
||||
import "dotenv/config";
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
import { PORT, PRESETS_DIR, TRACES_DIR, EVAL_CASES_DIR, REPORTS_DIR, TIMEZONE, ASSISTANT_SESSIONS_DIR } from "./config";
|
||||
import {
|
||||
PORT,
|
||||
PRESETS_DIR,
|
||||
TRACES_DIR,
|
||||
EVAL_CASES_DIR,
|
||||
REPORTS_DIR,
|
||||
TIMEZONE,
|
||||
ASSISTANT_SESSIONS_DIR,
|
||||
AUTORUN_ANNOTATIONS_DIR,
|
||||
AUTORUN_GENERATOR_DIR
|
||||
} from "./config";
|
||||
import { buildAccountingAgentRouter } from "./routes/accountingAgent";
|
||||
import { buildAssistantRouter } from "./routes/assistant";
|
||||
import { buildAutoRunsRouter } from "./routes/autoRuns";
|
||||
@@ -27,6 +37,8 @@ export function createApp(): express.Express {
|
||||
ensureDir(EVAL_CASES_DIR);
|
||||
ensureDir(REPORTS_DIR);
|
||||
ensureDir(ASSISTANT_SESSIONS_DIR);
|
||||
ensureDir(AUTORUN_ANNOTATIONS_DIR);
|
||||
ensureDir(AUTORUN_GENERATOR_DIR);
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
|
||||
@@ -1368,29 +1368,29 @@ function buildProblemCentricActions(input: {
|
||||
}
|
||||
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
}
|
||||
if (unitTypes.has("cross_branch_inconsistency_cluster")) {
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
}
|
||||
if (unitTypes.has("lifecycle_anomaly_node")) {
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
}
|
||||
for (const unit of input.units) {
|
||||
if (unit.lifecycle_defect_type === "stale_active_state") {
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "misclosed_state") {
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1400,21 +1400,21 @@ function buildProblemCentricActions(input: {
|
||||
|
||||
if (input.mode === "clarification_required") {
|
||||
if (input.missingAnchors.period) {
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
}
|
||||
}
|
||||
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(actions, 6);
|
||||
@@ -1437,25 +1437,25 @@ function buildProblemCentricClarifications(input: {
|
||||
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
}
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(questions, 6);
|
||||
@@ -1641,13 +1641,13 @@ function detectMissingAnchors(
|
||||
Boolean(options?.normalizationPeriodExplicit) ||
|
||||
hasPeriodAnchorInCompanyAnchors(options?.companyAnchors);
|
||||
const hasAccount =
|
||||
/(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(
|
||||
/(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(
|
||||
lower
|
||||
) || hasAccountAnchorInRetrieval(retrievalResults);
|
||||
const hasDocumentOrObject =
|
||||
/(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
|
||||
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
|
||||
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
|
||||
/(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
|
||||
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
|
||||
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
|
||||
|
||||
return {
|
||||
period: !hasPeriod,
|
||||
@@ -1674,19 +1674,19 @@ function buildClarificationQuestions(input: {
|
||||
questions.push("Уточните период проверки (например, июль 2020).");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(questions, 6);
|
||||
@@ -1701,31 +1701,31 @@ function buildRecommendedActions(input: {
|
||||
}): string[] {
|
||||
const actions: string[] = [];
|
||||
if (input.mode === "focused_grounded") {
|
||||
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
}
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
if (input.coverageReport.requirements_partially_covered.length > 0) {
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("snapshot_only")) {
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
}
|
||||
if (input.sourceRefs.length > 0) {
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(actions, 6);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import fs from "fs";
|
||||
import { ASSISTANT_CANON_FILE } from "../config";
|
||||
|
||||
const FALLBACK_CANON = [
|
||||
"Не выдумывай возможности.",
|
||||
"Не обещай настройку 1С и админ-действия.",
|
||||
"Не показывай внутренние технические термины пользователю.",
|
||||
"Говори по-человечески и предлагай ближайший полезный поддерживаемый шаг."
|
||||
].join(" ");
|
||||
|
||||
let cache: { mtimeMs: number; excerpt: string } | null = null;
|
||||
|
||||
function stripMarkdown(input: string): string {
|
||||
return input
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.replace(/[`*_>\-\[\]\(\)]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function loadAssistantCanonExcerpt(maxChars = 900): string {
|
||||
try {
|
||||
const mtimeMs = fs.existsSync(ASSISTANT_CANON_FILE) ? fs.statSync(ASSISTANT_CANON_FILE).mtimeMs : -1;
|
||||
if (cache && cache.mtimeMs === mtimeMs) {
|
||||
return cache.excerpt;
|
||||
}
|
||||
if (!fs.existsSync(ASSISTANT_CANON_FILE)) {
|
||||
return FALLBACK_CANON;
|
||||
}
|
||||
const raw = fs.readFileSync(ASSISTANT_CANON_FILE, "utf-8");
|
||||
const normalized = stripMarkdown(raw);
|
||||
const excerpt = normalized.length > maxChars ? `${normalized.slice(0, maxChars).trim()}...` : normalized;
|
||||
cache = { mtimeMs, excerpt: excerpt || FALLBACK_CANON };
|
||||
return cache.excerpt;
|
||||
} catch {
|
||||
return cache?.excerpt ?? FALLBACK_CANON;
|
||||
}
|
||||
}
|
||||
@@ -731,8 +731,8 @@ const P0_DOMAIN_CARDS: P0DomainCard[] = [
|
||||
/закрыт[а-яё]*\s+период/i,
|
||||
/close\s+operation/i,
|
||||
/allocation/i,
|
||||
/закр/i,
|
||||
/перио/i,
|
||||
/закр/i,
|
||||
/перио/i,
|
||||
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
|
||||
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
|
||||
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
|
||||
@@ -756,14 +756,14 @@ function parseDateCandidate(value: unknown): number | null {
|
||||
|
||||
function extractDate(record: SnapshotRecord): string | null {
|
||||
const attrs = record.attributes ?? {};
|
||||
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
|
||||
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
|
||||
for (const key of directKeys) {
|
||||
if (attrs[key] !== undefined && attrs[key] !== null) {
|
||||
return String(attrs[key]);
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
|
||||
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -795,7 +795,7 @@ function countNavigationLinks(record: SnapshotRecord): number {
|
||||
function findCounterpartyLinks(record: SnapshotRecord): SnapshotLink[] {
|
||||
return record.links.filter(
|
||||
(link) =>
|
||||
link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field)
|
||||
link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1723,14 +1723,14 @@ function inferPeriodScope(fragmentText: string): SemanticPeriodScope {
|
||||
granularity: "year"
|
||||
};
|
||||
}
|
||||
if (/квартал|quarter/i.test(fragmentText)) {
|
||||
if (/квартал|quarter/i.test(fragmentText)) {
|
||||
return {
|
||||
from: null,
|
||||
to: null,
|
||||
granularity: "quarter"
|
||||
};
|
||||
}
|
||||
if (/месяц|month|период/i.test(fragmentText)) {
|
||||
if (/месяц|month|период/i.test(fragmentText)) {
|
||||
return {
|
||||
from: null,
|
||||
to: null,
|
||||
@@ -1808,7 +1808,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
const rankingBasis: string[] = ["closure_risk", "repeatability", "financial_impact"];
|
||||
const explanationFocus: string[] = ["why_selected", "where_chain_breaks", "what_business_risk"];
|
||||
|
||||
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
|
||||
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
|
||||
pushMany(domainScope, ["bank", "settlements"]);
|
||||
pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]);
|
||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||
@@ -1823,22 +1823,22 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
/(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(
|
||||
lower
|
||||
) ||
|
||||
(/закр/i.test(lower) && /перио/i.test(lower));
|
||||
(/закр/i.test(lower) && /перио/i.test(lower));
|
||||
|
||||
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
|
||||
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
|
||||
pushMany(domainScope, ["suppliers", "settlements"]);
|
||||
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
|
||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||
}
|
||||
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
|
||||
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
|
||||
pushMany(domainScope, ["customers", "settlements"]);
|
||||
pushMany(documentTypes, ["sales_document", "settlement_document"]);
|
||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||
}
|
||||
if (
|
||||
/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(
|
||||
/ндс|vat|книга\s+покупок|книга\s+продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(
|
||||
lower
|
||||
) ||
|
||||
hasVatAccountScope
|
||||
@@ -1849,7 +1849,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
||||
}
|
||||
if (
|
||||
/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
|
||||
/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
|
||||
lower
|
||||
) ||
|
||||
hasFixedAssetAccountScope
|
||||
@@ -1860,7 +1860,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
|
||||
}
|
||||
if (
|
||||
/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
|
||||
/рбп|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
|
||||
hasDeferredExpenseAccountScope
|
||||
) {
|
||||
pushMany(domainScope, ["deferred_expense", "period_close"]);
|
||||
@@ -1868,17 +1868,17 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
pushMany(entityTypes, ["document", "posting"]);
|
||||
pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]);
|
||||
}
|
||||
if (/цепоч|разрыв|СЃРІСЏР·|документ.*РїСЂРѕРІРѕРґ|РіРґРµ рвет|Р¶РёРІСѓС‚ отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
|
||||
if (/цепоч|разрыв|связ|документ.*провод|где рвет|живут отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
|
||||
pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]);
|
||||
pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]);
|
||||
}
|
||||
if (/аномал|СЂРёСЃРє|С…РІРѕСЃС‚|РїРѕРґРѕР·СЂ|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
|
||||
if (/аномал|риск|хвост|подозр|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
|
||||
pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]);
|
||||
}
|
||||
if (WRONG_DOCUMENT_MARKERS.test(lower)) {
|
||||
pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]);
|
||||
}
|
||||
if (/Р¶РёРІСѓС‚ отдельно|РЅРµ СЃРІСЏР·|без СЃРІСЏР·Рё|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
|
||||
if (/живут отдельно|не связ|без связи|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
|
||||
pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]);
|
||||
}
|
||||
if (REPEATED_ANOMALY_MARKERS.test(lower)) {
|
||||
@@ -1890,10 +1890,10 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
||||
pushMany(documentTypes, ["period_close_document"]);
|
||||
}
|
||||
if (/РЅРµ РІ платеже|не\s+в\s+платеже|not payment/i.test(lower)) {
|
||||
if (/не\s+в\s+платеже|not payment/i.test(lower)) {
|
||||
pushMany(excludedInterpretations, ["simple_payment_delay"]);
|
||||
}
|
||||
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|не\s+по\s+сумм|не\s+сумм|not by amount/i.test(lower)) {
|
||||
if (/не\s+по\s+сумм|не\s+сумм|не\s+сумма|not by amount/i.test(lower)) {
|
||||
pushMany(excludedInterpretations, ["amount_only_anomaly"]);
|
||||
pushMany(rankingBasis, ["amount_independent_risk"]);
|
||||
}
|
||||
@@ -2288,16 +2288,16 @@ function inferAccountsFromRecord(record: SnapshotRecord, corpus: string): string
|
||||
accounts.push(token.split(".")[0]);
|
||||
}
|
||||
for (const key of Object.keys(record.attributes ?? {})) {
|
||||
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
|
||||
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
|
||||
accounts.push("51");
|
||||
}
|
||||
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
|
||||
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
|
||||
accounts.push("60");
|
||||
}
|
||||
if (/счетучетандс/i.test(key)) {
|
||||
if (/счетучетандс/i.test(key)) {
|
||||
accounts.push("19");
|
||||
}
|
||||
if (/субконтодт/i.test(key)) {
|
||||
if (/субконтодт/i.test(key)) {
|
||||
accounts.push("60");
|
||||
}
|
||||
}
|
||||
@@ -2306,28 +2306,28 @@ function inferAccountsFromRecord(record: SnapshotRecord, corpus: string): string
|
||||
|
||||
function inferDocumentTypesFromRecord(record: SnapshotRecord, corpus: string): string[] {
|
||||
const items: string[] = [];
|
||||
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
|
||||
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
|
||||
pushMany(items, ["bank_statement", "payment_order"]);
|
||||
}
|
||||
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
|
||||
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
|
||||
items.push("supplier_receipt");
|
||||
}
|
||||
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
|
||||
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
|
||||
items.push("sales_document");
|
||||
}
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
pushMany(items, ["invoice", "vat_document"]);
|
||||
}
|
||||
if (/корректировк|ручн|manual/i.test(corpus)) {
|
||||
if (/корректировк|ручн|manual/i.test(corpus)) {
|
||||
items.push("manual_operation");
|
||||
}
|
||||
if (/закрытие|регламент/i.test(corpus)) {
|
||||
if (/закрытие|регламент/i.test(corpus)) {
|
||||
items.push("period_close_document");
|
||||
}
|
||||
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
|
||||
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
|
||||
pushMany(items, ["fixed_asset_card", "depreciation_document"]);
|
||||
}
|
||||
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
|
||||
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
|
||||
items.push("deferred_expense_document");
|
||||
}
|
||||
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
|
||||
@@ -2356,11 +2356,11 @@ function inferDomainsFromRecord(corpus: string, documentTypes: string[], record:
|
||||
if (documentTypes.some((item) => item === "deferred_expense_document")) {
|
||||
pushMany(domains, ["deferred_expense", "period_close"]);
|
||||
}
|
||||
if (/закрытие|регламент|period close/i.test(corpus)) {
|
||||
if (/закрытие|регламент|period close/i.test(corpus)) {
|
||||
domains.push("period_close");
|
||||
}
|
||||
const hasSettlementLexicalAnchor =
|
||||
/(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(
|
||||
/(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasSettlementDocAnchor = documentTypes.some(
|
||||
@@ -2386,13 +2386,13 @@ function inferEntityTypes(record: SnapshotRecord): string[] {
|
||||
entities.push("counterparty");
|
||||
}
|
||||
const corpus = collectTextFromRecord(record);
|
||||
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus)) {
|
||||
if (/договор|contract/i.test(corpus)) {
|
||||
entities.push("contract");
|
||||
}
|
||||
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
|
||||
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
|
||||
entities.push("fixed_asset");
|
||||
}
|
||||
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
entities.push("tax_entry");
|
||||
}
|
||||
return uniqueStrings(entities);
|
||||
@@ -2406,25 +2406,25 @@ function inferRelationPatterns(record: SnapshotRecord, corpus: string): string[]
|
||||
if (hasDocLinks) {
|
||||
patterns.push("document_to_posting");
|
||||
}
|
||||
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
|
||||
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
|
||||
patterns.push("payment_to_settlement");
|
||||
}
|
||||
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
|
||||
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
|
||||
patterns.push("statement_to_document");
|
||||
}
|
||||
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
|
||||
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
|
||||
patterns.push("asset_card_to_depreciation");
|
||||
}
|
||||
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
|
||||
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
|
||||
patterns.push("deferred_expense_to_writeoff");
|
||||
}
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
|
||||
patterns.push("invoice_to_vat");
|
||||
}
|
||||
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus) && hasDocLinks) {
|
||||
if (/договор|contract/i.test(corpus) && hasDocLinks) {
|
||||
patterns.push("contract_to_documents");
|
||||
}
|
||||
if (/склад|товар|материал|receipt/i.test(corpus)) {
|
||||
if (/склад|товар|материал|receipt/i.test(corpus)) {
|
||||
patterns.push("receipt_to_stock_movement");
|
||||
}
|
||||
return uniqueStrings(patterns);
|
||||
@@ -2466,7 +2466,7 @@ function inferAnomalyPatterns(record: SnapshotRecord, corpus: string, relationPa
|
||||
if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) {
|
||||
anomalies.push("posting_mismatch");
|
||||
}
|
||||
if (/ручн|manual|корректировк/.test(corpus)) {
|
||||
if (/ручн|manual|корректировк/.test(corpus)) {
|
||||
anomalies.push("manual_intervention_suspicion");
|
||||
}
|
||||
if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) {
|
||||
@@ -2478,7 +2478,7 @@ function inferAnomalyPatterns(record: SnapshotRecord, corpus: string, relationPa
|
||||
if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) {
|
||||
anomalies.push("silent_orphan");
|
||||
}
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
if (!hasAmountSignal && anomalies.length > 0) {
|
||||
anomalies.push("amount_independent_risk");
|
||||
}
|
||||
@@ -2530,15 +2530,15 @@ function evaluateExcludedInterpretations(
|
||||
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"];
|
||||
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
|
||||
if (!hasStructural) {
|
||||
reasons.push("Рсключено как simple_payment_delay без структурного дефекта.");
|
||||
reasons.push("Исключено как simple_payment_delay без структурного дефекта.");
|
||||
}
|
||||
}
|
||||
if (interpretationSet.has("amount_only_anomaly")) {
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
|
||||
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"];
|
||||
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
|
||||
if (hasAmountSignal && !hasStructural) {
|
||||
reasons.push("Рсключено как amount-only аномалия без структурных признаков.");
|
||||
reasons.push("Исключено как amount-only аномалия без структурных признаков.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2611,22 +2611,22 @@ function evaluateRecordAgainstProfile(record: SnapshotRecord, profile: SemanticR
|
||||
|
||||
const matchReasons: string[] = [];
|
||||
if (accountMatch && profile.account_scope.length > 0) {
|
||||
matchReasons.push("Совпал account_scope.");
|
||||
matchReasons.push("Совпал account_scope.");
|
||||
}
|
||||
if (domainMatch && profile.domain_scope.length > 0) {
|
||||
matchReasons.push("Совпал domain_scope.");
|
||||
matchReasons.push("Совпал domain_scope.");
|
||||
}
|
||||
if (documentMatch && profile.document_types.length > 0) {
|
||||
matchReasons.push("Совпал document_types.");
|
||||
matchReasons.push("Совпал document_types.");
|
||||
}
|
||||
if (relationMatch && profile.relation_patterns.length > 0) {
|
||||
matchReasons.push("Совпали relation_patterns.");
|
||||
matchReasons.push("Совпали relation_patterns.");
|
||||
}
|
||||
if (anomalyMatch && profile.anomaly_patterns.length > 0) {
|
||||
matchReasons.push("Совпали anomaly_patterns.");
|
||||
matchReasons.push("Совпали anomaly_patterns.");
|
||||
}
|
||||
if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) {
|
||||
matchReasons.push("Совпал lifecycle_stage_filters.");
|
||||
matchReasons.push("Совпал lifecycle_stage_filters.");
|
||||
}
|
||||
if (graphTraversal.domain_match) {
|
||||
matchReasons.push("Graph traversal domain matched.");
|
||||
@@ -2818,7 +2818,7 @@ export class AssistantDataLayer {
|
||||
business_interpretation: [],
|
||||
confidence: "low",
|
||||
limitations: ["Snapshot data files could not be loaded."],
|
||||
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
|
||||
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
|
||||
};
|
||||
}
|
||||
let result: RawRetrievalResult | null = null;
|
||||
@@ -3718,8 +3718,8 @@ export class AssistantDataLayer {
|
||||
: "low",
|
||||
business_interpretation:
|
||||
group.risk_factors.size > 0
|
||||
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
|
||||
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
|
||||
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
|
||||
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
|
||||
relation_types: Array.from(group.relations.entries())
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.map((item) => item[0]),
|
||||
@@ -3768,24 +3768,24 @@ export class AssistantDataLayer {
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [
|
||||
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
|
||||
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
|
||||
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
|
||||
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
|
||||
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
|
||||
guidFilter.length > 0
|
||||
? "GUID-фильтрация включена."
|
||||
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
|
||||
? "GUID-фильтрация включена."
|
||||
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
|
||||
`Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`,
|
||||
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
|
||||
],
|
||||
risk_factors: semanticProfile.anomaly_patterns,
|
||||
business_interpretation: [
|
||||
"По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
|
||||
"Для точечного drilldown добавьте GUID или уточните период/контрагента."
|
||||
"По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
|
||||
"Для точечного drilldown добавьте GUID или уточните период/контрагента."
|
||||
],
|
||||
confidence: "medium",
|
||||
limitations: [
|
||||
guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
|
||||
"Рсточник данных — snapshot 2020 (read-only), Р° РЅРµ live состояние базы 1РЎ.",
|
||||
guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
|
||||
"Источник данных — snapshot 2020 (read-only), а не live состояние базы 1С.",
|
||||
domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся."
|
||||
],
|
||||
errors: []
|
||||
@@ -3833,18 +3833,18 @@ export class AssistantDataLayer {
|
||||
},
|
||||
evidence: evidence.slice(0, 12),
|
||||
why_included: [
|
||||
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
|
||||
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
|
||||
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
|
||||
semanticProfile.account_scope.length > 0
|
||||
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
|
||||
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
|
||||
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
|
||||
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
|
||||
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
|
||||
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
|
||||
`Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.`
|
||||
],
|
||||
selection_reason: [
|
||||
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
|
||||
"GUID-mode отключен: full scan без ограничителей не использовался.",
|
||||
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
|
||||
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
|
||||
"GUID-mode отключен: full scan без ограничителей не использовался.",
|
||||
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
|
||||
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.",
|
||||
`Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`,
|
||||
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
|
||||
@@ -3852,15 +3852,15 @@ export class AssistantDataLayer {
|
||||
risk_factors:
|
||||
aggregatedRiskFactors.length > 0
|
||||
? aggregatedRiskFactors
|
||||
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
|
||||
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
|
||||
business_interpretation: [
|
||||
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
|
||||
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
|
||||
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
|
||||
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
|
||||
],
|
||||
confidence: "high",
|
||||
limitations: [
|
||||
guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
|
||||
"Рсточник данных — snapshot 2020 (read-only), РЅРµ live контур 1РЎ.",
|
||||
guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
|
||||
"Источник данных — snapshot 2020 (read-only), не live контур 1С.",
|
||||
domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся."
|
||||
],
|
||||
errors: []
|
||||
@@ -4177,12 +4177,12 @@ export class AssistantDataLayer {
|
||||
})),
|
||||
why_included: items.length > 0
|
||||
? [
|
||||
"Показаны сущности с максимальным количеством записей.",
|
||||
"Показаны сущности с максимальным количеством записей.",
|
||||
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
|
||||
]
|
||||
: [],
|
||||
selection_reason: [
|
||||
"Ранжирование выполнено по records_count по убыванию.",
|
||||
"Ранжирование выполнено по records_count по убыванию.",
|
||||
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
|
||||
],
|
||||
risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]),
|
||||
@@ -4191,7 +4191,7 @@ export class AssistantDataLayer {
|
||||
],
|
||||
confidence: "medium",
|
||||
limitations: [
|
||||
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
|
||||
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
|
||||
domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся."
|
||||
],
|
||||
errors: []
|
||||
@@ -4354,7 +4354,7 @@ export class AssistantDataLayer {
|
||||
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
|
||||
],
|
||||
risk_factors: semanticProfile.anomaly_patterns,
|
||||
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
|
||||
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
|
||||
confidence: "high",
|
||||
limitations: [
|
||||
"Это read-only snapshot, а не онлайн-состояние 1С.",
|
||||
@@ -4385,11 +4385,11 @@ export class AssistantDataLayer {
|
||||
},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
|
||||
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
|
||||
risk_factors: [],
|
||||
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
|
||||
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
|
||||
confidence: "low",
|
||||
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
|
||||
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
@@ -4440,14 +4440,14 @@ export class AssistantDataLayer {
|
||||
evidence: matches.slice(0, 10),
|
||||
why_included:
|
||||
matches.length > 0
|
||||
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
|
||||
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
|
||||
: [],
|
||||
selection_reason: [
|
||||
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
|
||||
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
|
||||
],
|
||||
risk_factors: [],
|
||||
business_interpretation: [
|
||||
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
|
||||
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
|
||||
],
|
||||
confidence: matches.length > 0 ? "medium" : "low",
|
||||
limitations: [
|
||||
@@ -4478,12 +4478,12 @@ export class AssistantDataLayer {
|
||||
matched_records: matches.length
|
||||
},
|
||||
evidence: matches.slice(0, 10),
|
||||
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
|
||||
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
|
||||
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
|
||||
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
|
||||
risk_factors: [],
|
||||
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
|
||||
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
|
||||
confidence: matches.length > 0 ? "high" : "medium",
|
||||
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
|
||||
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ function parseDateLike(raw: string): string | null {
|
||||
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
|
||||
}
|
||||
const rusMonthYear = value.match(
|
||||
/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
|
||||
/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
|
||||
);
|
||||
if (rusMonthYear) {
|
||||
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
|
||||
@@ -530,7 +530,7 @@ function normalizedAnchorFromFragments(normalized: NormalizedPayload | null | un
|
||||
source: `normalized_time_scope:${type || "unknown"}`
|
||||
};
|
||||
}
|
||||
if (/(?:июл|july|РёСЋР»)/i.test(value)) {
|
||||
if (/(?:июл|july|июл)/i.test(value)) {
|
||||
return {
|
||||
value: `${JULY_YEAR}-${JULY_MONTH}`,
|
||||
source: `normalized_time_scope:${type || "unknown"}`
|
||||
@@ -564,7 +564,7 @@ function resolveJulyAnchor(rawText: string): TemporalAnchorResolution {
|
||||
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
|
||||
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
|
||||
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
|
||||
const monthByNamed = /(?:июл|july|июл)/i.test(lower);
|
||||
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
|
||||
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
|
||||
return {
|
||||
@@ -771,7 +771,7 @@ export function applyTemporalHintToExecutionPlan<
|
||||
return item;
|
||||
}
|
||||
const text = String(item.fragment_text ?? "").trim();
|
||||
if (/2020-07|июл|РёСЋР»|july/i.test(text)) {
|
||||
if (/2020-07|июл|июл|july/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
@@ -819,7 +819,7 @@ export function resolveDomainPolarityGuard(input: {
|
||||
prefixes.has("62") ||
|
||||
prefixes.has("51") ||
|
||||
prefixes.has("76") ||
|
||||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|С…РІРѕСЃС‚)/i.test(lower);
|
||||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|хвост)/i.test(lower);
|
||||
if (!settlementSignal) {
|
||||
return {
|
||||
applied: false,
|
||||
@@ -839,13 +839,13 @@ export function resolveDomainPolarityGuard(input: {
|
||||
};
|
||||
}
|
||||
const supplierScore =
|
||||
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
|
||||
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
|
||||
(prefixes.has("60") ? 2 : 0) +
|
||||
(/(?:сч[её]т\s*60|по\s*60|счет\s*60|РїРѕ\s*60)/i.test(lower) ? 1 : 0);
|
||||
(/(?:сч[её]т\s*60|по\s*60|счет\s*60)/i.test(lower) ? 1 : 0);
|
||||
const customerScore =
|
||||
(/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
|
||||
(/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
|
||||
(prefixes.has("62") ? 2 : 0) +
|
||||
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0);
|
||||
(/(?:сч[её]т\s*62|по\s*62|счет\s*62)/i.test(lower) ? 1 : 0);
|
||||
|
||||
let polarity: DomainPolarity = "mixed_or_unresolved";
|
||||
if (supplierScore > 0 || customerScore > 0) {
|
||||
@@ -903,10 +903,10 @@ export function applyPolarityHintToExecutionPlan<
|
||||
return item;
|
||||
}
|
||||
const text = String(item.fragment_text ?? "").trim();
|
||||
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) {
|
||||
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60)/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62|РїРѕ\s*62)/i.test(text)) {
|
||||
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62)/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
@@ -917,11 +917,11 @@ export function applyPolarityHintToExecutionPlan<
|
||||
}
|
||||
|
||||
function containsReceivableSignal(value: string): boolean {
|
||||
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
|
||||
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
|
||||
}
|
||||
|
||||
function containsPayableSignal(value: string): boolean {
|
||||
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
|
||||
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
|
||||
}
|
||||
|
||||
function problemUnitCorpus(unit: ProblemUnit): string {
|
||||
@@ -1590,15 +1590,15 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
|
||||
? "no_grounded_answer"
|
||||
: "partial";
|
||||
const reasonMap: Record<string, string> = {
|
||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
||||
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
|
||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
|
||||
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
|
||||
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
|
||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
|
||||
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
|
||||
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
|
||||
};
|
||||
const reasons = [
|
||||
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
||||
|
||||
@@ -19,6 +19,8 @@ import * as addressFilterExtractor_1 from "./addressFilterExtractor";
|
||||
import * as predecomposeContract_1 from "./address_runtime/predecomposeContract";
|
||||
import * as openaiResponsesClient_1 from "./openaiResponsesClient";
|
||||
import * as addressMcpClient_1 from "./addressMcpClient";
|
||||
import * as capabilitiesRegistry_1 from "./capabilitiesRegistry";
|
||||
import * as assistantCanon_1 from "./assistantCanon";
|
||||
import iconv from "iconv-lite";
|
||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||
const dataScopeProbeCache = new Map();
|
||||
@@ -3859,17 +3861,7 @@ function buildLivingChatPrompt(userMessage, conversationWindow) {
|
||||
return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
|
||||
}
|
||||
function buildAssistantCapabilityContractReply() {
|
||||
return [
|
||||
"Я ассистент по анализу данных 1С в режиме чтения.",
|
||||
"Что умею сейчас:",
|
||||
"1. Находить документы, операции, договоры и остатки по контрагенту/договору/периоду.",
|
||||
"2. Делать агрегаты по базе: активность, роли контрагентов, top-срезы по суммам и операциям.",
|
||||
"3. Кратко объяснять результат и подсказывать следующий точный запрос.",
|
||||
"Что не умею:",
|
||||
"1. Не настраиваю 1С и не меняю конфигурацию.",
|
||||
"2. Не создаю и не провожу документы в базе.",
|
||||
"3. Не выполняю админские действия на сервере."
|
||||
].join("\n");
|
||||
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
||||
}
|
||||
function normalizeScopeLabel(value) {
|
||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||
@@ -4971,6 +4963,7 @@ export class AssistantService {
|
||||
else {
|
||||
const conversationWindow = buildLivingChatContextWindow(session.items);
|
||||
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
||||
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
||||
const chatResponse = await this.chatClient.chat({
|
||||
llmProvider: payload.llmProvider,
|
||||
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
|
||||
@@ -4984,7 +4977,8 @@ export class AssistantService {
|
||||
"Работай честно: не заявляй действия, которые недоступны в этом рантайме.",
|
||||
"Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.",
|
||||
"Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.",
|
||||
"Если пользователь спрашивает про возможности, отвечай только по этому контракту."
|
||||
"Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
|
||||
`Канон поведения: ${canonExcerpt}`
|
||||
].join(" "),
|
||||
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
|
||||
userMessage: userPrompt,
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import fs from "fs";
|
||||
import { ASSISTANT_CAPABILITIES_REGISTRY_FILE } from "../config";
|
||||
|
||||
export type CapabilityMaturity = "production_ready" | "partial" | "planned" | "deprecated";
|
||||
|
||||
export interface CapabilityGroup {
|
||||
group_code: string;
|
||||
group_title: string;
|
||||
description: string;
|
||||
risk_level: "low" | "medium" | "high";
|
||||
maturity_status: CapabilityMaturity;
|
||||
supported_operations: string[];
|
||||
unsupported_operations: string[];
|
||||
required_entities: string[];
|
||||
optional_entities: string[];
|
||||
typical_queries: string[];
|
||||
related_routes: string[];
|
||||
safe_alternatives: string[];
|
||||
one_c_hints: string[];
|
||||
}
|
||||
|
||||
export interface CapabilityRegistry {
|
||||
schema_version: string;
|
||||
updated_at: string;
|
||||
assistant_mode: "read_only" | "mixed";
|
||||
groups: CapabilityGroup[];
|
||||
}
|
||||
|
||||
const FALLBACK_REGISTRY: CapabilityRegistry = {
|
||||
schema_version: "capabilities_registry_fallback_v1",
|
||||
updated_at: "2026-04-09T00:00:00.000Z",
|
||||
assistant_mode: "read_only",
|
||||
groups: [
|
||||
{
|
||||
group_code: "vat",
|
||||
group_title: "НДС",
|
||||
description: "Срезы и расчеты НДС на базе данных 1С.",
|
||||
risk_level: "high",
|
||||
maturity_status: "partial",
|
||||
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
|
||||
unsupported_operations: ["submit_tax_declaration"],
|
||||
required_entities: ["period", "organization"],
|
||||
optional_entities: ["counterparty"],
|
||||
typical_queries: ["Сколько НДС к уплате за период?"],
|
||||
related_routes: [],
|
||||
safe_alternatives: ["Показать движения по 68/19 за период"],
|
||||
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
|
||||
},
|
||||
{
|
||||
group_code: "counterparties",
|
||||
group_title: "Контрагенты",
|
||||
description: "Документы, операции, договоры и срезы по контрагентам.",
|
||||
risk_level: "medium",
|
||||
maturity_status: "production_ready",
|
||||
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||||
unsupported_operations: ["edit_counterparty_card"],
|
||||
required_entities: ["counterparty_scope_or_contract"],
|
||||
optional_entities: ["period", "organization"],
|
||||
typical_queries: ["Покажи документы по контрагенту"],
|
||||
related_routes: [],
|
||||
safe_alternatives: ["Уточнить ИНН/наименование контрагента"],
|
||||
one_c_hints: ["Справочник.Контрагенты"]
|
||||
},
|
||||
{
|
||||
group_code: "boundaries",
|
||||
group_title: "Ограничения",
|
||||
description: "Операции, которые ассистент не выполняет.",
|
||||
risk_level: "high",
|
||||
maturity_status: "production_ready",
|
||||
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
|
||||
unsupported_operations: ["configure_1c", "admin_server_actions", "create_or_post_documents"],
|
||||
required_entities: [],
|
||||
optional_entities: [],
|
||||
typical_queries: ["Можешь настроить 1С?"],
|
||||
related_routes: [],
|
||||
safe_alternatives: ["Сформировать план диагностики для 1С/ИТ-админа"],
|
||||
one_c_hints: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let cache: { mtimeMs: number; value: CapabilityRegistry } | null = null;
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toStringSafe(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function toArray(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function readRegistryFromFile(): CapabilityRegistry | null {
|
||||
if (!fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)) return null;
|
||||
try {
|
||||
const raw = fs.readFileSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE, "utf-8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
const root = toRecord(parsed);
|
||||
if (!root) return null;
|
||||
const groups = toArray(root.groups)
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => item !== null)
|
||||
.map((item) => ({
|
||||
group_code: toStringSafe(item.group_code) ?? "unknown_group",
|
||||
group_title: toStringSafe(item.group_title) ?? "Группа",
|
||||
description: toStringSafe(item.description) ?? "",
|
||||
risk_level: (toStringSafe(item.risk_level) as "low" | "medium" | "high" | null) ?? "medium",
|
||||
maturity_status:
|
||||
(toStringSafe(item.maturity_status) as CapabilityMaturity | null) ??
|
||||
("partial" as CapabilityMaturity),
|
||||
supported_operations: toArray(item.supported_operations)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
unsupported_operations: toArray(item.unsupported_operations)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
required_entities: toArray(item.required_entities)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
optional_entities: toArray(item.optional_entities)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
typical_queries: toArray(item.typical_queries)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
related_routes: toArray(item.related_routes)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
safe_alternatives: toArray(item.safe_alternatives)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null),
|
||||
one_c_hints: toArray(item.one_c_hints)
|
||||
.map((v) => toStringSafe(v))
|
||||
.filter((v): v is string => v !== null)
|
||||
}));
|
||||
|
||||
if (groups.length === 0) return null;
|
||||
|
||||
return {
|
||||
schema_version: toStringSafe(root.schema_version) ?? "capabilities_registry_v1",
|
||||
updated_at: toStringSafe(root.updated_at) ?? new Date().toISOString(),
|
||||
assistant_mode: (toStringSafe(root.assistant_mode) as "read_only" | "mixed" | null) ?? "read_only",
|
||||
groups
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadCapabilitiesRegistry(): CapabilityRegistry {
|
||||
try {
|
||||
const mtimeMs = fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)
|
||||
? fs.statSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE).mtimeMs
|
||||
: -1;
|
||||
if (cache && cache.mtimeMs === mtimeMs) {
|
||||
return cache.value;
|
||||
}
|
||||
const value = readRegistryFromFile() ?? FALLBACK_REGISTRY;
|
||||
cache = { mtimeMs, value };
|
||||
return value;
|
||||
} catch {
|
||||
return cache?.value ?? FALLBACK_REGISTRY;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCapabilityContractReplyFromRegistry(): string {
|
||||
const registry = loadCapabilitiesRegistry();
|
||||
const topGroups = registry.groups.slice(0, 6);
|
||||
const groupLines = topGroups.map((group, index) => {
|
||||
const ops = group.supported_operations.slice(0, 3).join(", ");
|
||||
return `${index + 1}. ${group.group_title}: ${group.description}${ops ? ` (например: ${ops})` : ""}.`;
|
||||
});
|
||||
|
||||
return [
|
||||
"Я ассистент по анализу данных 1С в режиме чтения.",
|
||||
"Что умею по группам:",
|
||||
...groupLines,
|
||||
"Если хотите, раскрою любую группу точечно и дам готовую формулировку запроса.",
|
||||
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function resolveNearestCapabilityGroup(input: { domain?: string | null; queryClass?: string | null }): CapabilityGroup | null {
|
||||
const registry = loadCapabilitiesRegistry();
|
||||
const haystack = `${String(input.domain ?? "")} ${String(input.queryClass ?? "")}`.toLowerCase();
|
||||
if (!haystack.trim()) return null;
|
||||
|
||||
const scoring: Array<{ group: CapabilityGroup; score: number }> = registry.groups.map((group) => {
|
||||
let score = 0;
|
||||
const bucket = `${group.group_code} ${group.group_title} ${group.description} ${group.supported_operations.join(" ")}`.toLowerCase();
|
||||
for (const token of haystack.split(/[\s._/-]+/g).filter(Boolean)) {
|
||||
if (bucket.includes(token)) score += 1;
|
||||
}
|
||||
return { group, score };
|
||||
});
|
||||
|
||||
scoring.sort((a, b) => b.score - a.score);
|
||||
return scoring[0] && scoring[0].score > 0 ? scoring[0].group : null;
|
||||
}
|
||||
@@ -150,53 +150,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "initiated_payment",
|
||||
state_label: "Платеж инициирован",
|
||||
state_label: "Платеж инициирован",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["payment_order_created"],
|
||||
exit_conditions: ["bank_recorded"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Есть инициирование платежа."
|
||||
business_meaning: "Есть инициирование платежа."
|
||||
},
|
||||
{
|
||||
state_code: "bank_recorded",
|
||||
state_label: "Платеж отражен банком",
|
||||
state_label: "Платеж отражен банком",
|
||||
state_class: "active",
|
||||
entry_conditions: ["bank_statement_recorded"],
|
||||
exit_conditions: ["settlement_linked"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
|
||||
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "settlement_closed",
|
||||
state_label: "Расчет закрыт",
|
||||
state_label: "Расчет закрыт",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["payment_to_settlement_linked"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Платеж доведен до расчетного результата."
|
||||
business_meaning: "Платеж доведен до расчетного результата."
|
||||
},
|
||||
{
|
||||
state_code: "stale_unlinked_payment",
|
||||
state_label: "Платеж завис без закрытия",
|
||||
state_label: "Платеж завис без закрытия",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["bank_recorded", "missing_link"],
|
||||
exit_conditions: ["settlement_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
|
||||
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
|
||||
},
|
||||
{
|
||||
state_code: "misclosed_payment",
|
||||
state_label: "Платеж закрыт некорректно",
|
||||
state_label: "Платеж закрыт некорректно",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["wrong_document_type_or_posting_mismatch"],
|
||||
exit_conditions: ["settlement_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
|
||||
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
|
||||
}
|
||||
],
|
||||
transitions: [
|
||||
@@ -207,7 +207,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["bank_statement_recorded"],
|
||||
optional_evidence: ["payment_order"],
|
||||
forbidden_conditions: [],
|
||||
business_meaning: "Платеж должен появиться во выписке."
|
||||
business_meaning: "Платеж должен появиться во выписке."
|
||||
},
|
||||
{
|
||||
from_state: "bank_recorded",
|
||||
@@ -216,7 +216,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["payment_to_settlement_link"],
|
||||
optional_evidence: ["document_to_posting"],
|
||||
forbidden_conditions: ["wrong_document_type"],
|
||||
business_meaning: "После выписки должен закрываться расчет."
|
||||
business_meaning: "После выписки должен закрываться расчет."
|
||||
}
|
||||
],
|
||||
defects: []
|
||||
@@ -228,43 +228,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "invoice_issued",
|
||||
state_label: "Реализация отражена",
|
||||
state_label: "Реализация отражена",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["realization_document_exists"],
|
||||
exit_conditions: ["payment_recorded"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Возникла дебиторская позиция."
|
||||
business_meaning: "Возникла дебиторская позиция."
|
||||
},
|
||||
{
|
||||
state_code: "payment_recorded",
|
||||
state_label: "Оплата отражена",
|
||||
state_label: "Оплата отражена",
|
||||
state_class: "active",
|
||||
entry_conditions: ["payment_document_exists"],
|
||||
exit_conditions: ["receivable_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Оплата есть, ожидается корректное закрытие."
|
||||
business_meaning: "Оплата есть, ожидается корректное закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "receivable_closed",
|
||||
state_label: "Дебиторка закрыта",
|
||||
state_label: "Дебиторка закрыта",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["closing_document_linked"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Дебиторская позиция закрыта корректно."
|
||||
business_meaning: "Дебиторская позиция закрыта корректно."
|
||||
},
|
||||
{
|
||||
state_code: "stale_receivable",
|
||||
state_label: "Дебиторка зависла",
|
||||
state_label: "Дебиторка зависла",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["unresolved_settlement"],
|
||||
exit_conditions: ["receivable_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
|
||||
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
|
||||
}
|
||||
],
|
||||
transitions: [
|
||||
@@ -275,7 +275,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["payment_document_exists"],
|
||||
optional_evidence: [],
|
||||
forbidden_conditions: [],
|
||||
business_meaning: "После реализации ожидается оплата/зачет."
|
||||
business_meaning: "После реализации ожидается оплата/зачет."
|
||||
},
|
||||
{
|
||||
from_state: "payment_recorded",
|
||||
@@ -284,7 +284,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["closing_document_linked"],
|
||||
optional_evidence: ["register_movement_exists"],
|
||||
forbidden_conditions: ["cross_branch_inconsistency"],
|
||||
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
|
||||
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
|
||||
}
|
||||
],
|
||||
defects: []
|
||||
@@ -296,43 +296,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "recognized",
|
||||
state_label: "РБП признан",
|
||||
state_label: "РБП признан",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["deferred_expense_created"],
|
||||
exit_conditions: ["writeoff_started"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "РБП поставлен на учет."
|
||||
business_meaning: "РБП поставлен на учет."
|
||||
},
|
||||
{
|
||||
state_code: "partially_written_off",
|
||||
state_label: "Частичное списание",
|
||||
state_label: "Частичное списание",
|
||||
state_class: "active",
|
||||
entry_conditions: ["partial_writeoff_exists"],
|
||||
exit_conditions: ["fully_written_off"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Списание идет по графику."
|
||||
business_meaning: "Списание идет по графику."
|
||||
},
|
||||
{
|
||||
state_code: "fully_written_off",
|
||||
state_label: "РБП полностью списан",
|
||||
state_label: "РБП полностью списан",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["full_writeoff_exists"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "РБП завершил lifecycle."
|
||||
business_meaning: "РБП завершил lifecycle."
|
||||
},
|
||||
{
|
||||
state_code: "overdue_writeoff",
|
||||
state_label: "Просроченное списание",
|
||||
state_label: "Просроченное списание",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["period_boundary", "missing_link"],
|
||||
exit_conditions: ["fully_written_off"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "РБП живет дольше допустимого окна."
|
||||
business_meaning: "РБП живет дольше допустимого окна."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -345,53 +345,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "capitalized",
|
||||
state_label: "Капвложения отражены",
|
||||
state_label: "Капвложения отражены",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["capitalization_document_exists"],
|
||||
exit_conditions: ["accepted_for_accounting"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Объект зафиксирован как вложение."
|
||||
business_meaning: "Объект зафиксирован как вложение."
|
||||
},
|
||||
{
|
||||
state_code: "accepted_for_accounting",
|
||||
state_label: "Принят к учету",
|
||||
state_label: "Принят к учету",
|
||||
state_class: "active",
|
||||
entry_conditions: ["acceptance_document_exists"],
|
||||
exit_conditions: ["depreciation_active"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Объект переведен в основной контур учета."
|
||||
business_meaning: "Объект переведен в основной контур учета."
|
||||
},
|
||||
{
|
||||
state_code: "depreciation_active",
|
||||
state_label: "Амортизация активна",
|
||||
state_label: "Амортизация активна",
|
||||
state_class: "active",
|
||||
entry_conditions: ["depreciation_register_movement"],
|
||||
exit_conditions: ["disposed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Жизненный цикл ОС идет штатно."
|
||||
business_meaning: "Жизненный цикл ОС идет штатно."
|
||||
},
|
||||
{
|
||||
state_code: "contradictory_asset_state",
|
||||
state_label: "Противоречивый статус ОС",
|
||||
state_label: "Противоречивый статус ОС",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["posting_mismatch_or_wrong_path"],
|
||||
exit_conditions: ["depreciation_active"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
|
||||
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
|
||||
},
|
||||
{
|
||||
state_code: "disposed",
|
||||
state_label: "Выбыл",
|
||||
state_label: "Выбыл",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["disposal_document_exists"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Жизненный цикл ОС завершен."
|
||||
business_meaning: "Жизненный цикл ОС завершен."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -404,43 +404,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "vat_registered",
|
||||
state_label: "НДС отражен документно",
|
||||
state_label: "НДС отражен документно",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["invoice_registered"],
|
||||
exit_conditions: ["vat_reflected"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Сформирован первичный документный слой НДС."
|
||||
business_meaning: "Сформирован первичный документный слой НДС."
|
||||
},
|
||||
{
|
||||
state_code: "vat_reflected",
|
||||
state_label: "НДС отражен в учете",
|
||||
state_label: "НДС отражен в учете",
|
||||
state_class: "active",
|
||||
entry_conditions: ["vat_register_movement"],
|
||||
exit_conditions: ["vat_deducted"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "НДС проходит штатную стадию отражения."
|
||||
business_meaning: "НДС проходит штатную стадию отражения."
|
||||
},
|
||||
{
|
||||
state_code: "vat_deducted",
|
||||
state_label: "НДС принят к вычету",
|
||||
state_label: "НДС принят к вычету",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["deduction_confirmed"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "НДС-цепочка завершена корректно."
|
||||
business_meaning: "НДС-цепочка завершена корректно."
|
||||
},
|
||||
{
|
||||
state_code: "vat_conflict",
|
||||
state_label: "Конфликт НДС-цепочки",
|
||||
state_label: "Конфликт НДС-цепочки",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["cross_branch_inconsistency"],
|
||||
exit_conditions: ["vat_reflected"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
|
||||
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -453,53 +453,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "preclose_checks",
|
||||
state_label: "Предзакрытие",
|
||||
state_label: "Предзакрытие",
|
||||
state_class: "active",
|
||||
entry_conditions: ["period_scope_detected"],
|
||||
exit_conditions: ["close_ready"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Рдет проверка готовности периода."
|
||||
business_meaning: "Идет проверка готовности периода."
|
||||
},
|
||||
{
|
||||
state_code: "close_ready",
|
||||
state_label: "Готов к закрытию",
|
||||
state_label: "Готов к закрытию",
|
||||
state_class: "active",
|
||||
entry_conditions: ["no_blockers_detected"],
|
||||
exit_conditions: ["close_completed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Период может быть закрыт."
|
||||
business_meaning: "Период может быть закрыт."
|
||||
},
|
||||
{
|
||||
state_code: "close_completed",
|
||||
state_label: "Закрытие завершено",
|
||||
state_label: "Закрытие завершено",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["close_operation_done"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Период закрыт."
|
||||
business_meaning: "Период закрыт."
|
||||
},
|
||||
{
|
||||
state_code: "close_blocked",
|
||||
state_label: "Закрытие заблокировано",
|
||||
state_label: "Закрытие заблокировано",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["period_close_risk_or_stale_state"],
|
||||
exit_conditions: ["close_ready"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
|
||||
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "close_contradicted",
|
||||
state_label: "Закрыт формально, но с противоречием",
|
||||
state_label: "Закрыт формально, но с противоречием",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["misclosed_or_cross_branch_conflict"],
|
||||
exit_conditions: ["close_completed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
|
||||
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -512,7 +512,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "missing_expected_transition",
|
||||
defect_class: "path",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Ожидаемый переход не произошел.",
|
||||
business_meaning: "Ожидаемый переход не произошел.",
|
||||
evidence_requirements: ["expected_state", "missing_transition_signal"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -520,7 +520,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "invalid_transition",
|
||||
defect_class: "path",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Переход произошел по некорректному пути.",
|
||||
business_meaning: "Переход произошел по некорректному пути.",
|
||||
evidence_requirements: ["invalid_transition_signal"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -528,7 +528,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "stale_active_state",
|
||||
defect_class: "timing",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Объект завис в активном состоянии.",
|
||||
business_meaning: "Объект завис в активном состоянии.",
|
||||
evidence_requirements: ["stale_marker", "missing_transition_signal"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -536,7 +536,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "contradictory_state",
|
||||
defect_class: "consistency",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Статусы объекта противоречат друг другу.",
|
||||
business_meaning: "Статусы объекта противоречат друг другу.",
|
||||
evidence_requirements: ["contradiction_signal"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -544,7 +544,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "premature_terminal_state",
|
||||
defect_class: "closure",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Терминальное состояние наступило преждевременно.",
|
||||
business_meaning: "Терминальное состояние наступило преждевременно.",
|
||||
evidence_requirements: ["terminal_state", "missing_required_previous_state"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -552,7 +552,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "misclosed_state",
|
||||
defect_class: "closure",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Контур формально закрыт, но закрыт неверно.",
|
||||
business_meaning: "Контур формально закрыт, но закрыт неверно.",
|
||||
evidence_requirements: ["wrong_closure_path"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -560,7 +560,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "orphan_intermediate_state",
|
||||
defect_class: "path",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
|
||||
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
|
||||
evidence_requirements: ["intermediate_state_without_next"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -568,7 +568,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "cross_branch_state_conflict",
|
||||
defect_class: "consistency",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
|
||||
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
|
||||
evidence_requirements: ["cross_branch_conflict_signal"],
|
||||
period_impact_potential: "direct"
|
||||
}
|
||||
@@ -905,23 +905,23 @@ function lifecycleInterpretation(input: {
|
||||
missingTransition: string | null;
|
||||
invalidTransition: string | null;
|
||||
}): string {
|
||||
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
|
||||
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
|
||||
if (input.defect === "stale_active_state") {
|
||||
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
|
||||
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
|
||||
}
|
||||
if (input.defect === "misclosed_state") {
|
||||
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
|
||||
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
|
||||
}
|
||||
if (input.defect === "cross_branch_state_conflict") {
|
||||
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
|
||||
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
|
||||
}
|
||||
if (input.defect === "missing_expected_transition") {
|
||||
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
|
||||
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
|
||||
}
|
||||
if (input.defect === "invalid_transition") {
|
||||
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
|
||||
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
|
||||
}
|
||||
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||||
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||||
}
|
||||
|
||||
export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolution {
|
||||
|
||||
@@ -78,10 +78,10 @@ const BUILTIN_PROMPT_PRESETS: Record<PromptVersion, BuiltinPromptPresetDefinitio
|
||||
},
|
||||
normalizer_v1_1_2_1: {
|
||||
id: "default-normalizer-v1_1_2_1",
|
||||
name: "Стандартный пресет NDC 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 без изменений.",
|
||||
"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.join("system", "default.txt"),
|
||||
developer: path.join("developer", "normalizer_v1_1_2_1.txt"),
|
||||
@@ -91,10 +91,10 @@ const BUILTIN_PROMPT_PRESETS: Record<PromptVersion, BuiltinPromptPresetDefinitio
|
||||
},
|
||||
normalizer_v2: {
|
||||
id: "default-normalizer-v2",
|
||||
name: "Стандартный пресет NDC 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.",
|
||||
"v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.",
|
||||
files: {
|
||||
system: path.join("system", "default.txt"),
|
||||
developer: path.join("developer", "normalizer_v2.txt"),
|
||||
|
||||
@@ -79,7 +79,7 @@ const AMBIGUITY_PATTERN =
|
||||
/(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
|
||||
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
|
||||
const DOMAIN_LEXICAL_ANCHOR_PATTERN =
|
||||
/(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С‚.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i;
|
||||
/(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|ндс|сч[её]С‚.?фактур|книг[аи]|затрат|закрыт|остатк)/i;
|
||||
|
||||
export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user