Усилить семантический контур агента и большой прогон

This commit is contained in:
dctouch 2026-05-25 13:00:50 +03:00
parent 7cc65e808e
commit 8ce007724a
87 changed files with 4941 additions and 14691 deletions

View File

@ -0,0 +1,276 @@
{
"schema_version": "domain_scenario_pack_v1",
"pack_id": "agent_business_overview_counterparty_reliability_20260524",
"domain": "agent_business_overview_counterparty_reliability",
"title": "Agent business overview and counterparty documents reliability proof",
"description": "Focused proof pack for business-useful company overview, money flow, VAT/document boundaries, all-time scope reset, and explicit SVK counterparty document separation.",
"analysis_context": {
"as_of_date": "2026-05-24",
"source": "agent_reliability_uplift_business_overview_counterparty_slice"
},
"bindings": {
"main_organization": "ООО Альтернатива Плюс",
"control_year": "2020",
"svk_counterparty": "Группа СВК"
},
"scenarios": [
{
"scenario_id": "company_business_overview_scope",
"title": "Company overview, money flow, VAT boundary, and all-time scope reset",
"description": "Validates that the assistant can answer a broad business question directly, keep period scope honest, and reset from 2020 to all available time without carrying stale tax claims.",
"steps": [
{
"step_id": "s01_business_overview_2020",
"title": "Direct business overview for the company and year",
"node_role": "root",
"question": "Дай взрослый бизнес-обзор {{bindings.main_organization}} за {{bindings.control_year}} год по данным 1С: обороты, входящие и исходящие деньги, нетто, НДС, долги, склад, клиенты, поставщики и что пока нельзя утверждать.",
"expected_intents": ["business_overview"],
"semantic_tags": ["business_overview", "money", "vat", "debt", "inventory", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"Альтернатива",
"2020"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)selected_chain_id",
"(?i)runtime_",
"(?i)business_overview_route_template_v1",
"(?i)mcp_discovery"
]
},
{
"step_id": "s02_money_followup",
"title": "Money follow-up with counterparty highlights",
"question": "Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в {{bindings.control_year}}.",
"depends_on": ["s01_business_overview_2020"],
"semantic_tags": ["money", "counterparty", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"2020"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_"
]
},
{
"step_id": "s03_vat_documents_boundary",
"title": "VAT and document evidence boundary",
"question": "Что с НДС за {{bindings.control_year}} год по {{bindings.main_organization}}: какая позиция видна, на чем она основана и чего не хватает для налогового вывода?",
"depends_on": ["s02_money_followup"],
"semantic_tags": ["vat", "documents", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"НДС",
"2020"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_",
"(?i)точно подтвержден.*налогов"
]
},
{
"step_id": "s04_all_time_scope_reset",
"title": "All-time company overview without stale VAT carryover",
"question": "Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за {{bindings.control_year}} как подтвержденную общую налоговую позицию.",
"depends_on": ["s03_vat_documents_boundary"],
"semantic_tags": ["business_overview", "vat", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"доступн",
"НДС"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_",
"(?i)НДС за 2020.*общая налоговая позиция"
]
}
]
},
{
"scenario_id": "svk_counterparty_documents_scope",
"title": "Explicit SVK counterparty money and document scope",
"description": "Validates that an explicit counterparty request is not confused with company overview context and that document evidence is separated from business conclusions.",
"steps": [
{
"step_id": "s01_svk_money_documents",
"title": "Explicit counterparty money flow and evidence",
"node_role": "root",
"question": "Отдельно по контрагенту {{bindings.svk_counterparty}}, без опоры на прошлый диалог: сколько денег прошло, что входящее, что исходящее и есть ли документы или движения, на которых это основано?",
"expected_intents": ["value_flow"],
"semantic_tags": ["counterparty", "money", "documents", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"СВК"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)selected_chain_id",
"(?i)runtime_",
"(?i)mcp_discovery",
"(?i)Альтернатива Плюс.*контрагент"
]
},
{
"step_id": "s02_svk_documents_followup",
"title": "Counterparty documents follow-up",
"question": "Покажи документы по этой цепочке и не смешивай {{bindings.svk_counterparty}} с организацией {{bindings.main_organization}}.",
"depends_on": ["s01_svk_money_documents"],
"semantic_tags": ["counterparty", "documents", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"СВК"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_",
"(?i)Альтернатива Плюс.*как контрагент"
]
},
{
"step_id": "s03_company_vs_svk_summary",
"title": "Separate company and counterparty conclusions",
"question": "Собери короткий итог: что мы подтвердили по компании, что отдельно по {{bindings.svk_counterparty}}, какие выводы можно делать и какие нельзя.",
"depends_on": ["s01_svk_money_documents", "s02_svk_documents_followup"],
"semantic_tags": ["business_overview", "counterparty", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"СВК",
"компан"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_"
]
}
]
},
{
"scenario_id": "company_business_overview_colloquial_scope",
"title": "Colloquial company overview and money follow-up",
"description": "Adds colloquial wording coverage for the same company-level business overview edge without changing the business contract.",
"steps": [
{
"step_id": "s01_colloquial_company_overview_2020",
"title": "Colloquial business overview for the company",
"node_role": "root",
"question": "По-человечески посмотри {{bindings.main_organization}} за {{bindings.control_year}}: что по бизнесу видно в 1С по деньгам, НДС, долгам, складу, клиентам/поставщикам и где пока нельзя делать вывод?",
"expected_intents": ["business_overview"],
"semantic_tags": ["business_overview", "colloquial", "money", "vat", "debt", "inventory", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"2020",
"НДС"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)selected_chain_id",
"(?i)runtime_",
"(?i)mcp_discovery"
]
},
{
"step_id": "s02_colloquial_money_leaders",
"title": "Colloquial money leaders follow-up",
"question": "А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки.",
"depends_on": ["s01_colloquial_company_overview_2020"],
"semantic_tags": ["money", "counterparty", "colloquial", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"2020"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_"
]
}
]
},
{
"scenario_id": "svk_selected_object_followup_scope",
"title": "Selected-object style SVK counterparty follow-up",
"description": "Adds pronoun and selected-object style coverage for the SVK counterparty edge: root counterparty fact, documents by selected object, and separated summary.",
"steps": [
{
"step_id": "s01_select_svk_counterparty_money",
"title": "Select SVK as the active counterparty",
"node_role": "root",
"question": "Возьми как выбранного контрагента {{bindings.svk_counterparty}}: сколько по нему входящих денег, исходящих денег и расчетное нетто, на чем это основано?",
"expected_intents": ["value_flow"],
"semantic_tags": ["counterparty", "money", "selected_object", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"СВК"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)selected_chain_id",
"(?i)runtime_",
"(?i)mcp_discovery"
]
},
{
"step_id": "s02_pronoun_documents_for_selected_svk",
"title": "Documents for the selected counterparty by pronoun",
"question": "Теперь по нему покажи документы и не переключайся на организацию.",
"depends_on": ["s01_select_svk_counterparty_money"],
"semantic_tags": ["counterparty", "documents", "selected_object", "pronoun_followup", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"СВК"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_",
"(?i)Альтернатива Плюс.*как контрагент"
]
},
{
"step_id": "s03_selected_svk_company_boundary_summary",
"title": "Boundary summary after selected-object document follow-up",
"question": "И коротко сравни: что у нас подтверждено по компании, что отдельно по выбранному контрагенту, и какие выводы нельзя делать?",
"depends_on": ["s01_select_svk_counterparty_money", "s02_pronoun_documents_for_selected_svk"],
"semantic_tags": ["business_overview", "counterparty", "selected_object", "scope_guard"],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"СВК",
"компан"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_"
]
},
{
"step_id": "s04_org_clarification_boundary_summary",
"title": "Complete boundary summary after organization clarification",
"question": "{{bindings.main_organization}}",
"depends_on": [
"s03_selected_svk_company_boundary_summary"
],
"semantic_tags": [
"business_overview",
"counterparty",
"selected_object",
"organization_clarification",
"scope_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"Альтернатива Плюс",
"СВК",
"Нельзя"
],
"forbidden_answer_patterns": [
"(?i)capability_id",
"(?i)runtime_",
"(?i)mcp_discovery"
]
}
]
}
]
}

View File

