АРЧ ЮИ - остановка прогона через гуй

This commit is contained in:
dctouch 2026-04-18 18:14:30 +03:00
parent 9710d81676
commit 12550606bb
39 changed files with 1783 additions and 93 deletions

View File

@ -278,6 +278,38 @@ Latest continuity-authority convergence evidence after the current route pass:
- this is still not the end of shaping work: heuristic debt shortlists and some residual catalog-style blocks still need the same cleanup;
- this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded.
Latest phase12 wider saved-session replay evidence after the current architecture pass:
- a new live replay `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun3` is accepted end-to-end with `20/20` steps green;
- this wider pack now proves one shared assistant session across a longer mixed path:
- first-turn smalltalk with proactive company offer;
- explicit company fixation;
- capability-meta interruption;
- inventory roots and historical roots;
- selected-object provenance and sale follow-up;
- selected-item -> VAT-period bridge;
- revenue aggregate pivot;
- payables/receivables polarity mirror;
- counterparty documents and short-name follow-up;
- account-60 tails;
- inventory aging and company activity-age;
- the replay exposed one real remaining seam before final acceptance:
- colloquial smalltalk entry reached the correct `living_chat + proactive_scope_offer` lane, but the first answer still depended on raw LLM preamble and leaked irrelevant generic chat (`Какой сегодня день?`);
- the fix was to convert `first-turn smalltalk + proactive organization offer` into a deterministic guarded entry path instead of trusting uncontrolled LLM preamble above the offer;
- colloquial living-chat detection is now unicode-safe and no longer depends on Cyrillic `\\b` boundaries that silently fail in JavaScript regex;
- this matters architecturally because another formerly ambient monolith behavior is now an explicit runtime contract:
- first-turn proactive company entry is controlled by runtime authority, not prompt luck;
- the broader saved-session pack now proves that organization authority, continuity carryover, and top-level chat entry survive one longer real-user trajectory rather than only the earlier flagship chains.
Still open after the accepted phase12 replay:
- replay breadth is now better than before, but still not yet broad enough to call multi-domain expansion low-risk by default;
- the biggest remaining architecture risk is no longer the original continuity collapse, but the unfinished convergence toward one true single-owner session authority across every hot path;
- the next execution slice should therefore continue prioritizing:
- more saved-session and multi-trajectory replay breadth;
- less duplicated state reconstruction in route / transition / living-chat glue;
- controlled expansion only after those broader proofs stay green.
## Next Execution Slice (2026-04-18)
The project is now moving from:

View File

