АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия + доп кля + конфиг дизапйна
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[] = [
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NDC AI Normalizer Playground</title>
|
||||
<script type="module" crossorigin src="/assets/index-D6Y_lHrc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BMWPMdQA.css">
|
||||
<script type="module" crossorigin src="/assets/index-BDtb8kxy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-iFxz5cXp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PromptPanel } from "./components/PromptPanel";
|
||||
import { QueryPanel } from "./components/QueryPanel";
|
||||
import { RuntimePanel } from "./components/RuntimePanel";
|
||||
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
|
||||
import { designConfig } from "../../../designconfig";
|
||||
import type {
|
||||
AssistantConversationItem,
|
||||
ConnectionState,
|
||||
@@ -23,7 +24,7 @@ import type {
|
||||
} from "./state/types";
|
||||
|
||||
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
||||
const ASSISTANT_STAGES = ["Analyzing request", "Fetching data", "Composing answer"];
|
||||
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
|
||||
const DEFAULT_UI_MODE: UiMode = "assistant";
|
||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||
@@ -79,6 +80,9 @@ export default function App() {
|
||||
const [lastError, setLastError] = useState("");
|
||||
|
||||
const [uiMode, setUiMode] = useState<UiMode>(DEFAULT_UI_MODE);
|
||||
const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true);
|
||||
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
|
||||
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
|
||||
const [assistantSessionId, setAssistantSessionId] = useState("");
|
||||
const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]);
|
||||
const [assistantInput, setAssistantInput] = useState("");
|
||||
@@ -87,6 +91,23 @@ export default function App() {
|
||||
const [assistantError, setAssistantError] = useState("");
|
||||
const presetAutoloadDoneRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const { colors } = designConfig;
|
||||
root.style.setProperty("--rgb-background", colors.backgroundRgb);
|
||||
root.style.setProperty("--rgb-surface-main", colors.mainSurfaceRgb);
|
||||
root.style.setProperty("--rgb-surface-horizontal", colors.horizontalSurfaceRgb);
|
||||
root.style.setProperty("--rgb-surface-focus", colors.focusSurfaceRgb);
|
||||
root.style.setProperty("--rgb-active", colors.activeRgb);
|
||||
root.style.setProperty("--rgb-active-text", colors.activeTextRgb);
|
||||
root.style.setProperty("--rgb-text-main", colors.textMainRgb);
|
||||
root.style.setProperty("--rgb-text-muted", colors.textMutedRgb);
|
||||
root.style.setProperty("--rgb-danger", colors.dangerRgb);
|
||||
root.style.setProperty("--rgb-scrollbar-track", colors.scrollbarTrackRgb);
|
||||
root.style.setProperty("--rgb-scrollbar-thumb", colors.scrollbarThumbRgb);
|
||||
root.style.setProperty("--rgb-scrollbar-thumb-hover", colors.scrollbarThumbHoverRgb);
|
||||
}, []);
|
||||
|
||||
const log = (message: string) => {
|
||||
setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300));
|
||||
};
|
||||
@@ -509,12 +530,12 @@ export default function App() {
|
||||
});
|
||||
setAssistantSessionId(response.session_id);
|
||||
setAssistantConversation(response.conversation);
|
||||
setAssistantStatus("Reply is ready");
|
||||
setAssistantStatus("Ответ готов");
|
||||
log(`Assistant reply received: trace=${response.debug.trace_id}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAssistantError(message);
|
||||
setAssistantStatus("Assistant error");
|
||||
setAssistantStatus("Ошибка ассистента");
|
||||
log(`Assistant error: ${message}`);
|
||||
} finally {
|
||||
stopTicker();
|
||||
@@ -534,23 +555,46 @@ export default function App() {
|
||||
}, [selectedRunId]);
|
||||
|
||||
return (
|
||||
<main className="app-root">
|
||||
<div className="hero">
|
||||
<h1>NDC AI First Layer</h1>
|
||||
<p>Three modes in one UI: assistant, decomposition diagnostics, and auto-run history with regression visibility.</p>
|
||||
</div>
|
||||
<main className={`app-root ${uiMode === "autoruns" ? "app-root-autoruns" : ""}`}>
|
||||
<header className="app-topbar">
|
||||
<div className="mode-switch-row">
|
||||
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
|
||||
Ассистент
|
||||
</button>
|
||||
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
||||
Декомпозиция
|
||||
</button>
|
||||
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
|
||||
История автопрогонов
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mode-switch-row">
|
||||
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
|
||||
Assistant
|
||||
</button>
|
||||
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
||||
Decomposition
|
||||
</button>
|
||||
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
|
||||
AutoRun History
|
||||
</button>
|
||||
</div>
|
||||
{uiMode === "autoruns" ? (
|
||||
<div className="mode-switch-row mode-switch-row-right">
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
|
||||
>
|
||||
Режим ассистента
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsDecompositionMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsDecompositionMode((prev) => !prev)}
|
||||
>
|
||||
Режим декомпозиции
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
|
||||
>
|
||||
Прогресс/регресс
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
{uiMode === "assistant" ? (
|
||||
<div className="layout-grid">
|
||||
@@ -660,12 +704,15 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="layout-grid">
|
||||
<div className="layout-grid layout-grid-autoruns">
|
||||
<AutoRunsHistoryPanel
|
||||
connection={connection}
|
||||
prompts={prompts}
|
||||
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
||||
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
||||
showAssistantMode={showAutorunsAssistantMode}
|
||||
showDecompositionMode={showAutorunsDecompositionMode}
|
||||
showProgressMode={showAutorunsProgressMode}
|
||||
onLog={log}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type {
|
||||
AutoGenHistoryResponse,
|
||||
AutoGenMode,
|
||||
AutoRunAnnotationsResponse,
|
||||
AutoRunAnnotationRecord,
|
||||
AutoRunDetailResponse,
|
||||
AutoRunDialogResponse,
|
||||
AutoRunHistoryResponse,
|
||||
AutoRunPostAnalysisResponse,
|
||||
AssistantMessageResultState,
|
||||
AssistantConversationItem,
|
||||
ConnectionState,
|
||||
HistoryItem,
|
||||
ManualCaseDecision,
|
||||
NormalizeResultState,
|
||||
PromptState,
|
||||
RuntimeRun
|
||||
@@ -281,5 +287,95 @@ export const apiClient = {
|
||||
|
||||
async loadAutoRunCaseDialog(runId: string, caseId: string): Promise<AutoRunDialogResponse> {
|
||||
return request(`/autoruns/history/${encodeURIComponent(runId)}/case/${encodeURIComponent(caseId)}/dialog`);
|
||||
},
|
||||
|
||||
async loadAutoRunAnnotations(input?: {
|
||||
run_id?: string;
|
||||
case_id?: string;
|
||||
min_rating?: number;
|
||||
manual_case_decision?: ManualCaseDecision | "all";
|
||||
limit?: number;
|
||||
}): Promise<AutoRunAnnotationsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (input?.run_id) params.set("run_id", input.run_id);
|
||||
if (input?.case_id) params.set("case_id", input.case_id);
|
||||
if (typeof input?.min_rating === "number") params.set("min_rating", String(input.min_rating));
|
||||
if (input?.manual_case_decision) params.set("manual_case_decision", input.manual_case_decision);
|
||||
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
|
||||
const query = params.toString();
|
||||
return request(`/autoruns/annotations${query ? `?${query}` : ""}`);
|
||||
},
|
||||
|
||||
async saveAutoRunAnnotation(input: {
|
||||
run_id: string;
|
||||
case_id: string;
|
||||
message_index: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
manual_case_decision: ManualCaseDecision;
|
||||
annotation_author?: string;
|
||||
}): Promise<{ ok: boolean; annotation: AutoRunAnnotationRecord; case_annotation_stats: { count: number; latest_at: string | null; avg_rating: number | null } | null }> {
|
||||
return request("/autoruns/annotations", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
},
|
||||
|
||||
async loadAutoRunPostAnalysis(input?: {
|
||||
run_id?: string;
|
||||
limit_per_queue?: number;
|
||||
annotation_limit?: number;
|
||||
scan_limit?: number;
|
||||
from?: string;
|
||||
to?: string;
|
||||
target?: string;
|
||||
mode?: string;
|
||||
use_mock?: "any" | "true" | "false";
|
||||
prompt_contains?: string;
|
||||
}): Promise<AutoRunPostAnalysisResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (input?.run_id) params.set("run_id", input.run_id);
|
||||
if (typeof input?.limit_per_queue === "number") params.set("limit_per_queue", String(input.limit_per_queue));
|
||||
if (typeof input?.annotation_limit === "number") params.set("annotation_limit", String(input.annotation_limit));
|
||||
if (typeof input?.scan_limit === "number") params.set("scan_limit", String(input.scan_limit));
|
||||
if (input?.from) params.set("from", input.from);
|
||||
if (input?.to) params.set("to", input.to);
|
||||
if (input?.target) params.set("target", input.target);
|
||||
if (input?.mode) params.set("mode", input.mode);
|
||||
if (input?.use_mock) params.set("use_mock", input.use_mock);
|
||||
if (input?.prompt_contains) params.set("prompt_contains", input.prompt_contains);
|
||||
const query = params.toString();
|
||||
return request(`/autoruns/post-analysis${query ? `?${query}` : ""}`);
|
||||
},
|
||||
|
||||
async loadAutoRunAutogenHistory(input?: {
|
||||
mode?: AutoGenMode;
|
||||
limit?: number;
|
||||
}): Promise<AutoGenHistoryResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (input?.mode) params.set("mode", input.mode);
|
||||
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
|
||||
const query = params.toString();
|
||||
return request(`/autoruns/autogen/history${query ? `?${query}` : ""}`);
|
||||
},
|
||||
|
||||
async generateAutoRunQuestions(input: {
|
||||
mode: AutoGenMode;
|
||||
count: number;
|
||||
domain?: string;
|
||||
persist_to_eval_cases?: boolean;
|
||||
generated_by?: string;
|
||||
context?: {
|
||||
llm_provider?: string;
|
||||
model?: string;
|
||||
assistant_prompt_version?: string;
|
||||
decomposition_prompt_version?: string;
|
||||
prompt_fingerprint?: string;
|
||||
};
|
||||
}): Promise<{ ok: boolean; generation: { generation_id: string; created_at: string; mode: AutoGenMode; count: number; domain: string | null; questions: string[]; generated_by: string | null; saved_case_set_file: string | null; context: Record<string, unknown> | null } }> {
|
||||
return request("/autoruns/autogen/generate", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,18 +4,22 @@ interface PanelFrameProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
hideHeader?: boolean;
|
||||
}
|
||||
|
||||
export function PanelFrame({ title, subtitle, actions, children }: PropsWithChildren<PanelFrameProps>) {
|
||||
export function PanelFrame({ title, subtitle, actions, className, hideHeader, children }: PropsWithChildren<PanelFrameProps>) {
|
||||
return (
|
||||
<section className="panel-frame">
|
||||
<header className="panel-header">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{subtitle ? <p>{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="panel-actions">{actions}</div> : null}
|
||||
</header>
|
||||
<section className={className ? `panel-frame ${className}` : "panel-frame"}>
|
||||
{!hideHeader ? (
|
||||
<header className="panel-header">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{subtitle ? <p>{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="panel-actions">{actions}</div> : null}
|
||||
</header>
|
||||
) : null}
|
||||
<div className="panel-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -70,6 +70,17 @@ export type UiMode = "assistant" | "decomposition" | "autoruns";
|
||||
|
||||
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
||||
export type AutoRunTrend = "up" | "down" | "flat";
|
||||
export type AutoGenMode = "qwen_seed" | "codex_creative";
|
||||
export type ManualCaseDecision =
|
||||
| "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";
|
||||
|
||||
export interface AutoRunDomainCoverage {
|
||||
domain: string;
|
||||
@@ -109,6 +120,9 @@ export interface AutoRunCaseSummary {
|
||||
reply_type: string | null;
|
||||
session_id: string;
|
||||
dialog_available: boolean;
|
||||
commented_count: number;
|
||||
latest_annotation_at: string | null;
|
||||
avg_rating: number | null;
|
||||
checks: Record<string, unknown> | null;
|
||||
metric_subscores: Record<string, unknown> | null;
|
||||
}
|
||||
@@ -158,15 +172,79 @@ export interface AutoRunDetailResponse {
|
||||
run: AutoRunSummary;
|
||||
coverage: AutoRunCoverage;
|
||||
cases: AutoRunCaseSummary[];
|
||||
annotations_summary?: {
|
||||
total: number;
|
||||
};
|
||||
report: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AutoRunAnnotationRecord {
|
||||
annotation_id: string;
|
||||
run_id: string;
|
||||
case_id: string;
|
||||
session_id: string;
|
||||
message_index: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
manual_case_decision: ManualCaseDecision;
|
||||
annotation_author: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
context: {
|
||||
message_id: string | null;
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
eval_target: AutoRunTarget | "unknown";
|
||||
prompt_version: string | null;
|
||||
domain: string | null;
|
||||
query_class: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AutoRunAnnotationListItem extends AutoRunAnnotationRecord {
|
||||
run: AutoRunSummary | null;
|
||||
case_summary: AutoRunCaseSummary | null;
|
||||
technical_context: {
|
||||
report_path: string | null;
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
domain: string | null;
|
||||
query_class: string | null;
|
||||
checks: Record<string, unknown> | null;
|
||||
metric_subscores: Record<string, unknown> | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AutoRunAnnotationsResponse {
|
||||
ok: boolean;
|
||||
generated_at: string;
|
||||
filters_applied: {
|
||||
run_id: string | null;
|
||||
case_id: string | null;
|
||||
min_rating: number | null;
|
||||
manual_case_decision: ManualCaseDecision | "all";
|
||||
limit: number;
|
||||
};
|
||||
stats: {
|
||||
total: number;
|
||||
avg_rating: number | null;
|
||||
by_decision: Record<string, number>;
|
||||
};
|
||||
available_manual_case_decisions: ManualCaseDecision[];
|
||||
manual_case_decision_schema: Record<string, unknown>;
|
||||
items: AutoRunAnnotationListItem[];
|
||||
}
|
||||
|
||||
export interface AutoRunDialogMessage {
|
||||
message_id: string | null;
|
||||
role: string;
|
||||
text: string;
|
||||
created_at: string | null;
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
message_index: number;
|
||||
commented: boolean;
|
||||
annotation: AutoRunAnnotationRecord | null;
|
||||
}
|
||||
|
||||
export interface AutoRunDialogResponse {
|
||||
@@ -178,6 +256,77 @@ export interface AutoRunDialogResponse {
|
||||
messages: AutoRunDialogMessage[];
|
||||
decomposition: string[];
|
||||
assistant_mode: Record<string, unknown> | null;
|
||||
annotations: AutoRunAnnotationRecord[];
|
||||
}
|
||||
|
||||
export interface AutoRunPostAnalysisQueueItem {
|
||||
annotation_id: string;
|
||||
run_id: string;
|
||||
case_id: string;
|
||||
message_index: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
manual_case_decision: ManualCaseDecision;
|
||||
annotation_author: string | null;
|
||||
updated_at: string;
|
||||
domain: string | null;
|
||||
query_class: string | null;
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
nearest_capability_group: {
|
||||
group_code: string;
|
||||
group_title: string;
|
||||
maturity_status: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AutoRunPostAnalysisResponse {
|
||||
ok: boolean;
|
||||
generated_at: string;
|
||||
filters_applied: {
|
||||
run_id: string | null;
|
||||
run_filters_applied: boolean;
|
||||
limit_per_queue: number;
|
||||
annotation_limit: number;
|
||||
scan_limit: number;
|
||||
};
|
||||
runs_considered: AutoRunSummary[];
|
||||
manual_case_decision_schema: Record<string, unknown>;
|
||||
post_analysis: {
|
||||
stats: {
|
||||
annotations_total: number;
|
||||
by_decision: Record<string, number>;
|
||||
by_queue: Record<string, number>;
|
||||
domains_total: number;
|
||||
};
|
||||
domain_summary: Array<{ domain: string; total: number }>;
|
||||
queues: Record<string, AutoRunPostAnalysisQueueItem[]>;
|
||||
recommended_regression_candidates: AutoRunPostAnalysisQueueItem[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AutoGenHistoryRecord {
|
||||
generation_id: string;
|
||||
created_at: string;
|
||||
mode: AutoGenMode;
|
||||
count: number;
|
||||
domain: string | null;
|
||||
questions: string[];
|
||||
generated_by: string | null;
|
||||
saved_case_set_file: string | null;
|
||||
context: {
|
||||
llm_provider: string | null;
|
||||
model: string | null;
|
||||
assistant_prompt_version: string | null;
|
||||
decomposition_prompt_version: string | null;
|
||||
prompt_fingerprint: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AutoGenHistoryResponse {
|
||||
ok: boolean;
|
||||
generated_at: string;
|
||||
items: AutoGenHistoryRecord[];
|
||||
}
|
||||
|
||||
export type AssistantFallbackType = "none" | "out_of_scope" | "clarification" | "partial" | "unknown";
|
||||
|
||||
@@ -1,64 +1,96 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Space+Grotesk:wght@500;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Space+Grotesk:wght@500;700&display=swap");
|
||||
|
||||
:root {
|
||||
--bg-main: #060a08;
|
||||
--bg-soft: #0e1511;
|
||||
--bg-panel: rgba(15, 24, 19, 0.85);
|
||||
--bg-panel-accent: rgba(38, 62, 45, 0.45);
|
||||
--line: rgba(143, 255, 173, 0.3);
|
||||
--line-strong: rgba(143, 255, 173, 0.72);
|
||||
--text-main: #f3ffee;
|
||||
--text-muted: #9bb8a5;
|
||||
--lime-main: #8fffad;
|
||||
--lime-press: #59db83;
|
||||
--danger: #ff7e7e;
|
||||
--rgb-background: 16, 16, 19;
|
||||
--rgb-surface-main: 26, 26, 31;
|
||||
--rgb-surface-horizontal: 32, 32, 38;
|
||||
--rgb-surface-focus: 40, 40, 47;
|
||||
--rgb-active: 228, 142, 92;
|
||||
--rgb-active-text: 18, 18, 18;
|
||||
--rgb-text-main: 240, 240, 240;
|
||||
--rgb-text-muted: 166, 166, 170;
|
||||
--rgb-danger: 255, 126, 126;
|
||||
--rgb-scrollbar-track: 31, 31, 36;
|
||||
--rgb-scrollbar-thumb: 74, 74, 82;
|
||||
--rgb-scrollbar-thumb-hover: 90, 90, 100;
|
||||
--bg-main: rgb(var(--rgb-background));
|
||||
--bg-soft: rgb(var(--rgb-surface-main));
|
||||
--bg-panel: rgb(var(--rgb-surface-main));
|
||||
--bg-panel-accent: rgb(var(--rgb-surface-horizontal));
|
||||
--surface-horizontal: rgb(var(--rgb-surface-horizontal));
|
||||
--surface-focus: rgb(var(--rgb-surface-focus));
|
||||
--line: transparent;
|
||||
--line-strong: rgba(var(--rgb-active), 0.48);
|
||||
--text-main: rgb(var(--rgb-text-main));
|
||||
--text-muted: rgb(var(--rgb-text-muted));
|
||||
--lime-main: rgb(var(--rgb-text-main));
|
||||
--lime-press: rgb(var(--rgb-text-main));
|
||||
--danger: rgb(var(--rgb-danger));
|
||||
--radius-lg: 20px;
|
||||
--radius-md: 14px;
|
||||
--shadow: 0 16px 44px rgba(0, 0, 0, 0.35);
|
||||
--shadow: none;
|
||||
--autoruns-col-width: 360px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(var(--rgb-scrollbar-thumb)) rgb(var(--rgb-scrollbar-track));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: rgb(var(--rgb-scrollbar-track));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: rgb(var(--rgb-scrollbar-thumb));
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgb(var(--rgb-scrollbar-track));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(var(--rgb-scrollbar-thumb-hover));
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
min-height: 100dvh;
|
||||
font-family: "Manrope", "Segoe UI", sans-serif;
|
||||
background: radial-gradient(circle at 15% -10%, #1f3b2f 0%, transparent 40%),
|
||||
radial-gradient(circle at 90% 10%, #1d2f24 0%, transparent 35%),
|
||||
linear-gradient(165deg, var(--bg-main) 0%, #070d0a 100%);
|
||||
background: var(--bg-main);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.app-root {
|
||||
max-width: 1720px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 24px 42px;
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 22px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(145deg, rgba(17, 29, 22, 0.94), rgba(10, 16, 13, 0.92));
|
||||
box-shadow: var(--shadow);
|
||||
animation: rise 0.5s ease-out;
|
||||
.app-root.app-root-autoruns {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
min-height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-family: "Space Grotesk", "Manrope", sans-serif;
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(1.5rem, 3vw, 2.3rem);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
.app-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 0 0 12px;
|
||||
padding: 0;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
@@ -67,20 +99,35 @@ body,
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.layout-grid.layout-grid-autoruns {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mode-switch-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mode-switch-row.mode-switch-row-right {
|
||||
margin-left: auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.panel-frame {
|
||||
grid-column: span 12;
|
||||
border: 1px solid var(--line);
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(165deg, var(--bg-panel), rgba(10, 17, 13, 0.88));
|
||||
background: var(--bg-panel);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: none;
|
||||
animation: rise 0.4s ease-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@@ -89,8 +136,8 @@ body,
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 14px 18px 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: linear-gradient(90deg, var(--bg-panel-accent), transparent 70%);
|
||||
border-bottom: none;
|
||||
background: var(--bg-panel-accent);
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
@@ -106,15 +153,31 @@ body,
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 14px 18px 18px;
|
||||
padding: 10px 12px 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-root-autoruns .autoruns-frame {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-root-autoruns .autoruns-frame .panel-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px 12px 12px;
|
||||
background: rgb(var(--rgb-background));
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
border: 1px solid var(--line);
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
color: var(--lime-main);
|
||||
font-size: 0.78rem;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
}
|
||||
|
||||
.assistant-panel-actions {
|
||||
@@ -127,14 +190,14 @@ body,
|
||||
|
||||
.assistant-copy-btn {
|
||||
background: transparent;
|
||||
border-color: var(--line);
|
||||
border-color: transparent;
|
||||
color: var(--text-main);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.assistant-copy-btn:hover {
|
||||
background: rgba(143, 255, 173, 0.14);
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
filter: none;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
@@ -171,20 +234,22 @@ label {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 1px solid rgba(170, 255, 194, 0.26);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(7, 13, 10, 0.92);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: var(--text-main);
|
||||
padding: 10px 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: 0 0 0 3px rgba(143, 255, 173, 0.2);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
}
|
||||
|
||||
textarea {
|
||||
@@ -193,28 +258,28 @@ textarea {
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(176, 255, 199, 0.35);
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(180deg, #8fffad 0%, #62e286 100%);
|
||||
color: #08100a;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: rgb(var(--rgb-text-main));
|
||||
font-weight: 700;
|
||||
font-size: 0.83rem;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
padding: 9px 14px;
|
||||
transition: transform 0.16s ease, filter 0.2s ease, box-shadow 0.18s ease;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.04);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 20px rgba(127, 255, 170, 0.32);
|
||||
border-color: transparent;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
opacity: 0.52;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
@@ -250,14 +315,15 @@ button:disabled {
|
||||
}
|
||||
|
||||
.tab {
|
||||
background: transparent;
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--line);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-color: var(--line-strong);
|
||||
background: linear-gradient(180deg, rgba(143, 255, 173, 0.25), rgba(143, 255, 173, 0.1));
|
||||
border-color: transparent;
|
||||
background: rgb(var(--rgb-active));
|
||||
color: rgb(var(--rgb-active-text));
|
||||
}
|
||||
|
||||
.assistant-chat-list {
|
||||
@@ -266,9 +332,9 @@ button:disabled {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(156, 255, 189, 0.2);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(6, 10, 8, 0.65);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
}
|
||||
|
||||
.assistant-empty {
|
||||
@@ -277,19 +343,19 @@ button:disabled {
|
||||
}
|
||||
|
||||
.assistant-msg {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(11, 18, 14, 0.74);
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.assistant-msg.user {
|
||||
border-color: rgba(110, 198, 255, 0.38);
|
||||
background: rgba(13, 20, 24, 0.76);
|
||||
border-color: transparent;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
}
|
||||
|
||||
.assistant-msg.assistant {
|
||||
border-color: rgba(157, 255, 190, 0.25);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.assistant-msg-head {
|
||||
@@ -335,11 +401,11 @@ button:disabled {
|
||||
min-height: 180px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
background: #050806;
|
||||
border: 1px solid rgba(156, 255, 189, 0.2);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
color: #d6ffdf;
|
||||
color: rgb(var(--rgb-text-main));
|
||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
@@ -352,8 +418,8 @@ button:disabled {
|
||||
}
|
||||
|
||||
.metrics-grid div {
|
||||
background: rgba(11, 18, 14, 0.7);
|
||||
border: 1px solid rgba(157, 255, 190, 0.24);
|
||||
background: rgba(var(--rgb-surface-main), 0.8);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@@ -382,8 +448,8 @@ button:disabled {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.24);
|
||||
background: rgba(11, 18, 14, 0.78);
|
||||
border: none;
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
color: var(--text-main);
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -455,16 +521,32 @@ button:disabled {
|
||||
}
|
||||
|
||||
.autoruns-columns {
|
||||
display: grid;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.autoruns-col {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
flex: 0 0 var(--autoruns-col-width);
|
||||
width: var(--autoruns-col-width);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: rgba(8, 13, 10, 0.72);
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
padding: 12px;
|
||||
min-height: 220px;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.autoruns-col:nth-child(3) {
|
||||
flex-basis: 440px;
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
.autoruns-col h3 {
|
||||
@@ -493,9 +575,9 @@ button:disabled {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.14);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgba(10, 18, 13, 0.65);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 8px 9px;
|
||||
font-size: 0.79rem;
|
||||
}
|
||||
@@ -506,7 +588,7 @@ button:disabled {
|
||||
|
||||
.autoruns-prompt-details summary {
|
||||
cursor: pointer;
|
||||
color: var(--lime-main);
|
||||
color: var(--text-main);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -523,9 +605,9 @@ button:disabled {
|
||||
}
|
||||
|
||||
.autoruns-stats-grid > div {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgba(10, 18, 13, 0.7);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
@@ -544,7 +626,9 @@ button:disabled {
|
||||
.autoruns-run-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 760px;
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
@@ -553,8 +637,8 @@ button:disabled {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.23);
|
||||
background: rgba(11, 18, 14, 0.75);
|
||||
border: none;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: var(--text-main);
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
@@ -589,7 +673,7 @@ button:disabled {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-height: 170px;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -597,8 +681,8 @@ button:disabled {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.22);
|
||||
background: rgba(9, 14, 11, 0.72);
|
||||
border: none;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: var(--text-main);
|
||||
padding: 7px 8px;
|
||||
display: flex;
|
||||
@@ -613,20 +697,22 @@ button:disabled {
|
||||
|
||||
.autoruns-dialog-view {
|
||||
margin-top: 10px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(5, 8, 6, 0.7);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 10px;
|
||||
max-height: 570px;
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-msg {
|
||||
border: 1px solid rgba(157, 255, 190, 0.22);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(11, 18, 14, 0.8);
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
padding: 8px 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -641,6 +727,12 @@ button:disabled {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.autoruns-msg-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-msg p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
@@ -648,14 +740,122 @@ button:disabled {
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon {
|
||||
border: none;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: var(--text-main);
|
||||
border-radius: 999px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
line-height: 1;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon:hover {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon.commented {
|
||||
border-color: var(--line-strong);
|
||||
color: var(--lime-main);
|
||||
background: rgb(var(--rgb-active));
|
||||
color: rgb(var(--rgb-active-text));
|
||||
}
|
||||
|
||||
.autoruns-msg-annotation {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 7px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.autoruns-comments-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.autoruns-autogen-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.autoruns-autogen-item {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.autoruns-autogen-item header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.autoruns-autogen-item p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.autoruns-comment-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: var(--text-main);
|
||||
padding: 9px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.autoruns-comment-item p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.79rem;
|
||||
}
|
||||
|
||||
.autoruns-comment-item.selected {
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.autoruns-comment-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.autoruns-msg.assistant {
|
||||
margin-right: 12%;
|
||||
}
|
||||
|
||||
.autoruns-msg.user {
|
||||
margin-left: 12%;
|
||||
border-color: rgba(95, 179, 255, 0.35);
|
||||
background: rgba(10, 18, 24, 0.75);
|
||||
border-color: transparent;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
}
|
||||
|
||||
.autoruns-decomposition-list {
|
||||
@@ -666,15 +866,83 @@ button:disabled {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.autoruns-comment-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(var(--rgb-background), 0.74);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 1800;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.autoruns-comment-modal {
|
||||
width: min(660px, 100%);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
box-shadow: var(--shadow);
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.autoruns-comment-modal h3 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.autoruns-comment-quote {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.autoruns-rating-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-rating-dot {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.autoruns-rating-dot:hover {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.autoruns-rating-dot.active {
|
||||
border-color: var(--line-strong);
|
||||
color: rgb(var(--rgb-active-text));
|
||||
background: rgb(var(--rgb-active));
|
||||
}
|
||||
|
||||
.autoruns-coverage-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-coverage-item {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgba(11, 18, 14, 0.68);
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@@ -693,27 +961,31 @@ button:disabled {
|
||||
.autoruns-coverage-bar {
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(157, 255, 190, 0.14);
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.autoruns-coverage-bar > div {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #6ee0ff, #8fffad);
|
||||
background: rgb(var(--rgb-active));
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
:root {
|
||||
--autoruns-col-width: 340px;
|
||||
}
|
||||
|
||||
.autoruns-columns {
|
||||
grid-template-columns: 1fr !important;
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
:root {
|
||||
--autoruns-col-width: 320px;
|
||||
}
|
||||
|
||||
.grid-two,
|
||||
.runtime-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -731,6 +1003,10 @@ button:disabled {
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
:root {
|
||||
--autoruns-col-width: 300px;
|
||||
}
|
||||
|
||||
.app-root {
|
||||
padding: 18px 12px 24px;
|
||||
}
|
||||
@@ -750,3 +1026,6 @@ button:disabled {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user