@ -1664,6 +1664,27 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
}
function countBroadBusinessOverviewBridgeAxes(text) {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewBridgeSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasBroadCue = /(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(normalized);
const hasCompanyScope = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(normalized);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewBridgeAxes(normalized) >= 3;
}
function hasNomenclatureMarginRankingSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
@ -1776,6 +1797,9 @@ function resolveUnicodeAddressIntentBridge(text) {
if (!hasContractCue && (hasTopYearRevenueRankingCue || hasCustomerRevenueRankingBridgeSignal(normalized))) {
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_ranking_bridge_signal_detected");
}
if (hasBroadBusinessOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_multi_surface_deferred_to_discovery");
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}

View File

@ -11,6 +11,7 @@ const MAX_RESULT_SETS = 40;
const MAX_NAVIGATION_EVENTS = 120;
const MAX_ENTITY_REFS_PER_RESULT_SET = 40;
const DISPLAY_ENTITY_TYPE_BY_INTENT = {
business_overview: "organization",
counterparty_activity_lifecycle: "counterparty",
customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty",
@ -33,6 +34,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
inventory_aging_by_purchase_date: "item"
};
const RESULT_SET_TYPE_BY_INTENT = {
business_overview: "profile_summary",
counterparty_activity_lifecycle: "counterparty_list",
customer_revenue_and_payments: "counterparty_list",
supplier_payouts_profile: "counterparty_list",
@ -69,6 +71,18 @@ function toObject(value) {
}
return value;
}
function cloneRecord(value) {
const record = toObject(value);
if (!record) {
return null;
}
try {
return JSON.parse(JSON.stringify(record));
}
catch {
return { ...record };
}
}
function toNonEmptyString(value) {
if (typeof value !== "string") {
return null;
@ -76,6 +90,40 @@ function toNonEmptyString(value) {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function candidateLabel(value) {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toObject(value);
if (!record) {
return null;
}
return (toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text));
}
function readNavigationDiscoveryCounterparty(debug) {
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entry?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const dataNeedGraph = toObject(turnInput?.data_need_graph);
const candidates = [
...(Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: []),
...(Array.isArray(turnMeaning?.explicit_entity_candidates) ? turnMeaning.explicit_entity_candidates : []),
...(Array.isArray(dataNeedGraph?.subject_candidates) ? dataNeedGraph.subject_candidates : [])
];
for (const candidate of candidates) {
const label = candidateLabel(candidate);
if (label) {
return label;
}
}
return null;
}
function toAddressFocusObjectType(value) {
const normalized = toNonEmptyString(value);
if (!normalized) {
@ -194,6 +242,21 @@ function cloneFocusObject(value) {
selected_at: value.selected_at
};
}
function cloneComparisonProofBundles(value) {
const record = toObject(value);
if (!record) {
return null;
}
const counterpartyValueFlowBundle = cloneRecord(record.counterparty_value_flow_bundle ?? record.previous_counterparty_value_flow_bundle);
const counterpartyDocumentBundle = cloneRecord(record.counterparty_document_bundle ?? record.previous_counterparty_document_bundle);
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function cloneResultSet(input) {
return {
result_set_id: input.result_set_id,
@ -252,8 +315,63 @@ function buildFocusObject(objectType, label, resultSetId, createdAt) {
selected_at: createdAt
};
}
function cloneComparisonScope(value) {
if (!value || typeof value !== "object") {
return null;
}
return {
organization: cloneFocusObject(value.organization),
counterparty: cloneFocusObject(value.counterparty),
proof_bundles: cloneComparisonProofBundles(value.proof_bundles)
};
}
function sameBusinessLabel(left, right) {
const normalizedLeft = toNonEmptyString(left)?.toLocaleLowerCase("ru-RU");
const normalizedRight = toNonEmptyString(right)?.toLocaleLowerCase("ru-RU");
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
}
function readBusinessOverviewComparisonProofBundles(debug) {
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty) ??
toNonEmptyString(counterpartyDocumentBundle?.counterparty) ??
readNavigationDiscoveryCounterparty(debug);
if (!counterparty) {
return null;
}
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
if (organization) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
}
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
if (canonicalType === "item") {
@ -282,9 +400,13 @@ function capNavigationEvents(events) {
return events.slice(events.length - MAX_NAVIGATION_EVENTS);
}
function isAddressAssistantItem(item) {
return (item.role === "assistant" &&
Boolean(item.debug) &&
toNonEmptyString(item.debug?.detected_mode) === "address_query");
if (item.role !== "assistant" || !item.debug) {
return false;
}
if (toNonEmptyString(item.debug.detected_mode) === "address_query") {
return true;
}
return item.debug.mcp_discovery_response_applied === true && Boolean(toNonEmptyString(item.debug.mcp_discovery_selected_chain_id));
}
function createEmptyAddressNavigationState(sessionId, nowIso = new Date().toISOString()) {
return {
@ -294,6 +416,7 @@ function createEmptyAddressNavigationState(sessionId, nowIso = new Date().toISOS
session_context: {
active_result_set_id: null,
active_focus_object: null,
comparison_scope: null,
last_confirmed_route: null,
date_scope: {
as_of_date: null,
@ -317,6 +440,7 @@ function cloneAddressNavigationState(value) {
session_context: {
active_result_set_id: value.session_context.active_result_set_id,
active_focus_object: cloneFocusObject(value.session_context.active_focus_object),
comparison_scope: cloneComparisonScope(value.session_context.comparison_scope),
last_confirmed_route: value.session_context.last_confirmed_route,
date_scope: {
as_of_date: value.session_context.date_scope.as_of_date,
@ -347,6 +471,7 @@ function normalizeAddressNavigationState(value, sessionId) {
session_context: {
active_result_set_id: toNonEmptyString(context.active_result_set_id),
active_focus_object: cloneFocusObject(context.active_focus_object),
comparison_scope: cloneComparisonScope(context.comparison_scope),
last_confirmed_route: toNonEmptyString(context.last_confirmed_route),
date_scope: {
as_of_date: toNonEmptyString(dateScope.as_of_date),
@ -400,21 +525,40 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
return state;
}
const debug = item.debug;
const intent = toAddressIntent(debug.detected_intent);
if (intent === "unknown") {
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
const discoveryIntent = selectedDiscoveryChain === "business_overview"
? "business_overview"
: selectedDiscoveryChain === "value_flow_comparison"
? "customer_revenue_and_payments"
: "unknown";
const detectedIntent = toNonEmptyString(debug.detected_intent);
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
if (intent === "unknown" && !trackableDiscoveryTurn) {
return state;
}
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe);
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
const derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
: null;
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
...filters,
organization: derivedOrganizationScope
organization: derivedOrganizationScope,
...(derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? { counterparty: derivedCounterpartyScope }
: {})
}
: filters;
: derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? {
...filters,
counterparty: derivedCounterpartyScope
}
: filters;
const sourceRefs = routeId ? [routeId] : [];
const entityRefs = extractEntityRefsFromAssistantReply(item.text, intent);
const resultSet = {
@ -430,6 +574,34 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
};
const previousResultSetId = state.session_context.active_result_set_id;
const focusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
const comparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readNavigationDiscoveryCounterparty(debug)
: null;
const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
: null;
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug)
: null;
const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles = comparisonCounterparty &&
sameBusinessLabel(inheritedComparisonScope?.counterparty?.label, comparisonCounterparty)
? cloneComparisonProofBundles(inheritedComparisonScope?.proof_bundles)
: null;
const comparisonProofBundles = currentComparisonProofBundles ?? inheritedComparisonProofBundles;
const comparisonOrganizationObject = comparisonOrganization
? buildFocusObject("organization", comparisonOrganization, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.organization ?? null);
const comparisonCounterpartyObject = comparisonCounterparty
? buildFocusObject("counterparty", comparisonCounterparty, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.counterparty ?? null);
const comparisonScope = comparisonOrganizationObject || comparisonCounterpartyObject || comparisonProofBundles
? {
organization: comparisonOrganizationObject,
counterparty: comparisonCounterpartyObject,
proof_bundles: comparisonProofBundles
}
: null;
const action = resolveNavigationAction(debug, Boolean(focusObject));
const navigationEvent = {
event_id: `nav-${(0, nanoid_1.nanoid)(10)}`,
@ -440,10 +612,11 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
turn_index: turnIndex,
created_at: createdAt
};
const discoveryTemporalScope = (0, assistantContinuityPolicy_1.readAddressDebugTemporalScope)(debug, toNonEmptyString);
const normalizedDateScope = {
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date) ?? discoveryTemporalScope.asOfDate,
period_from: toNonEmptyString(filtersWithDerivedScope.period_from) ?? discoveryTemporalScope.periodFrom,
period_to: toNonEmptyString(filtersWithDerivedScope.period_to) ?? discoveryTemporalScope.periodTo
};
const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
const nextResultSets = capResultSets([...state.result_sets.filter((itemSet) => itemSet.result_set_id !== resultSetId), resultSet].sort((left, right) => left.created_from_turn - right.created_from_turn));
@ -452,6 +625,7 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
? {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? null,
comparison_scope: comparisonScope,
last_confirmed_route: routeId ?? null,
date_scope: {
as_of_date: normalizedDateScope.as_of_date,
@ -463,6 +637,7 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
: {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? state.session_context.active_focus_object,
comparison_scope: comparisonScope ?? state.session_context.comparison_scope,
last_confirmed_route: routeId ?? state.session_context.last_confirmed_route,
date_scope: {
as_of_date: normalizedDateScope.as_of_date ?? state.session_context.date_scope.as_of_date,

View File

@ -542,6 +542,33 @@ function needsVatCalendarDetails(userMessage) {
}
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
}
function needsVatPurchaseDateAnchorDisclosure(userMessage) {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
}
function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
return null;
}
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const asOfTs = toUtcDayTimestamp(asOfDate);
const fromTs = toUtcDayTimestamp(periodFrom);
const toTs = toUtcDayTimestamp(periodTo);
if (asOfDate &&
asOfTs !== null &&
fromTs !== null &&
toTs !== null &&
asOfTs >= fromTs &&
asOfTs <= toTs) {
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
}
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
}
function detectRankingLimit(userMessage, fallback = 20) {
const text = normalizeQuestionText(userMessage);
if (!text) {
@ -3033,6 +3060,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`,
"Расчет сделан по книгам продаж и покупок.",
@ -3040,6 +3068,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
"Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`

View File

@ -31,6 +31,121 @@ function toNullableBoolean(value) {
function normalizeAddressReplyType(value) {
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
}
function sameBusinessLabel(left, right) {
const normalizedLeft = toNullableString(left)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
const normalizedRight = toNullableString(right)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
return Boolean(normalizedLeft &&
normalizedRight &&
(normalizedLeft === normalizedRight ||
normalizedLeft.includes(normalizedRight) ||
normalizedRight.includes(normalizedLeft)));
}
function firstString(values) {
for (const value of values) {
const text = toNullableString(value);
if (text) {
return text;
}
}
return null;
}
function legalOrganizationLabelFromClarification(value) {
const text = toNullableString(value);
if (!text) {
return null;
}
const compact = text.replace(/\s+/gu, " ").replace(/[.!?]+$/u, "").trim();
if (compact.length > 120 || !/^(?:ООО|ПАО|АО|ИП)\s+\S/iu.test(compact)) {
return null;
}
return compact;
}
function cleanComparisonScopeCompanyLine(line, organization) {
let clean = String(line ?? "")
.replace(/\bcompany-level\b/giu, "общий по компании")
.replace(/\breusable bundle\b/giu, "сохраненный подтвержденный срез");
if (organization) {
clean = clean.replace(/по компании\s+Альтернатива Плюс/iu, `по компании ${organization}`);
}
return clean.trim();
}
function buildComparisonScopeProofReply(input) {
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const isBusinessOverview = toNullableString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview) {
return null;
}
const separateCandidates = Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: [];
const separateSubject = firstString([...separateCandidates, turnMeaning?.metadata_scope_hint]);
if (!separateSubject) {
return null;
}
const sessionRecord = toRecordObject(input.session);
const addressNavigationState = toRecordObject(sessionRecord?.address_navigation_state);
const sessionContext = toRecordObject(addressNavigationState?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle || !sameBusinessLabel(separateSubject, valueBundle.counterparty ?? comparisonCounterparty?.label)) {
return null;
}
const incoming = toRecordObject(valueBundle.incoming_customer_revenue);
const outgoing = toRecordObject(valueBundle.outgoing_supplier_payout);
const incomingAmount = toNullableString(incoming?.total_amount_human_ru);
const outgoingAmount = toNullableString(outgoing?.total_amount_human_ru);
const netAmount = toNullableString(valueBundle.net_amount_human_ru);
if (!incomingAmount && !outgoingAmount && !netAmount) {
return null;
}
const organization = legalOrganizationLabelFromClarification(input.userMessage)
?? toNullableString(turnMeaning?.explicit_organization_scope)
?? toNullableString(toRecordObject(comparisonScope?.organization)?.label);
const documentCount = Number(toRecordObject(documentBundle)?.document_count);
const documentText = Number.isFinite(documentCount) && documentCount > 0 ? `, документы: ${documentCount}` : "";
const lines = String(input.baseReply ?? "")
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const companyLine = cleanComparisonScopeCompanyLine(lines[0] ?? `Коротко: по компании ${organization ?? "выбранной организации"} подтвержден общий денежный срез.`, organization);
const netDirection = valueBundle.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в нашу сторону";
const counterparty = toNullableString(valueBundle.counterparty) ?? separateSubject;
return {
reply: [
`${companyLine}; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ${netDirection} ${netAmount ?? "0 руб."}${documentText}.`,
`Отдельно по контрагенту ${counterparty}: это ранее подтвержденный контрагентский срез, а не перенос общих сумм компании на контрагента.`,
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${counterparty} из общих сумм компании без отдельного контрагентского среза.`
].join("\n"),
audit: {
applied: true,
source: "address_navigation_state.comparison_scope.proof_bundles",
counterparty,
organization: organization ?? null,
document_count: Number.isFinite(documentCount) && documentCount > 0 ? documentCount : null
}
};
}
function buildAppliedMcpDiscoveryRoutePatch(debug, applied) {
if (!applied) {
return {};
}
const selectedChain = toNullableString(debug.mcp_discovery_selected_chain_id);
if (selectedChain === "business_overview") {
return {
detected_intent: "business_overview",
detected_intent_confidence: "high",
selected_recipe: "business_overview",
response_type: "LIMITED_WITH_REASON",
mcp_discovery_effective_response_route: "business_overview"
};
}
return {};
}
function normalizeAddressLaneDebug(value) {
return (toRecordObject(value) ?? {});
}
@ -231,18 +346,27 @@ function runAssistantAddressLaneResponseRuntime(input) {
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: guardedResponse.assistantReply;
const comparisonScopeProofReply = buildComparisonScopeProofReply({
baseReply: finalAssistantReply,
debug: debugWithResponseGuard,
session: input.getSession(input.sessionId),
userMessage: input.userMessage
});
const finalAssistantReplyWithComparisonProof = comparisonScopeProofReply?.reply ?? finalAssistantReply;
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
const finalDebug = {
...debugWithResponseGuard,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
assistantReply: finalAssistantReply,
assistantReply: finalAssistantReplyWithComparisonProof,
replyType: finalReplyType,
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
debug: finalDebug,

View File

@ -151,6 +151,256 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
}
};
}
function firstString(values, toNonEmptyString) {
for (const value of values) {
const text = toNonEmptyString(value);
if (text) {
return text;
}
}
return null;
}
function comparableEntityName(value) {
const text = compactLower(value);
return text ? text.replace(/["'«»„“”]+/g, "") : null;
}
function sameEntityHint(expected, actual) {
const left = comparableEntityName(expected);
const right = comparableEntityName(actual);
if (!left || !right) {
return true;
}
return left === right || left.includes(right) || right.includes(left);
}
function isBusinessOverviewDiscoveryFollowup(followupContext, toNonEmptyString) {
if (!followupContext) {
return false;
}
return [
followupContext.previous_discovery_pilot_scope,
followupContext.previous_discovery_loop_selected_chain_id,
followupContext.previous_discovery_loop_asked_domain_family,
followupContext.previous_intent,
followupContext.target_intent
]
.map((value) => toNonEmptyString(value))
.some((value) => value === "business_overview" || value === "business_overview_route_template_v1");
}
function businessOverviewCounterpartyHint(followupContext, toNonEmptyString) {
const previousFilters = toRecordObject(followupContext.previous_filters);
const rootFilters = toRecordObject(followupContext.root_filters);
return (toNonEmptyString(followupContext.previous_discovery_loop_metadata_scope_hint) ??
(toNonEmptyString(followupContext.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext.previous_anchor_value)
: null) ??
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty));
}
function turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, toNonEmptyString) {
const separateEntities = Array.isArray(turnMeaningRef.business_overview_separate_entity_candidates)
? turnMeaningRef.business_overview_separate_entity_candidates
: [];
return (toNonEmptyString(valueBundle?.counterparty) ??
toNonEmptyString(documentBundle?.counterparty) ??
toNonEmptyString(turnMeaningRef.metadata_scope_hint) ??
firstString(separateEntities, toNonEmptyString));
}
function parseBusinessOverviewProofBundlesFromText(value, toNonEmptyString) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const valueMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu);
const directDocumentMatch = text.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено\s+документов:\s*(\d+)/iu);
const summaryDocumentMatch = text.match(/документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
const counterparty = toNonEmptyString(valueMatch?.[1]) ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueMatch
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueMatch[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueMatch[3])
},
net_amount_human_ru: toNonEmptyString(valueMatch[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function parseBusinessOverviewProofBundlesFromTextV2(value, counterpartyHint, toNonEmptyString) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const comparableHint = comparableEntityName(counterpartyHint);
const lines = text
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const candidateLines = comparableHint
? lines.filter((line) => comparableEntityName(line)?.includes(comparableHint))
: lines;
const rubAmountPattern = /[0-9][0-9\s.,]*\s*\u0440\u0443\u0431\.?/giu;
const valueLine = candidateLines.find((line) => (line.match(rubAmountPattern) ?? []).length >= 3) ?? null;
const valueAmounts = valueLine?.match(rubAmountPattern) ?? [];
const directDocumentMatch = candidateLines
.map((line) => line.match(/\u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^.\n]+)\.\s*\u041d\u0430\u0439\u0434\u0435\u043d\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432:\s*(\d+)/iu))
.find(Boolean);
const summaryDocumentMatch = candidateLines
.map((line) => line.match(/\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b\s+\u043f\u043e\s+\u0446\u0435\u043f\u043e\u0447\u043a\u0435:\s*\u043d\u0430\u0439\u0434\u0435\u043d\u043e\s*(\d+)/iu))
.find(Boolean);
const counterparty = counterpartyHint ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueAmounts.length >= 3
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueAmounts[0])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueAmounts[1])
},
net_amount_human_ru: toNonEmptyString(valueAmounts[2]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function findRecentBusinessOverviewProofBundles(input) {
if (!input.counterpartyHint) {
return null;
}
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(input.sessionItems[index]);
if (input.toNonEmptyString(item?.role) !== "assistant") {
continue;
}
const debug = toRecordObject(item?.debug);
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
if (turnMeaningRef) {
const valueBundle = toRecordObject(turnMeaningRef.previous_counterparty_value_flow_bundle);
const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle);
if (valueBundle || documentBundle) {
const bundleCounterparty = turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, input.toNonEmptyString);
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
return { valueBundle, documentBundle };
}
}
}
const parsedBundles = parseBusinessOverviewProofBundlesFromTextV2(item?.text, input.counterpartyHint, input.toNonEmptyString) ??
parseBusinessOverviewProofBundlesFromText(item?.text, input.toNonEmptyString);
if (!parsedBundles) {
continue;
}
const parsedCounterparty = input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
continue;
}
return parsedBundles;
}
return null;
}
function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const state = toRecordObject(input.sessionAddressNavigationState);
const sessionContext = toRecordObject(state?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle && !documentBundle) {
return input.followupContext;
}
const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext;
}
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const counterpartyHint = (input.followupContext ? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString) : null) ??
input.toNonEmptyString(comparisonCounterparty?.label);
const bundleCounterparty = input.toNonEmptyString(valueBundle?.counterparty) ?? input.toNonEmptyString(documentBundle?.counterparty);
if (!counterpartyHint || !sameEntityHint(counterpartyHint, bundleCounterparty)) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
previous_discovery_pilot_scope: input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
"business_overview_route_template_v1",
previous_discovery_loop_status: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_status) ?? "awaiting_clarification",
previous_discovery_loop_selected_chain_id: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
previous_discovery_loop_pending_axes: Array.isArray(input.followupContext?.previous_discovery_loop_pending_axes)
? input.followupContext?.previous_discovery_loop_pending_axes
: ["organization"],
previous_discovery_loop_asked_domain_family: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
previous_discovery_loop_asked_action_family: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
previous_discovery_loop_metadata_scope_hint: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? counterpartyHint,
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? "counterparty",
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? counterpartyHint,
previous_filters: toRecordObject(input.followupContext?.previous_filters) ?? {},
previous_discovery_bidirectional_value_flow: currentValueBundle ?? valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? documentBundle ?? undefined
};
}
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
return input.followupContext;
}
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const counterpartyHint = input.followupContext
? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString)
: null;
const proofBundles = findRecentBusinessOverviewProofBundles({
sessionItems: input.sessionItems,
counterpartyHint,
toNonEmptyString: input.toNonEmptyString
});
if (!proofBundles) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_discovery_bidirectional_value_flow: currentValueBundle ?? proofBundles.valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
};
}
function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? ""));
}
@ -324,6 +574,17 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
followupContext: discoveryFollowupContext,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithStateProofBundles = mergeBusinessOverviewProofBundlesFromNavigationState({
followupContext: discoveryFollowupContextWithProofBundles,
sessionAddressNavigationState: input.sessionAddressNavigationState,
predecomposeContract,
toNonEmptyString: input.toNonEmptyString
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint;
let mcpDiscoveryRuntimeEntryPoint = null;
@ -334,7 +595,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract,
followupContext: discoveryFollowupContext,
followupContext: discoveryFollowupContextWithStateProofBundles,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
}));
}

View File

@ -238,6 +238,9 @@ function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(debug, toNonEmptyS
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) {
if (pilotScope === "business_overview_route_template_v1" || actionFamily === "broad_evaluation") {
return "business_overview";
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}

View File

@ -42,6 +42,17 @@ function shouldProbeBareOrganizationScopeCandidate(input) {
function buildDeterministicSmalltalkLeadReply() {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function hasFirstTurnSmalltalkGreetingSignal(value) {
const normalized = String(value ?? "")
.toLowerCase()
.replace(/\u0451/gu, "\u0435")
.replace(/\s+/gu, " ")
.trim();
if (!normalized) {
return false;
}
return /^(?:привет(?:ик)?|здравствуй(?:те)?|хай|йо|yo|hello|hi|че\s+как|че\s+там)(?:[\s,.!?;:()\-]+(?:как|дела|там|че|что|у\s+тебя|как\s+там|как\s+дела))*[\s,.!?;:()\-]*$/iu.test(normalized);
}
function hasConversationExecutiveSummarySignal(value) {
const normalized = String(value ?? "")
.toLowerCase()
@ -114,6 +125,13 @@ async function runAssistantLivingChatRuntime(input) {
let knownOrganizations = [...organizationAuthority.knownOrganizations];
let selectedOrganization = organizationAuthority.selectedOrganization;
let activeOrganization = organizationAuthority.activeOrganization;
const shouldHandleFirstTurnSmalltalkDeterministically = !selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
hasFirstTurnSmalltalkGreetingSignal(userMessage) &&
input.hasLivingChatSignal(userMessage);
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
? input.addressRuntimeMeta
: {});
@ -263,6 +281,28 @@ async function runAssistantLivingChatRuntime(input) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
}
else if (shouldHandleFirstTurnSmalltalkDeterministically) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? proactiveScopeProbe.organizations : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = Boolean(proactiveOffer);
livingChatSource = proactiveOffer
? "deterministic_smalltalk_with_proactive_scope_offer"
: "deterministic_smalltalk";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
else {
chatText = await input.executeLlmChat();
const scriptGuard = input.applyScriptGuard(chatText, userMessage);
@ -283,34 +323,6 @@ async function runAssistantLivingChatRuntime(input) {
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope = !selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? proactiveScopeProbe.organizations : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {
return {

View File

@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryExecutionHandoff = buildAssistantMcpDiscoveryExecutionHandoff;
exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = "assistant_mcp_discovery_execution_handoff_v1";
const HOT_HANDOFF_CHAIN_ALLOWLIST = ["value_flow"];
const HOT_HANDOFF_CHAIN_ALLOWLIST = [
"value_flow",
"value_flow_comparison",
"business_overview"
];
function uniqueStrings(values) {
const result = [];
for (const value of values) {

View File

@ -24,16 +24,51 @@ function normalizeQuestionText(value) {
.replace(/\s+/g, " ")
.trim();
}
function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
const text = normalizeQuestionText([
function normalizedTurnAndGraphText(turnMeaning, graph) {
return normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
}
function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
}
function requestsBroadBusinessOverviewSurface(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:не\s+обзор|просто\s+ден\p{L}*|одной\s+строк\p{L}*|только\s+итог|без\s+разбив\p{L}*)/iu.test(text)) {
return false;
}
if (/(?:бизнес[-\s]*обзор|взросл\p{L}{0,10}\s+бизнес|что\s+(?:пока\s+)?нельзя\s+утвержд)/iu.test(text)) {
return true;
}
const markers = [
/(?:ндс|налог\p{L}*)/iu,
/(?:долг\p{L}*|дебитор|кредитор)/iu,
/(?:склад|остатк|товар\p{L}*)/iu,
/(?:клиент|заказчик|покупател)/iu,
/(?:поставщик|получател)/iu,
/(?:оборот\p{L}*)/iu,
/(?:ограничен|не\s+подтвержд|нельзя\s+утвержд)/iu
];
return markers.filter((marker) => marker.test(text)).length >= 3;
}
function requestsCounterpartyLeaderSurface(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:кто|кому)[\s\S]{0,60}(?:больше\s+всего|крупнее\s+всего|основн\p{L}*)[\s\S]{0,60}(?:зан[её]с|прин[её]с|платил|ушло|получил|перев[её]л|заплатил|внес|вн[её]с)/iu.test(text)) {
return true;
}
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
}
function requestsCompactCashflowAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
@ -716,6 +751,39 @@ function buildPreviousCounterpartyValueFlowSummary(flow, separateSubject, docume
`${basisText} Это не перенос сумм компании на контрагента, а отдельный ранее подтвержденный контрагентский срез.`
};
}
function buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint) {
const turnInput = toRecordObject(entryPoint.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const graph = toRecordObject(turnInput?.data_need_graph);
const bridge = toRecordObject(entryPoint.bridge);
const pilot = toRecordObject(bridge?.pilot);
const overview = toRecordObject(pilot?.derived_business_overview);
const isBusinessOverview = toNonEmptyString(graph?.business_fact_family) === "business_overview" ||
toNonEmptyString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview || overview) {
return null;
}
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
if (!separateSubject || !previousCounterpartySummary) {
return null;
}
const lines = organizationScope
? [
`Коротко: по компании ${organizationScope} в этом шаге нет нового полного company-level расчета; отдельно по выбранному контрагенту ${separateSubject} есть ранее подтвержденный контрагентский срез.`,
previousCounterpartySummary.line,
`Можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не подтверждает чистую прибыль, полный оборот или общую бизнес-роль ${separateSubject}; также нельзя смешивать этот контрагентский срез с выводами по компании без отдельного company-level расчета.`
]
: [
`Коротко: уточните, по какой компании/организации сравнить выбранного контрагента ${separateSubject}; company-level вывод без организации не подтверждаю.`,
previousCounterpartySummary.line,
`Уже можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не чистая прибыль, не полный оборот компании и не доказанная бизнес-роль ${separateSubject}; контрагентский срез нельзя смешивать с company-level выводом без выбранной компании.`
];
return joinBusinessReplyLines(lines);
}
function buildCompactBusinessOverviewReply(entryPoint, draft) {
const turnInput = toRecordObject(entryPoint.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
@ -773,6 +841,11 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
: null;
const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const broadOverviewSurfaceRequested = requestsBroadBusinessOverviewSurface(turnMeaning, graph);
const counterpartyLeaderSurfaceRequested = requestsCounterpartyLeaderSurface(turnMeaning, graph);
const directMoneyOnlyAnswer = directMoneyAnswer && !broadOverviewSurfaceRequested && !counterpartyLeaderSurfaceRequested;
const shouldIncludeCounterpartyLeaders = !directMoneyOnlyAnswer || counterpartyLeaderSurfaceRequested || broadOverviewSurfaceRequested;
const shouldIncludeOverviewSurface = !directMoneyAnswer || broadOverviewSurfaceRequested;
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
const lines = [];
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
@ -781,9 +854,20 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const rawMessageComparable = compactComparable(rawMessage);
const organizationScopeComparable = compactComparable(organizationScope);
const plainOrganizationClarificationSelection = Boolean(separateSubject &&
organizationScope &&
rawMessage &&
rawMessageComparable &&
organizationScopeComparable &&
rawMessageComparable.includes(organizationScopeComparable) &&
rawMessage.length <= 90 &&
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage));
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
@ -798,6 +882,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.");
return joinBusinessReplyLines(lines);
}
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
if (previousCounterpartySummary) {
lines.push(previousCounterpartySummary.line);
}
else {
lines.push(`Отдельно по выбранному контрагенту ${separateSubject}: суммы компании на него не переношу; в этом шаге держу только границу, что это отдельный контрагентский контур.`);
}
lines.push(`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${separateSubject} на основе company-level сумм без отдельного контрагентского среза.`);
return joinBusinessReplyLines(lines);
}
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) {
@ -973,7 +1068,11 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
return joinBusinessReplyLines(lines);
}
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
if (!separateSubject &&
!crossScopeExecutiveSummary &&
!counterpartyLeaderSurfaceRequested &&
!rankingNeed &&
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
const subject = organizationScope ?? "компания";
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
lines.push(`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`);
@ -997,6 +1096,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
? `- крупнейший получатель исходящих денег: ${topSupplier}; это похоже на финансовый контур, не на обычного поставщика;`
: `- крупнейший получатель исходящих денег: ${topSupplier};`);
}
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
lines.push(`- ${localizeLine(taxLine)}`);
}
const inventoryLine = businessOverviewInventoryLine(overview);
if (inventoryLine) {
lines.push(`- ${localizeLine(inventoryLine)}`);
@ -1007,7 +1110,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
}
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
const missingOverviewFamilies = [];
if (!businessOverviewTaxLine(overview)) {
if (!taxLine) {
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
}
if (!debtLine) {
@ -1040,7 +1143,22 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
(incomingAmount || outgoingAmount || netAmount);
if (explicitPeriodRankingOverview) {
lines.push(`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`);
if (counterpartyLeaderSurfaceRequested) {
const incomingLeaderText = customerName && customerAmount
? topCustomerLooksFinancial
? `${customerName}${sentenceAmount(customerAmount) ?? customerAmount}; это банк/финансовый контур, не называю его обычной клиентской выручкой без назначения платежа${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
: `${customerName}${sentenceAmount(customerAmount) ?? customerAmount}`
: "не распознан";
const outgoingLeaderText = topSupplier
? topSupplierLooksFinancial
? `${topSupplier}; это банк/финансовый контур, не называю его обычным поставщиком без назначения платежа/договора${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: topSupplier
: "не распознан";
lines.push(`Коротко: ${organizationPrefix}${period} больше всего занес ${incomingLeaderText}; больше всего ушло ${outgoingLeaderText}.`);
}
else {
lines.push(`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`);
}
lines.push(`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
if (customerName && customerAmount) {
lines.push(topCustomerLooksFinancial
@ -1093,7 +1211,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
else if (incomingAmount || outgoingAmount || netAmount) {
lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`);
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) {
if (shouldIncludeCounterpartyLeaders && customerName && customerAmount) {
lines.push(topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`);
@ -1109,17 +1227,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push(previousCounterpartySummary?.line ??
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
}
if (!directMoneyAnswer && topSupplier) {
if (shouldIncludeCounterpartyLeaders && topSupplier) {
lines.push(topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
}
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
if (shouldIncludeCounterpartyLeaders && (topCustomer || topSupplier)) {
lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
}
if (!directMoneyAnswer) {
if (shouldIncludeOverviewSurface) {
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
@ -1205,6 +1323,10 @@ function buildReplyText(entryPoint, status) {
}
return null;
}
const previousCounterpartyBoundaryReply = buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint);
if (previousCounterpartyBoundaryReply) {
return previousCounterpartyBoundaryReply;
}
const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft);
if (compactBidirectionalValueFlowReply) {
return compactBidirectionalValueFlowReply;

View File

@ -361,6 +361,11 @@ function hasExactDocumentListAddressReply(input, entryPoint) {
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
return false;
}
if (hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const isDocumentIntent = detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";

View File

@ -271,7 +271,9 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
});
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(reasonCodes, executionHandoff.can_use_guarded_response
? "runtime_bridge_wired_to_guarded_hot_assistant_answer"
: "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);

View File

@ -73,6 +73,7 @@ function isReferentialOrganizationPlaceholder(value) {
"этой компании",
"этой компанией",
"эту компанию",
"в целом",
"наша организация",
"нашей организации",
"нашей компанией"
@ -188,7 +189,10 @@ function normalizeFollowupCounterpartyCandidate(value) {
if (!text || isInvalidEntityCandidate(text)) {
return null;
}
return text;
const cleaned = text
.replace(/^(?:\u043f\u043e\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435|\u044b|\u0430\u043c|\u0430\u043c\u0438|\u0430\u0445)?\s+/iu, "")
.trim();
return cleaned && !isInvalidEntityCandidate(cleaned) ? cleaned : text;
}
function pushScopedEntityCandidate(target, value, groundedFollowupEntity) {
const text = candidateValue(value);
@ -543,10 +547,11 @@ function collectFollowupDiscoverySeed(followupContext) {
const normalizedDiscoveryEntities = discoveryEntities
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
.filter((entity) => Boolean(entity));
const normalizedLoopMetadataScopeHint = normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const groundedDiscoveryCounterparty = ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
? null
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const metadataScopeHint = loopMetadataScopeHint ??
: normalizedDiscoveryEntities[0] ?? normalizedLoopMetadataScopeHint;
const metadataScopeHint = normalizedLoopMetadataScopeHint ??
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
@ -561,8 +566,10 @@ function collectFollowupDiscoverySeed(followupContext) {
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null);
const dateScope = collectDateScopeFromFilters(previousFilters) ??
collectDateScopeFromFilters(rootFilters);
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
const dateScope = loopProvidedAllTimeScope
? "all_time_scope"
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
return {
pilotScope: effectivePilotScope,
domain: mapped.domain,
@ -713,7 +720,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text) {
return hasSupplierScopeCue && hasSupplierQualityCue && hasCompanyScopeCue;
}
function hasCrossScopeExecutiveSummarySignal(text) {
return (/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary)/iu.test(text) &&
return (/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0441\u0440\u0430\u0432\u043d\p{L}*|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary|brief(?:ly)?\s+compare)/iu.test(text) &&
/(?:\u0447\u0442\u043e\s+(?:\u043c\u044b\s+)?\u043f\u043e\u0434\u0442\u0432\u0435\u0440\p{L}*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043f\u043e\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|confirmed|company|organization)/iu.test(text) &&
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0433\u0440\u0443\u043f\u043f\p{L}*\s+\u0441\u0432\u043a|\u0441\u0432\u043a|counterpart(?:y|ies)?)/iu.test(text) &&
/(?:\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\p{L}*|\u0447\u0442\u043e\s+\u043d\u0435\u043b\u044c\u0437\p{L}*|\u0432\u044b\u0432\u043e\u0434\p{L}*|allowed|forbidden|cannot|can\s+say)/iu.test(text));
@ -723,6 +730,27 @@ function hasPlainBusinessOverviewSignal(text) {
const hasCompanyOrOperatingScopeCue = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\p{L}*|(?:19|20)\d{2}|company|organization|business)/iu.test(text);
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
}
function countBroadBusinessOverviewAxes(text) {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewSurfaceSignal(text) {
const normalized = compactLower(text);
if (!normalized) {
return false;
}
const hasBroadCue = /(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(normalized);
const hasCompanyScope = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(normalized);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewAxes(normalized) >= 3;
}
function hasBusinessOverviewSignal(text) {
if (hasCrossScopeExecutiveSummarySignal(text) ||
hasOrganizationLevelEarningsOverviewSignal(text) ||
@ -730,6 +758,7 @@ function hasBusinessOverviewSignal(text) {
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
hasPlainBusinessOverviewSignal(text) ||
hasBroadBusinessOverviewSurfaceSignal(text) ||
hasOrganizationLevelSupplierQualityOverviewSignal(text)) {
return true;
}
@ -774,6 +803,12 @@ function hasBusinessOverviewSeparateCounterpartySignal(text) {
return (/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|counterpart(?:y|ies)?)/iu.test(text) &&
/(?:\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|company|organization|\u0438\u0442\u043e\u0433|summary|\u0432\u044b\u0432\u043e\u0434\p{L}*)/iu.test(text));
}
function isGenericSelectedCounterpartyReference(value) {
if (!value) {
return false;
}
return /^(?:(?:\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*|\u044d\u0442\p{L}*|\u0434\u0430\u043d\p{L}*|\u0442\u0435\u043a\u0443\u0449\p{L}*)\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*$|^(?:selected|chosen|current|this)\s+counterpart(?:y|ies)?$/iu.test(value);
}
function businessOverviewSeparateCounterpartyCandidateFromText(text) {
const source = (0, addressTextRepair_1.repairAddressMojibakeText)(String(text ?? ""));
const patterns = [
@ -782,7 +817,7 @@ function businessOverviewSeparateCounterpartyCandidateFromText(text) {
];
for (const pattern of patterns) {
const candidate = normalizeFollowupCounterpartyCandidate(source.match(pattern)?.[1]);
if (candidate && !isInvalidEntityCandidate(candidate)) {
if (candidate && !isInvalidEntityCandidate(candidate) && !isGenericSelectedCounterpartyReference(candidate)) {
return candidate;
}
}
@ -868,6 +903,9 @@ function normalizeLooseOrganizationAlias(value) {
if (hasYearOnlyTimeTail) {
return null;
}
if (new Set(["в целом", "компания в целом", "организация в целом"]).has(comparable)) {
return null;
}
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
return null;
}
@ -1370,10 +1408,18 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: "broad_business_evaluation";
const businessOverviewSignal = !businessOverviewCounterpartyValueFlowPivot &&
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
const organizationClarificationBusinessOverviewLoop = Boolean(followupSeed.loopStatus === "awaiting_clarification" &&
followupSeed.loopSelectedChainId === "business_overview" &&
followupSeed.loopPendingAxes.includes("organization") &&
currentTurnOrganizationScope &&
!rawLifecycleSignal &&
!rawMetadataSignal);
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
: null;
: organizationClarificationBusinessOverviewLoop
? followupSeed.counterparty ?? followupSeed.discoveryEntity ?? followupSeed.metadataScopeHint
: null;
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
const currentTurnMovementLaneSignal = rawAction === "list_movements";
@ -1397,6 +1443,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
sameScopedName(followupSeed.counterparty, currentTurnOrganizationScope)));
const businessOverviewSuppressesFollowupCounterparty = Boolean(businessOverviewSignal &&
!businessOverviewSeparateCounterpartySignal &&
!organizationClarificationBusinessOverviewLoop &&
(rawBusinessOverviewSignal ||
businessOverviewContinuationSignal ||
broadBusinessEvaluationUnsupported ||
@ -1831,7 +1878,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
}
const businessOverviewSeparateCounterpartyDisplayCandidate = businessOverviewSeparateCounterpartySignal
const shouldPreserveBusinessOverviewSeparateCounterparty = businessOverviewSeparateCounterpartySignal || organizationClarificationBusinessOverviewLoop;
const businessOverviewSeparateCounterpartyDisplayCandidate = shouldPreserveBusinessOverviewSeparateCounterparty
? preferredScopedDisplayName(businessOverviewSeparateCounterpartyCandidate, [
groundedFollowupEntity,
effectiveFollowupCounterparty,
@ -1840,7 +1888,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
])
]) ??
preferredScopedDisplayName(groundedFollowupEntity ??
effectiveFollowupCounterparty ??
followupSeed.discoveryEntity ??
normalizedPredecomposeCounterparty ??
rawScopedEntityCandidate ??
rawEntityCandidate, [
groundedFollowupEntity,
effectiveFollowupCounterparty,
followupSeed.discoveryEntity,
normalizedPredecomposeCounterparty,
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
])
: null;
const businessOverviewSeparateEntityCandidates = businessOverviewSeparateCounterpartyDisplayCandidate
? [businessOverviewSeparateCounterpartyDisplayCandidate]
@ -1936,6 +1998,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: predecomposeDateScope;
const normalizedAssistantTurnMeaningDateScope = rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(organizationClarificationBusinessOverviewLoop && !currentTurnCarriesExplicitPeriod) ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
? null
: assistantTurnMeaningDateScope;
@ -1950,7 +2013,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
/^\d{4}$/.test(rawDateScope) &&
normalizedPredecomposeDateScope &&
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
const explicitDateScope = rawAllTimeScopeSignal
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
? null
: normalizedAssistantTurnMeaningDateScope ??
(businessOverviewRawYearOverridesPredecomposeAsOf ? rawDateScope : normalizedPredecomposeDateScope) ??
@ -1960,7 +2024,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!normalizedAssistantTurnMeaningDateScope &&
!normalizedPredecomposeDateScope &&
!rawDateScope &&
normalizedFollowupDateScope);
normalizedFollowupDateScope &&
normalizedFollowupDateScope !== "all_time_scope");
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
const turnMeaning = {
raw_message: repairedUserText ?? rawUserText ?? null,
@ -2007,7 +2072,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed
: undefined,
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
explicit_entity_candidates: businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
previous_counterparty_value_flow_bundle: businessOverviewSignal && followupSeed.previousBidirectionalValueFlow
? followupSeed.previousBidirectionalValueFlow
@ -2222,6 +2287,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (followupAllTimeScopeApplied) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_from_followup_context");
}
if (suppressNegatedTaxOnlyDateScope) {
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
}
@ -2324,7 +2392,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (businessOverviewSuppressesFollowupCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty");
}
if (businessOverviewSeparateCounterpartySignal) {
if (shouldPreserveBusinessOverviewSeparateCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope");
}
if (businessOverviewSeparateCounterpartyCandidate) {
@ -2346,7 +2414,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty && !businessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
}
if (effectiveFollowupCounterparty &&

View File

@ -75,6 +75,19 @@ function createAssistantTransitionPolicy(deps) {
}
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
}
function hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage = null) {
return [userMessage, alternateMessage]
.filter((value) => deps.toNonEmptyString(value))
.map((value) => normalizeFollowupText(value).replace(/С/g, "Рµ"))
.some((normalized) => {
if (!normalized) {
return false;
}
const hasDocumentCue = /(?:\u0434\u043e\u043a\p{L}*|\u0441\u0447\p{L}*|\u043d\u0430\u043a\u043b\u0430\u0434\p{L}*|\u0430\u043a\u0442|document|docs?|invoice|receipt)/iu.test(normalized) || hasReadableDocumentsPivotCue(normalized);
const hasSelectedCounterpartyCue = /(?:\u043f\u043e\s+\u043d(?:\u0435\u043c\u0443|\u0435\u0439)|\u043f\u043e\s+\u044d\u0442(?:\u043e\u043c\u0443|\u043e\u0439)|\u0442\u0435\u043a\u0443\u0449\p{L}*\s+\u043e\u0431\u044a\u0435\u043a\p{L}*|\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*\s+(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u044a\u0435\u043a\u0442)|selected\s+(?:counterparty|object)|current\s+object)/iu.test(normalized);
return hasDocumentCue && hasSelectedCounterpartyCue;
});
}
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
return null;
@ -298,32 +311,115 @@ function createAssistantTransitionPolicy(deps) {
}
return flow;
}
function readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_value_flow_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_document_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readCounterpartyDocumentSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
const match = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (!match?.[1] || !match?.[2]) {
const directMatch = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (directMatch?.[1] && directMatch?.[2]) {
return {
counterparty: deps.toNonEmptyString(directMatch[1]),
document_count: Number(directMatch[2]),
direct_answer: firstLine
};
}
const summaryMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):[\s\S]{0,260}документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
if (!summaryMatch?.[1] || !summaryMatch?.[2]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(summaryMatch[1]),
document_count: Number(summaryMatch[2]),
direct_answer: summaryMatch[0].replace(/\s+/g, " ").trim()
};
}
function readCounterpartyValueFlowSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const match = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu);
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(match[1]),
document_count: Number(match[2]),
direct_answer: firstLine
incoming_customer_revenue: {
total_amount_human_ru: deps.toNonEmptyString(match[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: deps.toNonEmptyString(match[3])
},
net_amount_human_ru: deps.toNonEmptyString(match[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_confirmed_counterparty_boundary_summary"
};
}
function findRecentDiscoveryValueFlowBundle(items) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
if (!item || item.role !== "assistant" || !debug || typeof debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
if (debug && typeof debug === "object") {
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
}
}
const parsedFlow = readCounterpartyValueFlowSummaryFromItem(item);
if (parsedFlow) {
return parsedFlow;
}
}
return null;
}
function sameCounterpartyHint(expected, actual) {
const left = normalizeFollowupText(expected);
const right = normalizeFollowupText(actual);
if (!left || !right) {
return false;
}
return left === right || left.includes(right) || right.includes(left);
}
function findRecentPreviousCounterpartyValueFlowBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
@ -331,7 +427,8 @@ function createAssistantTransitionPolicy(deps) {
function findRecentCounterpartyDocumentBundle(items) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const summary = readCounterpartyDocumentSummaryFromItem(item);
@ -341,6 +438,28 @@ function createAssistantTransitionPolicy(deps) {
}
return null;
}
function findRecentPreviousCounterpartyDocumentBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
}
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
!hasInventoryItemFocusHint &&
@ -504,9 +623,15 @@ function createAssistantTransitionPolicy(deps) {
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false);
const earlyNavigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const earlyNavigationFocusObject = earlyNavigationSessionState.focusObject;
const selectedCounterpartyDocumentFollowupSignal = Boolean(deps.toNonEmptyString(earlyNavigationFocusObject?.label) &&
deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty" &&
hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage));
if (assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal) {
!compactCashflowFollowupSignal &&
!selectedCounterpartyDocumentFollowupSignal) {
return null;
}
const latestAddressItem = deps.findLastAddressAssistantItem(items);
@ -574,7 +699,7 @@ function createAssistantTransitionPolicy(deps) {
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationSessionState = earlyNavigationSessionState;
const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
@ -662,6 +787,7 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@ -686,6 +812,7 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@ -713,6 +840,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@ -729,6 +857,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@ -762,8 +891,19 @@ function createAssistantTransitionPolicy(deps) {
const sourceDiscoveryLoopSubjectResolutionOptional = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSubjectResolutionOptional)(carryoverSourceDebug);
const sourceDiscoveryRankingNeed = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryRankingNeed)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryBidirectionalValueFlow = readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ?? findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary = findRecentCounterpartyDocumentBundle(items);
const sourceDiscoveryCounterpartyHint = sourceDiscoveryLoopMetadataScopeHint ??
(deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty"
? deps.toNonEmptyString(earlyNavigationFocusObject?.label)
: null);
const sourceDiscoveryBidirectionalValueFlow = readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ??
readMcpDiscoveryPreviousCounterpartyValueFlowBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyValueFlowBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyValueFlowSummaryFromItem(previousAddressItem) ??
findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary = readMcpDiscoveryPreviousCounterpartyDocumentBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyDocumentBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyDocumentSummaryFromItem(previousAddressItem) ??
findRecentCounterpartyDocumentBundle(items);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent;
@ -917,6 +1057,14 @@ function createAssistantTransitionPolicy(deps) {
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(carryoverSourceDebug, deps.toNonEmptyString);
const navigationCounterpartyFocus = navigationFocusObjectType === "counterparty" ? navigationFocusObjectLabel : null;
const hasNavigationCounterpartyFocusCarryover = Boolean(navigationCounterpartyFocus &&
(hasValueFlowCarryoverSourceHint ||
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "open_items_by_counterparty_or_contract" ||
sourceDiscoveryLoopSelectedChainIdHint === "value_flow_comparison"));
const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
@ -924,6 +1072,15 @@ function createAssistantTransitionPolicy(deps) {
sourceIntentHint === "bank_operations_by_contract" ||
sourceIntentHint === "open_items_by_counterparty_or_contract";
previousFilters = (0, assistantContinuityPolicy_1.applyHistoricalPartyCarryoverFilters)(previousFilters, shouldBackfillHistoricalPartyAnchors, deps.findRecentAddressFilterValue(items, "contract"), deps.findRecentAddressFilterValue(items, "counterparty"), deps.toNonEmptyString);
if (hasNavigationCounterpartyFocusCarryover && navigationCounterpartyFocus) {
if (!previousAnchor) {
previousAnchorType = "counterparty";
previousAnchor = navigationCounterpartyFocus;
}
if (!deps.toNonEmptyString(previousFilters.counterparty)) {
previousFilters.counterparty = navigationCounterpartyFocus;
}
}
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
const authorityActiveOrganization = deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??
deps.normalizeOrganizationScopeValue(organizationAuthority.continuityActiveOrganization);

View File

@ -2152,6 +2152,35 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
}
function countBroadBusinessOverviewBridgeAxes(text: string): number {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasBroadCue =
/(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(
normalized
);
const hasCompanyScope =
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(
normalized
);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewBridgeAxes(normalized) >= 3;
}
function hasNomenclatureMarginRankingSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
@ -2357,6 +2386,10 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
);
}
if (hasBroadBusinessOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_multi_surface_deferred_to_discovery");
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}

View File

@ -3,7 +3,9 @@ import type { AssistantConversationItem } from "../types/assistant";
import type { AddressIntent } from "../types/addressQuery";
import {
readAddressDebugCounterparty,
readAddressDebugItem
readAddressDebugItem,
readAddressDebugOrganization,
readAddressDebugTemporalScope
} from "./assistantContinuityPolicy";
import {
ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
@ -21,7 +23,11 @@ const MAX_RESULT_SETS = 40;
const MAX_NAVIGATION_EVENTS = 120;
const MAX_ENTITY_REFS_PER_RESULT_SET = 40;
type AddressComparisonScope = NonNullable<AddressNavigationState["session_context"]["comparison_scope"]>;
type AddressComparisonProofBundles = NonNullable<AddressComparisonScope["proof_bundles"]>;
const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusObjectType>> = {
business_overview: "organization",
counterparty_activity_lifecycle: "counterparty",
customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty",
@ -45,6 +51,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
};
const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetType>> = {
business_overview: "profile_summary",
counterparty_activity_lifecycle: "counterparty_list",
customer_revenue_and_payments: "counterparty_list",
supplier_payouts_profile: "counterparty_list",
@ -83,6 +90,18 @@ function toObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function cloneRecord(value: unknown): Record<string, unknown> | null {
const record = toObject(value);
if (!record) {
return null;
}
try {
return JSON.parse(JSON.stringify(record)) as Record<string, unknown>;
} catch {
return { ...record };
}
}
function toNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
@ -91,6 +110,44 @@ function toNonEmptyString(value: unknown): string | null {
return trimmed.length > 0 ? trimmed : null;
}
function candidateLabel(value: unknown): string | null {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toObject(value);
if (!record) {
return null;
}
return (
toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text)
);
}
function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): string | null {
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entry?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const dataNeedGraph = toObject(turnInput?.data_need_graph);
const candidates = [
...(Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: []),
...(Array.isArray(turnMeaning?.explicit_entity_candidates) ? turnMeaning.explicit_entity_candidates : []),
...(Array.isArray(dataNeedGraph?.subject_candidates) ? dataNeedGraph.subject_candidates : [])
];
for (const candidate of candidates) {
const label = candidateLabel(candidate);
if (label) {
return label;
}
}
return null;
}
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
const normalized = toNonEmptyString(value);
if (!normalized) {
@ -228,6 +285,26 @@ function cloneFocusObject(value: AddressFocusObject | null): AddressFocusObject
};
}
function cloneComparisonProofBundles(value: unknown): AddressComparisonProofBundles | null {
const record = toObject(value);
if (!record) {
return null;
}
const counterpartyValueFlowBundle = cloneRecord(
record.counterparty_value_flow_bundle ?? record.previous_counterparty_value_flow_bundle
);
const counterpartyDocumentBundle = cloneRecord(
record.counterparty_document_bundle ?? record.previous_counterparty_document_bundle
);
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function cloneResultSet(input: AddressResultSet): AddressResultSet {
return {
result_set_id: input.result_set_id,
@ -296,8 +373,71 @@ function buildFocusObject(
};
}
function cloneComparisonScope(
value: AddressNavigationState["session_context"]["comparison_scope"]
): AddressNavigationState["session_context"]["comparison_scope"] {
if (!value || typeof value !== "object") {
return null;
}
return {
organization: cloneFocusObject(value.organization),
counterparty: cloneFocusObject(value.counterparty),
proof_bundles: cloneComparisonProofBundles(value.proof_bundles)
};
}
function sameBusinessLabel(left: unknown, right: unknown): boolean {
const normalizedLeft = toNonEmptyString(left)?.toLocaleLowerCase("ru-RU");
const normalizedRight = toNonEmptyString(right)?.toLocaleLowerCase("ru-RU");
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
}
function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknown>): AddressComparisonProofBundles | null {
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const counterpartyValueFlowBundle =
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
const counterparty =
toNonEmptyString(counterpartyValueFlowBundle?.counterparty) ??
toNonEmptyString(counterpartyDocumentBundle?.counterparty) ??
readNavigationDiscoveryCounterparty(debug);
if (!counterparty) {
return null;
}
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (organization) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty =
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
}
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
if (canonicalType === "item") {
@ -330,11 +470,13 @@ function capNavigationEvents(events: AddressNavigationEvent[]): AddressNavigatio
}
function isAddressAssistantItem(item: AssistantConversationItem): boolean {
return (
item.role === "assistant" &&
Boolean(item.debug) &&
toNonEmptyString(item.debug?.detected_mode) === "address_query"
);
if (item.role !== "assistant" || !item.debug) {
return false;
}
if (toNonEmptyString(item.debug.detected_mode) === "address_query") {
return true;
}
return item.debug.mcp_discovery_response_applied === true && Boolean(toNonEmptyString(item.debug.mcp_discovery_selected_chain_id));
}
export function createEmptyAddressNavigationState(
@ -348,6 +490,7 @@ export function createEmptyAddressNavigationState(
session_context: {
active_result_set_id: null,
active_focus_object: null,
comparison_scope: null,
last_confirmed_route: null,
date_scope: {
as_of_date: null,
@ -372,6 +515,7 @@ export function cloneAddressNavigationState(value: AddressNavigationState | null
session_context: {
active_result_set_id: value.session_context.active_result_set_id,
active_focus_object: cloneFocusObject(value.session_context.active_focus_object),
comparison_scope: cloneComparisonScope(value.session_context.comparison_scope),
last_confirmed_route: value.session_context.last_confirmed_route,
date_scope: {
as_of_date: value.session_context.date_scope.as_of_date,
@ -406,6 +550,9 @@ export function normalizeAddressNavigationState(
session_context: {
active_result_set_id: toNonEmptyString(context.active_result_set_id),
active_focus_object: cloneFocusObject(context.active_focus_object as AddressFocusObject | null),
comparison_scope: cloneComparisonScope(
context.comparison_scope as AddressNavigationState["session_context"]["comparison_scope"]
),
last_confirmed_route: toNonEmptyString(context.last_confirmed_route),
date_scope: {
as_of_date: toNonEmptyString(dateScope.as_of_date),
@ -464,20 +611,42 @@ export function evolveAddressNavigationStateWithAssistantItem(
return state;
}
const debug = item.debug as unknown as Record<string, unknown>;
const intent = toAddressIntent(debug.detected_intent);
if (intent === "unknown") {
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
const discoveryIntent =
selectedDiscoveryChain === "business_overview"
? "business_overview"
: selectedDiscoveryChain === "value_flow_comparison"
? "customer_revenue_and_payments"
: "unknown";
const detectedIntent = toNonEmptyString(debug.detected_intent);
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
if (intent === "unknown" && !trackableDiscoveryTurn) {
return state;
}
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe);
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
const derivedOrganizationScope =
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
const derivedCounterpartyScope =
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
: null;
const filtersWithDerivedScope =
derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
...filters,
organization: derivedOrganizationScope
organization: derivedOrganizationScope,
...(derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? { counterparty: derivedCounterpartyScope }
: {})
}
: derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? {
...filters,
counterparty: derivedCounterpartyScope
}
: filters;
const sourceRefs = routeId ? [routeId] : [];
@ -495,6 +664,41 @@ export function evolveAddressNavigationStateWithAssistantItem(
};
const previousResultSetId = state.session_context.active_result_set_id;
const focusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
const comparisonCounterparty =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readNavigationDiscoveryCounterparty(debug)
: null;
const comparisonOrganization =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
: null;
const currentComparisonProofBundles =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug)
: null;
const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles =
comparisonCounterparty &&
sameBusinessLabel(inheritedComparisonScope?.counterparty?.label, comparisonCounterparty)
? cloneComparisonProofBundles(inheritedComparisonScope?.proof_bundles)
: null;
const comparisonProofBundles = currentComparisonProofBundles ?? inheritedComparisonProofBundles;
const comparisonOrganizationObject =
comparisonOrganization
? buildFocusObject("organization", comparisonOrganization, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.organization ?? null);
const comparisonCounterpartyObject =
comparisonCounterparty
? buildFocusObject("counterparty", comparisonCounterparty, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.counterparty ?? null);
const comparisonScope =
comparisonOrganizationObject || comparisonCounterpartyObject || comparisonProofBundles
? {
organization: comparisonOrganizationObject,
counterparty: comparisonCounterpartyObject,
proof_bundles: comparisonProofBundles
}
: null;
const action = resolveNavigationAction(debug, Boolean(focusObject));
const navigationEvent: AddressNavigationEvent = {
event_id: `nav-${nanoid(10)}`,
@ -505,10 +709,11 @@ export function evolveAddressNavigationStateWithAssistantItem(
turn_index: turnIndex,
created_at: createdAt
};
const discoveryTemporalScope = readAddressDebugTemporalScope(debug, toNonEmptyString);
const normalizedDateScope = {
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date) ?? discoveryTemporalScope.asOfDate,
period_from: toNonEmptyString(filtersWithDerivedScope.period_from) ?? discoveryTemporalScope.periodFrom,
period_to: toNonEmptyString(filtersWithDerivedScope.period_to) ?? discoveryTemporalScope.periodTo
};
const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
const nextResultSets = capResultSets(
@ -522,6 +727,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
? {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? null,
comparison_scope: comparisonScope,
last_confirmed_route: routeId ?? null,
date_scope: {
as_of_date: normalizedDateScope.as_of_date,
@ -533,6 +739,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
: {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? state.session_context.active_focus_object,
comparison_scope: comparisonScope ?? state.session_context.comparison_scope,
last_confirmed_route: routeId ?? state.session_context.last_confirmed_route,
date_scope: {
as_of_date: normalizedDateScope.as_of_date ?? state.session_context.date_scope.as_of_date,

View File

@ -733,6 +733,44 @@ function needsVatCalendarDetails(userMessage: string | null | undefined): boolea
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
}
function needsVatPurchaseDateAnchorDisclosure(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(
text
);
}
function buildVatPurchaseDateAnchorDisclosureLine(
options: ComposeFactualReplyOptions,
periodWindowLabel: string | null
): string | null {
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
return null;
}
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const asOfTs = toUtcDayTimestamp(asOfDate);
const fromTs = toUtcDayTimestamp(periodFrom);
const toTs = toUtcDayTimestamp(periodTo);
if (
asOfDate &&
asOfTs !== null &&
fromTs !== null &&
toTs !== null &&
asOfTs >= fromTs &&
asOfTs <= toTs
) {
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
}
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
}
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
const text = normalizeQuestionText(userMessage);
if (!text) {
@ -3896,6 +3934,7 @@ function composeFactualReplyBody(
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`,
@ -3904,6 +3943,7 @@ function composeFactualReplyBody(
"Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`

View File

@ -76,6 +76,141 @@ function normalizeAddressReplyType(value: unknown): AssistantReplyType {
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
}
function sameBusinessLabel(left: unknown, right: unknown): boolean {
const normalizedLeft = toNullableString(left)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
const normalizedRight = toNullableString(right)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
return Boolean(
normalizedLeft &&
normalizedRight &&
(normalizedLeft === normalizedRight ||
normalizedLeft.includes(normalizedRight) ||
normalizedRight.includes(normalizedLeft))
);
}
function firstString(values: unknown[]): string | null {
for (const value of values) {
const text = toNullableString(value);
if (text) {
return text;
}
}
return null;
}
function legalOrganizationLabelFromClarification(value: unknown): string | null {
const text = toNullableString(value);
if (!text) {
return null;
}
const compact = text.replace(/\s+/gu, " ").replace(/[.!?]+$/u, "").trim();
if (compact.length > 120 || !/^(?:ООО|ПАО|АО|ИП)\s+\S/iu.test(compact)) {
return null;
}
return compact;
}
function cleanComparisonScopeCompanyLine(line: string, organization: string | null): string {
let clean = String(line ?? "")
.replace(/\bcompany-level\b/giu, "общий по компании")
.replace(/\breusable bundle\b/giu, "сохраненный подтвержденный срез");
if (organization) {
clean = clean.replace(/по компании\s+Альтернатива Плюс/iu, `по компании ${organization}`);
}
return clean.trim();
}
function buildComparisonScopeProofReply(input: {
baseReply: string;
debug: Record<string, unknown>;
session: unknown;
userMessage?: unknown;
}): { reply: string; audit: Record<string, unknown> } | null {
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const isBusinessOverview = toNullableString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview) {
return null;
}
const separateCandidates = Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: [];
const separateSubject = firstString([...separateCandidates, turnMeaning?.metadata_scope_hint]);
if (!separateSubject) {
return null;
}
const sessionRecord = toRecordObject(input.session);
const addressNavigationState = toRecordObject(sessionRecord?.address_navigation_state);
const sessionContext = toRecordObject(addressNavigationState?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle || !sameBusinessLabel(separateSubject, valueBundle.counterparty ?? comparisonCounterparty?.label)) {
return null;
}
const incoming = toRecordObject(valueBundle.incoming_customer_revenue);
const outgoing = toRecordObject(valueBundle.outgoing_supplier_payout);
const incomingAmount = toNullableString(incoming?.total_amount_human_ru);
const outgoingAmount = toNullableString(outgoing?.total_amount_human_ru);
const netAmount = toNullableString(valueBundle.net_amount_human_ru);
if (!incomingAmount && !outgoingAmount && !netAmount) {
return null;
}
const organization = legalOrganizationLabelFromClarification(input.userMessage)
?? toNullableString(turnMeaning?.explicit_organization_scope)
?? toNullableString(toRecordObject(comparisonScope?.organization)?.label);
const documentCount = Number(toRecordObject(documentBundle)?.document_count);
const documentText = Number.isFinite(documentCount) && documentCount > 0 ? `, документы: ${documentCount}` : "";
const lines = String(input.baseReply ?? "")
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const companyLine = cleanComparisonScopeCompanyLine(
lines[0] ?? `Коротко: по компании ${organization ?? "выбранной организации"} подтвержден общий денежный срез.`,
organization
);
const netDirection =
valueBundle.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в нашу сторону";
const counterparty = toNullableString(valueBundle.counterparty) ?? separateSubject;
return {
reply: [
`${companyLine}; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ${netDirection} ${netAmount ?? "0 руб."}${documentText}.`,
`Отдельно по контрагенту ${counterparty}: это ранее подтвержденный контрагентский срез, а не перенос общих сумм компании на контрагента.`,
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${counterparty} из общих сумм компании без отдельного контрагентского среза.`
].join("\n"),
audit: {
applied: true,
source: "address_navigation_state.comparison_scope.proof_bundles",
counterparty,
organization: organization ?? null,
document_count: Number.isFinite(documentCount) && documentCount > 0 ? documentCount : null
}
};
}
function buildAppliedMcpDiscoveryRoutePatch(
debug: Record<string, unknown>,
applied: boolean
): Record<string, unknown> {
if (!applied) {
return {};
}
const selectedChain = toNullableString(debug.mcp_discovery_selected_chain_id);
if (selectedChain === "business_overview") {
return {
detected_intent: "business_overview",
detected_intent_confidence: "high",
selected_recipe: "business_overview",
response_type: "LIMITED_WITH_REASON",
mcp_discovery_effective_response_route: "business_overview"
};
}
return {};
}
function normalizeAddressLaneDebug(value: unknown): AddressExecutionDebug {
return (toRecordObject(value) ?? {}) as unknown as AddressExecutionDebug;
}
@ -290,18 +425,27 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: guardedResponse.assistantReply;
const comparisonScopeProofReply = buildComparisonScopeProofReply({
baseReply: finalAssistantReply,
debug: debugWithResponseGuard,
session: input.getSession(input.sessionId),
userMessage: input.userMessage
});
const finalAssistantReplyWithComparisonProof = comparisonScopeProofReply?.reply ?? finalAssistantReply;
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
const finalDebug = {
...debugWithResponseGuard,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
assistantReply: finalAssistantReply,
assistantReply: finalAssistantReplyWithComparisonProof,
replyType: finalReplyType,
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
debug: finalDebug,

View File

@ -254,6 +254,332 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
};
}
function firstString(
values: unknown[],
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): string | null {
for (const value of values) {
const text = toNonEmptyString(value);
if (text) {
return text;
}
}
return null;
}
function comparableEntityName(value: string | null): string | null {
const text = compactLower(value);
return text ? text.replace(/["'«»„“”]+/g, "") : null;
}
function sameEntityHint(expected: string | null, actual: string | null): boolean {
const left = comparableEntityName(expected);
const right = comparableEntityName(actual);
if (!left || !right) {
return true;
}
return left === right || left.includes(right) || right.includes(left);
}
function isBusinessOverviewDiscoveryFollowup(
followupContext: Record<string, unknown> | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): boolean {
if (!followupContext) {
return false;
}
return [
followupContext.previous_discovery_pilot_scope,
followupContext.previous_discovery_loop_selected_chain_id,
followupContext.previous_discovery_loop_asked_domain_family,
followupContext.previous_intent,
followupContext.target_intent
]
.map((value) => toNonEmptyString(value))
.some((value) => value === "business_overview" || value === "business_overview_route_template_v1");
}
function businessOverviewCounterpartyHint(
followupContext: Record<string, unknown>,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): string | null {
const previousFilters = toRecordObject(followupContext.previous_filters);
const rootFilters = toRecordObject(followupContext.root_filters);
return (
toNonEmptyString(followupContext.previous_discovery_loop_metadata_scope_hint) ??
(toNonEmptyString(followupContext.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext.previous_anchor_value)
: null) ??
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty)
);
}
function turnMeaningCounterpartyName(
turnMeaningRef: Record<string, unknown>,
valueBundle: Record<string, unknown> | null,
documentBundle: Record<string, unknown> | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): string | null {
const separateEntities = Array.isArray(turnMeaningRef.business_overview_separate_entity_candidates)
? turnMeaningRef.business_overview_separate_entity_candidates
: [];
return (
toNonEmptyString(valueBundle?.counterparty) ??
toNonEmptyString(documentBundle?.counterparty) ??
toNonEmptyString(turnMeaningRef.metadata_scope_hint) ??
firstString(separateEntities, toNonEmptyString)
);
}
function parseBusinessOverviewProofBundlesFromText(
value: unknown,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const valueMatch = text.match(
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
);
const directDocumentMatch = text.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено\s+документов:\s*(\d+)/iu);
const summaryDocumentMatch = text.match(/документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
const counterparty =
toNonEmptyString(valueMatch?.[1]) ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueMatch
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueMatch[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueMatch[3])
},
net_amount_human_ru: toNonEmptyString(valueMatch[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function parseBusinessOverviewProofBundlesFromTextV2(
value: unknown,
counterpartyHint: string | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const comparableHint = comparableEntityName(counterpartyHint);
const lines = text
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const candidateLines = comparableHint
? lines.filter((line) => comparableEntityName(line)?.includes(comparableHint))
: lines;
const rubAmountPattern = /[0-9][0-9\s.,]*\s*\u0440\u0443\u0431\.?/giu;
const valueLine = candidateLines.find((line) => (line.match(rubAmountPattern) ?? []).length >= 3) ?? null;
const valueAmounts = valueLine?.match(rubAmountPattern) ?? [];
const directDocumentMatch = candidateLines
.map((line) =>
line.match(
/\u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^.\n]+)\.\s*\u041d\u0430\u0439\u0434\u0435\u043d\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432:\s*(\d+)/iu
)
)
.find(Boolean);
const summaryDocumentMatch = candidateLines
.map((line) =>
line.match(
/\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b\s+\u043f\u043e\s+\u0446\u0435\u043f\u043e\u0447\u043a\u0435:\s*\u043d\u0430\u0439\u0434\u0435\u043d\u043e\s*(\d+)/iu
)
)
.find(Boolean);
const counterparty = counterpartyHint ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueAmounts.length >= 3
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueAmounts[0])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueAmounts[1])
},
net_amount_human_ru: toNonEmptyString(valueAmounts[2]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function findRecentBusinessOverviewProofBundles(input: {
sessionItems: unknown[];
counterpartyHint: string | null;
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
if (!input.counterpartyHint) {
return null;
}
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(input.sessionItems[index]);
if (input.toNonEmptyString(item?.role) !== "assistant") {
continue;
}
const debug = toRecordObject(item?.debug);
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
if (turnMeaningRef) {
const valueBundle = toRecordObject(turnMeaningRef.previous_counterparty_value_flow_bundle);
const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle);
if (valueBundle || documentBundle) {
const bundleCounterparty = turnMeaningCounterpartyName(
turnMeaningRef,
valueBundle,
documentBundle,
input.toNonEmptyString
);
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
return { valueBundle, documentBundle };
}
}
}
const parsedBundles =
parseBusinessOverviewProofBundlesFromTextV2(item?.text, input.counterpartyHint, input.toNonEmptyString) ??
parseBusinessOverviewProofBundlesFromText(item?.text, input.toNonEmptyString);
if (!parsedBundles) {
continue;
}
const parsedCounterparty =
input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
continue;
}
return parsedBundles;
}
return null;
}
function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
followupContext: Record<string, unknown> | null;
sessionAddressNavigationState: unknown;
predecomposeContract: Record<string, unknown> | null;
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): Record<string, unknown> | null {
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const state = toRecordObject(input.sessionAddressNavigationState);
const sessionContext = toRecordObject(state?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle && !documentBundle) {
return input.followupContext;
}
const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext;
}
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const counterpartyHint =
(input.followupContext ? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString) : null) ??
input.toNonEmptyString(comparisonCounterparty?.label);
const bundleCounterparty =
input.toNonEmptyString(valueBundle?.counterparty) ?? input.toNonEmptyString(documentBundle?.counterparty);
if (!counterpartyHint || !sameEntityHint(counterpartyHint, bundleCounterparty)) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
previous_discovery_pilot_scope:
input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
"business_overview_route_template_v1",
previous_discovery_loop_status:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_status) ?? "awaiting_clarification",
previous_discovery_loop_selected_chain_id:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
previous_discovery_loop_pending_axes: Array.isArray(input.followupContext?.previous_discovery_loop_pending_axes)
? input.followupContext?.previous_discovery_loop_pending_axes
: ["organization"],
previous_discovery_loop_asked_domain_family:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
previous_discovery_loop_asked_action_family:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
previous_discovery_loop_metadata_scope_hint:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? counterpartyHint,
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? "counterparty",
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? counterpartyHint,
previous_filters: toRecordObject(input.followupContext?.previous_filters) ?? {},
previous_discovery_bidirectional_value_flow: currentValueBundle ?? valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? documentBundle ?? undefined
};
}
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
followupContext: Record<string, unknown> | null;
sessionItems: unknown[];
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): Record<string, unknown> | null {
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
return input.followupContext;
}
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const counterpartyHint = input.followupContext
? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString)
: null;
const proofBundles = findRecentBusinessOverviewProofBundles({
sessionItems: input.sessionItems,
counterpartyHint,
toNonEmptyString: input.toNonEmptyString
});
if (!proofBundles) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_discovery_bidirectional_value_flow:
currentValueBundle ?? proofBundles.valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
};
}
function hasSelectedObjectInventorySignal(text: string | null): boolean {
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
String(text ?? "")
@ -525,6 +851,17 @@ export async function buildAssistantAddressOrchestrationRuntime(
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
followupContext: discoveryFollowupContext,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithStateProofBundles = mergeBusinessOverviewProofBundlesFromNavigationState({
followupContext: discoveryFollowupContextWithProofBundles,
sessionAddressNavigationState: input.sessionAddressNavigationState,
predecomposeContract,
toNonEmptyString: input.toNonEmptyString
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
input.userMessage,
addressInputMessage,
@ -540,7 +877,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract,
followupContext: discoveryFollowupContext,
followupContext: discoveryFollowupContextWithStateProofBundles,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
})) as Record<string, unknown>;
} catch (error) {

View File

@ -414,6 +414,9 @@ function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
pilotScope: string | null,
actionFamily: string | null
): string | null {
if (pilotScope === "business_overview_route_template_v1" || actionFamily === "broad_evaluation") {
return "business_overview";
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}

View File

@ -131,6 +131,20 @@ function buildDeterministicSmalltalkLeadReply(): string {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function hasFirstTurnSmalltalkGreetingSignal(value: unknown): boolean {
const normalized = String(value ?? "")
.toLowerCase()
.replace(/\u0451/gu, "\u0435")
.replace(/\s+/gu, " ")
.trim();
if (!normalized) {
return false;
}
return /^(?:привет(?:ик)?|здравствуй(?:те)?|хай|йо|yo|hello|hi|че\s+как|че\s+там)(?:[\s,.!?;:()\-]+(?:как|дела|там|че|что|у\s+тебя|как\s+там|как\s+дела))*[\s,.!?;:()\-]*$/iu.test(
normalized
);
}
function hasConversationExecutiveSummarySignal(value: unknown): boolean {
const normalized = String(value ?? "")
.toLowerCase()
@ -215,6 +229,14 @@ export async function runAssistantLivingChatRuntime(
let knownOrganizations = [...organizationAuthority.knownOrganizations];
let selectedOrganization = organizationAuthority.selectedOrganization;
let activeOrganization = organizationAuthority.activeOrganization;
const shouldHandleFirstTurnSmalltalkDeterministically =
!selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
hasFirstTurnSmalltalkGreetingSignal(userMessage) &&
input.hasLivingChatSignal(userMessage);
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
? input.addressRuntimeMeta
: {}) as Record<string, unknown>;
@ -365,6 +387,27 @@ export async function runAssistantLivingChatRuntime(
} else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
} else if (shouldHandleFirstTurnSmalltalkDeterministically) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = Boolean(proactiveOffer);
livingChatSource = proactiveOffer
? "deterministic_smalltalk_with_proactive_scope_offer"
: "deterministic_smalltalk";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
} else {
chatText = await input.executeLlmChat();
const scriptGuard = input.applyScriptGuard(chatText, userMessage);
@ -385,36 +428,6 @@ export async function runAssistantLivingChatRuntime(
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope =
!selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {

View File

@ -42,7 +42,11 @@ export interface AssistantMcpDiscoveryExecutionHandoffContract {
reason_codes: string[];
}
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = ["value_flow"];
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = [
"value_flow",
"value_flow_comparison",
"business_overview"
];
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];

View File

@ -47,19 +47,66 @@ function normalizeQuestionText(value: unknown): string {
.trim();
}
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
const text = normalizeQuestionText([
function normalizedTurnAndGraphText(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): string {
return normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
}
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
return (
/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)
);
}
function requestsBroadBusinessOverviewSurface(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:не\s+обзор|просто\s+ден\p{L}*|одной\s+строк\p{L}*|только\s+итог|без\s+разбив\p{L}*)/iu.test(text)) {
return false;
}
if (/(?:бизнес[-\s]*обзор|взросл\p{L}{0,10}\s+бизнес|что\s+(?:пока\s+)?нельзя\s+утвержд)/iu.test(text)) {
return true;
}
const markers = [
/(?:ндс|налог\p{L}*)/iu,
/(?:долг\p{L}*|дебитор|кредитор)/iu,
/(?:склад|остатк|товар\p{L}*)/iu,
/(?:клиент|заказчик|покупател)/iu,
/(?:поставщик|получател)/iu,
/(?:оборот\p{L}*)/iu,
/(?:ограничен|не\s+подтвержд|нельзя\s+утвержд)/iu
];
return markers.filter((marker) => marker.test(text)).length >= 3;
}
function requestsCounterpartyLeaderSurface(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:кто|кому)[\s\S]{0,60}(?:больше\s+всего|крупнее\s+всего|основн\p{L}*)[\s\S]{0,60}(?:зан[её]с|прин[её]с|платил|ушло|получил|перев[её]л|заплатил|внес|вн[её]с)/iu.test(text)) {
return true;
}
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
}
function requestsCompactCashflowAnswer(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
@ -850,6 +897,49 @@ function buildPreviousCounterpartyValueFlowSummary(
};
}
function buildBoundarySummaryFromPreviousCounterpartyBundles(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract
): string | null {
const turnInput = toRecordObject(entryPoint.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const graph = toRecordObject(turnInput?.data_need_graph);
const bridge = toRecordObject(entryPoint.bridge);
const pilot = toRecordObject(bridge?.pilot);
const overview = toRecordObject(pilot?.derived_business_overview);
const isBusinessOverview =
toNonEmptyString(graph?.business_fact_family) === "business_overview" ||
toNonEmptyString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview || overview) {
return null;
}
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
separateSubject,
toRecordObject(turnMeaning?.previous_counterparty_document_bundle)
);
if (!separateSubject || !previousCounterpartySummary) {
return null;
}
const lines = organizationScope
? [
`Коротко: по компании ${organizationScope} в этом шаге нет нового полного company-level расчета; отдельно по выбранному контрагенту ${separateSubject} есть ранее подтвержденный контрагентский срез.`,
previousCounterpartySummary.line,
`Можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не подтверждает чистую прибыль, полный оборот или общую бизнес-роль ${separateSubject}; также нельзя смешивать этот контрагентский срез с выводами по компании без отдельного company-level расчета.`
]
: [
`Коротко: уточните, по какой компании/организации сравнить выбранного контрагента ${separateSubject}; company-level вывод без организации не подтверждаю.`,
previousCounterpartySummary.line,
`Уже можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не чистая прибыль, не полный оборот компании и не доказанная бизнес-роль ${separateSubject}; контрагентский срез нельзя смешивать с company-level выводом без выбранной компании.`
];
return joinBusinessReplyLines(lines);
}
function buildCompactBusinessOverviewReply(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
draft: Record<string, unknown>
@ -921,6 +1011,13 @@ function buildCompactBusinessOverviewReply(
: null;
const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const broadOverviewSurfaceRequested = requestsBroadBusinessOverviewSurface(turnMeaning, graph);
const counterpartyLeaderSurfaceRequested = requestsCounterpartyLeaderSurface(turnMeaning, graph);
const directMoneyOnlyAnswer =
directMoneyAnswer && !broadOverviewSurfaceRequested && !counterpartyLeaderSurfaceRequested;
const shouldIncludeCounterpartyLeaders =
!directMoneyOnlyAnswer || counterpartyLeaderSurfaceRequested || broadOverviewSurfaceRequested;
const shouldIncludeOverviewSurface = !directMoneyAnswer || broadOverviewSurfaceRequested;
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
const lines: string[] = [];
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
@ -931,9 +1028,22 @@ function buildCompactBusinessOverviewReply(
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const rawMessageComparable = compactComparable(rawMessage);
const organizationScopeComparable = compactComparable(organizationScope);
const plainOrganizationClarificationSelection = Boolean(
separateSubject &&
organizationScope &&
rawMessage &&
rawMessageComparable &&
organizationScopeComparable &&
rawMessageComparable.includes(organizationScopeComparable) &&
rawMessage.length <= 90 &&
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)
);
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
@ -955,6 +1065,23 @@ function buildCompactBusinessOverviewReply(
return joinBusinessReplyLines(lines);
}
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(
`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
);
if (previousCounterpartySummary) {
lines.push(previousCounterpartySummary.line);
} else {
lines.push(
`Отдельно по выбранному контрагенту ${separateSubject}: суммы компании на него не переношу; в этом шаге держу только границу, что это отдельный контрагентский контур.`
);
}
lines.push(
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${separateSubject} на основе company-level сумм без отдельного контрагентского среза.`
);
return joinBusinessReplyLines(lines);
}
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) {
@ -1178,7 +1305,13 @@ function buildCompactBusinessOverviewReply(
return joinBusinessReplyLines(lines);
}
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
if (
!separateSubject &&
!crossScopeExecutiveSummary &&
!counterpartyLeaderSurfaceRequested &&
!rankingNeed &&
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")
) {
const subject = organizationScope ?? "компания";
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
lines.push(
@ -1208,6 +1341,10 @@ function buildCompactBusinessOverviewReply(
: `- крупнейший получатель исходящих денег: ${topSupplier};`
);
}
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
lines.push(`- ${localizeLine(taxLine)}`);
}
const inventoryLine = businessOverviewInventoryLine(overview);
if (inventoryLine) {
lines.push(`- ${localizeLine(inventoryLine)}`);
@ -1220,7 +1357,7 @@ function buildCompactBusinessOverviewReply(
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
);
const missingOverviewFamilies: string[] = [];
if (!businessOverviewTaxLine(overview)) {
if (!taxLine) {
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
}
if (!debtLine) {
@ -1264,9 +1401,26 @@ function buildCompactBusinessOverviewReply(
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
(incomingAmount || outgoingAmount || netAmount);
if (explicitPeriodRankingOverview) {
lines.push(
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
);
if (counterpartyLeaderSurfaceRequested) {
const incomingLeaderText =
customerName && customerAmount
? topCustomerLooksFinancial
? `${customerName}${sentenceAmount(customerAmount) ?? customerAmount}; это банк/финансовый контур, не называю его обычной клиентской выручкой без назначения платежа${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
: `${customerName}${sentenceAmount(customerAmount) ?? customerAmount}`
: "не распознан";
const outgoingLeaderText = topSupplier
? topSupplierLooksFinancial
? `${topSupplier}; это банк/финансовый контур, не называю его обычным поставщиком без назначения платежа/договора${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: topSupplier
: "не распознан";
lines.push(
`Коротко: ${organizationPrefix}${period} больше всего занес ${incomingLeaderText}; больше всего ушло ${outgoingLeaderText}.`
);
} else {
lines.push(
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
);
}
lines.push(
`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
);
@ -1332,7 +1486,7 @@ function buildCompactBusinessOverviewReply(
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
);
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) {
if (shouldIncludeCounterpartyLeaders && customerName && customerAmount) {
lines.push(
topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
@ -1353,21 +1507,21 @@ function buildCompactBusinessOverviewReply(
);
}
if (!directMoneyAnswer && topSupplier) {
if (shouldIncludeCounterpartyLeaders && topSupplier) {
lines.push(
topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
);
}
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
if (shouldIncludeCounterpartyLeaders && (topCustomer || topSupplier)) {
lines.push(
topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
);
}
if (!directMoneyAnswer) {
if (shouldIncludeOverviewSurface) {
lines.push(
`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`
);
@ -1466,6 +1620,11 @@ function buildReplyText(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContra
return null;
}
const previousCounterpartyBoundaryReply = buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint);
if (previousCounterpartyBoundaryReply) {
return previousCounterpartyBoundaryReply;
}
const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft);
if (compactBidirectionalValueFlowReply) {
return compactBidirectionalValueFlowReply;

View File

@ -510,6 +510,13 @@ function hasExactDocumentListAddressReply(
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
return false;
}
if (
hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)
) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const isDocumentIntent =

View File

@ -432,7 +432,12 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(
reasonCodes,
executionHandoff.can_use_guarded_response
? "runtime_bridge_wired_to_guarded_hot_assistant_answer"
: "runtime_bridge_not_wired_to_hot_assistant_answer"
);
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);

View File

@ -117,6 +117,7 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
"этой компании",
"этой компанией",
"эту компанию",
"в целом",
"наша организация",
"нашей организации",
"нашей компанией"
@ -250,7 +251,13 @@ function normalizeFollowupCounterpartyCandidate(value: unknown): string | null {
if (!text || isInvalidEntityCandidate(text)) {
return null;
}
return text;
const cleaned = text
.replace(
/^(?:\u043f\u043e\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435|\u044b|\u0430\u043c|\u0430\u043c\u0438|\u0430\u0445)?\s+/iu,
""
)
.trim();
return cleaned && !isInvalidEntityCandidate(cleaned) ? cleaned : text;
}
function pushScopedEntityCandidate(
@ -702,12 +709,13 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
const normalizedDiscoveryEntities = discoveryEntities
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
.filter((entity): entity is string => Boolean(entity));
const normalizedLoopMetadataScopeHint = normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const groundedDiscoveryCounterparty =
ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
? null
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
: normalizedDiscoveryEntities[0] ?? normalizedLoopMetadataScopeHint;
const metadataScopeHint =
loopMetadataScopeHint ??
normalizedLoopMetadataScopeHint ??
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
@ -724,9 +732,10 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null);
const dateScope =
collectDateScopeFromFilters(previousFilters) ??
collectDateScopeFromFilters(rootFilters);
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
const dateScope = loopProvidedAllTimeScope
? "all_time_scope"
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
return {
pilotScope: effectivePilotScope,
domain: mapped.domain,
@ -986,7 +995,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text: string): boolea
function hasCrossScopeExecutiveSummarySignal(text: string): boolean {
return (
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary)/iu.test(
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0441\u0440\u0430\u0432\u043d\p{L}*|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary|brief(?:ly)?\s+compare)/iu.test(
text
) &&
/(?:\u0447\u0442\u043e\s+(?:\u043c\u044b\s+)?\u043f\u043e\u0434\u0442\u0432\u0435\u0440\p{L}*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043f\u043e\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|confirmed|company|organization)/iu.test(
@ -1013,6 +1022,35 @@ function hasPlainBusinessOverviewSignal(text: string): boolean {
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
}
function countBroadBusinessOverviewAxes(text: string): number {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewSurfaceSignal(text: string): boolean {
const normalized = compactLower(text);
if (!normalized) {
return false;
}
const hasBroadCue =
/(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(
normalized
);
const hasCompanyScope =
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(
normalized
);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewAxes(normalized) >= 3;
}
function hasBusinessOverviewSignal(text: string): boolean {
if (
hasCrossScopeExecutiveSummarySignal(text) ||
@ -1021,6 +1059,7 @@ function hasBusinessOverviewSignal(text: string): boolean {
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
hasPlainBusinessOverviewSignal(text) ||
hasBroadBusinessOverviewSurfaceSignal(text) ||
hasOrganizationLevelSupplierQualityOverviewSignal(text)
) {
return true;
@ -1101,6 +1140,15 @@ function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
);
}
function isGenericSelectedCounterpartyReference(value: string | null): boolean {
if (!value) {
return false;
}
return /^(?:(?:\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*|\u044d\u0442\p{L}*|\u0434\u0430\u043d\p{L}*|\u0442\u0435\u043a\u0443\u0449\p{L}*)\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*$|^(?:selected|chosen|current|this)\s+counterpart(?:y|ies)?$/iu.test(
value
);
}
function businessOverviewSeparateCounterpartyCandidateFromText(text: string): string | null {
const source = repairAddressMojibakeText(String(text ?? ""));
const patterns = [
@ -1109,7 +1157,7 @@ function businessOverviewSeparateCounterpartyCandidateFromText(text: string): st
];
for (const pattern of patterns) {
const candidate = normalizeFollowupCounterpartyCandidate(source.match(pattern)?.[1]);
if (candidate && !isInvalidEntityCandidate(candidate)) {
if (candidate && !isInvalidEntityCandidate(candidate) && !isGenericSelectedCounterpartyReference(candidate)) {
return candidate;
}
}
@ -1245,6 +1293,9 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
if (hasYearOnlyTimeTail) {
return null;
}
if (new Set(["в целом", "компания в целом", "организация в целом"]).has(comparable)) {
return null;
}
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
return null;
}
@ -1910,11 +1961,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
const businessOverviewSignal =
!businessOverviewCounterpartyValueFlowPivot &&
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
const organizationClarificationBusinessOverviewLoop = Boolean(
followupSeed.loopStatus === "awaiting_clarification" &&
followupSeed.loopSelectedChainId === "business_overview" &&
followupSeed.loopPendingAxes.includes("organization") &&
currentTurnOrganizationScope &&
!rawLifecycleSignal &&
!rawMetadataSignal
);
const businessOverviewSeparateCounterpartySignal = Boolean(
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
);
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
: organizationClarificationBusinessOverviewLoop
? followupSeed.counterparty ?? followupSeed.discoveryEntity ?? followupSeed.metadataScopeHint
: null;
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
@ -1944,6 +2005,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const businessOverviewSuppressesFollowupCounterparty = Boolean(
businessOverviewSignal &&
!businessOverviewSeparateCounterpartySignal &&
!organizationClarificationBusinessOverviewLoop &&
(rawBusinessOverviewSignal ||
businessOverviewContinuationSignal ||
broadBusinessEvaluationUnsupported ||
@ -2452,7 +2514,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
}
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
}
const businessOverviewSeparateCounterpartyDisplayCandidate = businessOverviewSeparateCounterpartySignal
const shouldPreserveBusinessOverviewSeparateCounterparty =
businessOverviewSeparateCounterpartySignal || organizationClarificationBusinessOverviewLoop;
const businessOverviewSeparateCounterpartyDisplayCandidate = shouldPreserveBusinessOverviewSeparateCounterparty
? preferredScopedDisplayName(businessOverviewSeparateCounterpartyCandidate, [
groundedFollowupEntity,
effectiveFollowupCounterparty,
@ -2461,7 +2525,24 @@ export function buildAssistantMcpDiscoveryTurnInput(
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
])
]) ??
preferredScopedDisplayName(
groundedFollowupEntity ??
effectiveFollowupCounterparty ??
followupSeed.discoveryEntity ??
normalizedPredecomposeCounterparty ??
rawScopedEntityCandidate ??
rawEntityCandidate,
[
groundedFollowupEntity,
effectiveFollowupCounterparty,
followupSeed.discoveryEntity,
normalizedPredecomposeCounterparty,
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
]
)
: null;
const businessOverviewSeparateEntityCandidates = businessOverviewSeparateCounterpartyDisplayCandidate
? [businessOverviewSeparateCounterpartyDisplayCandidate]
@ -2586,6 +2667,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const normalizedAssistantTurnMeaningDateScope =
rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(organizationClarificationBusinessOverviewLoop && !currentTurnCarriesExplicitPeriod) ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
? null
: assistantTurnMeaningDateScope;
@ -2603,8 +2685,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
normalizedPredecomposeDateScope &&
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
);
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
const explicitDateScope =
rawAllTimeScopeSignal
rawAllTimeScopeSignal || followupAllTimeScopeApplied
? null
: normalizedAssistantTurnMeaningDateScope ??
(businessOverviewRawYearOverridesPredecomposeAsOf ? rawDateScope : normalizedPredecomposeDateScope) ??
@ -2615,7 +2698,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
!normalizedAssistantTurnMeaningDateScope &&
!normalizedPredecomposeDateScope &&
!rawDateScope &&
normalizedFollowupDateScope
normalizedFollowupDateScope &&
normalizedFollowupDateScope !== "all_time_scope"
);
const clarificationLoopSeedApplied = Boolean(
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId
@ -2668,7 +2752,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed
: undefined,
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
explicit_entity_candidates:
businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
previous_counterparty_value_flow_bundle:
businessOverviewSignal && followupSeed.previousBidirectionalValueFlow
@ -2897,6 +2982,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (followupAllTimeScopeApplied) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_from_followup_context");
}
if (suppressNegatedTaxOnlyDateScope) {
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
}
@ -2999,7 +3087,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (businessOverviewSuppressesFollowupCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty");
}
if (businessOverviewSeparateCounterpartySignal) {
if (shouldPreserveBusinessOverviewSeparateCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope");
}
if (businessOverviewSeparateCounterpartyCandidate) {
@ -3023,7 +3111,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty && !businessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
}
if (

View File

@ -136,6 +136,26 @@ export function createAssistantTransitionPolicy(deps) {
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
}
function hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage = null) {
return [userMessage, alternateMessage]
.filter((value) => deps.toNonEmptyString(value))
.map((value) => normalizeFollowupText(value).replace(/С/g, "Рµ"))
.some((normalized) => {
if (!normalized) {
return false;
}
const hasDocumentCue =
/(?:\u0434\u043e\u043a\p{L}*|\u0441\u0447\p{L}*|\u043d\u0430\u043a\u043b\u0430\u0434\p{L}*|\u0430\u043a\u0442|document|docs?|invoice|receipt)/iu.test(
normalized
) || hasReadableDocumentsPivotCue(normalized);
const hasSelectedCounterpartyCue =
/(?:\u043f\u043e\s+\u043d(?:\u0435\u043c\u0443|\u0435\u0439)|\u043f\u043e\s+\u044d\u0442(?:\u043e\u043c\u0443|\u043e\u0439)|\u0442\u0435\u043a\u0443\u0449\p{L}*\s+\u043e\u0431\u044a\u0435\u043a\p{L}*|\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*\s+(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u044a\u0435\u043a\u0442)|selected\s+(?:counterparty|object)|current\s+object)/iu.test(
normalized
);
return hasDocumentCue && hasSelectedCounterpartyCue;
});
}
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
return null;
@ -425,20 +445,71 @@ export function createAssistantTransitionPolicy(deps) {
return flow;
}
function readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_value_flow_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_document_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readCounterpartyDocumentSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
const match = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (!match?.[1] || !match?.[2]) {
const directMatch = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (directMatch?.[1] && directMatch?.[2]) {
return {
counterparty: deps.toNonEmptyString(directMatch[1]),
document_count: Number(directMatch[2]),
direct_answer: firstLine
};
}
const summaryMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):[\s\S]{0,260}документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
if (!summaryMatch?.[1] || !summaryMatch?.[2]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(summaryMatch[1]),
document_count: Number(summaryMatch[2]),
direct_answer: summaryMatch[0].replace(/\s+/g, " ").trim()
};
}
function readCounterpartyValueFlowSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const match = text.match(
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
);
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(match[1]),
document_count: Number(match[2]),
direct_answer: firstLine
incoming_customer_revenue: {
total_amount_human_ru: deps.toNonEmptyString(match[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: deps.toNonEmptyString(match[3])
},
net_amount_human_ru: deps.toNonEmptyString(match[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_confirmed_counterparty_boundary_summary"
};
}
@ -446,12 +517,51 @@ export function createAssistantTransitionPolicy(deps) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
if (!item || item.role !== "assistant" || !debug || typeof debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
if (debug && typeof debug === "object") {
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
}
}
const parsedFlow = readCounterpartyValueFlowSummaryFromItem(item);
if (parsedFlow) {
return parsedFlow;
}
}
return null;
}
function sameCounterpartyHint(expected, actual) {
const left = normalizeFollowupText(expected);
const right = normalizeFollowupText(actual);
if (!left || !right) {
return false;
}
return left === right || left.includes(right) || right.includes(left);
}
function findRecentPreviousCounterpartyValueFlowBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
@ -460,7 +570,8 @@ export function createAssistantTransitionPolicy(deps) {
function findRecentCounterpartyDocumentBundle(items) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const summary = readCounterpartyDocumentSummaryFromItem(item);
@ -471,6 +582,29 @@ export function createAssistantTransitionPolicy(deps) {
return null;
}
function findRecentPreviousCounterpartyDocumentBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
}
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
if (
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
@ -681,10 +815,22 @@ export function createAssistantTransitionPolicy(deps) {
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false);
const earlyNavigationSessionState = resolveNavigationSessionContextState(
addressNavigationState,
deps.toNonEmptyString,
deps.normalizeOrganizationScopeValue
);
const earlyNavigationFocusObject = earlyNavigationSessionState.focusObject;
const selectedCounterpartyDocumentFollowupSignal = Boolean(
deps.toNonEmptyString(earlyNavigationFocusObject?.label) &&
deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty" &&
hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage)
);
if (
assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal
!compactCashflowFollowupSignal &&
!selectedCounterpartyDocumentFollowupSignal
) {
return null;
}
@ -784,11 +930,7 @@ export function createAssistantTransitionPolicy(deps) {
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const hasBusinessOverviewCarryoverSourceHint =
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = resolveNavigationSessionContextState(
addressNavigationState,
deps.toNonEmptyString,
deps.normalizeOrganizationScopeValue
);
const navigationSessionState = earlyNavigationSessionState;
const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
@ -912,6 +1054,7 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@ -937,6 +1080,7 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@ -969,6 +1113,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@ -987,6 +1132,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@ -1062,9 +1208,22 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryCounterpartyHint =
sourceDiscoveryLoopMetadataScopeHint ??
(deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty"
? deps.toNonEmptyString(earlyNavigationFocusObject?.label)
: null);
const sourceDiscoveryBidirectionalValueFlow =
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ?? findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary = findRecentCounterpartyDocumentBundle(items);
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ??
readMcpDiscoveryPreviousCounterpartyValueFlowBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyValueFlowBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyValueFlowSummaryFromItem(previousAddressItem) ??
findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary =
readMcpDiscoveryPreviousCounterpartyDocumentBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyDocumentBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyDocumentSummaryFromItem(previousAddressItem) ??
findRecentCounterpartyDocumentBundle(items);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@ -1240,6 +1399,17 @@ export function createAssistantTransitionPolicy(deps) {
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
let previousFilters = resolveAddressDebugCarryoverFilters(carryoverSourceDebug, deps.toNonEmptyString);
const navigationCounterpartyFocus =
navigationFocusObjectType === "counterparty" ? navigationFocusObjectLabel : null;
const hasNavigationCounterpartyFocusCarryover = Boolean(
navigationCounterpartyFocus &&
(hasValueFlowCarryoverSourceHint ||
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "open_items_by_counterparty_or_contract" ||
sourceDiscoveryLoopSelectedChainIdHint === "value_flow_comparison")
);
const shouldBackfillHistoricalPartyAnchors =
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
@ -1254,6 +1424,15 @@ export function createAssistantTransitionPolicy(deps) {
deps.findRecentAddressFilterValue(items, "counterparty"),
deps.toNonEmptyString
);
if (hasNavigationCounterpartyFocusCarryover && navigationCounterpartyFocus) {
if (!previousAnchor) {
previousAnchorType = "counterparty";
previousAnchor = navigationCounterpartyFocus;
}
if (!deps.toNonEmptyString(previousFilters.counterparty)) {
previousFilters.counterparty = navigationCounterpartyFocus;
}
}
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
const authorityActiveOrganization =
deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??

View File

@ -65,6 +65,14 @@ export interface AddressNavigationEvent {
export interface AddressNavigationSessionContext {
active_result_set_id: string | null;
active_focus_object: AddressFocusObject | null;
comparison_scope: {
organization: AddressFocusObject | null;
counterparty: AddressFocusObject | null;
proof_bundles: {
counterparty_value_flow_bundle: Record<string, unknown> | null;
counterparty_document_bundle: Record<string, unknown> | null;
} | null;
} | null;
last_confirmed_route: string | null;
date_scope: {
as_of_date: string | null;

View File

@ -7,6 +7,7 @@ import type {
} from "./assistantRuntimeContracts";
export type AddressIntent =
| "business_overview"
| "period_coverage_profile"
| "document_type_and_account_section_profile"
| "counterparty_population_and_roles"

View File

@ -529,6 +529,11 @@ export interface AssistantDebugPayload {
fa_live_route_audit?: FaLiveRouteAuditDebug;
eligibility_time_basis?: GroundedAnswerEligibilityGuardDebug["eligibility_time_basis"];
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
mcp_discovery_response_applied?: boolean;
mcp_discovery_selected_chain_id?: string | null;
mcp_discovery_effective_response_route?: string | null;
assistant_mcp_discovery_entry_point_v1?: unknown;
mcp_discovery_response_candidate_v1?: unknown;
followup_state_usage?: FollowupStateUsageDebug;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;

View File

@ -72,6 +72,15 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.reasons).toContain("unicode_business_overview_debt_position_deferred_to_discovery");
});
it("defers colloquial multi-surface company overview to discovery before exact VAT", () => {
const result = resolveAddressIntent(
"\u041f\u043e-\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0447\u0442\u043e \u043f\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u0432\u0438\u0434\u043d\u043e \u0432 1\u0421 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0430\u043c, \u0441\u043a\u043b\u0430\u0434\u0443, \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c \u0438 \u0433\u0434\u0435 \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c \u0432\u044b\u0432\u043e\u0434?"
);
expect(result.intent).toBe("unknown");
expect(result.reasons).toContain("unicode_business_overview_multi_surface_deferred_to_discovery");
});
it("detects specific counterparty turnover wording as revenue profile", () => {
const result = resolveAddressIntent(
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a"

View File

@ -55,6 +55,231 @@ describe("address navigation state", () => {
expect(evolved.navigation_history[0]?.action).toBe("open");
});
it("captures organization focus from applied business overview discovery turns", () => {
const base = createEmptyAddressNavigationState("asst-bo", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-bo1",
session_id: "asst-bo",
role: "assistant",
text: "Коротко: по данным ООО Альтернатива Плюс за 2020 подтвержден бизнес-обзор.",
reply_type: "partial_coverage",
created_at: "2026-04-12T10:10:00.000Z",
trace_id: "address-bo",
debug: {
detected_mode: "address_query",
detected_intent: "unknown",
selected_recipe: null,
extracted_filters: {},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "business_overview",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
},
dialog_continuation_contract_v2: {
decision: "new_topic"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
expect(evolved.result_sets[0]?.route_id).toBe("business_overview");
expect(evolved.result_sets[0]?.filters.organization).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.active_focus_object?.object_type).toBe("organization");
expect(evolved.session_context.active_focus_object?.label).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.organization_scope).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => {
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-vf1",
session_id: "asst-vf",
role: "assistant",
text: "\u041f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438.",
reply_type: "partial_coverage",
created_at: "2026-04-12T10:12:00.000Z",
trace_id: "chat-vf",
debug: {
detected_mode: "chat",
detected_intent: "unknown",
selected_recipe: null,
extracted_filters: {},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "value_flow_comparison",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "compare_incoming_outgoing",
explicit_entity_candidates: ["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"],
stale_replay_forbidden: true,
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
answer_draft: {
answer_mode: "confirmed_factual"
}
}
},
dialog_continuation_contract_v2: {
decision: "new_topic"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
expect(evolved.result_sets[0]?.route_id).toBe("value_flow_comparison");
expect(evolved.result_sets[0]?.intent).toBe("customer_revenue_and_payments");
expect(evolved.result_sets[0]?.filters.counterparty).toBe("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
expect(evolved.session_context.active_focus_object?.label).toBe("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("keeps selected counterparty focus for company boundary summaries", () => {
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-boundary",
session_id: "asst-boundary",
role: "assistant",
text:
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез.\n" +
"Отдельно по выбранному контрагенту Группа СВК: суммы компании на него не переношу.",
created_at: "2026-04-12T10:05:00.000Z",
debug: {
detected_intent: "unknown",
mcp_discovery_selected_chain_id: "business_overview",
mcp_discovery_response_applied: true,
extracted_filters: {
organization: "Альтернатива Плюс"
},
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "Альтернатива Плюс"
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: []
}
}
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(initial, assistantItem, 1);
expect(evolved.session_context.organization_scope).toBe("Альтернатива Плюс");
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(evolved.session_context.comparison_scope?.organization?.label).toBe("Альтернатива Плюс");
expect(evolved.session_context.comparison_scope?.counterparty?.label).toBe("Группа СВК");
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
});
it("carries comparison proof bundles through organization clarification", () => {
const org = "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const counterparty = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
const valueFlowBundle = {
counterparty,
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
};
const documentBundle = { counterparty, document_count: 19 };
const initial = createEmptyAddressNavigationState("asst-proof", "2026-04-12T10:00:00.000Z");
const boundaryItem = {
message_id: "msg-boundary-proof",
session_id: "asst-proof",
role: "assistant",
text: `Коротко: уточните организацию для сравнения ${counterparty}.`,
created_at: "2026-04-12T10:05:00.000Z",
debug: {
detected_intent: "unknown",
mcp_discovery_selected_chain_id: "business_overview",
mcp_discovery_response_applied: true,
extracted_filters: {},
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterparty],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle
}
}
}
}
} as any;
const afterBoundary = evolveAddressNavigationStateWithAssistantItem(initial, boundaryItem, 1);
expect(afterBoundary.session_context.comparison_scope?.organization).toBeNull();
expect(afterBoundary.session_context.comparison_scope?.counterparty?.label).toBe(counterparty);
expect(afterBoundary.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
const clarifiedItem = {
message_id: "msg-boundary-org",
session_id: "asst-proof",
role: "assistant",
text: `Коротко: по компании ${org} подтвержден company-level срез. Отдельно по ${counterparty}: границу не теряю.`,
created_at: "2026-04-12T10:06:00.000Z",
debug: {
detected_intent: "business_overview",
mcp_discovery_selected_chain_id: "business_overview",
mcp_discovery_response_applied: true,
extracted_filters: { organization: org },
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterparty],
explicit_organization_scope: org
}
}
}
}
} as any;
const afterClarification = evolveAddressNavigationStateWithAssistantItem(afterBoundary, clarifiedItem, 2);
expect(afterClarification.session_context.comparison_scope?.organization?.label).toBe(org);
expect(afterClarification.session_context.comparison_scope?.counterparty?.label).toBe(counterparty);
expect(afterClarification.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
expect(afterClarification.session_context.comparison_scope?.proof_bundles?.counterparty_document_bundle).toEqual(
documentBundle
);
});
it("tracks drilldown event for follow-up continuation turn", () => {
const initial = normalizeAddressNavigationState(
{

View File

@ -2145,6 +2145,43 @@ describe("address compose stage utf8 headers", () => {
expect(reply.semantics?.balance_confirmed).toBe(true);
});
it("explains purchase-date anchor for confirmed VAT tax-period reply", () => {
const reply = composeFactualReply(
"vat_liability_confirmed_for_tax_period",
[
{
period: "2015-02-28T23:59:59Z",
registrator: "VAT_BOOK_SALES",
account_dt: "68.02",
account_kt: "",
amount: 3500000,
analytics: []
},
{
period: "2015-02-28T23:59:59Z",
registrator: "VAT_BOOK_PURCHASES",
account_dt: "19",
account_kt: "",
amount: 868612,
analytics: []
}
],
{
userMessage: "ндс можешь прикинуть на дату покупки рабочей станции?",
periodFrom: "2015-02-01",
periodTo: "2015-02-28",
asOfDate: "2016-03-31",
organizationHint: "ООО Альтернатива Плюс",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Якорь периода: дата покупки из вопроса/контекста");
expect(reply.text).toContain("01.02.2015..28.02.2015");
expect(reply.text).toContain("книг продаж/покупок");
});
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
const reply = composeFactualReply(
"vat_payable_forecast",

View File

@ -236,6 +236,216 @@ describe("assistant address lane response runtime adapter", () => {
expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний");
});
it("aligns final route metadata when a business-overview discovery candidate replaces a stale exact reply", () => {
const finalizeAddressTurn = vi.fn((input) => ({
response: {
ok: true,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
debug: input.debug
}
}));
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-business-overview-address",
userMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
effectiveAddressUserMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
addressLane: {
handled: true,
reply_text: "Контрагент: Группа СВК. Найдено документов: 19.",
reply_type: "factual",
debug: {
detected_intent: "list_documents_by_counterparty",
selected_recipe: "address_documents_by_counterparty_v1",
response_type: "FACTUAL_LIST"
}
},
llmPreDecomposeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
raw_message: "Собери короткий бизнес-итог по компании и отдельно по СВК."
},
data_need_graph: {
business_fact_family: "business_overview",
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
planner: {
selected_chain_id: "business_overview"
},
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Business overview answer",
confirmed_lines: ["Confirmed company business overview facts"],
inference_lines: [],
unknown_lines: ["Pure profit is not proven"],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: (debug) => ({ ...(debug as Record<string, unknown>) }),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
appendItem: () => {},
getSession: () => ({ session_id: "asst-business-overview-address", updated_at: "", items: [], investigation_state: null } as any),
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-business-overview-address",
finalizeAddressTurn
});
expect((runtime.response as any).debug).toEqual(
expect.objectContaining({
mcp_discovery_response_applied: true,
detected_intent: "business_overview",
selected_recipe: "business_overview",
mcp_discovery_effective_response_route: "business_overview"
})
);
expect(String((runtime.response as any).assistant_reply)).toContain("47 628 853,03");
});
it("keeps comparison-scope proof augmentation business-facing for organization clarification", () => {
const finalizeAddressTurn = vi.fn((input) => ({
response: {
ok: true,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
debug: input.debug
}
}));
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-comparison-proof",
userMessage: "ООО Альтернатива Плюс",
effectiveAddressUserMessage: "ООО Альтернатива Плюс",
addressLane: {
handled: true,
reply_text:
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез: входящие 285 819 547,57 руб.",
reply_type: "partial_coverage",
debug: {}
},
llmPreDecomposeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: true,
discovery_attempted: true,
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
business_overview_separate_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "Альтернатива Плюс"
}
},
bridge: {
bridge_status: "not_applicable",
user_facing_response_allowed: false,
business_fact_answer_allowed: false,
requires_user_clarification: false
},
reason_codes: []
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: () => ({}),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
appendItem: () => {},
getSession: () =>
({
session_id: "asst-comparison-proof",
updated_at: "",
items: [],
investigation_state: null,
address_navigation_state: {
session_context: {
comparison_scope: {
organization: { label: "Альтернатива Плюс" },
counterparty: { label: "Группа СВК" },
proof_bundles: {
counterparty_value_flow_bundle: {
counterparty: "Группа СВК",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб.",
net_direction: "net_incoming"
},
counterparty_document_bundle: {
document_count: 19
}
}
}
}
}
}) as any,
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-comparison-proof",
finalizeAddressTurn
});
const reply = String((runtime.response as any).assistant_reply);
expect(reply).toContain("по компании ООО Альтернатива Плюс");
expect(reply).toContain("получили 20 653 490 руб.");
expect(reply).toContain("документы: 19");
expect(reply).not.toContain("company-level");
expect(reply).not.toContain("reusable bundle");
expect((runtime.response as any).debug.comparison_scope_response_augmentation_v1).toEqual(
expect.objectContaining({
applied: true,
organization: "ООО Альтернатива Плюс",
counterparty: "Группа СВК",
document_count: 19
})
);
});
it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => {
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-2",

View File

@ -307,6 +307,320 @@ describe("assistant address orchestration runtime adapter", () => {
);
});
it("recovers business overview proof bundles from session history before MCP discovery", async () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: "Group SVK",
document_count: 19
};
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
debug: {
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Group SVK"],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
metadata_scope_hint: "Group SVK"
}
}
}
}
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" },
semantics: { anchor_kind: "organization", anchor_value: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_anchor_type: "counterparty",
previous_anchor_value: "Group SVK",
previous_filters: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
})
})
);
});
it("recovers business overview proof bundles from previous assistant summary text", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
text:
"\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443 Group SVK: " +
"\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 20 653 490 \u0440\u0443\u0431., " +
"\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 2 129 651 \u0440\u0443\u0431., " +
"\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e \u0432 \u043d\u0430\u0448\u0443 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 18 523 839 \u0440\u0443\u0431. " +
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u0446\u0435\u043f\u043e\u0447\u043a\u0435: \u043d\u0430\u0439\u0434\u0435\u043d\u043e 19."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_anchor_type: "counterparty",
previous_anchor_value: "Group SVK",
previous_filters: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_bidirectional_value_flow: expect.objectContaining({
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 \u0440\u0443\u0431." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 \u0440\u0443\u0431." },
net_amount_human_ru: "18 523 839 \u0440\u0443\u0431."
}),
previous_discovery_document_summary: expect.objectContaining({
counterparty: "Group SVK",
document_count: 19
})
})
})
);
});
it("recovers business overview proof bundles from navigation comparison state", async () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
};
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionAddressNavigationState: {
session_context: {
comparison_scope: {
organization: null,
counterparty: { label: "Group SVK" },
proof_bundles: {
counterparty_value_flow_bundle: valueFlowBundle,
counterparty_document_bundle: documentBundle
}
}
}
},
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: null })),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
})
})
);
});
it("does not infer a counterparty proof bundle from company-only overview totals", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "who brought money",
sessionItems: [
{
role: "assistant",
text:
"Деньги: входящие 47 628 853,03 руб., исходящие 43 763 351,53 руб., расчетное операционное нетто 3 865 501,50 руб.\n" +
"НДС: продажи 2 002 138,93 руб., покупки 1 784 850,48 руб., НДС к уплате 217 288,45 руб."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "who brought money",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_filters: { organization: "Alt Plus" }
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
const call = runMcpDiscoveryRuntimeEntryPoint.mock.calls[0]?.[0] as any;
expect(call.followupContext.previous_discovery_bidirectional_value_flow).toBeUndefined();
expect(call.followupContext.previous_discovery_document_summary).toBeUndefined();
});
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",

View File

@ -176,6 +176,31 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("handles first-turn smalltalk deterministically without relying on LLM", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: [],
error: null
}));
const input = buildRuntimeInput({
userMessage: "приветик - че как там дела",
resolveDataScopeProbe,
buildAssistantProactiveOrganizationOfferReply: () => ""
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Привет! Всё нормально.");
expect(output.chatText).not.toContain("llm-text");
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(false);
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(0);
expect(input.__spies.executeLlmChat).not.toHaveBeenCalled();
expect(resolveDataScopeProbe).toHaveBeenCalledTimes(1);
});
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
@ -438,6 +463,7 @@ describe("assistant living chat runtime adapter", () => {
"ООО Лайсвуд",
"РАЙМ"
]);
expect(input.__spies.executeLlmChat).not.toHaveBeenCalled();
});
it("does not add proactive organization offer after the session already has assistant context", async () => {

View File

@ -475,6 +475,7 @@ describe("assistant MCP discovery response candidate", () => {
effective_message:
"Дать краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и банковскую границу.",
explicit_organization_scope: "ООО Альтернатива Плюс"
,asked_action_family: "broad_evaluation"
},
data_need_graph: {
business_fact_family: "business_overview",
@ -778,6 +779,199 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("wide overview");
});
it("keeps broad overview layers when direct-money reason is present", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020 год: деньги, нетто, НДС, долги, склад, клиенты, поставщики и что нельзя утверждать.",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
is_likely_financial_institution: true
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб."
}
],
top_suppliers: [
{
axis_value: "Департамент капитального ремонта города Москвы",
total_amount_human_ru: "9 612 904,90 руб."
}
],
tax_position: {
sales_vat_amount_human_ru: "2 717 288,45 руб.",
purchase_vat_amount_human_ru: "2 500 000 руб.",
net_vat_amount_human_ru: "217 288,45 руб.",
net_vat_direction: "vat_to_pay"
},
debt_position: {
receivables: { total_amount_human_ru: "25 000 000 руб." },
payables: { total_amount_human_ru: "3 318 824,40 руб." },
net_debt_position_amount_human_ru: "21 681 175,60 руб.",
net_debt_position_direction: "net_receivable"
},
inventory_position: {
total_amount_human_ru: "716 418,33 руб.",
rows_matched: 34
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "wide overview should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("Что видно:");
expect(candidate.reply_text).toContain("НДС:");
expect(candidate.reply_text).toContain("Долги:");
expect(candidate.reply_text).toContain("Склад:");
expect(candidate.reply_text).toContain("Группа СВК");
expect(candidate.reply_text).not.toContain("по деньгам плюс");
expect(candidate.reply_text).not.toContain("wide overview");
});
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в 2020.",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
is_likely_financial_institution: true
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб."
}
],
top_suppliers: [
{
axis_value: "Департамент капитального ремонта города Москвы",
total_amount_human_ru: "9 612 904,90 руб."
}
],
tax_position: {
net_vat_amount_human_ru: "217 288,45 руб.",
net_vat_direction: "vat_to_pay"
},
debt_position: {
receivables: { total_amount_human_ru: "25 000 000 руб." },
payables: { total_amount_human_ru: "3 318 824,40 руб." },
net_debt_position_amount_human_ru: "21 681 175,60 руб.",
net_debt_position_direction: "net_receivable"
},
inventory_position: {
total_amount_human_ru: "716 418,33 руб.",
rows_matched: 34
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "wide overview should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("47 628 853,03");
expect(candidate.reply_text).toContain("43 763 351,53");
expect(candidate.reply_text).toContain("3 865 501,50");
expect(candidate.reply_text).toContain("СБЕРБАНК, ПАО");
expect(candidate.reply_text).toContain("Группа СВК");
expect(candidate.reply_text).toContain("Департамент капитального ремонта города Москвы");
expect(candidate.reply_text).not.toContain("по деньгам плюс");
expect(candidate.reply_text).not.toContain("НДС:");
expect(candidate.reply_text).not.toContain("Склад:");
expect(candidate.reply_text).not.toContain("wide overview");
});
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@ -902,6 +1096,83 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("Самый крупный подтвержденный клиент");
});
it("answers colloquial money leader follow-ups with leaders first", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки.",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: "top_desc",
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
counterparty_role_hint: "financial_institution"
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб.",
counterparty_role_hint: "ordinary_counterparty"
}
],
top_suppliers: [
{
axis_value: "Департамент капитального ремонта города Москвы.",
total_amount_human_ru: "9 612 904,90 руб."
}
],
yearly_breakdown: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Company summary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("больше всего занес СБЕРБАНК, ПАО");
expect(firstLine).toContain("не называю его обычной клиентской выручкой");
expect(firstLine).toContain("крупнейший небанковский входящий контрагент: Группа СВК");
expect(firstLine).toContain("больше всего ушло Департамент капитального ремонта города Москвы.");
expect(firstLine).not.toContain("выглядит как бизнес");
});
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@ -1107,6 +1378,128 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).toContain("ранее подтвержденный контрагентский срез");
});
it("answers selected counterparty boundary summaries from previous bundles without asking for organization", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Группа СВК"],
previous_counterparty_value_flow_bundle: {
counterparty: "Группа СВК",
incoming_customer_revenue: {
total_amount_human_ru: "20 653 490 руб.",
rows_with_amount: 26,
rows_matched: 26
},
outgoing_supplier_payout: {
total_amount_human_ru: "2 129 651 руб.",
rows_with_amount: 1,
rows_matched: 1
},
net_amount_human_ru: "18 523 839 руб.",
net_direction: "net_incoming"
},
previous_counterparty_document_bundle: {
counterparty: "Группа СВК",
document_count: 19
}
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: [],
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_family_business_overview", "data_need_graph_has_clarification_gaps"]
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "Нужно уточнить контекст перед поиском в 1С.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: "Уточните организацию."
}
}
})
);
expect(candidate.candidate_status).toBe("clarification_candidate");
expect(candidate.reply_text).toContain("уточните, по какой компании");
expect(candidate.reply_text).toContain("Группа СВК");
expect(candidate.reply_text).toContain("20 653 490 руб.");
expect(candidate.reply_text).toContain("документы по цепочке: найдено 19");
expect(candidate.reply_text).toContain("Нельзя утверждать");
expect(candidate.reply_text).not.toContain("перед поиском");
});
it("keeps organization clarification boundary summaries compact", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "ООО Альтернатива Плюс",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "Альтернатива Плюс"
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: [],
metadata_scope_hint: "Группа СВК",
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "all_time",
incoming_customer_revenue: { total_amount_human_ru: "285 819 547,57 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "137 963 720,37 руб." },
net_amount_human_ru: "147 855 827,20 руб.",
net_direction: "net_incoming",
top_customers: [{ axis_value: "Комитет государственных услуг г. Москвы", total_amount_human_ru: "133 839 880,82 руб." }],
top_suppliers: [{ axis_value: "ГТК-Интер, ООО", total_amount_human_ru: "44 943 460 руб." }]
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Company summary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
const lines = candidate.reply_text?.split("\n").filter(Boolean) ?? [];
expect(lines.length).toBeLessThanOrEqual(3);
expect(lines[0]).toContain("по компании Альтернатива Плюс");
expect(lines[0]).toContain("285 819 547,57");
expect(candidate.reply_text).toContain("Отдельно по выбранному контрагенту Группа СВК");
expect(candidate.reply_text).toContain("Нельзя утверждать");
expect(candidate.reply_text).not.toContain("Метод:");
expect(candidate.reply_text).not.toContain("Для ответа именно про чистую прибыль");
});
it("localizes value-flow evidence without leaking pilot mechanics", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({

View File

@ -310,6 +310,67 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "list_documents_by_counterparty",
selected_recipe: "address_documents_by_counterparty_v1",
mcp_call_status: "matched_non_empty",
truth_mode: "confirmed",
capability_binding_status: "bound",
capability_binding_violations: [],
answer_shape_contract: {
reply_type: "factual",
capability_contract_id: "documents_drilldown"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: ["Группа СВК"],
reason_codes: ["data_need_graph_built", "data_need_graph_bidirectional_value_flow"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Группа СВК: получили 20 653 490 руб., заплатили 2 129 651 руб.; нетто 18 523 839 руб. в нашу сторону.",
confirmed_lines: ["получили 20 653 490 руб.; заплатили 2 129 651 руб.; нетто 18 523 839 руб."],
inference_lines: [],
unknown_lines: [],
limitation_lines: ["Это денежный срез, не бухгалтерская прибыль."],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
expect(result.reply_text).toContain("20 653 490");
expect(result.reply_text).toContain("2 129 651");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
});
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",

View File

@ -328,8 +328,17 @@ describe("assistant MCP discovery runtime bridge", () => {
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.hot_runtime_wired).toBe(true);
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.hot_runtime_wired).toBe(true);
expect(result.execution_handoff).toMatchObject({
handoff_status: "ready_for_guarded_response",
selected_chain_id: "value_flow_comparison",
allowed_hot_chain: true,
can_use_guarded_response: true
});
expect(result.reason_codes).toContain("runtime_bridge_wired_to_guarded_hot_assistant_answer");
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
period_scope: "2020",
incoming_customer_revenue: {
@ -389,6 +398,14 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.requires_user_clarification).toBe(false);
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.hot_runtime_wired).toBe(true);
expect(result.execution_handoff).toMatchObject({
handoff_status: "ready_for_guarded_response",
selected_chain_id: "value_flow_comparison",
allowed_hot_chain: true,
can_use_guarded_response: true
});
expect(result.reason_codes).toContain("runtime_bridge_wired_to_guarded_hot_assistant_answer");
expect(result.planner.required_axes).toContain("all_time_scope");
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
@ -581,6 +598,11 @@ describe("assistant MCP discovery runtime bridge", () => {
executable_now: true,
enablement_reason: null
});
expect(result.execution_handoff).toMatchObject({
handoff_status: "ready_for_guarded_response",
allowed_hot_chain: true,
can_use_guarded_response: true
});
expect(result.pilot.derived_business_overview?.inventory_quality_events?.evidence_status).toBe(
"reviewed_no_quality_events_found"
);

View File

@ -166,6 +166,34 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
});
it("normalizes role-prefixed counterparty carryover before document discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "Покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс.",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "list_documents",
explicit_intent_candidate: "list_documents_by_counterparty"
},
predecomposeContract: {
entities: { counterparty: null, organization: null },
period: { scope: "unspecified", period_from: null, period_to: null }
},
followupContext: {
previous_intent: "customer_revenue_and_payments",
target_intent: "list_documents_by_counterparty",
previous_discovery_pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
previous_discovery_loop_metadata_scope_hint: "контрагенту группа свк",
previous_anchor_type: "counterparty",
previous_anchor_value: "контрагенту группа свк",
previous_filters: { counterparty: "контрагенту группа свк" }
}
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["группа свк"]);
expect(result.turn_meaning_ref?.metadata_scope_hint).toBe("группа свк");
expect(result.data_need_graph?.subject_candidates).toEqual(["группа свк"]);
});
it("extracts compact scoped counterparty from net follow-up wording when LLM entities are empty", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
@ -1653,6 +1681,7 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
@ -1699,6 +1728,22 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected");
});
it("routes colloquial multi-surface company overview into business overview over exact VAT", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u041f\u043e-\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0447\u0442\u043e \u043f\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u0432\u0438\u0434\u043d\u043e \u0432 1\u0421 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0430\u043c, \u0441\u043a\u043b\u0430\u0434\u0443, \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c \u0438 \u0433\u0434\u0435 \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c \u0432\u044b\u0432\u043e\u0434?",
assistantTurnMeaning: {
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
}
});
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref?.asked_action_family).toBe("broad_evaluation");
expect(result.turn_meaning_ref?.explicit_intent_candidate).toBeUndefined();
});
it("keeps explicit year out of the organization scope for raw business overview wording", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
@ -1789,6 +1834,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it("keeps previous organization scope when all-time business overview says company in general", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за 2020 как подтвержденную общую налоговую позицию.",
assistantTurnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "в целом",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_provided_axes: ["organization"],
previous_filters: {
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_signal_detected");
expect(result.reason_codes).not.toContain("mcp_discovery_organization_scope_from_semantic_text");
});
it("grounds organization aliases from known organizations for clean-session earnings questions", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "скока денег альтернатива заработала за 20 год?",
@ -3690,6 +3765,135 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("routes brief compare boundary wording to business overview without implicit current date", () => {
const today = new Date().toISOString().slice(0, 10);
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
const valueFlowBundle = {
counterparty: counterpartyName,
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: counterpartyName,
document_count: 19
};
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0418 \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u0441\u0440\u0430\u0432\u043d\u0438: \u0447\u0442\u043e \u0443 \u043d\u0430\u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438, \u0447\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443, \u0438 \u043a\u0430\u043a\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u044b \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_intent_candidate: "list_documents_by_counterparty",
explicit_entity_candidates: ["\u043d\u0430\u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e"]
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1",
previous_intent: "list_documents_by_counterparty",
target_intent: "list_documents_by_counterparty",
previous_filters: {
organization: orgName,
counterparty: counterpartyName,
as_of_date: today
},
previous_anchor_type: "counterparty",
previous_anchor_value: counterpartyName,
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterpartyName],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.reason_codes).toContain(
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
);
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_raw_scope");
});
it("continues business overview organization clarification without current-date reset", () => {
const today = new Date().toISOString().slice(0, 10);
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
const valueFlowBundle = {
counterparty: counterpartyName,
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: counterpartyName,
document_count: 19
};
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: orgName,
assistantTurnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_date_scope: today,
stale_replay_forbidden: true
},
predecomposeContract: {
entities: { organization: orgName },
period: { as_of_date: today }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_provided_axes: ["all_time_scope", "metadata_scope"],
previous_discovery_loop_asked_domain_family: "business_overview",
previous_discovery_loop_asked_action_family: "broad_evaluation",
previous_discovery_loop_unsupported_family: "broad_business_evaluation",
previous_discovery_loop_metadata_scope_hint: counterpartyName,
previous_anchor_type: "counterparty",
previous_anchor_value: counterpartyName,
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
}
});
expect(result.source_signal).toBe("assistant_turn_meaning");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterpartyName],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_from_followup_context");
expect(result.reason_codes).toContain(
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
);
expect(result.reason_codes).not.toContain("mcp_discovery_date_scope_from_followup_context");
});
it("preserves explicit counterparty scope for company plus counterparty business summaries", () => {
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";

View File

@ -339,6 +339,71 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.previous_anchor_value).toBe("Workstation Focus");
});
it("keeps selected counterparty document follow-up through stale discovery guard", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Confirmed value-flow profile for SVK Group.",
debug: {
detected_intent: "customer_revenue_and_payments",
extracted_filters: {
organization: "ACME",
period_from: "2017-01-01",
period_to: "2017-12-31"
},
assistant_mcp_discovery_entry_point_v1: {
bridge: {
loop_state: {
selected_chain_id: "value_flow_comparison"
}
}
}
}
}),
resolveAssistantTurnMeaning: () => ({
stale_replay_forbidden: true,
explicit_intent_candidate: "list_documents_by_counterparty"
}),
resolveAddressIntent: () => ({ intent: "list_documents_by_counterparty" }),
resolveAddressIntentFamily: (intent: unknown) =>
["customer_revenue_and_payments", "list_documents_by_counterparty"].includes(String(intent ?? ""))
? "counterparty"
: toNonEmptyString(intent),
hasAddressFollowupContextSignal: () => false,
hasReferentialPointer: () => false,
findRecentInventoryRootFrame: () => null,
findRecentAddressFilterValue: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"Show docs for selected counterparty",
[],
null,
{
predecomposeContract: {
intent: "list_documents_by_counterparty"
}
},
{
session_context: {
active_focus_object: {
object_type: "counterparty",
label: "SVK Group"
}
}
}
);
expect(carryover?.followupContext?.target_intent).toBe("list_documents_by_counterparty");
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.followupContext?.previous_anchor_value).toBe("SVK Group");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization: "ACME",
counterparty: "SVK Group",
period_from: "2017-01-01",
period_to: "2017-12-31"
});
});
it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
@ -1628,6 +1693,164 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});
it("carries selected counterparty proof bundles through business overview organization clarification", () => {
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
const valueFlowBundle = {
counterparty: counterpartyName,
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: counterpartyName,
document_count: 19
};
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: "Коротко: уточните, по какой компании/организации сравнить выбранного контрагента Группа СВК.",
debug: {
execution_lane: "living_chat",
detected_intent: "business_overview",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterpartyName],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
metadata_scope_hint: counterpartyName,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "business_overview_route_template_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
loop_status: "awaiting_clarification",
selected_chain_id: "business_overview",
pilot_scope: "business_overview_route_template_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
pending_axes: ["organization"],
provided_axes: ["all_time_scope", "metadata_scope"],
metadata_scope_hint: counterpartyName
}
}
}
}
}),
resolveOrganizationSelectionFromMessage: (message: string) =>
String(message ?? "").includes("Альтернатива") ? orgName : null,
hasAddressFollowupContextSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
orgName,
[],
null,
{
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
null
);
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("business_overview");
expect(carryover?.followupContext?.previous_discovery_loop_provided_axes).toEqual([
"all_time_scope",
"metadata_scope"
]);
expect(carryover?.followupContext?.previous_discovery_loop_metadata_scope_hint).toBe(counterpartyName);
expect(carryover?.followupContext?.previous_discovery_bidirectional_value_flow).toEqual(valueFlowBundle);
expect(carryover?.followupContext?.previous_discovery_document_summary).toEqual(documentBundle);
});
it("recovers selected counterparty proof bundles from prior boundary-summary text", () => {
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text:
"Коротко: уточните, по какой компании/организации сравнить выбранного контрагента Группа СВК.\n" +
"Отдельно по контрагенту Группа СВК: подтверждено получили 20 653 490 руб., заплатили 2 129 651 руб., расчетное нетто в нашу сторону 18 523 839 руб. Основа: проверенные входящие платежи и исходящие платежи и документы по цепочке: найдено 19.",
debug: {
execution_lane: "living_chat",
detected_intent: "business_overview",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "business_overview_route_template_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
loop_status: "awaiting_clarification",
selected_chain_id: "business_overview",
pilot_scope: "business_overview_route_template_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
pending_axes: ["organization"],
provided_axes: ["all_time_scope", "metadata_scope"],
metadata_scope_hint: counterpartyName
}
}
}
}
}),
resolveOrganizationSelectionFromMessage: (message: string) =>
String(message ?? "").includes("Альтернатива") ? orgName : null,
hasAddressFollowupContextSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
orgName,
[],
null,
{
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
null
);
expect(carryover?.followupContext?.previous_discovery_bidirectional_value_flow).toMatchObject({
counterparty: counterpartyName,
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
});
expect(carryover?.followupContext?.previous_discovery_document_summary).toMatchObject({
counterparty: counterpartyName,
document_count: 19
});
});
it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,