@ -0,0 +1,488 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"domain": "address_phase12_wider_saved_session_pool",
"title": "Phase 12 wider saved-session replay for multi-trajectory organization authority",
"description": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"bindings": {},
"steps": [
{
"step_id": "step_01_smalltalk_with_scope_offer",
"title": "Fresh smalltalk offers organization scope without technical garbage",
"question": "приветик - че как там дела",
"required_answer_patterns_all": [
"(?i)привет|дела|норм|помочь",
"(?i)организац|компан|альтернатива плюс|лайсвуд|райм|по какой организации|название компании"
],
"forbidden_answer_patterns": [
"(?i)tool_gate_reason",
"(?i)address_mode",
"(?i)living_reason",
"(?i)mcp",
"(?i)read_only",
"(?i)snapshot_items",
"(?i)какой сегодня день"
],
"criticality": "critical",
"semantic_tags": [
"meta_smalltalk",
"proactive_scope_offer"
]
},
{
"step_id": "step_02_choose_organization",
"title": "Explicit company choice fixes active organization for the full session",
"question": "Альтернатива Плюс",
"required_answer_patterns_all": [
"(?i)зафиксир|рабочую организац|работаем по",
"(?i)альтернатива плюс"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)snapshot_items",
"(?i)программная платформа",
"(?i)нет моего прямого доступа"
],
"criticality": "critical",
"semantic_tags": [
"company_selected",
"organization_authority"
]
},
{
"step_id": "step_03_capability_meta_human",
"title": "Capability meta answer stays human and business-first",
"question": "расскажи что можешь интересного",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)могу|умею",
"(?i)ндс|документ|контрагент|долг|склад|остатк"
],
"forbidden_direct_answer_patterns": [
"(?i)read_only",
"(?i)mcp",
"(?i)snapshot",
"(?i)capability",
"(?i)assistant_state",
"(?i)open item"
],
"criticality": "critical",
"semantic_tags": [
"meta_capability",
"human_answer_quality"
]
},
{
"step_id": "step_04_inventory_root_today",
"title": "Inventory root respects the selected organization",
"question": "кайф - что там на складе по остаткам?",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2026-04-18",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"(?i)на складе|остат",
"18\\.04\\.2026"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"company_authority"
]
},
{
"step_id": "step_05_inventory_history_capability",
"title": "Historical inventory capability follow-up stays human",
"question": "а исторические остатки на другие даты умеешь?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)историческ|история",
"(?i)могу|умею"
],
"forbidden_answer_patterns": [
"(?i)tool_gate_reason",
"(?i)hard_meta_mode",
"(?i)mcp",
"(?i)read_only"
],
"criticality": "important",
"semantic_tags": [
"meta_historical_capability",
"inventory_root"
]
},
{
"step_id": "step_06_inventory_march_2016",
"title": "Historical inventory root on March 2016",
"question": "март 2016",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2016-03-31",
"period_from": "2016-03-01",
"period_to": "2016-03-31",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"31\\.03\\.2016",
"(?i)на складе|остат"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"historical_date_anchor"
]
},
{
"step_id": "step_07_selected_item_purchase_provenance",
"title": "Selected workstation purchase provenance",
"question": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"allowed_reply_types": [
"factual",
"partial_coverage"
],
"expected_intents": [
"inventory_purchase_provenance_for_item"
],
"expected_recipe": "address_inventory_purchase_provenance_for_item_v1",
"required_filters": {
"item": "Рабочая станция универсального специалиста (индивидуальное изготовление)"
},
"required_direct_answer_patterns_any": [
"(?i)закуп|поступлен|постав",
"(?i)рабочая станция"
],
"criticality": "critical",
"semantic_tags": [
"selected_object",
"inventory_provenance"
]
},
{
"step_id": "step_08_selected_item_sale_trace",
"title": "Selected item sale trace keeps the same object contour",
"question": "а кому продали?",
"allowed_reply_types": [
"factual",
"partial_coverage"
],
"expected_intents": [
"inventory_sale_trace_for_item"
],
"required_direct_answer_patterns_any": [
"(?i)рабочая станция|товару",
"(?i)покупател|выбыти|продал|не выделен"
],
"criticality": "critical",
"semantic_tags": [
"selected_object",
"inventory_sale_trace"
]
},
{
"step_id": "step_09_vat_on_purchase_date_bridge",
"title": "VAT bridge works from selected item purchase provenance",
"question": "ндс можешь прикинуть на дату покупки рабочей станции?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_intents": [
"vat_liability_confirmed_for_tax_period"
],
"required_direct_answer_patterns_any": [
"(?i)ндс",
"(?i)2015|феврал|налогов"
],
"forbidden_direct_answer_patterns": [
"(?i)сценарий пока не поддерживается",
"(?i)mcp",
"(?i)tool_gate_reason"
],
"criticality": "critical",
"semantic_tags": [
"bridge_inventory_to_vat",
"selected_object"
]
},
{
"step_id": "step_10_best_customer_all_time",
"title": "Top customer aggregate survives after the VAT bridge",
"question": "кто у нас самый доходный клиент за все время",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"customer_revenue_and_payments"
],
"required_direct_answer_patterns_any": [
"(?i)доходн|поступлен|клиент|заказчик",
"(?i)свк|группа"
],
"criticality": "important",
"semantic_tags": [
"aggregate_revenue",
"cross_domain_pivot"
]
},
{
"step_id": "step_11_receivables_may_2017",
"title": "Receivables root establishes May 2017 carryover",
"question": "кто нам должен денег на май 2017",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"receivables_confirmed_as_of_date"
],
"required_filters": {
"as_of_date": "2017-05-31",
"period_from": "2017-05-01",
"period_to": "2017-05-31"
},
"required_direct_answer_patterns_any": [
"(?i)дебитор",
"31\\.05\\.2017"
],
"criticality": "critical",
"semantic_tags": [
"settlements_receivables",
"date_carryover"
]
},
{
"step_id": "step_12_vat_same_period_followup",
"title": "VAT follow-up reuses the same period instead of drifting",
"question": "а какой ндс мы должны примерно заплатить за этот период?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_intents": [
"vat_liability_confirmed_for_tax_period"
],
"required_direct_answer_patterns_any": [
"(?i)ндс",
"(?i)2017|май|налогов"
],
"criticality": "critical",
"semantic_tags": [
"vat_followup",
"same_period_restore"
]
},
{
"step_id": "step_13_payables_today",
"title": "Payables root on today stays exact and organization-scoped",
"question": "мы должны комуто денег на сегодня?",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"payables_confirmed_as_of_date"
],
"required_filters": {
"as_of_date": "2026-04-18"
},
"required_direct_answer_patterns_any": [
"(?i)долг к оплате|обязательств",
"18\\.04\\.2026"
],
"criticality": "critical",
"semantic_tags": [
"settlements_payables",
"today_scope"
]
},
{
"step_id": "step_14_receivables_mirror_today",
"title": "Short mirror follow-up flips from payables to receivables on the same date",
"question": "а нам?",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"receivables_confirmed_as_of_date"
],
"required_direct_answer_patterns_any": [
"(?i)дебитор",
"18\\.04\\.2026"
],
"required_filters": {
"as_of_date": "2026-04-18"
},
"criticality": "critical",
"semantic_tags": [
"settlements_mirror_followup",
"same_date_restore"
]
},
{
"step_id": "step_15_contract_delta_capability_meta",
"title": "Contract-delta capability question stays in capability-meta lane",
"question": "ты умеешь считать дельту по договорам?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"required_direct_answer_patterns_any": [
"(?i)могу|умею|не умею|пока не",
"(?i)договор"
],
"forbidden_direct_answer_patterns": [
"(?i)ндс к уплате",
"(?i)налоговый период",
"(?i)топ-5 заказчиков",
"(?i)чепурнов",
"(?i)группа свк"
],
"criticality": "critical",
"semantic_tags": [
"meta_capability",
"capability_over_followup"
]
},
{
"step_id": "step_16_counterparty_docs_root",
"title": "Counterparty documents root for Чепурнов stays exact",
"question": "по чепурнову покажи все доки",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"list_documents_by_counterparty"
],
"expected_recipe": "address_documents_by_counterparty_v1",
"required_direct_answer_patterns_any": [
"(?i)чепурнов",
"(?i)документ|поступление"
],
"criticality": "important",
"semantic_tags": [
"counterparty_root",
"documents"
]
},
{
"step_id": "step_17_short_counterparty_followup",
"title": "Short counterparty follow-up keeps the real name instead of generic garbage",
"question": "а по свк",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"list_documents_by_counterparty"
],
"required_direct_answer_patterns_any": [
"(?i)свк|группа свк",
"(?i)документ|поступление"
],
"forbidden_direct_answer_patterns": [
"(?i)контрагент: группа\\s+найдено",
"(?im)^контрагент: группа\\.?$",
"(?i)mcp"
],
"criticality": "important",
"semantic_tags": [
"counterparty_followup",
"display_label_integrity"
]
},
{
"step_id": "step_18_open_items_account_60",
"title": "Account 60 tails stay exact after the long mixed session",
"question": "хвосты покажи по счету 60 на август 2022",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"open_items_by_counterparty_or_contract"
],
"required_filters": {
"account": "60",
"period_from": "2022-08-01",
"period_to": "2022-08-31",
"as_of_date": "2022-08-31"
},
"required_direct_answer_patterns_any": [
"(?i)счету 60|счёту 60",
"(?i)хвост|открыт"
],
"criticality": "critical",
"semantic_tags": [
"settlements_account_60"
]
},
{
"step_id": "step_19_inventory_aging_old_purchases",
"title": "Old-purchase inventory aging stays reachable after the long mixed session",
"question": "Есть ли остатки товара, которые закупались очень давно",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"inventory_aging_by_purchase_date"
],
"required_direct_answer_patterns_any": [
"(?i)стар|давно|ранней первой закупк",
"(?i)остат|закуп"
],
"criticality": "critical",
"semantic_tags": [
"inventory_aging",
"late_session_stability"
]
},
{
"step_id": "step_20_company_activity_age",
"title": "Organization activity age remains reachable at the tail of the long session",
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_intents": [
"counterparty_activity_lifecycle"
],
"expected_recipe": "address_counterparty_activity_lifecycle_v1",
"required_direct_answer_patterns_any": [
"(?i)активност",
"(?i)первая подтвержденная|последняя подтвержденная|лет"
],
"forbidden_direct_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните точное наименование организации"
],
"criticality": "critical",
"semantic_tags": [
"organization_activity_age",
"tail_authority_proof"
]
}
]
}

View File