File diff suppressed because one or more lines are too long

View File

@ -1,353 +0,0 @@
{
"saved_at": "2026-04-24T14:06:30+00:00",
"generation_id": "gen-ag04241406-abe4d8",
"mode": "saved_user_sessions",
"title": "AGENT | Post-F cross-stage semantic integrity canary",
"agent_run": true,
"questions": [
"Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
"Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
"За 2020 год.",
"А теперь по документам?",
"А теперь за 2021 год?",
"А теперь за все время?",
"С НДС закончили. Новая тема: покажи документы по Жуковке 51.",
"Хорошо, а теперь платежи по нему тоже покажи.",
"А по нему договоры?",
"А по нему документы?",
"А за 2021?",
"С Жуковкой закончили. Теперь другая задача: быстрый денежный срез по одной организации. Если для ответа нужна организация, просто уточни ее. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?",
"Теперь отдельная тема по конкретному контрагенту. Найди в 1С Группу СВК.",
"Сколько получили по нему за 2020 год?",
"А теперь сколько заплатили?",
"А какое нетто?",
"А по документам?",
"А по движениям?",
"А теперь тот же смысл за 2021 год."
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "cross-stage canary: VAT metadata, numeric counterparty suffix, open organization value-flow, SVK grounded reset",
"architecture_phase": "Post-F Semantic Integrity Hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_post_f_cross_stage_canary_agent_20260424.json",
"scenario_id": "address_truth_harness_post_f_cross_stage_canary_agent_20260424",
"semantic_tags": [
"account_injection_guard",
"all_time_followup",
"bidirectional_value_flow",
"clarification_resume",
"contracts_followup",
"counterparty_pronoun_resolution",
"cross_stage_canary",
"document_lane_continuity",
"document_pivot_after_movement",
"documents_followup",
"documents_pivot",
"grounded_counterparty",
"incoming_value_flow",
"legacy_phase39",
"legacy_phase64",
"legacy_phase67",
"metadata_surface",
"movement_execution",
"movement_lane_after_metadata",
"movements_pivot",
"net_value_flow",
"numeric_counterparty_suffix",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payments_followup",
"payout_value_flow",
"period_clarification_resume",
"post_f",
"repeated_pivot",
"scope_reuse",
"topic_reset",
"value_flow_ranking",
"vat_orientation",
"year_switch",
"year_switch_after_document_pivot",
"year_switch_after_repeated_pivot"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "За 2020 год.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А теперь по документам?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "А теперь за 2021 год?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А теперь за все время?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "С НДС закончили. Новая тема: покажи документы по Жуковке 51.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "Хорошо, а теперь платежи по нему тоже покажи.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "А по нему договоры?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "А по нему документы?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "А за 2021?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "С Жуковкой закончили. Теперь другая задача: быстрый денежный срез по одной организации. Если для ответа нужна организация, просто уточни ее. Сколько вообще входящих денег было за 2020 год?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "По ООО Альтернатива Плюс.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "Понял, тогда за все время.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-016",
"role": "user",
"text": "И кто больше всего принес денег этой организации в 2020 году?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-017",
"role": "user",
"text": "А в 2021 году?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-018",
"role": "user",
"text": "Теперь отдельная тема по конкретному контрагенту. Найди в 1С Группу СВК.",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-019",
"role": "user",
"text": "Сколько получили по нему за 2020 год?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-020",
"role": "user",
"text": "А теперь сколько заплатили?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-021",
"role": "user",
"text": "А какое нетто?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-022",
"role": "user",
"text": "А по документам?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-023",
"role": "user",
"text": "А по движениям?",
"created_at": "2026-04-24T14:06:30+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-024",
"role": "user",
"text": "А теперь тот же смысл за 2021 год.",
"created_at": "2026-04-24T14:06:30+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": "cross-stage canary: VAT metadata, numeric counterparty suffix, open organization value-flow, SVK grounded reset",
"architecture_phase": "Post-F Semantic Integrity Hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_post_f_cross_stage_canary_agent_20260424.json",
"scenario_id": "address_truth_harness_post_f_cross_stage_canary_agent_20260424",
"semantic_tags": [
"account_injection_guard",
"all_time_followup",
"bidirectional_value_flow",
"clarification_resume",
"contracts_followup",
"counterparty_pronoun_resolution",
"cross_stage_canary",
"document_lane_continuity",
"document_pivot_after_movement",
"documents_followup",
"documents_pivot",
"grounded_counterparty",
"incoming_value_flow",
"legacy_phase39",
"legacy_phase64",
"legacy_phase67",
"metadata_surface",
"movement_execution",
"movement_lane_after_metadata",
"movements_pivot",
"net_value_flow",
"numeric_counterparty_suffix",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payments_followup",
"payout_value_flow",
"period_clarification_resume",
"post_f",
"repeated_pivot",
"scope_reuse",
"topic_reset",
"value_flow_ranking",
"vat_orientation",
"year_switch",
"year_switch_after_document_pivot",
"year_switch_after_repeated_pivot"
]
}
}
}

View File

@ -1,177 +0,0 @@
{
"saved_at": "2026-04-24T17:10:31+00:00",
"generation_id": "gen-ag04241710-bdb248",
"mode": "saved_user_sessions",
"title": "AGENT | Post-F ручные провалы VAT revenue item-flow live3",
"agent_run": true,
"questions": [
"кайф - что там на складе по остаткам?",
"АЛЬТЕРНАТИВА",
"март 2016",
"По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"ндс можешь прикинуть на дату покупки рабочей станции?",
"прикинь какой ндс нам надо заплатить на февраль 2017",
"кто у нас самый доходный клиент за все время",
"по чепурнову покажи все доки",
"а по свк",
"а сейчас у нас есть что на складе?",
"что нам отгружал чепурнов? какой товар или услугу?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "manual failures 11/13/14/26 from assistant-stage1-9liEOh-7JP",
"architecture_phase": "Post-F Semantic Integrity Hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_post_f_manual_failures_20260424.json",
"scenario_id": "address_truth_harness_post_f_manual_failures_20260424",
"semantic_tags": [
"counterparty_grounding",
"counterparty_retarget",
"inventory_context",
"item_flow_failure_26",
"manual_9liEOh",
"materialization_gap",
"organization_scope",
"purchase_date_context",
"revenue_ranking_failure_14",
"selected_object_purchase",
"stale_inventory_scope",
"stale_scope_guard",
"vat_failure_11",
"vat_failure_13"
]
},
"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-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "АЛЬТЕРНАТИВА",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "март 2016",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "ндс можешь прикинуть на дату покупки рабочей станции?",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "прикинь какой ндс нам надо заплатить на февраль 2017",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "кто у нас самый доходный клиент за все время",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "по чепурнову покажи все доки",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "а по свк",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "а сейчас у нас есть что на складе?",
"created_at": "2026-04-24T17:10:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "что нам отгружал чепурнов? какой товар или услугу?",
"created_at": "2026-04-24T17:10:31+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": "manual failures 11/13/14/26 from assistant-stage1-9liEOh-7JP",
"architecture_phase": "Post-F Semantic Integrity Hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_post_f_manual_failures_20260424.json",
"scenario_id": "address_truth_harness_post_f_manual_failures_20260424",
"semantic_tags": [
"counterparty_grounding",
"counterparty_retarget",
"inventory_context",
"item_flow_failure_26",
"manual_9liEOh",
"materialization_gap",
"organization_scope",
"purchase_date_context",
"revenue_ranking_failure_14",
"selected_object_purchase",
"stale_inventory_scope",
"stale_scope_guard",
"vat_failure_11",
"vat_failure_13"
]
}
}
}