@ -355,7 +355,7 @@ function readReportedCaseIds(job) {
return output;
}
function isTerminalCaseStatus(status) {
return status === "completed" || status === "failed";
return status === "completed" || status === "failed" || status === "canceled";
}
function syncJobWithSessions(job) {
if (!job.run_id || !job.eval_target.startsWith("assistant_")) {
@ -473,6 +473,7 @@ function buildEvalRouter(services) {
throw new http_1.ApiError("ASYNC_CASESET_EMPTY", "No runnable cases found in selected case-set.", 400);
}
const nowIso = new Date().toISOString();
const abortController = new AbortController();
const job = {
job_id: jobId,
status: "queued",
@ -491,7 +492,8 @@ function buildEvalRouter(services) {
messages: []
})),
error: null,
report: null
report: null,
abort_controller: abortController
};
ASYNC_JOBS.set(job.job_id, job);
trimAsyncJobsStore();
@ -500,25 +502,48 @@ function buildEvalRouter(services) {
const target = ASYNC_JOBS.get(job.job_id);
if (!target)
return;
if (target.status === "canceled") {
return;
}
target.status = "running";
target.updated_at = new Date().toISOString();
try {
const report = await services.evalService.run({
...payload,
caseSetFile: runtimeCaseSetFile,
runId
runId,
abortSignal: abortController.signal
});
target.report = report;
syncJobWithSessions(target);
target.completed_cases = target.total_cases;
target.status = "completed";
target.updated_at = new Date().toISOString();
const latestAfterRun = ASYNC_JOBS.get(job.job_id);
if (!latestAfterRun) {
return;
}
if (latestAfterRun.status === "canceled") {
latestAfterRun.updated_at = new Date().toISOString();
return;
}
latestAfterRun.report = report;
syncJobWithSessions(latestAfterRun);
latestAfterRun.completed_cases = latestAfterRun.total_cases;
latestAfterRun.status = "completed";
latestAfterRun.updated_at = new Date().toISOString();
latestAfterRun.abort_controller = null;
}
catch (error) {
syncJobWithSessions(target);
target.status = "failed";
target.error = error instanceof Error ? error.message : String(error);
target.updated_at = new Date().toISOString();
const latestAfterError = ASYNC_JOBS.get(job.job_id);
if (!latestAfterError) {
return;
}
if (latestAfterError.status === "canceled") {
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
return;
}
syncJobWithSessions(latestAfterError);
latestAfterError.status = "failed";
latestAfterError.error = error instanceof Error ? error.message : String(error);
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
}
})();
});
@ -552,5 +577,33 @@ function buildEvalRouter(services) {
next(error);
}
});
router.post("/api/eval/run-async/:job_id/cancel", (req, res, next) => {
try {
const jobId = String(req.params.job_id ?? "").trim();
if (!jobId) {
throw new http_1.ApiError("INVALID_ASYNC_JOB_ID", "job_id is required.", 400);
}
const job = ASYNC_JOBS.get(jobId);
if (!job) {
throw new http_1.ApiError("ASYNC_JOB_NOT_FOUND", `Async eval job not found: ${jobId}`, 404);
}
if (!isTerminalCaseStatus(job.status)) {
job.status = "canceled";
job.error = "Остановлено оператором.";
job.updated_at = new Date().toISOString();
job.abort_controller?.abort();
job.abort_controller = null;
job.cases = job.cases.map((item) => item.status === "completed" ? item : { ...item, status: "canceled" });
syncJobWithSessions(job);
}
(0, http_1.ok)(res, {
ok: true,
job: snapshotJob(job)
});
}
catch (error) {
next(error);
}
});
return router;
}

View File