View File

@ -1,153 +0,0 @@
{
"saved_at": "2026-05-10T09:49:21+00:00",
"generation_id": "gen-ag05100949-2a5ed8",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 91 route-candidate-driven enablement loop canary",
"agent_run": true,
"questions": [
"какой контрагент принес больше всего денег за 2020 год?",
"ООО Альтернатива Плюс",
"а теперь сколько лет мы работаем с Группа СВК?",
"покажи движения по НДС за 2020 по ООО Альтернатива Плюс",
"можно ли по этим данным точно подтвердить резерв под неликвиды на складе?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "route candidate repair-target grouping + handoff canary",
"architecture_phase": "Route-Candidate-Driven Enablement Loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase91_route_candidate_driven_enablement_loop.json",
"scenario_id": "address_truth_harness_phase91_route_candidate_driven_enablement_loop",
"semantic_tags": [
"clarification_recovery",
"counterparty_lifecycle",
"inventory_reserve_boundary",
"metadata_scope",
"movement_evidence",
"needs_user_scope",
"no_overclaim",
"open_organization_scope",
"ready_for_reviewed_execution",
"route_candidate_handoff",
"stale_scope_guard",
"unreviewed_proof_family",
"value_flow_ranking",
"vat"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 5,
"steps_passed": 5,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "какой контрагент принес больше всего денег за 2020 год?",
"created_at": "2026-05-10T09:49:21+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "ООО Альтернатива Плюс",
"created_at": "2026-05-10T09:49:21+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "а теперь сколько лет мы работаем с Группа СВК?",
"created_at": "2026-05-10T09:49:21+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "покажи движения по НДС за 2020 по ООО Альтернатива Плюс",
"created_at": "2026-05-10T09:49:21+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "можно ли по этим данным точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-10T09:49:21+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": "route candidate repair-target grouping + handoff canary",
"architecture_phase": "Route-Candidate-Driven Enablement Loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase91_route_candidate_driven_enablement_loop.json",
"scenario_id": "address_truth_harness_phase91_route_candidate_driven_enablement_loop",
"semantic_tags": [
"clarification_recovery",
"counterparty_lifecycle",
"inventory_reserve_boundary",
"metadata_scope",
"movement_evidence",
"needs_user_scope",
"no_overclaim",
"open_organization_scope",
"ready_for_reviewed_execution",
"route_candidate_handoff",
"stale_scope_guard",
"unreviewed_proof_family",
"value_flow_ranking",
"vat"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 5,
"steps_passed": 5,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,157 +0,0 @@
{
"saved_at": "2026-05-10T12:13:26+00:00",
"generation_id": "gen-ag05101213-596d99",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 93 accounting profit-margin reviewed route",
"agent_run": true,
"questions": [
"по ООО Альтернатива Плюс за 2020 можно точно сказать чистую прибыль и маржу?",
"а это прибыль или убыток, коротко?",
"по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"тогда НДС за 2020 по ООО Альтернатива Плюс какой?",
"а какой контрагент принес больше всего денег за 2020?",
"по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "accounting profit-margin reviewed route",
"architecture_phase": "Route-Candidate-Driven Enablement Loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase93_accounting_profit_margin_reviewed_route.json",
"scenario_id": "address_truth_harness_phase93_accounting_profit_margin_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"context_carryover",
"debt_due_date_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"needs_route_enablement",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "по ООО Альтернатива Плюс за 2020 можно точно сказать чистую прибыль и маржу?",
"created_at": "2026-05-10T12:13:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "а это прибыль или убыток, коротко?",
"created_at": "2026-05-10T12:13:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-05-10T12:13:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "тогда НДС за 2020 по ООО Альтернатива Плюс какой?",
"created_at": "2026-05-10T12:13:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а какой контрагент принес больше всего денег за 2020?",
"created_at": "2026-05-10T12:13:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-10T12:13:26+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": "accounting profit-margin reviewed route",
"architecture_phase": "Route-Candidate-Driven Enablement Loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase93_accounting_profit_margin_reviewed_route.json",
"scenario_id": "address_truth_harness_phase93_accounting_profit_margin_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"context_carryover",
"debt_due_date_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"needs_route_enablement",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,173 +0,0 @@
{
"saved_at": "2026-05-10T13:19:22+00:00",
"generation_id": "gen-ag05101319-c04f79",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 94 debt due-date aging reviewed route",
"agent_run": true,
"questions": [
"по ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"то есть просрочку доказать нельзя, коротко почему?",
"а чистая прибыль и маржа за 2020 по этой же компании какие?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"а кто принес больше всего денег за 2020?",
"по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"а зависимость от одного поставщика за 2020 можно точно оценить?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for promoting debt_due_date_aging_quality from proxy-only enablement to a reviewed payment-term/open-balance route while preserving profit, VAT, inventory, and vendor-risk boundaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase94_debt_due_date_aging_reviewed_route.json",
"scenario_id": "address_truth_harness_phase94_debt_due_date_aging_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"checked_negative",
"context_carryover",
"debt_due_date_aging_quality",
"debt_due_date_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase94_debt_due_date_aging_reviewed_route_live4",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase94_debt_due_date_aging_reviewed_route_live4",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "по ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-05-10T13:19:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "то есть просрочку доказать нельзя, коротко почему?",
"created_at": "2026-05-10T13:19:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "а чистая прибыль и маржа за 2020 по этой же компании какие?",
"created_at": "2026-05-10T13:19:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "НДС за 2020 по ООО Альтернатива Плюс какой?",
"created_at": "2026-05-10T13:19:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а кто принес больше всего денег за 2020?",
"created_at": "2026-05-10T13:19:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-10T13:19:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "а зависимость от одного поставщика за 2020 можно точно оценить?",
"created_at": "2026-05-10T13:19:22+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": "Focused semantic replay for promoting debt_due_date_aging_quality from proxy-only enablement to a reviewed payment-term/open-balance route while preserving profit, VAT, inventory, and vendor-risk boundaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase94_debt_due_date_aging_reviewed_route.json",
"scenario_id": "address_truth_harness_phase94_debt_due_date_aging_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"checked_negative",
"context_carryover",
"debt_due_date_aging_quality",
"debt_due_date_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase94_debt_due_date_aging_reviewed_route_live4",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase94_debt_due_date_aging_reviewed_route_live4",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,173 +0,0 @@
{
"saved_at": "2026-05-12T13:57:26+00:00",
"generation_id": "gen-ag05121357-9ea5d6",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"agent_run": true,
"questions": [
"по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?",
"а банк из этого ответа считать обычным поставщиком?",
"по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"а чистая прибыль и маржа за 2020 какие?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"а кто принес больше всего денег за 2020?",
"по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for promoting vendor_risk_procurement_quality from proxy-only enablement to reviewed procurement-concentration evidence while preserving debt, profit, VAT, value-flow, and inventory reserve canaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase95_vendor_procurement_quality_reviewed_route.json",
"scenario_id": "address_truth_harness_phase95_vendor_procurement_quality_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"context_carryover",
"debt_due_date_boundary",
"financial_institution_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "а банк из этого ответа считать обычным поставщиком?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а чистая прибыль и маржа за 2020 какие?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "НДС за 2020 по ООО Альтернатива Плюс какой?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а кто принес больше всего денег за 2020?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-12T13:57:26+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": "Focused semantic replay for promoting vendor_risk_procurement_quality from proxy-only enablement to reviewed procurement-concentration evidence while preserving debt, profit, VAT, value-flow, and inventory reserve canaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase95_vendor_procurement_quality_reviewed_route.json",
"scenario_id": "address_truth_harness_phase95_vendor_procurement_quality_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"context_carryover",
"debt_due_date_boundary",
"financial_institution_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,155 +0,0 @@
{
"saved_at": "2026-05-12T16:28:41+00:00",
"generation_id": "gen-ag05121628-50ea6c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"agent_run": true,
"questions": [
"кто у нас самый доходный клиент за все время?",
"какой у нас самый доходный год?",
"кто нам должен денег на май 2017?",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "ranking catalog alignment, self-scope pronoun, compact debt answers",
"architecture_phase": "MCP автономка / semantic integrity",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase20_continuity_stabilization.json",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"semantic_tags": [
"exact_not_overwritten",
"garbage_anchor_forbidden",
"payables_snapshot",
"period_carryover",
"pronoun_followup",
"receivables_snapshot",
"temporal_tail_not_entity",
"value_flow_ranking",
"vat_followup",
"year_tail_not_entity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "кто у нас самый доходный клиент за все время?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "какой у нас самый доходный год?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "кто нам должен денег на май 2017?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а какой ндс мы должны примерно заплатить за этот период?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "мы должны комуто денег на сегодня?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а нам?",
"created_at": "2026-05-12T16:28:41+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": "ranking catalog alignment, self-scope pronoun, compact debt answers",
"architecture_phase": "MCP автономка / semantic integrity",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase20_continuity_stabilization.json",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"semantic_tags": [
"exact_not_overwritten",
"garbage_anchor_forbidden",
"payables_snapshot",
"period_carryover",
"pronoun_followup",
"receivables_snapshot",
"temporal_tail_not_entity",
"value_flow_ranking",
"vat_followup",
"year_tail_not_entity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,109 +0,0 @@
{
"saved_at": "2026-05-12T20:57:28+00:00",
"generation_id": "gen-ag05122057-c9786e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 96 inventory reserve/liquidation quality-events",
"agent_run": true,
"questions": [
"По ООО Альтернатива Плюс за 2020 год проверь склад: были ли списания товаров, оприходования/корректировки, инвентаризации, переоценки, резервы под неликвиды или ликвидационная стоимость? Скажи коротко и честно, что подтверждено 1С, а что нельзя утверждать.",
"А по этим же данным можно сказать, что склад ликвидный и неликвидов нет?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "inventory_reserve_liquidation_quality",
"architecture_phase": "Route-Candidate-Driven Enablement Loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase96_inventory_reserve_liquidation_quality.json",
"scenario_id": "address_truth_harness_phase96_inventory_reserve_liquidation_quality",
"semantic_tags": [
"direct_answer_first",
"followup_context_carryover",
"inventory_liquidity_boundary",
"inventory_reserve_liquidation_quality",
"no_inventory_health_overclaim",
"no_market_liquidation_overclaim",
"reviewed_inventory_quality_events"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase96_inventory_reserve_liquidation_quality_rerun",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase96_inventory_reserve_liquidation_quality_rerun",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 2,
"steps_passed": 2,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "По ООО Альтернатива Плюс за 2020 год проверь склад: были ли списания товаров, оприходования/корректировки, инвентаризации, переоценки, резервы под неликвиды или ликвидационная стоимость? Скажи коротко и честно, что подтверждено 1С, а что нельзя утверждать.",
"created_at": "2026-05-12T20:57:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "А по этим же данным можно сказать, что склад ликвидный и неликвидов нет?",
"created_at": "2026-05-12T20:57:28+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": "inventory_reserve_liquidation_quality",
"architecture_phase": "Route-Candidate-Driven Enablement Loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase96_inventory_reserve_liquidation_quality.json",
"scenario_id": "address_truth_harness_phase96_inventory_reserve_liquidation_quality",
"semantic_tags": [
"direct_answer_first",
"followup_context_carryover",
"inventory_liquidity_boundary",
"inventory_reserve_liquidation_quality",
"no_inventory_health_overclaim",
"no_market_liquidation_overclaim",
"reviewed_inventory_quality_events"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase96_inventory_reserve_liquidation_quality_rerun",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase96_inventory_reserve_liquidation_quality_rerun",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 2,
"steps_passed": 2,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,135 +0,0 @@
{
"saved_at": "2026-05-12T22:50:23+00:00",
"generation_id": "gen-ag05122250-4451a8",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"agent_run": true,
"questions": [
"По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
"А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
"Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
"А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase97_financial_counterparty_flow_hints.json",
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
"semantic_tags": [
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_counterparty_flow_hint",
"profit_margin_boundary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
"created_at": "2026-05-12T22:50:23+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
"created_at": "2026-05-12T22:50:23+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика.",
"created_at": "2026-05-12T22:50:23+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"created_at": "2026-05-12T22:50:23+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": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase97_financial_counterparty_flow_hints.json",
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
"semantic_tags": [
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_counterparty_flow_hint",
"profit_margin_boundary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase97_financial_counterparty_flow_hints_live4",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,155 +0,0 @@
{
"saved_at": "2026-05-12T23:15:48+00:00",
"generation_id": "gen-ag05122315-f1e27c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"agent_run": true,
"questions": [
"По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"То есть просрочку доказать нельзя, коротко почему?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"А кто принес больше всего денег за 2020?",
"По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"А зависимость от одного поставщика за 2020 можно точно оценить?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay from assistant-stage1-v2qsm_R0fF: answers may be bounded, but they must stay business-readable, direct-first, and must not leak MCP/proxy/route/debug wording when explaining row limits, incomplete coverage, debt due-date proof, inventory reserve proof, supplier dependency, VAT, or bank-like counterparties.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase98_limit_honesty_business_language.json",
"scenario_id": "address_truth_harness_phase98_limit_honesty_business_language",
"semantic_tags": [
"business_language",
"customer_revenue_and_payments",
"debt_due_date_aging_quality",
"debug_leak_guard",
"financial_counterparty_flow_hint",
"followup_directness",
"inventory_reserve_liquidation_quality",
"limit_honesty",
"vat",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "То есть просрочку доказать нельзя, коротко почему?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "НДС за 2020 по ООО Альтернатива Плюс какой?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А кто принес больше всего денег за 2020?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А зависимость от одного поставщика за 2020 можно точно оценить?",
"created_at": "2026-05-12T23:15:48+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": "Focused semantic replay from assistant-stage1-v2qsm_R0fF: answers may be bounded, but they must stay business-readable, direct-first, and must not leak MCP/proxy/route/debug wording when explaining row limits, incomplete coverage, debt due-date proof, inventory reserve proof, supplier dependency, VAT, or bank-like counterparties.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase98_limit_honesty_business_language.json",
"scenario_id": "address_truth_harness_phase98_limit_honesty_business_language",
"semantic_tags": [
"business_language",
"customer_revenue_and_payments",
"debt_due_date_aging_quality",
"debug_leak_guard",
"financial_counterparty_flow_hint",
"followup_directness",
"inventory_reserve_liquidation_quality",
"limit_honesty",
"vat",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,133 +0,0 @@
{
"saved_at": "2026-05-13T10:09:01+00:00",
"generation_id": "gen-ag05131009-f08174",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 99 large-query budget and continuation policy replay",
"agent_run": true,
"questions": [
"Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто, кто основные источники денег и где важные ограничения.",
"То есть это можно считать прибылью за 2020 или нет? Коротко.",
"А кто за 2020 принес больше всего денег, и если там банк, не называй его обычным клиентом.",
"По исходящим за 2020 есть зависимость от одного поставщика или это только денежная концентрация?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for explicit-year large business questions: the assistant should use chunked 1C evidence where available, answer direct-first, avoid fake profit claims, keep bank/counterparty boundaries, and not collapse into row-limit refusal wording when yearly money coverage can be recovered.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase99_large_query_budget_continuation.json",
"scenario_id": "address_truth_harness_phase99_large_query_budget_continuation",
"semantic_tags": [
"business_language",
"business_overview",
"context_continuity",
"direct_answer_first",
"financial_counterparty_flow_hint",
"followup_directness",
"large_query_budget",
"profit_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase99_large_query_budget_continuation_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase99_large_query_budget_continuation_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто, кто основные источники денег и где важные ограничения.",
"created_at": "2026-05-13T10:09:01+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "То есть это можно считать прибылью за 2020 или нет? Коротко.",
"created_at": "2026-05-13T10:09:01+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А кто за 2020 принес больше всего денег, и если там банк, не называй его обычным клиентом.",
"created_at": "2026-05-13T10:09:01+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "По исходящим за 2020 есть зависимость от одного поставщика или это только денежная концентрация?",
"created_at": "2026-05-13T10:09:01+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": "Focused semantic replay for explicit-year large business questions: the assistant should use chunked 1C evidence where available, answer direct-first, avoid fake profit claims, keep bank/counterparty boundaries, and not collapse into row-limit refusal wording when yearly money coverage can be recovered.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase99_large_query_budget_continuation.json",
"scenario_id": "address_truth_harness_phase99_large_query_budget_continuation",
"semantic_tags": [
"business_language",
"business_overview",
"context_continuity",
"direct_answer_first",
"financial_counterparty_flow_hint",
"followup_directness",
"large_query_budget",
"profit_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase99_large_query_budget_continuation_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase99_large_query_budget_continuation_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,121 +0,0 @@
{
"saved_at": "2026-05-13T10:28:45+00:00",
"generation_id": "gen-ag05131028-234e5e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 100 large-query continuation UX replay",
"agent_run": true,
"questions": [
"Дай общий бизнес-обзор ООО Альтернатива Плюс за весь доступный период: входящие, исходящие, нетто, лучший год. Если срез слишком широкий, не выдумывай полный итог, а скажи как безопасно дособрать.",
"Ок, тогда дособери конкретно 2020: входящие, исходящие и расчетное денежное нетто.",
"Это можно считать прибылью за 2020 или нет? Ответь коротко и по делу."
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for all-time or very broad business overview questions: the assistant should answer from checked evidence, disclose row-limit coverage honestly, offer a safe period-narrowing continuation path, and then recover the explicit-year follow-up through chunked evidence without leaking technical mechanics.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase100_large_query_continuation_ux.json",
"scenario_id": "address_truth_harness_phase100_large_query_continuation_ux",
"semantic_tags": [
"business_language",
"business_overview",
"followup_continuation",
"followup_directness",
"large_query_budget",
"large_query_continuation",
"limit_honesty",
"profit_boundary"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase100_large_query_continuation_ux_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase100_large_query_continuation_ux_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Дай общий бизнес-обзор ООО Альтернатива Плюс за весь доступный период: входящие, исходящие, нетто, лучший год. Если срез слишком широкий, не выдумывай полный итог, а скажи как безопасно дособрать.",
"created_at": "2026-05-13T10:28:45+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Ок, тогда дособери конкретно 2020: входящие, исходящие и расчетное денежное нетто.",
"created_at": "2026-05-13T10:28:45+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "Это можно считать прибылью за 2020 или нет? Ответь коротко и по делу.",
"created_at": "2026-05-13T10:28:45+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": "Focused semantic replay for all-time or very broad business overview questions: the assistant should answer from checked evidence, disclose row-limit coverage honestly, offer a safe period-narrowing continuation path, and then recover the explicit-year follow-up through chunked evidence without leaking technical mechanics.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase100_large_query_continuation_ux.json",
"scenario_id": "address_truth_harness_phase100_large_query_continuation_ux",
"semantic_tags": [
"business_language",
"business_overview",
"followup_continuation",
"followup_directness",
"large_query_budget",
"large_query_continuation",
"limit_honesty",
"profit_boundary"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase100_large_query_continuation_ux_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase100_large_query_continuation_ux_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,165 +0,0 @@
{
"saved_at": "2026-05-13T10:44:38+00:00",
"generation_id": "gen-ag05131044-cbe2ff",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 101 inventory root scope without warehouse clarification replay",
"agent_run": true,
"questions": [
"приветик - че как там дела",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"АЛЬТЕРНАТИВА",
"а исторические остатки на другие даты умеешь?",
"давай на июнь 2017",
"март 2016"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for the manual assistant-stage1-hyh1A1WR3j signal: after a broad stock-on-hand question and organization clarification, the assistant must resume the root inventory snapshot for the selected company across available warehouses instead of asking the user to name a warehouse, item, category, or material.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase101_inventory_root_scope_no_warehouse_clarification.json",
"scenario_id": "address_truth_harness_phase101_inventory_root_scope_no_warehouse_clarification",
"semantic_tags": [
"capability_meta",
"clarification_required",
"clarification_resume",
"date_followup",
"historical_inventory",
"human_answer_quality",
"inventory_capability_meta",
"inventory_root",
"smalltalk_entry",
"warehouse_not_required"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase101_inventory_root_scope_no_warehouse_clarification_live1",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase101_inventory_root_scope_no_warehouse_clarification_live1",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "приветик - че как там дела",
"created_at": "2026-05-13T10:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "расскажи что можешь интересного",
"created_at": "2026-05-13T10:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "кайф - что там на складе по остаткам?",
"created_at": "2026-05-13T10:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "АЛЬТЕРНАТИВА",
"created_at": "2026-05-13T10:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а исторические остатки на другие даты умеешь?",
"created_at": "2026-05-13T10:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "давай на июнь 2017",
"created_at": "2026-05-13T10:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "март 2016",
"created_at": "2026-05-13T10:44:38+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": "Focused semantic replay for the manual assistant-stage1-hyh1A1WR3j signal: after a broad stock-on-hand question and organization clarification, the assistant must resume the root inventory snapshot for the selected company across available warehouses instead of asking the user to name a warehouse, item, category, or material.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase101_inventory_root_scope_no_warehouse_clarification.json",
"scenario_id": "address_truth_harness_phase101_inventory_root_scope_no_warehouse_clarification",
"semantic_tags": [
"capability_meta",
"clarification_required",
"clarification_resume",
"date_followup",
"historical_inventory",
"human_answer_quality",
"inventory_capability_meta",
"inventory_root",
"smalltalk_entry",
"warehouse_not_required"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase101_inventory_root_scope_no_warehouse_clarification_live1",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase101_inventory_root_scope_no_warehouse_clarification_live1",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,155 +0,0 @@
{
"saved_at": "2026-05-13T11:21:52+00:00",
"generation_id": "gen-ag05131121-8c41ab",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 102 debt mirror clean-scope polarity replay",
"agent_run": true,
"questions": [
"Альтернатива Плюс",
"мы кому реально должны денег на сегодня? коротко, чистый долг к оплате, без встречных обеспечений как основного долга",
"а Комитету государственных услуг мы реально должны эти 3,6 млн или это встречное обеспечение/зачет? объясни именно по смыслу долга",
"а нам Комитет государственных услуг тоже должен 3,6 млн? это же та же сумма — скажи честно, это дебиторка или встречная часть?",
"тогда кто нам реально должен денег на сегодня? именно чистая дебиторка",
"а мы кому?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "phase102 debt mirror clean-scope polarity",
"architecture_phase": "Open-World Schema/Primitive Discovery",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase102_debt_mirror_clean_scope_polarity.json",
"scenario_id": "address_truth_harness_phase102_debt_mirror_clean_scope_polarity",
"semantic_tags": [
"company_selected",
"debt_mirror_clean_scope",
"direct_answer_first",
"organization_scope",
"payables_counterparty_check",
"payables_snapshot",
"polarity_honesty",
"receivables_counterparty_check",
"receivables_snapshot",
"settlements_mirror_followup"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase102_debt_mirror_clean_scope_polarity_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase102_debt_mirror_clean_scope_polarity_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Альтернатива Плюс",
"created_at": "2026-05-13T11:21:52+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "мы кому реально должны денег на сегодня? коротко, чистый долг к оплате, без встречных обеспечений как основного долга",
"created_at": "2026-05-13T11:21:52+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "а Комитету государственных услуг мы реально должны эти 3,6 млн или это встречное обеспечение/зачет? объясни именно по смыслу долга",
"created_at": "2026-05-13T11:21:52+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а нам Комитет государственных услуг тоже должен 3,6 млн? это же та же сумма — скажи честно, это дебиторка или встречная часть?",
"created_at": "2026-05-13T11:21:52+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "тогда кто нам реально должен денег на сегодня? именно чистая дебиторка",
"created_at": "2026-05-13T11:21:52+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а мы кому?",
"created_at": "2026-05-13T11:21:52+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": "phase102 debt mirror clean-scope polarity",
"architecture_phase": "Open-World Schema/Primitive Discovery",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase102_debt_mirror_clean_scope_polarity.json",
"scenario_id": "address_truth_harness_phase102_debt_mirror_clean_scope_polarity",
"semantic_tags": [
"company_selected",
"debt_mirror_clean_scope",
"direct_answer_first",
"organization_scope",
"payables_counterparty_check",
"payables_snapshot",
"polarity_honesty",
"receivables_counterparty_check",
"receivables_snapshot",
"settlements_mirror_followup"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase102_debt_mirror_clean_scope_polarity_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase102_debt_mirror_clean_scope_polarity_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,161 +0,0 @@
{
"saved_at": "2026-05-13T12:00:47+00:00",
"generation_id": "gen-ag05131200-0ed59a",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 103 financial role and purpose arbitration replay",
"agent_run": true,
"questions": [
"Альтернатива Плюс",
"По СБЕРБАНКУ за 2020 покажи коротко: сколько денег входило и уходило, и что это по смыслу в 1С — клиентская выручка, поставщик, комиссия, кредит или другой финансовый поток?",
"Если СБЕРБАНК есть во входящих поступлениях, можно ли считать его нашим клиентом и выручкой? Скажи по подтвержденным строкам, без притягивания.",
"А если деньги уходили в СБЕРБАНК, это наш поставщик или финансовые списания? Раздели по смыслу и покажи основание.",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "phase103 financial role and purpose arbitration",
"architecture_phase": "Open-World Schema/Primitive Discovery",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase103_financial_role_purpose_arbitration.json",
"scenario_id": "address_truth_harness_phase103_financial_role_purpose_arbitration",
"semantic_tags": [
"bank_like_counterparty",
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"bank_operations_by_counterparty",
"bare_org_scope",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_role_purpose",
"phase102_canary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase103_financial_role_purpose_arbitration_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase103_financial_role_purpose_arbitration_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Альтернатива Плюс",
"created_at": "2026-05-13T12:00:47+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "По СБЕРБАНКУ за 2020 покажи коротко: сколько денег входило и уходило, и что это по смыслу в 1С — клиентская выручка, поставщик, комиссия, кредит или другой финансовый поток?",
"created_at": "2026-05-13T12:00:47+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "Если СБЕРБАНК есть во входящих поступлениях, можно ли считать его нашим клиентом и выручкой? Скажи по подтвержденным строкам, без притягивания.",
"created_at": "2026-05-13T12:00:47+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А если деньги уходили в СБЕРБАНК, это наш поставщик или финансовые списания? Раздели по смыслу и покажи основание.",
"created_at": "2026-05-13T12:00:47+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"created_at": "2026-05-13T12:00:47+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"created_at": "2026-05-13T12:00:47+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": "phase103 financial role and purpose arbitration",
"architecture_phase": "Open-World Schema/Primitive Discovery",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase103_financial_role_purpose_arbitration.json",
"scenario_id": "address_truth_harness_phase103_financial_role_purpose_arbitration",
"semantic_tags": [
"bank_like_counterparty",
"bank_like_customer_boundary",
"bank_like_supplier_boundary",
"bank_operations_by_counterparty",
"bare_org_scope",
"business_overview",
"canary",
"counterparty_net_cash_flow",
"customer_revenue_and_payments",
"financial_role_purpose",
"phase102_canary",
"stale_scope_guard",
"supplier_payouts_profile"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase103_financial_role_purpose_arbitration_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase103_financial_role_purpose_arbitration_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,135 +0,0 @@
{
"saved_at": "2026-05-13T12:26:18+00:00",
"generation_id": "gen-ag05131226-630ddf",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 104 generic role-tail anchor hygiene replay",
"agent_run": true,
"questions": [
"Альтернатива Плюс",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "phase104 generic role-tail anchor hygiene",
"architecture_phase": "Open-World Schema/Primitive Discovery",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase104_generic_role_tail_anchor_hygiene.json",
"scenario_id": "address_truth_harness_phase104_generic_role_tail_anchor_hygiene",
"semantic_tags": [
"bank_like_counterparty",
"bare_org_scope",
"business_overview",
"counterparty_net_cash_flow",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"phase102_canary",
"post_overview_anchor_integrity",
"stale_scope_guard",
"supplier_prefix_canary"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Альтернатива Плюс",
"created_at": "2026-05-13T12:26:18+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"created_at": "2026-05-13T12:26:18+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"created_at": "2026-05-13T12:26:18+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
"created_at": "2026-05-13T12:26:18+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": "phase104 generic role-tail anchor hygiene",
"architecture_phase": "Open-World Schema/Primitive Discovery",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase104_generic_role_tail_anchor_hygiene.json",
"scenario_id": "address_truth_harness_phase104_generic_role_tail_anchor_hygiene",
"semantic_tags": [
"bank_like_counterparty",
"bare_org_scope",
"business_overview",
"counterparty_net_cash_flow",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"phase102_canary",
"post_overview_anchor_integrity",
"stale_scope_guard",
"supplier_prefix_canary"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 4,
"steps_passed": 4,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,123 +0,0 @@
{
"saved_at": "2026-05-22T13:19:31+00:00",
"generation_id": "gen-ag05221319-4035f5",
"mode": "saved_user_sessions",
"title": "AGENT | Inventory margin-ranking accepted replay 2026-05-22",
"agent_run": true,
"questions": [
"Какая номеклатура товара реализована с высокой прибылью какая с низкой",
"сентябрь 2017",
"покажи найденные строки себестоимостной базы",
"расширь до 2017 года",
"анализ по 41 счету а не 01"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "inventory_margin_ranking_followup_and_business_modes",
"architecture_phase": "Business Answer Contract / margin-ranking safety gate",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\inventory_margin_ranking_agent_loop_20260522.json",
"scenario_id": "inventory_margin_ranking_agent_loop_20260522",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_margin_ranking_agent_loop_live7",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_margin_ranking_agent_loop_live7",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 5,
"steps_passed": 5,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Какая номеклатура товара реализована с высокой прибылью какая с низкой",
"created_at": "2026-05-22T13:19:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "сентябрь 2017",
"created_at": "2026-05-22T13:19:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "покажи найденные строки себестоимостной базы",
"created_at": "2026-05-22T13:19:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "расширь до 2017 года",
"created_at": "2026-05-22T13:19:31+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "анализ по 41 счету а не 01",
"created_at": "2026-05-22T13:19:31+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": "inventory_margin_ranking_followup_and_business_modes",
"architecture_phase": "Business Answer Contract / margin-ranking safety gate",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\inventory_margin_ranking_agent_loop_20260522.json",
"scenario_id": "inventory_margin_ranking_agent_loop_20260522",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_margin_ranking_agent_loop_live7",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_margin_ranking_agent_loop_live7",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 5,
"steps_passed": 5,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,119 +0,0 @@
{
"saved_at": "2026-05-22T19:57:37+00:00",
"generation_id": "gen-ag05221957-713bbd",
"mode": "saved_user_sessions",
"title": "AGENT | Hot value-flow discovery handoff",
"agent_run": true,
"questions": [
"Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted AGENT replay for the current Autonomy Core slice: organization-scoped value-flow questions must be answered through guarded MCP discovery response with hot handoff, not through stale exact fallback or a counterparty-only route.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_hot_value_flow_handoff_20260522.json",
"scenario_id": "agent_hot_value_flow_handoff_20260522",
"semantic_tags": [
"autonomy_core",
"colloquial_total",
"guarded_response",
"hot_handoff",
"incoming_total",
"outgoing_total",
"value_flow"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_hot_value_flow_handoff_live5",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_hot_value_flow_handoff_live5",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"created_at": "2026-05-22T19:57:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"created_at": "2026-05-22T19:57:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?",
"created_at": "2026-05-22T19:57: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": "Targeted AGENT replay for the current Autonomy Core slice: organization-scoped value-flow questions must be answered through guarded MCP discovery response with hot handoff, not through stale exact fallback or a counterparty-only route.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_hot_value_flow_handoff_20260522.json",
"scenario_id": "agent_hot_value_flow_handoff_20260522",
"semantic_tags": [
"autonomy_core",
"colloquial_total",
"guarded_response",
"hot_handoff",
"incoming_total",
"outgoing_total",
"value_flow"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_hot_value_flow_handoff_live5",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_hot_value_flow_handoff_live5",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,287 +0,0 @@
{
"saved_at": "2026-05-23T18:58:48+00:00",
"generation_id": "gen-ag05231858-323f86",
"mode": "saved_user_sessions",
"title": "AGENT | Autonomy business quality pack",
"agent_run": true,
"questions": [
"Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?",
"Какая чистая прибыль по ООО Альтернатива Плюс за 2020?",
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"кому мы должны на конец 2020?",
"а нам кто должен на конец 2020?",
"сколько НДС надо заплатить в налоговую за декабрь 2019?",
"Как ты оценишь деятельность компании?",
"Какая номенклатура товара реализована с высокой прибылью какая с низкой?",
"май 2020"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Expanded targeted AGENT replay for the autonomy milestone: value-flow, business overview, debts, VAT, profit/cashflow distinction, nomenclature margin boundary, and final answer quality must survive realistic business questions.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_autonomy_business_quality_20260523.json",
"scenario_id": "agent_autonomy_business_quality_20260523",
"semantic_tags": [
"autonomy_core",
"bank_boundary",
"bank_classification",
"business_answer_quality",
"business_evaluation",
"business_overview",
"cashflow_overview",
"cashflow_vs_profit",
"clarification",
"colloquial_money",
"colloquial_total",
"counterparty_value_flow",
"debt_answer_quality",
"direct_profit",
"domain_purity",
"followup_context",
"incoming_total",
"limit_honesty",
"limited_answer",
"net_flow",
"next_action",
"no_profit_substitution",
"no_top_guard",
"nomenclature_margin",
"outgoing_total",
"payables",
"profit_vs_cashflow",
"receivables",
"technical_garbage_guard",
"value_flow",
"vat"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_autonomy_business_quality_live_semantic_gate_accepted_20260523",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_autonomy_business_quality_live_semantic_gate_accepted_20260523",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 15,
"steps_passed": 15,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток.",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "скока денег альтернатива заработала за 20 год?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а это чистая прибыль?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "Какая чистая прибыль по ООО Альтернатива Плюс за 2020?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "кому мы должны на конец 2020?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "а нам кто должен на конец 2020?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "сколько НДС надо заплатить в налоговую за декабрь 2019?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "Как ты оценишь деятельность компании?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "Какая номенклатура товара реализована с высокой прибылью какая с низкой?",
"created_at": "2026-05-23T18:58:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "май 2020",
"created_at": "2026-05-23T18:58:48+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": "Expanded targeted AGENT replay for the autonomy milestone: value-flow, business overview, debts, VAT, profit/cashflow distinction, nomenclature margin boundary, and final answer quality must survive realistic business questions.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_autonomy_business_quality_20260523.json",
"scenario_id": "agent_autonomy_business_quality_20260523",
"semantic_tags": [
"autonomy_core",
"bank_boundary",
"bank_classification",
"business_answer_quality",
"business_evaluation",
"business_overview",
"cashflow_overview",
"cashflow_vs_profit",
"clarification",
"colloquial_money",
"colloquial_total",
"counterparty_value_flow",
"debt_answer_quality",
"direct_profit",
"domain_purity",
"followup_context",
"incoming_total",
"limit_honesty",
"limited_answer",
"net_flow",
"next_action",
"no_profit_substitution",
"no_top_guard",
"nomenclature_margin",
"outgoing_total",
"payables",
"profit_vs_cashflow",
"receivables",
"technical_garbage_guard",
"value_flow",
"vat"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_autonomy_business_quality_live_semantic_gate_accepted_20260523",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\agent_autonomy_business_quality_live_semantic_gate_accepted_20260523",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 15,
"steps_passed": 15,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,54 +1,58 @@
{
"saved_at": "2026-05-09T18:30:50+00:00",
"generation_id": "gen-ag05091830-062fc9",
"saved_at": "2026-05-24T23:06:41+00:00",
"generation_id": "gen-ag05242306-7b49ab",
"mode": "saved_user_sessions",
"title": "AGENT | Agentic semantic development loop dogfood gate",
"title": "AGENT | Бизнес-обзор компании и СВК accepted replay 2026-05-24",
"agent_run": true,
"questions": [
"Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020 год по данным 1С: обороты, входящие и исходящие деньги, нетто, НДС, долги, склад, клиенты, поставщики и что пока нельзя утверждать.",
"Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в 2020.",
"А если смотреть за все доступное время, какой самый доходный год по подтвержденным оборотам и почему? Не называй это бухгалтерской прибылью, если чистой прибыли нет.",
"Что с НДС за 2020 год по ООО Альтернатива Плюс: какая позиция видна, на чем она основана и чего не хватает для налогового вывода?",
"Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за 2020 как подтвержденную общую налоговую позицию.",
"Отдельно по контрагенту Группа СВК, без опоры на прошлый диалог: сколько денег прошло, что входящее, что исходящее и есть ли документы или движения, на которых это основано?",
"Покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс.",
"Собери короткий итог: что мы подтвердили по компании, что отдельно по Группа СВК, какие выводы можно делать и какие нельзя.",
"Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК.",
"Покажи хвосты по счету 60 на август 2020 по ООО Альтернатива Плюс; если точных данных нет, скажи это прямо и не подменяй ответ общим обзором.",
"Что было на складе на март 2021 по доступным данным? Дай прямой ответ и не уводи его в контрагента Группа СВК.",
"Вернись к ООО Альтернатива Плюс: сколько всего денег получили и заплатили по всем подтвержденным данным, но не смешивай это с отдельной цепочкой Группа СВК и не называй оборот чистой прибылью."
"По-человечески посмотри ООО Альтернатива Плюс за 2020: что по бизнесу видно в 1С по деньгам, НДС, долгам, складу, клиентам/поставщикам и где пока нельзя делать вывод?",
"А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки.",
"Возьми как выбранного контрагента Группа СВК: сколько по нему входящих денег, исходящих денег и расчетное нетто, на чем это основано?",
"Теперь по нему покажи документы и не переключайся на организацию.",
"И коротко сравни: что у нас подтверждено по компании, что отдельно по выбранному контрагенту, и какие выводы нельзя делать?",
"ООО Альтернатива Плюс"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Automate stage question review, live semantic replay, strong business audit, Lead Codex repair handoff, rerun validation, and final human confirmation.",
"architecture_phase": "turnaround_11_agentic_semantic_development_loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agentic_semantic_development_loop_stage_pack.json",
"agent_focus": "Accepted proof pack for company business overview, SVK counterparty money/documents, selected-object carryover, org clarification, and all-time scope reset",
"architecture_phase": "agent_reliability_uplift_business_overview_counterparty",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_business_overview_counterparty_reliability_20260524.json",
"scenario_id": null,
"semantic_tags": [
"business_overview",
"colloquial",
"counterparty",
"debt",
"documents",
"inventory",
"memory",
"money",
"organization_clarification",
"pronoun_followup",
"scope_guard",
"selected_object",
"vat"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\stage_agent_loops\\agentic_semantic_development_loop\\domain_loops\\asl",
"validated_run_dir": "artifacts\\domain_runs\\agent_business_overview_counterparty_proof_20260524_39",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\stage_agent_loops\\agentic_semantic_development_loop\\domain_loops\\asl",
"validated_run_dir": "artifacts\\domain_runs\\agent_business_overview_counterparty_proof_20260524_39",
"final_status": "accepted",
"loop_id": "asl",
"target_score": 88,
"loop_id": "agent_business_overview_counterparty_proof_20260524_39",
"target_score": 90,
"iterations_ran": 1,
"quality_score": 91,
"quality_score": 92,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
@ -56,6 +60,21 @@
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\agent_business_overview_counterparty_proof_20260524_39\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "7cc65e808eb2430e7d27f2ac786c340aecc7fc50",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
@ -68,7 +87,7 @@
"message_id": "agent-user-001",
"role": "user",
"text": "Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020 год по данным 1С: обороты, входящие и исходящие деньги, нетто, НДС, долги, склад, клиенты, поставщики и что пока нельзя утверждать.",
"created_at": "2026-05-09T18:30:50+00:00",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -77,7 +96,7 @@
"message_id": "agent-user-002",
"role": "user",
"text": "Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в 2020.",
"created_at": "2026-05-09T18:30:50+00:00",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -85,8 +104,8 @@
{
"message_id": "agent-user-003",
"role": "user",
"text": "А если смотреть за все доступное время, какой самый доходный год по подтвержденным оборотам и почему? Не называй это бухгалтерской прибылью, если чистой прибыли нет.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Что с НДС за 2020 год по ООО Альтернатива Плюс: какая позиция видна, на чем она основана и чего не хватает для налогового вывода?",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -94,8 +113,8 @@
{
"message_id": "agent-user-004",
"role": "user",
"text": "Что с НДС за 2020 год по ООО Альтернатива Плюс: какая позиция видна, на чем она основана и чего не хватает для налогового вывода?",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за 2020 как подтвержденную общую налоговую позицию.",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -103,8 +122,8 @@
{
"message_id": "agent-user-005",
"role": "user",
"text": "Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за 2020 как подтвержденную общую налоговую позицию.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Отдельно по контрагенту Группа СВК, без опоры на прошлый диалог: сколько денег прошло, что входящее, что исходящее и есть ли документы или движения, на которых это основано?",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -112,8 +131,8 @@
{
"message_id": "agent-user-006",
"role": "user",
"text": "Отдельно по контрагенту Группа СВК, без опоры на прошлый диалог: сколько денег прошло, что входящее, что исходящее и есть ли документы или движения, на которых это основано?",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс.",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -121,8 +140,8 @@
{
"message_id": "agent-user-007",
"role": "user",
"text": "Покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Собери короткий итог: что мы подтвердили по компании, что отдельно по Группа СВК, какие выводы можно делать и какие нельзя.",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -130,8 +149,8 @@
{
"message_id": "agent-user-008",
"role": "user",
"text": "Собери короткий итог: что мы подтвердили по компании, что отдельно по Группа СВК, какие выводы можно делать и какие нельзя.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "По-человечески посмотри ООО Альтернатива Плюс за 2020: что по бизнесу видно в 1С по деньгам, НДС, долгам, складу, клиентам/поставщикам и где пока нельзя делать вывод?",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -139,8 +158,8 @@
{
"message_id": "agent-user-009",
"role": "user",
"text": "Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки.",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -148,8 +167,8 @@
{
"message_id": "agent-user-010",
"role": "user",
"text": "Покажи хвосты по счету 60 на август 2020 по ООО Альтернатива Плюс; если точных данных нет, скажи это прямо и не подменяй ответ общим обзором.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Возьми как выбранного контрагента Группа СВК: сколько по нему входящих денег, исходящих денег и расчетное нетто, на чем это основано?",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -157,8 +176,8 @@
{
"message_id": "agent-user-011",
"role": "user",
"text": "Что было на складе на март 2021 по доступным данным? Дай прямой ответ и не уводи его в контрагента Группа СВК.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "Теперь по нему покажи документы и не переключайся на организацию.",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -166,8 +185,17 @@
{
"message_id": "agent-user-012",
"role": "user",
"text": "Вернись к ООО Альтернатива Плюс: сколько всего денег получили и заплатили по всем подтвержденным данным, но не смешивай это с отдельной цепочкой Группа СВК и не называй оборот чистой прибылью.",
"created_at": "2026-05-09T18:30:50+00:00",
"text": "И коротко сравни: что у нас подтверждено по компании, что отдельно по выбранному контрагенту, и какие выводы нельзя делать?",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "ООО Альтернатива Плюс",
"created_at": "2026-05-24T23:06:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
@ -178,33 +206,36 @@
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Automate stage question review, live semantic replay, strong business audit, Lead Codex repair handoff, rerun validation, and final human confirmation.",
"architecture_phase": "turnaround_11_agentic_semantic_development_loop",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agentic_semantic_development_loop_stage_pack.json",
"agent_focus": "Accepted proof pack for company business overview, SVK counterparty money/documents, selected-object carryover, org clarification, and all-time scope reset",
"architecture_phase": "agent_reliability_uplift_business_overview_counterparty",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_business_overview_counterparty_reliability_20260524.json",
"scenario_id": null,
"semantic_tags": [
"business_overview",
"colloquial",
"counterparty",
"debt",
"documents",
"inventory",
"memory",
"money",
"organization_clarification",
"pronoun_followup",
"scope_guard",
"selected_object",
"vat"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\stage_agent_loops\\agentic_semantic_development_loop\\domain_loops\\asl",
"validated_run_dir": "artifacts\\domain_runs\\agent_business_overview_counterparty_proof_20260524_39",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\stage_agent_loops\\agentic_semantic_development_loop\\domain_loops\\asl",
"validated_run_dir": "artifacts\\domain_runs\\agent_business_overview_counterparty_proof_20260524_39",
"final_status": "accepted",
"loop_id": "asl",
"target_score": 88,
"loop_id": "agent_business_overview_counterparty_proof_20260524_39",
"target_score": 90,
"iterations_ran": 1,
"quality_score": 91,
"quality_score": 92,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
@ -212,6 +243,21 @@
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\agent_business_overview_counterparty_proof_20260524_39\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "7cc65e808eb2430e7d27f2ac786c340aecc7fc50",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}

View File

@ -1,63 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-mo2kcds2-tlqmvng",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-17T07:04:48.578Z",
"generation_id": "gen-mo2kcds2-tlqmvng",
"mode": "saved_user_sessions",
"title": "Ручная сессия 17.04.2026, 10:04:19 ТЕМП",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions",
"title": "Ручная сессия 17.04.2026, 10:04:19 ТЕМП",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "покажи все документы по чепурнову"
},
{
"user_message": "что нам отгружал чепурнов, какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня?"
},
{
"user_message": "хвосты по счету 60 на август 2022"
},
{
"user_message": "какие остатки на складе на март 2021"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": покажи документы по этой позиции"
},
{
"user_message": "покажи еще раз остатки на эту же дату"
},
{
"user_message": "какие остатки на складе на март 2016"
},
{
"user_message": "на июль 2019"
},
{
"user_message": "на сентябрь"
},
{
"user_message": "а на март"
},
{
"user_message": "это по общей базе"
}
]
}
]
}

View File

@ -1,97 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag04241406-abe4d8",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-24T14:06:30+00:00",
"generation_id": "gen-ag04241406-abe4d8",
"mode": "saved_user_sessions",
"title": "AGENT | Post-F cross-stage semantic integrity canary",
"domain": "address_post_f_cross_stage_canary_agent",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Post-F cross-stage semantic integrity canary",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?"
},
{
"user_message": "Хорошо, тогда покажи движения по ООО Альтернатива Плюс."
},
{
"user_message": "За 2020 год."
},
{
"user_message": "А теперь по документам?"
},
{
"user_message": "А теперь за 2021 год?"
},
{
"user_message": "А теперь за все время?"
},
{
"user_message": "С НДС закончили. Новая тема: покажи документы по Жуковке 51."
},
{
"user_message": "Хорошо, а теперь платежи по нему тоже покажи."
},
{
"user_message": "А по нему договоры?"
},
{
"user_message": "А по нему документы?"
},
{
"user_message": "А за 2021?"
},
{
"user_message": "С Жуковкой закончили. Теперь другая задача: быстрый денежный срез по одной организации. Если для ответа нужна организация, просто уточни ее. Сколько вообще входящих денег было за 2020 год?"
},
{
"user_message": "По ООО Альтернатива Плюс."
},
{
"user_message": "Понял, тогда за все время."
},
{
"user_message": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?"
},
{
"user_message": "И кто больше всего принес денег этой организации в 2020 году?"
},
{
"user_message": "А в 2021 году?"
},
{
"user_message": "Теперь отдельная тема по конкретному контрагенту. Найди в 1С Группу СВК."
},
{
"user_message": "Сколько получили по нему за 2020 год?"
},
{
"user_message": "А теперь сколько заплатили?"
},
{
"user_message": "А какое нетто?"
},
{
"user_message": "А по документам?"
},
{
"user_message": "А по движениям?"
},
{
"user_message": "А теперь тот же смысл за 2021 год."
}
]
}
]
}

View File

@ -1,58 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag04241710-bdb248",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-24T17:10:31+00:00",
"generation_id": "gen-ag04241710-bdb248",
"mode": "saved_user_sessions",
"title": "AGENT | Post-F ручные провалы VAT revenue item-flow live3",
"domain": "address_post_f_manual_failures",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Post-F ручные провалы VAT revenue item-flow live3",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
}
]
}
]
}

View File

@ -1,40 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05100949-2a5ed8",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-10T09:49:21+00:00",
"generation_id": "gen-ag05100949-2a5ed8",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 91 route-candidate-driven enablement loop canary",
"domain": "address_phase91_route_candidate_driven_enablement_loop",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 91 route-candidate-driven enablement loop canary",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "какой контрагент принес больше всего денег за 2020 год?"
},
{
"user_message": "ООО Альтернатива Плюс"
},
{
"user_message": "а теперь сколько лет мы работаем с Группа СВК?"
},
{
"user_message": "покажи движения по НДС за 2020 по ООО Альтернатива Плюс"
},
{
"user_message": "можно ли по этим данным точно подтвердить резерв под неликвиды на складе?"
}
]
}
]
}

View File

@ -1,43 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05101213-596d99",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-10T12:13:26+00:00",
"generation_id": "gen-ag05101213-596d99",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 93 accounting profit-margin reviewed route",
"domain": "address_phase93_accounting_profit_margin_reviewed_route",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 93 accounting profit-margin reviewed route",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "по ООО Альтернатива Плюс за 2020 можно точно сказать чистую прибыль и маржу?"
},
{
"user_message": "а это прибыль или убыток, коротко?"
},
{
"user_message": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "тогда НДС за 2020 по ООО Альтернатива Плюс какой?"
},
{
"user_message": "а какой контрагент принес больше всего денег за 2020?"
},
{
"user_message": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
}
]
}
]
}

View File

@ -1,46 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05101319-c04f79",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-10T13:19:22+00:00",
"generation_id": "gen-ag05101319-c04f79",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 94 debt due-date aging reviewed route",
"domain": "address_phase94_debt_due_date_aging_reviewed_route",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 94 debt due-date aging reviewed route",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "по ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "то есть просрочку доказать нельзя, коротко почему?"
},
{
"user_message": "а чистая прибыль и маржа за 2020 по этой же компании какие?"
},
{
"user_message": "НДС за 2020 по ООО Альтернатива Плюс какой?"
},
{
"user_message": "а кто принес больше всего денег за 2020?"
},
{
"user_message": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
},
{
"user_message": "а зависимость от одного поставщика за 2020 можно точно оценить?"
}
]
}
]
}

View File

@ -1,46 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05121357-9ea5d6",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T13:57:26+00:00",
"generation_id": "gen-ag05121357-9ea5d6",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"domain": "address_phase95_vendor_procurement_quality_reviewed_route",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?"
},
{
"user_message": "а банк из этого ответа считать обычным поставщиком?"
},
{
"user_message": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "а чистая прибыль и маржа за 2020 какие?"
},
{
"user_message": "НДС за 2020 по ООО Альтернатива Плюс какой?"
},
{
"user_message": "а кто принес больше всего денег за 2020?"
},
{
"user_message": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
}
]
}
]
}

View File

@ -1,43 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05121628-50ea6c",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T16:28:41+00:00",
"generation_id": "gen-ag05121628-50ea6c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"domain": "address_phase20_continuity_stabilization",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "кто у нас самый доходный клиент за все время?"
},
{
"user_message": "какой у нас самый доходный год?"
},
{
"user_message": "кто нам должен денег на май 2017?"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
}
]
}
]
}

View File

@ -1,31 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05122057-c9786e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T20:57:28+00:00",
"generation_id": "gen-ag05122057-c9786e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 96 inventory reserve/liquidation quality-events",
"domain": "address_phase96_inventory_reserve_liquidation_quality",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 96 inventory reserve/liquidation quality-events",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "По ООО Альтернатива Плюс за 2020 год проверь склад: были ли списания товаров, оприходования/корректировки, инвентаризации, переоценки, резервы под неликвиды или ликвидационная стоимость? Скажи коротко и честно, что подтверждено 1С, а что нельзя утверждать."
},
{
"user_message": "А по этим же данным можно сказать, что склад ликвидный и неликвидов нет?"
}
]
}
]
}

View File

@ -1,37 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05122250-4451a8",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T22:50:23+00:00",
"generation_id": "gen-ag05122250-4451a8",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"domain": "address_phase97_financial_counterparty_flow_hints",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 97 financial counterparty flow hints replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С."
},
{
"user_message": "А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено."
},
{
"user_message": "Теперь дай взрослый краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и отдельно отметь, если в топах есть банк, почему его нельзя читать как обычного клиента или поставщика."
},
{
"user_message": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
}
]
}
]
}

View File

@ -1,43 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05122315-f1e27c",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T23:15:48+00:00",
"generation_id": "gen-ag05122315-f1e27c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"domain": "address_phase98_limit_honesty_business_language",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "То есть просрочку доказать нельзя, коротко почему?"
},
{
"user_message": "НДС за 2020 по ООО Альтернатива Плюс какой?"
},
{
"user_message": "А кто принес больше всего денег за 2020?"
},
{
"user_message": "По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
},
{
"user_message": "А зависимость от одного поставщика за 2020 можно точно оценить?"
}
]
}
]
}

View File

@ -1,37 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05131009-f08174",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-13T10:09:01+00:00",
"generation_id": "gen-ag05131009-f08174",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 99 large-query budget and continuation policy replay",
"domain": "address_phase99_large_query_budget_continuation",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 99 large-query budget and continuation policy replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто, кто основные источники денег и где важные ограничения."
},
{
"user_message": "То есть это можно считать прибылью за 2020 или нет? Коротко."
},
{
"user_message": "А кто за 2020 принес больше всего денег, и если там банк, не называй его обычным клиентом."
},
{
"user_message": "По исходящим за 2020 есть зависимость от одного поставщика или это только денежная концентрация?"
}
]
}
]
}

View File

@ -1,34 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05131028-234e5e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-13T10:28:45+00:00",
"generation_id": "gen-ag05131028-234e5e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 100 large-query continuation UX replay",
"domain": "address_phase100_large_query_continuation_ux",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 100 large-query continuation UX replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Дай общий бизнес-обзор ООО Альтернатива Плюс за весь доступный период: входящие, исходящие, нетто, лучший год. Если срез слишком широкий, не выдумывай полный итог, а скажи как безопасно дособрать."
},
{
"user_message": "Ок, тогда дособери конкретно 2020: входящие, исходящие и расчетное денежное нетто."
},
{
"user_message": "Это можно считать прибылью за 2020 или нет? Ответь коротко и по делу."
}
]
}
]
}

View File

@ -1,46 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05131044-cbe2ff",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-13T10:44:38+00:00",
"generation_id": "gen-ag05131044-cbe2ff",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 101 inventory root scope without warehouse clarification replay",
"domain": "address_phase101_inventory_root_scope_no_warehouse_clarification",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 101 inventory root scope without warehouse clarification replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июнь 2017"
},
{
"user_message": "март 2016"
}
]
}
]
}

View File

@ -1,43 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05131121-8c41ab",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-13T11:21:52+00:00",
"generation_id": "gen-ag05131121-8c41ab",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 102 debt mirror clean-scope polarity replay",
"domain": "address_phase102_debt_mirror_clean_scope_polarity",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 102 debt mirror clean-scope polarity replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Альтернатива Плюс"
},
{
"user_message": "мы кому реально должны денег на сегодня? коротко, чистый долг к оплате, без встречных обеспечений как основного долга"
},
{
"user_message": "а Комитету государственных услуг мы реально должны эти 3,6 млн или это встречное обеспечение/зачет? объясни именно по смыслу долга"
},
{
"user_message": "а нам Комитет государственных услуг тоже должен 3,6 млн? это же та же сумма — скажи честно, это дебиторка или встречная часть?"
},
{
"user_message": "тогда кто нам реально должен денег на сегодня? именно чистая дебиторка"
},
{
"user_message": "а мы кому?"
}
]
}
]
}

View File

@ -1,43 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05131200-0ed59a",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-13T12:00:47+00:00",
"generation_id": "gen-ag05131200-0ed59a",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 103 financial role and purpose arbitration replay",
"domain": "address_phase103_financial_role_purpose_arbitration",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 103 financial role and purpose arbitration replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Альтернатива Плюс"
},
{
"user_message": "По СБЕРБАНКУ за 2020 покажи коротко: сколько денег входило и уходило, и что это по смыслу в 1С — клиентская выручка, поставщик, комиссия, кредит или другой финансовый поток?"
},
{
"user_message": "Если СБЕРБАНК есть во входящих поступлениях, можно ли считать его нашим клиентом и выручкой? Скажи по подтвержденным строкам, без притягивания."
},
{
"user_message": "А если деньги уходили в СБЕРБАНК, это наш поставщик или финансовые списания? Раздели по смыслу и покажи основание."
},
{
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик."
},
{
"user_message": "А теперь отдельно по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
}
]
}
]
}

View File

@ -1,37 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05131226-630ddf",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-13T12:26:18+00:00",
"generation_id": "gen-ag05131226-630ddf",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 104 generic role-tail anchor hygiene replay",
"domain": "address_phase104_generic_role_tail_anchor_hygiene",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 104 generic role-tail anchor hygiene replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Альтернатива Плюс"
},
{
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик."
},
{
"user_message": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам."
},
{
"user_message": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
}
]
}
]
}