@ -56,6 +56,7 @@ async function runAssistantLivingChatLlmRuntime(input) {
apiKey: String(input.payload.apiKey ?? input.defaultApiKey ?? ""),
model: String(input.payload.model ?? input.defaultModel),
baseUrl: input.payload.baseUrl ?? input.defaultBaseUrl,
abortSignal: input.payload.abortSignal,
temperature,
maxOutputTokens
}, {

View File

@ -72,6 +72,9 @@ function hasPriorAssistantTurn(items) {
}
return items.some((item) => item && typeof item === "object" && item.role === "assistant");
}
function buildDeterministicSmalltalkLeadReply() {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function findLastAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
@ -295,9 +298,11 @@ async function runAssistantLivingChatRuntime(input) {
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [chatText, proactiveOffer].filter((part) => String(part ?? "").trim().length > 0).join(" ");
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "llm_chat_with_proactive_scope_offer";
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}

View File

@ -8,7 +8,7 @@ function createAssistantLivingModePolicy(deps) {
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower);
}
function hasDataRetrievalRequestSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
const lower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!lower) {
return false;
}
@ -248,20 +248,33 @@ function createAssistantLivingModePolicy(deps) {
return hasCapabilitySignal && !hasRetrievalSignal;
}
function hasLivingChatSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
if (!lower) {
const rawLower = compactWhitespace(String(text ?? "").toLowerCase());
const repairedLower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
const variants = [rawLower, repairedLower].filter((value, index, items) => value.length > 0 && items.indexOf(value) === index);
if (variants.length === 0) {
return false;
}
if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) {
const hasDirectionalReply = variants.some((value) => /^(?:\u0430\s+)?(?:\u0442\u0443\u0442|\u0437\u0434\u0435\u0441\u044c|\u0442\u0430\u043c|\u0441\u044e\u0434\u0430|\u0442\u0443\u0434\u0430)[\s!?.,:;\-]*$/iu.test(value));
if (hasDirectionalReply) {
return true;
}
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
const hasShortGreeting = variants.some((value) => /^(?:\u0430\u0433\u0430|\u0443\u0433\u0443|\u043e\u043a|\u043e\u043a\u0435\u0439|\u044f\u0441\u043d\u043e|\u043f\u043e\u043d\u044f\u043b(?:\u0430)?|\u043f\u0440\u0438\u043d\u044f\u0442\u043e|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044e|\u0441\u0443\u043f\u0435\u0440|\u043a\u043b\u0430\u0441\u0441|\u043d\u043e\u0440\u043c|\u0433\u043e|\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u0433\u043d\u0430\u043b\u0438|\u043f\u0440\u0438\u0432\u0435\u0442(?:\u0438\u043a)?|\u0445\u0430\u0439|\u0439\u043e|yo|\u0447[её]\s+\u0442\u0430\u043c|\u0447[её]\s+\u043a\u0430\u043a|hello|hi|thanks?)$/iu.test(value));
if (hasShortGreeting) {
return true;
}
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
const hasGreetingWithFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u043f\u0440\u0438\u0432\u0435\u0442\p{L}*|\u0445\u0430\u0439|\u0439\u043e|hello|hi|yo)(?=$|[\s,.!?;:()\-]).*(?:^|[\s,.!?;:()\-])(?:\u043a\u0430\u043a|\u0434\u0435\u043b\u0430|\u0442\u0430\u043c|\u0447[её])(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasGreetingWithFollowup) {
return true;
}
return hasSmallTalkSignal(lower);
const hasColloquialFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u0447[её]\s+\u043a\u0430\u043a|\u0447[её]\s+\u0442\u0430\u043c|\u043a\u0430\u043a\s+\u0442\u0430\u043c(?:\s+\u0434\u0435\u043b\u0430)?)(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasColloquialFollowup) {
return true;
}
const hasOpenSmalltalkPrompt = variants.some((value) => /(?:\u043a\u0430\u043a\s+\u0434\u0435\u043b\u0430|\u043a\u0430\u043a\s+\u0442\u044b|\u0447\u0442\u043e\s+\u043d\u043e\u0432\u043e\u0433\u043e|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438\s+\u043e\s+\u0441\u0435\u0431\u0435|\u0447\u0435\u043c\s+\u043c\u043e\u0436\u0435\u0448\u044c\s+\u043f\u043e\u043c\u043e\u0447\u044c|\u0434\u0430\u0432\u0430\u0439\s+\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043e\u0431\u0441\u0443\u0434\u0438\u043c|\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439|\u0447\u0442\u043e\s+\u0434\u0443\u043c\u0430\u0435\u0448\u044c)/iu.test(value));
if (hasOpenSmalltalkPrompt) {
return true;
}
return variants.some((value) => hasSmallTalkSignal(value));
}
function resolveLivingAssistantModeDecision(input) {
const userMessage = String(input?.userMessage ?? "");

View File

@ -3335,6 +3335,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
apiKey: payload?.apiKey,
model: payload?.model,
baseUrl: payload?.baseUrl,
abortSignal: payload?.abortSignal,
temperature: 0,
maxOutputTokens: payload?.maxOutputTokens,
promptVersion: "normalizer_v2_0_2",

View File

@ -15,6 +15,11 @@ const http_1 = require("../utils/http");
const assistantService_1 = require("./assistantService");
const assistantSessionStore_1 = require("./assistantSessionStore");
const files_1 = require("../utils/files");
function throwIfAborted(signal) {
if (signal?.aborted) {
throw new Error("EVAL_RUN_ABORTED");
}
}
const BASELINE_METRICS = {
schema_validation_pass_rate: 100,
intent_class_accuracy: 72.73,
@ -1601,17 +1606,20 @@ class EvalService {
const diagnostics = [];
let requestsTotal = 0;
for (const suiteCase of suiteCases) {
throwIfAborted(payload.abortSignal);
const sessionId = `${runId}-${suiteCase.case_id}`;
const turnResponses = [];
const notes = [];
const limitations = [];
try {
for (const turn of suiteCase.turns) {
throwIfAborted(payload.abortSignal);
const response = (await assistantService.handleMessage({
session_id: sessionId,
user_message: turn.user_message,
message: turn.user_message,
mode: "assistant",
abortSignal: payload.abortSignal,
llmProvider: payload.normalizeConfig.llmProvider,
apiKey: payload.normalizeConfig.apiKey,
model: payload.normalizeConfig.model,
@ -1636,10 +1644,14 @@ class EvalService {
}));
turnResponses.push(response);
requestsTotal += 1;
throwIfAborted(payload.abortSignal);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage === "EVAL_RUN_ABORTED") {
throw error;
}
diagnostics.push({
suite_case: suiteCase,
session_id: sessionId,
@ -2254,7 +2266,8 @@ class EvalService {
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
runId: payload.runId,
abortSignal: payload.abortSignal
});
}
if (evalTarget === "assistant_stage2") {

View File

@ -1363,6 +1363,7 @@ class NormalizerService {
apiKey: String(apiKey ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens
}, {
@ -1408,6 +1409,7 @@ class NormalizerService {
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens: retryMaxOutputTokens
}, {

View File

@ -408,7 +408,8 @@ class OpenAIResponsesClient {
response = await fetch(url, {
method,
headers,
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined,
signal: config.abortSignal
});
}
catch (error) {

View File

@ -8,7 +8,7 @@ import { ApiError, ok } from "../utils/http";
import type { EvalRunMode, NormalizeRequestPayload } from "../types/normalizer";
import type { EvalTarget } from "../types/assistantEval";
type EvalAsyncStatus = "queued" | "running" | "completed" | "failed";
type EvalAsyncStatus = "queued" | "running" | "completed" | "failed" | "canceled";
interface EvalAsyncCaseInfo {
case_id: string;
@ -41,6 +41,7 @@ interface EvalAsyncJob {
cases: EvalAsyncCaseInfo[];
error: string | null;
report: Record<string, unknown> | null;
abort_controller?: AbortController | null;
}
const ASYNC_JOBS = new Map<string, EvalAsyncJob>();
@ -428,7 +429,7 @@ function readReportedCaseIds(job: EvalAsyncJob): Set<string> {
}
function isTerminalCaseStatus(status: EvalAsyncStatus): boolean {
return status === "completed" || status === "failed";
return status === "completed" || status === "failed" || status === "canceled";
}
function syncJobWithSessions(job: EvalAsyncJob): void {
@ -559,6 +560,7 @@ export function buildEvalRouter(services: AppServices): Router {
}
const nowIso = new Date().toISOString();
const abortController = new AbortController();
const job: EvalAsyncJob = {
job_id: jobId,
status: "queued",
@ -577,7 +579,8 @@ export function buildEvalRouter(services: AppServices): Router {
messages: []
})),
error: null,
report: null
report: null,
abort_controller: abortController
};
ASYNC_JOBS.set(job.job_id, job);
trimAsyncJobsStore();
@ -586,24 +589,47 @@ export function buildEvalRouter(services: AppServices): Router {
void (async () => {
const target = ASYNC_JOBS.get(job.job_id);
if (!target) return;
if (target.status === "canceled") {
return;
}
target.status = "running";
target.updated_at = new Date().toISOString();
try {
const report = await services.evalService.run({
...payload,
caseSetFile: runtimeCaseSetFile,
runId
runId,
abortSignal: abortController.signal
});
target.report = report;
syncJobWithSessions(target);
target.completed_cases = target.total_cases;
target.status = "completed";
target.updated_at = new Date().toISOString();
const latestAfterRun = ASYNC_JOBS.get(job.job_id);
if (!latestAfterRun) {
return;
}
if (latestAfterRun.status === "canceled") {
latestAfterRun.updated_at = new Date().toISOString();
return;
}
latestAfterRun.report = report;
syncJobWithSessions(latestAfterRun);
latestAfterRun.completed_cases = latestAfterRun.total_cases;
latestAfterRun.status = "completed";
latestAfterRun.updated_at = new Date().toISOString();
latestAfterRun.abort_controller = null;
} catch (error) {
syncJobWithSessions(target);
target.status = "failed";
target.error = error instanceof Error ? error.message : String(error);
target.updated_at = new Date().toISOString();
const latestAfterError = ASYNC_JOBS.get(job.job_id);
if (!latestAfterError) {
return;
}
if (latestAfterError.status === "canceled") {
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
return;
}
syncJobWithSessions(latestAfterError);
latestAfterError.status = "failed";
latestAfterError.error = error instanceof Error ? error.message : String(error);
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
}
})();
});
@ -638,5 +664,35 @@ export function buildEvalRouter(services: AppServices): Router {
}
});
router.post("/api/eval/run-async/:job_id/cancel", (req, res, next) => {
try {
const jobId = String(req.params.job_id ?? "").trim();
if (!jobId) {
throw new ApiError("INVALID_ASYNC_JOB_ID", "job_id is required.", 400);
}
const job = ASYNC_JOBS.get(jobId);
if (!job) {
throw new ApiError("ASYNC_JOB_NOT_FOUND", `Async eval job not found: ${jobId}`, 404);
}
if (!isTerminalCaseStatus(job.status)) {
job.status = "canceled";
job.error = "Остановлено оператором.";
job.updated_at = new Date().toISOString();
job.abort_controller?.abort();
job.abort_controller = null;
job.cases = job.cases.map((item) =>
item.status === "completed" ? item : { ...item, status: "canceled" }
);
syncJobWithSessions(job);
}
ok(res, {
ok: true,
job: snapshotJob(job)
});
} catch (error) {
next(error);
}
});
return router;
}

View File

@ -25,6 +25,7 @@ export interface RunAssistantLivingChatLlmRuntimeInput {
apiKey?: unknown;
model?: unknown;
baseUrl?: unknown;
abortSignal?: AbortSignal;
temperature?: number;
maxOutputTokens?: number;
};
@ -35,6 +36,7 @@ export interface RunAssistantLivingChatLlmRuntimeInput {
apiKey: string;
model: string;
baseUrl: unknown;
abortSignal?: AbortSignal;
temperature: number;
maxOutputTokens: number;
},
@ -104,6 +106,7 @@ export async function runAssistantLivingChatLlmRuntime(
apiKey: String(input.payload.apiKey ?? input.defaultApiKey ?? ""),
model: String(input.payload.model ?? input.defaultModel),
baseUrl: input.payload.baseUrl ?? input.defaultBaseUrl,
abortSignal: input.payload.abortSignal,
temperature,
maxOutputTokens
},

View File

@ -144,6 +144,10 @@ function hasPriorAssistantTurn(items: unknown[]): boolean {
return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant");
}
function buildDeterministicSmalltalkLeadReply(): string {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function findLastAddressDebug(items: unknown[]): Record<string, unknown> | null {
if (!Array.isArray(items)) {
return null;
@ -392,9 +396,11 @@ export async function runAssistantLivingChatRuntime(
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [chatText, proactiveOffer].filter((part) => String(part ?? "").trim().length > 0).join(" ");
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "llm_chat_with_proactive_scope_offer";
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}

View File

@ -68,7 +68,7 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
}
function hasDataRetrievalRequestSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
const lower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!lower) {
return false;
}
@ -319,20 +319,33 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
}
function hasLivingChatSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
if (!lower) {
const rawLower = compactWhitespace(String(text ?? "").toLowerCase());
const repairedLower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
const variants = [rawLower, repairedLower].filter((value, index, items) => value.length > 0 && items.indexOf(value) === index);
if (variants.length === 0) {
return false;
}
if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) {
const hasDirectionalReply = variants.some((value) => /^(?:\u0430\s+)?(?:\u0442\u0443\u0442|\u0437\u0434\u0435\u0441\u044c|\u0442\u0430\u043c|\u0441\u044e\u0434\u0430|\u0442\u0443\u0434\u0430)[\s!?.,:;\-]*$/iu.test(value));
if (hasDirectionalReply) {
return true;
}
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
const hasShortGreeting = variants.some((value) => /^(?:\u0430\u0433\u0430|\u0443\u0433\u0443|\u043e\u043a|\u043e\u043a\u0435\u0439|\u044f\u0441\u043d\u043e|\u043f\u043e\u043d\u044f\u043b(?:\u0430)?|\u043f\u0440\u0438\u043d\u044f\u0442\u043e|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044e|\u0441\u0443\u043f\u0435\u0440|\u043a\u043b\u0430\u0441\u0441|\u043d\u043e\u0440\u043c|\u0433\u043e|\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u0433\u043d\u0430\u043b\u0438|\u043f\u0440\u0438\u0432\u0435\u0442(?:\u0438\u043a)?|\u0445\u0430\u0439|\u0439\u043e|yo|\u0447[её]\s+\u0442\u0430\u043c|\u0447[её]\s+\u043a\u0430\u043a|hello|hi|thanks?)$/iu.test(value));
if (hasShortGreeting) {
return true;
}
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
const hasGreetingWithFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u043f\u0440\u0438\u0432\u0435\u0442\p{L}*|\u0445\u0430\u0439|\u0439\u043e|hello|hi|yo)(?=$|[\s,.!?;:()\-]).*(?:^|[\s,.!?;:()\-])(?:\u043a\u0430\u043a|\u0434\u0435\u043b\u0430|\u0442\u0430\u043c|\u0447[её])(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasGreetingWithFollowup) {
return true;
}
return hasSmallTalkSignal(lower);
const hasColloquialFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u0447[её]\s+\u043a\u0430\u043a|\u0447[её]\s+\u0442\u0430\u043c|\u043a\u0430\u043a\s+\u0442\u0430\u043c(?:\s+\u0434\u0435\u043b\u0430)?)(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasColloquialFollowup) {
return true;
}
const hasOpenSmalltalkPrompt = variants.some((value) => /(?:\u043a\u0430\u043a\s+\u0434\u0435\u043b\u0430|\u043a\u0430\u043a\s+\u0442\u044b|\u0447\u0442\u043e\s+\u043d\u043e\u0432\u043e\u0433\u043e|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438\s+\u043e\s+\u0441\u0435\u0431\u0435|\u0447\u0435\u043c\s+\u043c\u043e\u0436\u0435\u0448\u044c\s+\u043f\u043e\u043c\u043e\u0447\u044c|\u0434\u0430\u0432\u0430\u0439\s+\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043e\u0431\u0441\u0443\u0434\u0438\u043c|\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439|\u0447\u0442\u043e\s+\u0434\u0443\u043c\u0430\u0435\u0448\u044c)/iu.test(value));
if (hasOpenSmalltalkPrompt) {
return true;
}
return variants.some((value) => hasSmallTalkSignal(value));
}
function resolveLivingAssistantModeDecision(input) {

View File

@ -3290,6 +3290,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
apiKey: payload?.apiKey,
model: payload?.model,
baseUrl: payload?.baseUrl,
abortSignal: payload?.abortSignal,
temperature: 0,
maxOutputTokens: payload?.maxOutputTokens,
promptVersion: "normalizer_v2_0_2",

View File

@ -54,6 +54,12 @@ import { AssistantSessionStore } from "./assistantSessionStore";
import { NormalizerService } from "./normalizerService";
import { ensureDir, writeJsonFile } from "../utils/files";
function throwIfAborted(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new Error("EVAL_RUN_ABORTED");
}
}
interface EvalCaseFile {
case_id: string;
raw_question: string;
@ -1925,6 +1931,7 @@ export class EvalService {
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
abortSignal?: AbortSignal;
}): Promise<Record<string, unknown>> {
if (!FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1) {
throw new ApiError(
@ -1943,6 +1950,7 @@ export class EvalService {
let requestsTotal = 0;
for (const suiteCase of suiteCases) {
throwIfAborted(payload.abortSignal);
const sessionId = `${runId}-${suiteCase.case_id}`;
const turnResponses: AssistantMessageResponsePayload[] = [];
const notes: string[] = [];
@ -1950,11 +1958,13 @@ export class EvalService {
try {
for (const turn of suiteCase.turns) {
throwIfAborted(payload.abortSignal);
const response = (await assistantService.handleMessage({
session_id: sessionId,
user_message: turn.user_message,
message: turn.user_message,
mode: "assistant",
abortSignal: payload.abortSignal,
llmProvider: payload.normalizeConfig.llmProvider,
apiKey: payload.normalizeConfig.apiKey,
model: payload.normalizeConfig.model,
@ -1979,9 +1989,13 @@ export class EvalService {
})) as AssistantMessageResponsePayload;
turnResponses.push(response);
requestsTotal += 1;
throwIfAborted(payload.abortSignal);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage === "EVAL_RUN_ABORTED") {
throw error;
}
diagnostics.push({
suite_case: suiteCase,
session_id: sessionId,
@ -2634,6 +2648,7 @@ export class EvalService {
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
abortSignal?: AbortSignal;
}): Promise<Record<string, unknown>> {
const mode = payload.mode ?? "standard";
const evalTarget = payload.evalTarget ?? "normalizer";
@ -2648,7 +2663,8 @@ export class EvalService {
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
runId: payload.runId,
abortSignal: payload.abortSignal
});
}

View File

@ -1608,6 +1608,7 @@ export class NormalizerService {
apiKey: String(apiKey ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens
},
@ -1656,6 +1657,7 @@ export class NormalizerService {
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens: retryMaxOutputTokens
},

View File

@ -9,6 +9,7 @@ export interface OpenAIRequestConfig {
apiKey: string;
model: string;
baseUrl?: string;
abortSignal?: AbortSignal;
temperature?: number;
maxOutputTokens?: number;
}
@ -491,7 +492,8 @@ export class OpenAIResponsesClient {
response = await fetch(url, {
method,
headers,
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined,
signal: config.abortSignal
});
} catch (error) {
lastNetworkError = error;

View File

@ -325,6 +325,7 @@ export interface AssistantMessageRequestPayload {
user_message?: string;
message?: string;
mode?: "assistant" | string;
abortSignal?: AbortSignal;
llmProvider?: NormalizeRequestPayload["llmProvider"];
apiKey?: string;
model?: string;

View File

@ -267,6 +267,7 @@ export interface NormalizeRequestPayload {
apiKey?: string;
model?: string;
baseUrl?: string;
abortSignal?: AbortSignal;
temperature?: number;
maxOutputTokens?: number;
promptVersion?: PromptVersion | string;

View File

@ -155,9 +155,10 @@ describe("assistant living chat runtime adapter", () => {
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("llm-text");
expect(output.chatText).toContain("Привет! Всё нормально.");
expect(output.chatText).toContain("offer:ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ");
expect(output.debug?.living_chat_response_source).toBe("llm_chat_with_proactive_scope_offer");
expect(output.chatText).not.toContain("llm-text");
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk_with_proactive_scope_offer");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(true);
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(3);
expect(output.debug?.assistant_known_organizations).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]);

View File

@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import { createAssistantLivingModePolicy } from "../src/services/assistantLivingModePolicy";
function buildPolicy() {
function buildPolicy(options?: { repairAddressMojibake?: (text: string) => string }) {
return createAssistantLivingModePolicy({
featureAssistantLivingChatRouterV1: true,
compactWhitespace: (text: string) => text.replace(/\s+/g, " ").trim(),
repairAddressMojibake: (text: string) => text,
repairAddressMojibake: options?.repairAddressMojibake ?? ((text: string) => text),
toNonEmptyString: (value: unknown) => {
if (value === null || value === undefined) {
return null;
@ -34,6 +34,8 @@ function buildPolicy() {
}
describe("assistantLivingModePolicy", () => {
const colloquialGreeting = "\u043f\u0440\u0438\u0432\u0435\u0442\u0438\u043a - \u0447\u0435 \u043a\u0430\u043a \u0442\u0430\u043c \u0434\u0435\u043b\u0430";
it("detects explicit recap wording as memory signal even when selected-object words are present", () => {
const policy = buildPolicy();
@ -63,6 +65,21 @@ describe("assistantLivingModePolicy", () => {
expect(decision.reason).toBe("living_chat_signal_detected");
});
it("detects colloquial greeting with extra words as living chat signal", () => {
const policy = buildPolicy();
expect(policy.hasLivingChatSignal(colloquialGreeting)).toBe(true);
});
it("detects mojibake colloquial greeting after repair", () => {
const policy = buildPolicy({
repairAddressMojibake: (text: string) =>
text === "приветик - че как там дела" ? colloquialGreeting : text
});
expect(policy.hasLivingChatSignal("приветик - че как там дела")).toBe(true);
});
it("keeps explicit inventory question in deep mode", () => {
const policy = buildPolicy();

View File

@ -1,4 +1,90 @@
[
{
"generation_id": "gen-ag04181425-56e29e",
"created_at": "2026-04-18T14:25:37+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"count": 20,
"domain": "address_phase12_wider_saved_session_pool",
"questions": [
"приветик - че как там дела",
"Альтернатива Плюс",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"а исторические остатки на другие даты умеешь?",
"март 2016",
"По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"а кому продали?",
"ндс можешь прикинуть на дату покупки рабочей станции?",
"кто у нас самый доходный клиент за все время",
"кто нам должен денег на май 2017",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?",
"ты умеешь считать дельту по договорам?",
"по чепурнову покажи все доки",
"а по свк",
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"а по Альтернативе Плюс сколько лет активности в базе 1С?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260418142537_gen-ag04181425-56e29e.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260418142537_gen-ag04181425-56e29e.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase12_wider_saved_session_pool.json",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"semantic_tags": [
"aggregate_revenue",
"bridge_inventory_to_vat",
"capability_over_followup",
"company_authority",
"company_selected",
"counterparty_followup",
"counterparty_root",
"cross_domain_pivot",
"date_carryover",
"display_label_integrity",
"documents",
"historical_date_anchor",
"human_answer_quality",
"inventory_aging",
"inventory_provenance",
"inventory_root",
"inventory_sale_trace",
"late_session_stability",
"meta_capability",
"meta_historical_capability",
"meta_smalltalk",
"organization_activity_age",
"organization_authority",
"proactive_scope_offer",
"same_date_restore",
"same_period_restore",
"selected_object",
"settlements_account_60",
"settlements_mirror_followup",
"settlements_payables",
"settlements_receivables",
"tail_authority_proof",
"today_scope",
"vat_followup"
],
"latest_acceptance": null
}
},
{
"generation_id": "gen-ag04171508-760111",
"created_at": "2026-04-17T15:08:06+00:00",
@ -389,12 +475,13 @@
"created_at": "2026-04-16T18:26:26.191Z",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"count": 31,
"count": 33,
"domain": null,
"questions": [
"приветик - че как там дела",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"АЛЬТЕРНАТИВА",
"а исторические остатки на другие даты умеешь?",
"давай на июль 2017",
"март 2016",
@ -422,7 +509,8 @@
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"Какие конкретно номенклатуры формируют остаток по складу на май 2020",
"а по Альтернативе Плюс сколько лет активности в базе 1С?"
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
"Как ты оценишь деятельность компании?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260416182626_gen-mo1t93wq-jy0453e.json",

View File

@ -0,0 +1,307 @@
{
"saved_at": "2026-04-18T14:25:37+00:00",
"generation_id": "gen-ag04181425-56e29e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"agent_run": true,
"questions": [
"приветик - че как там дела",
"Альтернатива Плюс",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"а исторические остатки на другие даты умеешь?",
"март 2016",
"По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"а кому продали?",
"ндс можешь прикинуть на дату покупки рабочей станции?",
"кто у нас самый доходный клиент за все время",
"кто нам должен денег на май 2017",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?",
"ты умеешь считать дельту по договорам?",
"по чепурнову покажи все доки",
"а по свк",
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"а по Альтернативе Плюс сколько лет активности в базе 1С?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase12_wider_saved_session_pool.json",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"semantic_tags": [
"aggregate_revenue",
"bridge_inventory_to_vat",
"capability_over_followup",
"company_authority",
"company_selected",
"counterparty_followup",
"counterparty_root",
"cross_domain_pivot",
"date_carryover",
"display_label_integrity",
"documents",
"historical_date_anchor",
"human_answer_quality",
"inventory_aging",
"inventory_provenance",
"inventory_root",
"inventory_sale_trace",
"late_session_stability",
"meta_capability",
"meta_historical_capability",
"meta_smalltalk",
"organization_activity_age",
"organization_authority",
"proactive_scope_offer",
"same_date_restore",
"same_period_restore",
"selected_object",
"settlements_account_60",
"settlements_mirror_followup",
"settlements_payables",
"settlements_receivables",
"tail_authority_proof",
"today_scope",
"vat_followup"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "приветик - че как там дела",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Альтернатива Плюс",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "расскажи что можешь интересного",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "кайф - что там на складе по остаткам?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а исторические остатки на другие даты умеешь?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "март 2016",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "а кому продали?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "ндс можешь прикинуть на дату покупки рабочей станции?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "кто у нас самый доходный клиент за все время",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "кто нам должен денег на май 2017",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "а какой ндс мы должны примерно заплатить за этот период?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "мы должны комуто денег на сегодня?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "а нам?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "ты умеешь считать дельту по договорам?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-016",
"role": "user",
"text": "по чепурнову покажи все доки",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-017",
"role": "user",
"text": "а по свк",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-018",
"role": "user",
"text": "хвосты покажи по счету 60 на август 2022",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-019",
"role": "user",
"text": "Есть ли остатки товара, которые закупались очень давно",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-020",
"role": "user",
"text": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase12_wider_saved_session_pool.json",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"semantic_tags": [
"aggregate_revenue",
"bridge_inventory_to_vat",
"capability_over_followup",
"company_authority",
"company_selected",
"counterparty_followup",
"counterparty_root",
"cross_domain_pivot",
"date_carryover",
"display_label_integrity",
"documents",
"historical_date_anchor",
"human_answer_quality",
"inventory_aging",
"inventory_provenance",
"inventory_root",
"inventory_sale_trace",
"late_session_stability",
"meta_capability",
"meta_historical_capability",
"meta_smalltalk",
"organization_activity_age",
"organization_authority",
"proactive_scope_offer",
"same_date_restore",
"same_period_restore",
"selected_object",
"settlements_account_60",
"settlements_mirror_followup",
"settlements_payables",
"settlements_receivables",
"tail_authority_proof",
"today_scope",
"vat_followup"
]
}
}
}

View File

@ -2,7 +2,7 @@
"suite_id": "assistant_saved_session_gen-mo1t93wq-jy0453e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-18T11:29:49.384Z",
"generated_at": "2026-04-18T14:55:46.799Z",
"generation_id": "gen-mo1t93wq-jy0453e",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
@ -27,6 +27,9 @@
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
@ -110,6 +113,9 @@
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}

View File

@ -0,0 +1,85 @@
{
"suite_id": "assistant_saved_session_gen-ag04181425-56e29e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-18T14:25:37+00:00",
"generation_id": "gen-ag04181425-56e29e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"domain": "address_phase12_wider_saved_session_pool",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "Альтернатива Плюс"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
}
]
}
]
}

View File

@ -0,0 +1,120 @@
{
"suite_id": "assistant_saved_session_runtime_job-IXCkeIOaTz",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}

View File

@ -0,0 +1,117 @@
{
"suite_id": "assistant_saved_session_runtime_job-W3Bqj1Q5Ar",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}

View File

@ -0,0 +1,36 @@
{
"suite_id": "assistant_saved_session_runtime_job-lHMIky2oa-",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "AGENT | Phase 6 provider/runtime replay across chat, meta, and address boundaries",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "AGENT | Phase 6 provider/runtime replay across chat, meta, and address boundaries",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет, как дела?"
},
{
"user_message": "по какой компании мы сейчас работаем?"
},
{
"user_message": "что ты можешь по 1С?"
},
{
"user_message": "какие остатки на складе на март 2021"
},
{
"user_message": "а исторические остатки тоже можешь?"
}
]
}
]
}

View File

@ -0,0 +1,117 @@
{
"suite_id": "assistant_saved_session_runtime_job-uh5xR80M-d",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}

View File

@ -1,6 +1,6 @@
{
"schema_version": "shared_llm_connection_v1",
"updated_at": "2026-04-17T10:55:35.860Z",
"updated_at": "2026-04-18T14:51:55.518Z",
"connection": {
"llmProvider": "local",
"model": "unsloth/qwen3-30b-a3b-instruct-2507",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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-CLCo6iY2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DkvyfMZP.css">
<script type="module" crossorigin src="/assets/index-DWgwfAPc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-sbJz56_k.css">
</head>
<body>
<div id="root"></div>

View File

@ -270,6 +270,12 @@ export const apiClient = {
return request(`/eval/run-async/${encodeURIComponent(jobId)}`);
},
async cancelEvalRunAsync(jobId: string): Promise<AsyncEvalRunStatusResponse> {
return request(`/eval/run-async/${encodeURIComponent(jobId)}/cancel`, {
method: "POST"
});
},
async startRun(): Promise<{ ok: boolean; run: RuntimeRun; runId: string; sessionId: string; status: string }> {
return request("/accounting-agent/v1/runs/start", {
method: "POST",

View File

@ -612,6 +612,14 @@ function CardLaunchIcon() {
);
}
function CardStopIcon() {
return (
<svg className="autoruns-card-stop-svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<rect x="4.2" y="4.2" width="7.6" height="7.6" rx="0.8" />
</svg>
);
}
function GroupChevronIcon({ expanded }: { expanded: boolean }) {
return (
<svg className={expanded ? "autoruns-group-chevron-svg expanded" : "autoruns-group-chevron-svg"} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
@ -702,6 +710,8 @@ export function AutoRunsHistoryPanel({
const [limitInput, setLimitInput] = useState(String(DEFAULT_FILTERS.limit));
const [autogenCountInput, setAutogenCountInput] = useState(String(DEFAULT_AUTOGEN_SETTINGS.count));
const [autogenPersonalityPromptHeight, setAutogenPersonalityPromptHeight] = useState(160);
const [runningAutogenGenerationId, setRunningAutogenGenerationId] = useState("");
const [autogenStopBusy, setAutogenStopBusy] = useState(false);
const [filtersGroupExpanded, setFiltersGroupExpanded] = useState(true);
const [generationContextGroupExpanded, setGenerationContextGroupExpanded] = useState(true);
const [autogenGroupExpanded, setAutogenGroupExpanded] = useState(true);
@ -1507,6 +1517,8 @@ export function AutoRunsHistoryPanel({
if (payload.job.status === "completed") {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
const finalRunId = payload.job.report_summary?.run_id ?? payload.job.run_id;
await loadHistory({
keepSelection: true,
@ -1521,11 +1533,25 @@ export function AutoRunsHistoryPanel({
if (payload.job.status === "failed") {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
setErrorText(`Запуск прогонов: ${payload.job.error ?? "неизвестная ошибка"}`);
log(`Autogen async run failed: ${payload.job.error ?? "unknown error"}`);
return;
}
if (payload.job.status === "canceled") {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
setActiveAsyncJob(null);
await loadHistory({ keepSelection: false });
await loadAutoGenHistory();
log(`Autogen async run canceled: job=${payload.job.job_id}`);
return;
}
stopAsyncJobPolling();
asyncJobPollTimerRef.current = window.setTimeout(() => {
void pollAsyncJobStatus(jobId);
@ -1533,6 +1559,8 @@ export function AutoRunsHistoryPanel({
} catch (error) {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
const message = error instanceof Error ? error.message : String(error);
setErrorText(`Запуск прогонов: ${message}`);
log(`Autogen async status error: ${message}`);
@ -1579,6 +1607,8 @@ export function AutoRunsHistoryPanel({
});
const liveJob = payload.job;
setRunningAutogenGenerationId(generation.generation_id);
setAutogenStopBusy(false);
setActiveAsyncJob(liveJob);
const liveRunId = toLiveRunId(liveJob.job_id);
const live = buildLiveRunDetail(liveJob, ALL_CASES_ID);
@ -1602,6 +1632,8 @@ export function AutoRunsHistoryPanel({
setErrorText(`Запуск прогонов: ${message}`);
log(`Autogen run error: ${message}`);
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
}
}, [
analysisDate,
@ -1616,6 +1648,36 @@ export function AutoRunsHistoryPanel({
stopAsyncJobPolling
]);
const stopAutogenCampaign = useCallback(async () => {
const jobId = activeAsyncJob?.job_id ?? "";
if (!jobId) {
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
setActiveAsyncJob(null);
stopAsyncJobPolling();
return;
}
setAutogenStopBusy(true);
setErrorText("");
try {
const payload = await apiClient.cancelEvalRunAsync(jobId);
stopAsyncJobPolling();
setActiveAsyncJob(null);
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
await loadHistory({ keepSelection: false });
await loadAutoGenHistory();
log(`Autogen async run stopped: job=${payload.job.job_id}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAutogenStopBusy(false);
setErrorText(`Остановка прогона: ${message}`);
log(`Autogen stop error: ${message}`);
}
}, [activeAsyncJob, loadAutoGenHistory, loadHistory, log, stopAsyncJobPolling]);
const openCommentModal = useCallback((message: AutoRunDialogMessage) => {
if (message.role !== "assistant") return;
const resolvedCaseId = message.case_id ?? selectedCaseId;
@ -2750,10 +2812,13 @@ export function AutoRunsHistoryPanel({
type="button"
className="autoruns-run-launch-btn"
style={isSavedUserSessionsMode ? { display: "none" } : undefined}
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration}
onClick={() => void runAutogenCampaign()}
disabled={
autogenStopBusy ||
(!autogenRunBusy && (editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration))
}
onClick={() => void (autogenRunBusy ? stopAutogenCampaign() : runAutogenCampaign())}
>
{autogenRunBusy ? "Запускаю..." : "Запустить прогон"}
{autogenRunBusy ? (autogenStopBusy ? "Останавливаю..." : "Остановить прогон") : "Запустить прогон"}
</button>
</div>
@ -2955,7 +3020,10 @@ export function AutoRunsHistoryPanel({
{isSavedUserSessionsMode ? "Сохраненные пользовательские сессии пока пусты." : "История автогенераций пока пустая."}
</p>
) : null}
{visibleAutoGenHistory.slice(0, 30).map((item) => (
{visibleAutoGenHistory.slice(0, 30).map((item) => {
const isRunningThisGeneration = autogenRunBusy && runningAutogenGenerationId === item.generation_id;
const isAnotherGenerationRunning = autogenRunBusy && runningAutogenGenerationId !== item.generation_id;
return (
<article
key={item.generation_id}
className={[
@ -2971,20 +3039,24 @@ export function AutoRunsHistoryPanel({
<button
type="button"
className="autoruns-saved-session-icon-btn"
disabled={autogenRunBusy}
disabled={autogenStopBusy || isAnotherGenerationRunning}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
if (isRunningThisGeneration) {
void stopAutogenCampaign();
return;
}
setSelectedAutogenGenerationId(item.generation_id);
void runAutogenCampaign(
item,
selectedAutogenGenerationId === item.generation_id ? editableGeneratedQuestions : item.questions
);
}}
title="Запустить прогон"
aria-label={`Запустить прогон для ${formatAutoGenGenerationTitle(item)}`}
title={isRunningThisGeneration ? "Остановить прогон" : "Запустить прогон"}
aria-label={`${isRunningThisGeneration ? "Остановить прогон" : "Запустить прогон"} для ${formatAutoGenGenerationTitle(item)}`}
>
<CardLaunchIcon />
{isRunningThisGeneration ? <CardStopIcon /> : <CardLaunchIcon />}
</button>
<button
type="button"
@ -3182,7 +3254,8 @@ export function AutoRunsHistoryPanel({
</>
) : null}
</article>
))}
);
})}
</div>
) : null}

View File

@ -392,13 +392,13 @@ export interface AutoGenPersonalityCatalogResponse {
export interface AsyncEvalRunCase {
case_id: string;
turns_total: number;
status: "queued" | "running" | "completed" | "failed";
status: "queued" | "running" | "completed" | "failed" | "canceled";
messages: AutoRunDialogMessage[];
}
export interface AsyncEvalRunJob {
job_id: string;
status: "queued" | "running" | "completed" | "failed";
status: "queued" | "running" | "completed" | "failed" | "canceled";
created_at: string;
updated_at: string;
eval_target: AutoRunTarget;

View File

@ -1627,6 +1627,13 @@ button:disabled {
stroke: none;
}
.autoruns-card-stop-svg {
width: 14px;
height: 14px;
fill: currentColor;
stroke: none;
}
.autoruns-card-chevron-svg.expanded {
transform: rotate(180deg);
}