View File

@ -1,40 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05221319-4035f5",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-22T13:19:31+00:00",
"generation_id": "gen-ag05221319-4035f5",
"mode": "saved_user_sessions",
"title": "AGENT | Inventory margin-ranking accepted replay 2026-05-22",
"domain": "inventory_margin_ranking",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Inventory margin-ranking accepted replay 2026-05-22",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какая номеклатура товара реализована с высокой прибылью какая с низкой"
},
{
"user_message": "сентябрь 2017"
},
{
"user_message": "покажи найденные строки себестоимостной базы"
},
{
"user_message": "расширь до 2017 года"
},
{
"user_message": "анализ по 41 счету а не 01"
}
]
}
]
}

View File

@ -1,34 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05221957-713bbd",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-22T19:57:37+00:00",
"generation_id": "gen-ag05221957-713bbd",
"mode": "saved_user_sessions",
"title": "AGENT | Hot value-flow discovery handoff",
"domain": "autonomy_hot_value_flow_handoff",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Hot value-flow discovery handoff",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?"
},
{
"user_message": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?"
},
{
"user_message": "А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?"
}
]
}
]
}

View File

@ -1,70 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag05231858-323f86",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-23T18:58:48+00:00",
"generation_id": "gen-ag05231858-323f86",
"mode": "saved_user_sessions",
"title": "AGENT | Autonomy business quality pack",
"domain": "autonomy_business_quality",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Autonomy business quality pack",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?"
},
{
"user_message": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс без разреза по контрагентам?"
},
{
"user_message": "А всего сколько денег пришло в ООО Альтернатива Плюс за 2020, без топов и без контрагентов?"
},
{
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток."
},
{
"user_message": "скока денег альтернатива заработала за 20 год?"
},
{
"user_message": "а это чистая прибыль?"
},
{
"user_message": "Какая чистая прибыль по ООО Альтернатива Плюс за 2020?"
},
{
"user_message": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам."
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
},
{
"user_message": "кому мы должны на конец 2020?"
},
{
"user_message": "а нам кто должен на конец 2020?"
},
{
"user_message": "сколько НДС надо заплатить в налоговую за декабрь 2019?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "Какая номенклатура товара реализована с высокой прибылью какая с низкой?"
},
{
"user_message": "май 2020"
}
]
}
]
}

View File

@ -1,12 +1,12 @@
{
"suite_id": "assistant_saved_session_gen-ag05091830-062fc9",
"suite_id": "assistant_saved_session_gen-ag05242306-7b49ab",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-09T18:30:50+00:00",
"generation_id": "gen-ag05091830-062fc9",
"generated_at": "2026-05-24T23:06:41+00:00",
"generation_id": "gen-ag05242306-7b49ab",
"mode": "saved_user_sessions",
"title": "AGENT | Agentic semantic development loop dogfood gate",
"domain": "agentic_semantic_development_loop_control",
"title": "AGENT | Бизнес-обзор компании и СВК accepted replay 2026-05-24",
"domain": "agent_business_overview_counterparty_reliability",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
@ -15,7 +15,7 @@
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Agentic semantic development loop dogfood gate",
"title": "AGENT | Бизнес-обзор компании и СВК accepted replay 2026-05-24",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
@ -25,9 +25,6 @@
{
"user_message": "Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в 2020."
},
{
"user_message": "А если смотреть за все доступное время, какой самый доходный год по подтвержденным оборотам и почему? Не называй это бухгалтерской прибылью, если чистой прибыли нет."
},
{
"user_message": "Что с НДС за 2020 год по ООО Альтернатива Плюс: какая позиция видна, на чем она основана и чего не хватает для налогового вывода?"
},
@ -44,16 +41,22 @@
"user_message": "Собери короткий итог: что мы подтвердили по компании, что отдельно по Группа СВК, какие выводы можно делать и какие нельзя."
},
{
"user_message": "Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК."
"user_message": "По-человечески посмотри ООО Альтернатива Плюс за 2020: что по бизнесу видно в 1С по деньгам, НДС, долгам, складу, клиентам/поставщикам и где пока нельзя делать вывод?"
},
{
"user_message": "Покажи хвосты по счету 60 на август 2020 по ООО Альтернатива Плюс; если точных данных нет, скажи это прямо и не подменяй ответ общим обзором."
"user_message": "А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки."
},
{
"user_message": "Что было на складе на март 2021 по доступным данным? Дай прямой ответ и не уводи его в контрагента Группа СВК."
"user_message": "Возьми как выбранного контрагента Группа СВК: сколько по нему входящих денег, исходящих денег и расчетное нетто, на чем это основано?"
},
{
"user_message": "Вернись к ООО Альтернатива Плюс: сколько всего денег получили и заплатили по всем подтвержденным данным, но не смешивай это с отдельной цепочкой Группа СВК и не называй оборот чистой прибылью."
"user_message": "Теперь по нему покажи документы и не переключайся на организацию."
},
{
"user_message": "И коротко сравни: что у нас подтверждено по компании, что отдельно по выбранному контрагенту, и какие выводы нельзя делать?"
},
{
"user_message": "ООО Альтернатива Плюс"
}
]
}

View File

@ -1972,6 +1972,11 @@ def derive_step_execution_status(reply_type: str | None, debug_payload: dict[str
and mcp_discovery_candidate_status == "ready_for_guarded_use"
and reply_type in {"factual", "factual_with_explanation", "partial_coverage"}
):
if (
mcp_discovery_candidate.get("hot_runtime_wired") is True
and debug_payload.get("mcp_discovery_execution_handoff_can_use_guarded_response") is True
):
return "exact"
return "partial"
capability_route_mode = str(debug_payload.get("capability_route_mode") or "").strip()
fallback_type = str(debug_payload.get("fallback_type") or "").strip()
@ -2360,6 +2365,8 @@ def is_validated_memory_checkpoint_answer(
business_review: dict[str, Any],
violations: list[str],
) -> bool:
if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations):
return True
tags = set(normalize_string_list(state.get("semantic_tags")))
if "memory" not in tags:
return False
@ -2375,6 +2382,46 @@ def is_validated_memory_checkpoint_answer(
)
def is_validated_comparison_boundary_memory_checkpoint(
state: dict[str, Any],
business_review: dict[str, Any],
violations: list[str],
) -> bool:
if violations:
return False
tags = set(normalize_string_list(state.get("semantic_tags")))
if not {"business_overview", "selected_object", "organization_clarification"} <= tags:
return False
comparison_scope = state.get("comparison_scope") if isinstance(state.get("comparison_scope"), dict) else {}
organization = comparison_scope.get("organization") if isinstance(comparison_scope.get("organization"), dict) else {}
counterparty = comparison_scope.get("counterparty") if isinstance(comparison_scope.get("counterparty"), dict) else {}
proof_bundles = comparison_scope.get("proof_bundles") if isinstance(comparison_scope.get("proof_bundles"), dict) else {}
value_flow_bundle = (
proof_bundles.get("counterparty_value_flow_bundle")
if isinstance(proof_bundles.get("counterparty_value_flow_bundle"), dict)
else None
)
document_bundle = (
proof_bundles.get("counterparty_document_bundle")
if isinstance(proof_bundles.get("counterparty_document_bundle"), dict)
else None
)
organization_label = str(organization.get("label") or "").strip()
counterparty_label = str(counterparty.get("label") or "").strip()
focus_object = state.get("focus_object") if isinstance(state.get("focus_object"), dict) else {}
focus_label = str(focus_object.get("label") or "").strip()
if not organization_label or not counterparty_label or not (value_flow_bundle or document_bundle):
return False
if focus_label and focus_label.casefold() != counterparty_label.casefold():
return False
return (
business_review.get("direct_answer_first_ok") is True
and business_review.get("answer_layering_ok") is True
and business_review.get("technical_garbage_present") is False
and business_review.get("business_usefulness_ok") is True
)
def is_validated_confirmed_runtime_answer(
state: dict[str, Any],
execution_status: str,
@ -2386,7 +2433,17 @@ def is_validated_confirmed_runtime_answer(
if violations:
return False
if state.get("mcp_discovery_response_applied") is True:
return False
return (
str(state.get("mcp_discovery_response_candidate_status") or "").strip() == "ready_for_guarded_use"
and state.get("mcp_discovery_response_candidate_hot_runtime_wired") is True
and state.get("mcp_discovery_execution_handoff_can_use_guarded_response") is True
and str(state.get("reply_type") or "").strip()
in {"factual", "factual_with_explanation", "partial_coverage"}
and business_review.get("business_usefulness_ok") is True
and business_review.get("direct_answer_first_ok") is True
and business_review.get("answer_layering_ok") is True
and business_review.get("technical_garbage_present") is False
)
if str(state.get("reply_type") or "").strip() not in {"factual", "factual_with_explanation", "empty_but_valid"}:
return False
if str(state.get("fallback_type") or "").strip() not in {"", "none"}:
@ -2489,6 +2546,33 @@ def is_validated_clarification_answer(
)
def is_validated_missing_axis_clarification_answer(
state: dict[str, Any],
execution_status: str,
business_review: dict[str, Any],
violations: list[str],
) -> bool:
if execution_status != "partial":
return False
if violations:
return False
if str(state.get("mcp_discovery_response_candidate_status") or "").strip() != "clarification_candidate":
return False
if str(state.get("mcp_discovery_route_candidate_status") or "").strip() != "needs_user_scope":
return False
if str(state.get("mcp_discovery_response_reply_type") or "").strip() != "clarification_required":
return False
if not normalize_string_list(state.get("mcp_discovery_route_candidate_missing_axes")):
return False
return (
business_review.get("business_usefulness_ok") is True
and business_review.get("direct_answer_first_ok") is True
and business_review.get("answer_layering_ok") is True
and business_review.get("technical_garbage_present") is False
and business_review.get("next_action_present") is True
)
def _business_review_is_clean(step_state: dict[str, Any]) -> bool:
business_review = step_state.get("business_first_review")
if not isinstance(business_review, dict):
@ -2736,6 +2820,12 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
business_review,
unique_violations,
)
missing_axis_clarification_validated = is_validated_missing_axis_clarification_answer(
state,
execution_status,
business_review,
unique_violations,
)
state["violated_invariants"] = unique_violations
state["warnings"] = list(dict.fromkeys(warnings))
state["hard_fail"] = hard_fail
@ -2744,6 +2834,7 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
state["runtime_factual_answer_validated"] = runtime_factual_validated
state["guarded_insufficiency_validated"] = guarded_insufficiency_validated
state["clarification_answer_validated"] = clarification_validated
state["missing_axis_clarification_validated"] = missing_axis_clarification_validated
state["acceptance_status"] = acceptance_status_from_execution(
execution_status,
hard_fail,
@ -2753,6 +2844,7 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
or runtime_factual_validated
or guarded_insufficiency_validated
or clarification_validated
or missing_axis_clarification_validated
),
)
state["status"] = state["acceptance_status"]
@ -2913,6 +3005,7 @@ def build_scenario_step_state(
"actual_direct_answer": top_non_empty[0] if top_non_empty else None,
"extracted_filters": debug.get("extracted_filters") if isinstance(debug.get("extracted_filters"), dict) else {},
"focus_object": context.get("active_focus_object") if isinstance(context.get("active_focus_object"), dict) else None,
"comparison_scope": context.get("comparison_scope") if isinstance(context.get("comparison_scope"), dict) else None,
"fallback_type": debug.get("fallback_type"),
"mcp_call_status": debug.get("mcp_call_status"),
"balance_confirmed": debug.get("balance_confirmed"),
@ -2947,6 +3040,20 @@ def save_scenario_step_bundle(
write_text(step_dir / "resolved_question.txt", f"{step_state['question_resolved']}\n")
def is_effectively_complete_partial_step(step_output: dict[str, Any]) -> bool:
execution_status = str(step_output.get("execution_status") or step_output.get("status") or "").strip()
if execution_status == "exact":
return True
if execution_status != "partial":
return False
if str(step_output.get("acceptance_status") or "").strip() != "validated":
return False
return (
step_output.get("missing_axis_clarification_validated") is True
or step_output.get("clarification_answer_validated") is True
)
def derive_scenario_execution_status(step_outputs: dict[str, dict[str, Any]]) -> str:
statuses = [str(item.get("execution_status") or item.get("status") or "") for item in step_outputs.values()]
if not statuses:
@ -2955,7 +3062,7 @@ def derive_scenario_execution_status(step_outputs: dict[str, dict[str, Any]]) ->
return "blocked"
if any(status == "needs_exact_capability" for status in statuses):
return "needs_exact_capability"
if any(status == "partial" for status in statuses):
if any(not is_effectively_complete_partial_step(item) for item in step_outputs.values()):
return "partial"
return "exact"
@ -3419,6 +3526,7 @@ def build_pack_summary(
scenario_results: list[dict[str, Any]],
final_status: str,
execution_status: str,
acceptance_status: str,
) -> str:
lines = [
"# Pack summary",
@ -3427,6 +3535,7 @@ def build_pack_summary(
f"- domain: `{pack['domain']}`",
f"- title: {pack['title']}",
f"- execution_status: `{execution_status}`",
f"- acceptance_status: `{acceptance_status}`",
f"- final_status: `{final_status}`",
"",
"## Scenarios",
@ -3450,6 +3559,7 @@ def build_pack_final_status(
scenario_results: list[dict[str, Any]],
final_status: str,
execution_status: str,
acceptance_status: str,
) -> str:
expected_scenarios = len(pack.get("scenarios") or [])
executed_scenarios = len(scenario_results)
@ -3471,6 +3581,7 @@ def build_pack_final_status(
- status: `{final_status}`
- execution_status: `{execution_status}`
- acceptance_status: `{acceptance_status}`
- pack_id: `{pack['pack_id']}`
- domain: `{pack['domain']}`
- reason: {reason}
@ -3522,6 +3633,18 @@ def derive_pack_final_status(pack: dict[str, Any], scenario_results: list[dict[s
return "accepted" if len(scenario_results) == len(pack.get("scenarios") or []) else "partial"
def derive_pack_effective_final_status(acceptance_status: str, execution_status: str) -> str:
normalized_acceptance = str(acceptance_status or "").strip() or "partial"
normalized_execution = str(execution_status or "").strip() or "partial"
if normalized_acceptance in {"blocked", "needs_exact_capability"}:
return normalized_acceptance
if normalized_execution in {"blocked", "needs_exact_capability"}:
return normalized_execution
if normalized_acceptance == "accepted" and normalized_execution == "exact":
return "accepted"
return "partial"
def build_scenario_acceptance_matrix(pack: dict[str, Any], scenario_results: list[dict[str, Any]]) -> str:
scenario_status_map = {
str(item.get("scenario_id") or ""): str(item.get("final_status") or "unknown")
@ -3906,6 +4029,7 @@ def compact_step_output_for_review(step_output: Any) -> dict[str, Any]:
"required_carryover_invariants": step_output.get("required_carryover_invariants"),
"extracted_filters": step_output.get("extracted_filters"),
"focus_object": step_output.get("focus_object"),
"comparison_scope": step_output.get("comparison_scope"),
"date_scope": step_output.get("date_scope"),
"result_mode": step_output.get("result_mode"),
"truth_mode": step_output.get("truth_mode"),
@ -4436,6 +4560,8 @@ def build_deterministic_repair_targets(
"schema_version": "domain_pack_repair_targets_v1",
"pack_id": pack_state.get("pack_id"),
"domain": pack_state.get("domain"),
"execution_status": pack_state.get("execution_status"),
"acceptance_status": pack_state.get("acceptance_status"),
"final_status": pack_state.get("final_status"),
"target_count": len(targets),
"severity_counts": severity_counts,
@ -4588,6 +4714,18 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
):
return True
if step_snapshot.get("memory_checkpoint_validated") is True and evidence_problem:
memory_checkpoint_markers = (
"machine-readable",
"comparison boundary",
"carryover",
"comparison bundle",
"reusable comparison",
"selected counterparty",
)
if any(marker in target_text for marker in memory_checkpoint_markers):
return True
followup_problem = problem_type in {
"followup_action_resolution_gap",
"object_memory_gap",
@ -4980,8 +5118,9 @@ def evaluate_deterministic_loop_gate(
repair_targets: dict[str, Any],
) -> tuple[bool, str]:
pack_final_status = str(pack_state.get("final_status") or "").strip() or "partial"
if pack_final_status != "accepted":
return False, f"pack_final_status={pack_final_status}"
pack_acceptance_status = str(pack_state.get("acceptance_status") or pack_final_status).strip() or "partial"
if pack_acceptance_status != "accepted":
return False, f"pack_acceptance_status={pack_acceptance_status};pack_final_status={pack_final_status}"
severity_counts = repair_targets.get("severity_counts") if isinstance(repair_targets, dict) else {}
if isinstance(severity_counts, dict):
p0_count = int(severity_counts.get("P0") or 0)
@ -5120,7 +5259,8 @@ def build_analyst_loop_prompt(
Rules:
- `accepted` is allowed only if quality_score >= {target_score}, unresolved_p0_count = 0, and regression_detected = false;
- `accepted` is forbidden if the evidence bundle shows `pack_state.final_status != accepted` or the deterministic repair targets still contain any `P0` or `P1` items;
- `accepted` is forbidden if the evidence bundle shows `pack_state.acceptance_status != accepted` (or, for old artifacts without `acceptance_status`, `pack_state.final_status != accepted`) or the deterministic repair targets still contain any `P0` or `P1` items;
- `pack_state.final_status=partial` with `acceptance_status=accepted` means the run still had partial execution evidence; treat it as a proof-strength signal, not as a presentation bug by itself;
- `accepted` also requires `direct_answer_ok = true`, `business_usefulness_ok = true`, `temporal_honesty_ok = true`, and `field_truth_ok = true`;
- Treat validated bounded MCP discovery as the semantic route when `bounded_mcp_answer_validated = true`, `mcp_discovery_response_applied = true`, `mcp_discovery_response_candidate_status = ready_for_guarded_use`, and `mcp_discovery_effective_intents` matches the business question. The legacy address route may be only a seed lane; do not call that silent heuristic masking unless the selected discovery chain is wrong, not ready, not applied, or the user-facing answer/state is semantically wrong.
- Use `mcp_discovery_route_candidate_status`, `mcp_discovery_route_candidate_missing_axes`, and `mcp_discovery_route_candidate_enablement_reason` to distinguish a valid user-scope clarification from a real missing reviewed route. Do not ask the coder to overfit the visible answer when the correct next action is route enablement or missing-axis clarification.
@ -5998,7 +6138,8 @@ def handle_run_pack(args: argparse.Namespace) -> int:
)
execution_status = derive_pack_execution_status(scenario_results)
final_status = derive_pack_final_status(pack, scenario_results)
acceptance_status = derive_pack_final_status(pack, scenario_results)
final_status = derive_pack_effective_final_status(acceptance_status, execution_status)
pack_state = {
"schema_version": SCENARIO_PACK_SCHEMA_VERSION,
@ -6009,6 +6150,7 @@ def handle_run_pack(args: argparse.Namespace) -> int:
"bindings": pack.get("bindings") or {},
"scenario_results": scenario_results,
"execution_status": execution_status,
"acceptance_status": acceptance_status,
"final_status": final_status,
"updated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
}
@ -6018,8 +6160,8 @@ def handle_run_pack(args: argparse.Namespace) -> int:
write_json(pack_dir / "pack_state.json", pack_state)
write_json(pack_dir / "repair_targets.json", repair_targets)
write_text(pack_dir / "repair_targets.md", build_repair_targets_summary(repair_targets))
write_text(pack_dir / "pack_summary.md", build_pack_summary(pack, scenario_results, final_status, execution_status))
write_text(pack_dir / "final_status.md", build_pack_final_status(pack, scenario_results, final_status, execution_status))
write_text(pack_dir / "pack_summary.md", build_pack_summary(pack, scenario_results, final_status, execution_status, acceptance_status))
write_text(pack_dir / "final_status.md", build_pack_final_status(pack, scenario_results, final_status, execution_status, acceptance_status))
print(f"[domain-case-loop] saved pack artifacts to {pack_dir}")
print(f"[domain-case-loop] execution_status={execution_status} final_status={final_status}")
return 0

View File

@ -135,6 +135,45 @@ COMPANY_PROFIT_ANSWER_RE = re.compile(
"(?:\u0447\u0438\u0441\u0442\u0430\u044f\\s+\u043f\u0440\u0438\u0431\u044b\u043b\u044c|\u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e|90/91/99|\u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|\u0443\u0431\u044b\u0442\u043e\u043a)",
re.IGNORECASE,
)
BACKEND_ERROR_ANSWER_RE = re.compile(
"(?:backend_error|internal\\s+error|"
"\u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\\w*\\s+\u043e\u0448\u0438\u0431\u043a|"
"\u043d\u0435\\s+\u0443\u0434\u0430\u043b\u043e\u0441\u044c\\s+\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\\s+\u0440\u0430\u0437\u0431\u043e\u0440)",
re.IGNORECASE,
)
TOP_YEAR_QUESTION_RE = re.compile(
"(?:\u0441\u0430\u043c\\w*\\s+\u0434\u043e\u0445\u043e\u0434\u043d\\w*\\s+\u0433\u043e\u0434|"
"\u0434\u043e\u0445\u043e\u0434\u043d\\w*\\s+\u0433\u043e\u0434|"
"\u043b\u0443\u0447\u0448\\w*\\s+\u0433\u043e\u0434|top[-\\s]*year|best\\s+year)",
re.IGNORECASE,
)
DIRECT_TOP_YEAR_ANSWER_RE = re.compile(
"(?:(?:\u0441\u0430\u043c\\w*\\s+\u0434\u043e\u0445\u043e\u0434\u043d\\w*\\s+\u0433\u043e\u0434|"
"\u043b\u0443\u0447\u0448\\w*\\s+\u0433\u043e\u0434|\u043b\u0438\u0434\u0438\u0440\\w*|"
"\u0442\u043e\u043f[-\\s]*1|top[-\\s]*year|best\\s+year)[^\\n]{0,160}(?:19|20)\\d{2}|"
"(?:19|20)\\d{2}[^\\n]{0,120}(?:\u043b\u0438\u0434\u0438\u0440\\w*|"
"\u0441\u0430\u043c\\w*\\s+\u0434\u043e\u0445\u043e\u0434\u043d\\w*|"
"\u043b\u0443\u0447\u0448\\w*\\s+\u0433\u043e\u0434))",
re.IGNORECASE,
)
GENERIC_BUSINESS_OVERVIEW_ANSWER_RE = re.compile(
"(?:\u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442\\s+\u043a\u0430\u043a\\s+\u0431\u0438\u0437\u043d\u0435\u0441|"
"\u043a\u0440\u0443\u043f\u043d\\w*\\s+\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\\w*\\s+\u0434\u0435\u043d\u0435\u0436\u043d\\w*\\s+\u043f\u043e\u0442\u043e\u043a|"
"\u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0449\\w*\\s+\u0431\u0438\u0437\u043d\u0435\u0441\\w*\\s+\u043e\u0431\u0437\u043e\u0440)",
re.IGNORECASE,
)
VAT_PURCHASE_DATE_QUESTION_RE = re.compile(
"(?:\u043d\u0434\u0441[^\\n]{0,80}\u0434\u0430\u0442[ау]\\s+\u043f\u043e\u043a\u0443\u043f\u043a|"
"\u0434\u0430\u0442[ау]\\s+\u043f\u043e\u043a\u0443\u043f\u043a[^\\n]{0,80}\u043d\u0434\u0441)",
re.IGNORECASE,
)
VAT_PURCHASE_DATE_BASIS_RE = re.compile(
"(?:\u0434\u0430\u0442[ауы]\\s+\u043f\u043e\u043a\u0443\u043f\u043a|\u043f\u043e\u043a\u0443\u043f\u043a[аи]\\s+\u043e\u0442|"
"\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438[ея]\\s+\u043e\u0442|\u043e\u043f\u043e\u0440\u043d\\w*\\s+\u0434\u0430\u0442|"
"\u043f\u0435\u0440\u0432\\w*\\s+(?:\u043d\u0430\u0439\u0434\u0435\u043d\u043d\\w*\\s+)?(?:\u043f\u043e\u043a\u0443\u043f\u043a|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d)|"
"\u0434\u0430\u0442\\w*\\s+\u0437\u0430\u043a\u0443\u043f)",
re.IGNORECASE,
)
def now_iso() -> str:
@ -360,6 +399,9 @@ def build_step_for_pair(pair: dict[str, Any]) -> dict[str, Any]:
"business_answer_too_verbose": "P1",
"bank_counterparty_misclassified_as_business_partner": "P1",
"counterparty_value_flow_misrouted_to_company_profit": "P0",
"backend_error_response": "P0",
"year_ranking_direct_answer_missing": "P0",
"purchase_date_vat_anchor_ambiguous": "P1",
},
}
@ -374,6 +416,20 @@ def marker_hits(text: str, markers: tuple[str, ...]) -> list[str]:
return [marker for marker in markers if marker and marker.casefold() in lowered]
def add_review_issue(
*,
issue_codes: list[str],
root_layers: list[str],
issue_code: str,
layers: tuple[str, ...],
) -> None:
if issue_code not in issue_codes:
issue_codes.append(issue_code)
for layer in layers:
if layer and layer not in root_layers:
root_layers.append(layer)
def detect_counterparty_value_flow_profit_mismatch(question: str, assistant_text: str) -> dict[str, Any] | None:
question_text = str(question or "")
answer_text = str(assistant_text or "")
@ -412,6 +468,51 @@ def augment_gui_business_review(step_state: dict[str, Any]) -> dict[str, Any]:
assistant_text = str(step_state.get("assistant_text") or "")
issue_codes = [str(item) for item in review.get("issue_codes", []) if str(item).strip()]
root_layers = [str(item) for item in review.get("suggested_root_cause_layers", []) if str(item).strip()]
semantic_details = (
dict(review.get("semantic_mismatch_details"))
if isinstance(review.get("semantic_mismatch_details"), dict)
else {}
)
first_answer_line = next((line.strip() for line in assistant_text.splitlines() if line.strip()), "")
if str(step_state.get("reply_type") or "").strip() == "backend_error" or BACKEND_ERROR_ANSWER_RE.search(assistant_text):
add_review_issue(
issue_codes=issue_codes,
root_layers=root_layers,
issue_code="backend_error_response",
layers=("runtime_error", "business_utility_gap"),
)
semantic_details["backend_error_response"] = {
"reply_type": step_state.get("reply_type"),
"first_answer_line": first_answer_line,
}
if TOP_YEAR_QUESTION_RE.search(question):
has_direct_top_year = bool(DIRECT_TOP_YEAR_ANSWER_RE.search(assistant_text))
generic_overview_first = bool(GENERIC_BUSINESS_OVERVIEW_ANSWER_RE.search(first_answer_line))
if not has_direct_top_year or generic_overview_first:
add_review_issue(
issue_codes=issue_codes,
root_layers=root_layers,
issue_code="year_ranking_direct_answer_missing",
layers=("answer_shape_mismatch", "business_utility_gap"),
)
semantic_details["year_ranking_direct_answer_missing"] = {
"has_direct_top_year": has_direct_top_year,
"generic_overview_first": generic_overview_first,
"first_answer_line": first_answer_line,
}
if VAT_PURCHASE_DATE_QUESTION_RE.search(question) and not VAT_PURCHASE_DATE_BASIS_RE.search(assistant_text):
add_review_issue(
issue_codes=issue_codes,
root_layers=root_layers,
issue_code="purchase_date_vat_anchor_ambiguous",
layers=("domain_anchor_gap", "field_mapping_gap"),
)
semantic_details["purchase_date_vat_anchor_ambiguous"] = {
"first_answer_line": first_answer_line,
}
technical_hits = [str(item) for item in review.get("technical_garbage_hits", []) if str(item).strip()]
for hit in marker_hits(assistant_text, GUI_TECHNICAL_LEAK_MARKERS):
@ -434,17 +535,18 @@ def augment_gui_business_review(step_state: dict[str, Any]) -> dict[str, Any]:
mismatch_details = detect_counterparty_value_flow_profit_mismatch(question, assistant_text)
if mismatch_details:
issue_code = "counterparty_value_flow_misrouted_to_company_profit"
if issue_code not in issue_codes:
issue_codes.append(issue_code)
if "followup_action_resolution_gap" not in root_layers:
root_layers.append("followup_action_resolution_gap")
if "answer_shape_mismatch" not in root_layers:
root_layers.append("answer_shape_mismatch")
review["semantic_mismatch_details"] = mismatch_details
add_review_issue(
issue_codes=issue_codes,
root_layers=root_layers,
issue_code="counterparty_value_flow_misrouted_to_company_profit",
layers=("followup_action_resolution_gap", "answer_shape_mismatch"),
)
semantic_details["counterparty_value_flow_misrouted_to_company_profit"] = mismatch_details
review["technical_garbage_present"] = bool(technical_hits)
review["technical_garbage_hits"] = technical_hits
if semantic_details:
review["semantic_mismatch_details"] = semantic_details
review["issue_codes"] = issue_codes
review["suggested_root_cause_layers"] = list(dict.fromkeys(root_layers))
review["business_usefulness_ok"] = not issue_codes

View File

@ -104,6 +104,117 @@ class AssistantStage1RunReviewTests(unittest.TestCase):
self.assertEqual(review["question_quality_review"]["turns_total"], 2)
self.assertIn("contextual_followup", review["question_quality_review"]["tag_counts"])
def test_review_flags_backend_error_as_business_blocker(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
sessions_dir = root / "sessions"
reports_dir = root / "reports"
run_id = "assistant-stage1-backend-error"
session_file = sessions_dir / f"{run_id}-SAVED-001.json"
report_file = reports_dir / f"{run_id}.md"
write_json(
session_file,
session_payload(
[
{"role": "user", "text": "приветик - че как там дела"},
{
"role": "assistant",
"text": "Сейчас не удалось завершить разбор из-за внутренней ошибки контуров LLM.",
"reply_type": "backend_error",
"message_id": "a-backend-error",
"trace_id": "trace-backend-error",
"debug": {},
},
]
),
)
report_file.parent.mkdir(parents=True, exist_ok=True)
report_file.write_text(f"# Assistant Stage 1 Eval Run\n\n- run_id: {run_id}\n", encoding="utf-8")
review = reviewer.build_run_review(
run_id=run_id,
session_files=[session_file],
report_path=report_file,
)
self.assertEqual(review["summary"]["overall_business_status"], "fail")
self.assertIn("backend_error_response", review["summary"]["issue_counts"])
target_by_issue = {item["issue_code"]: item for item in review["repair_targets"]}
self.assertEqual(target_by_issue["backend_error_response"]["severity"], "P0")
def test_review_flags_top_year_generic_overview_without_direct_year(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
sessions_dir = root / "sessions"
reports_dir = root / "reports"
run_id = "assistant-stage1-top-year-generic"
session_file = sessions_dir / f"{run_id}-SAVED-001.json"
report_file = reports_dir / f"{run_id}.md"
write_json(
session_file,
session_payload(
[
{"role": "user", "text": "какой у нас самый доходный год"},
{
"role": "assistant",
"text": "Коротко: по доступным данным компания выглядит как бизнес с крупными контрактными денежными потоками. Входящие деньги за доступное время: 10 000 руб.",
"reply_type": "partial_coverage",
"message_id": "a-top-year-generic",
"trace_id": "trace-top-year-generic",
"debug": {"capability_id": "address_customer_revenue_and_payments"},
},
]
),
)
report_file.parent.mkdir(parents=True, exist_ok=True)
report_file.write_text(f"# Assistant Stage 1 Eval Run\n\n- run_id: {run_id}\n", encoding="utf-8")
review = reviewer.build_run_review(
run_id=run_id,
session_files=[session_file],
report_path=report_file,
)
self.assertEqual(review["summary"]["overall_business_status"], "fail")
self.assertIn("year_ranking_direct_answer_missing", review["summary"]["issue_counts"])
target_by_issue = {item["issue_code"]: item for item in review["repair_targets"]}
self.assertEqual(target_by_issue["year_ranking_direct_answer_missing"]["severity"], "P0")
def test_review_accepts_direct_top_year_answer(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
sessions_dir = root / "sessions"
reports_dir = root / "reports"
run_id = "assistant-stage1-top-year-clean"
session_file = sessions_dir / f"{run_id}-SAVED-001.json"
report_file = reports_dir / f"{run_id}.md"
write_json(
session_file,
session_payload(
[
{"role": "user", "text": "какой у нас самый доходный год"},
{
"role": "assistant",
"text": "Самый доходный год по подтвержденным поступлениям: 2021 (320 000 руб. по 1 операции). Это денежный поток, а не чистая прибыль.",
"reply_type": "factual",
"message_id": "a-top-year-clean",
"trace_id": "trace-top-year-clean",
"debug": {"capability_id": "address_customer_revenue_and_payments"},
},
]
),
)
report_file.parent.mkdir(parents=True, exist_ok=True)
report_file.write_text(f"# Assistant Stage 1 Eval Run\n\n- run_id: {run_id}\n", encoding="utf-8")
review = reviewer.build_run_review(
run_id=run_id,
session_files=[session_file],
report_path=report_file,
)
self.assertNotIn("year_ranking_direct_answer_missing", review["summary"]["issue_counts"])
def test_save_run_review_materializes_machine_and_markdown_artifacts(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
output_dir = Path(tmp) / "review"