АРЧ АП11 - Оркестрация: дожать агентный прогон по selected-object continuity и сохранить accepted
This commit is contained in:
parent
c2bafcff9b
commit
7f42d8ab50
|
|
@ -25,16 +25,16 @@ This snapshot is based on:
|
|||
|
||||
Latest graph rebuild:
|
||||
|
||||
- `5251 nodes`
|
||||
- `11337 edges`
|
||||
- `136 communities`
|
||||
- `5261 nodes`
|
||||
- `11347 edges`
|
||||
- `134 communities`
|
||||
|
||||
Most relevant current god nodes for turnaround `11`:
|
||||
|
||||
1. `resolveAddressIntent()`
|
||||
2. `composeFactualReplyBody()`
|
||||
3. `ChannelRegistry`
|
||||
4. `CanonicalStore`
|
||||
2. `ChannelRegistry`
|
||||
3. `CanonicalStore`
|
||||
4. `composeFactualReplyBody()`
|
||||
5. `compactWhitespace()`
|
||||
|
||||
The relevant conclusion is not that every god node is part of turnaround `11`.
|
||||
|
|
@ -42,7 +42,7 @@ The relevant conclusion is not that every god node is part of turnaround `11`.
|
|||
The relevant conclusion is:
|
||||
|
||||
- `resolveAddressIntent()` remains the main unresolved domain-intent concentration point;
|
||||
- `composeFactualReplyBody()` now carries the remaining answer-shaping concentration after packaging extraction;
|
||||
- `composeFactualReplyBody()` still carries answer-shaping pressure, but it is no longer a top-3 god node after the latest extractions;
|
||||
- `assistantService` still appears as a large coordinator-heavy community rather than a thin shell.
|
||||
|
||||
## What Is Already Real In Code
|
||||
|
|
@ -126,7 +126,7 @@ This is enough to build targeted semantic packs that are not single-domain toy s
|
|||
|
||||
## Honest Phase Status
|
||||
|
||||
Estimated overall turnaround completion: `~88%`
|
||||
Estimated overall turnaround completion: `~90%`
|
||||
|
||||
### Phase 0. Shared Baseline
|
||||
|
||||
|
|
@ -177,19 +177,21 @@ Remaining debt:
|
|||
|
||||
### Phase 4. Coverage / Evidence / Truth Gate Isolation
|
||||
|
||||
Status: `86%`
|
||||
Status: `89%`
|
||||
|
||||
Reason:
|
||||
|
||||
- explicit truth and coverage/evidence contracts exist;
|
||||
- answer policy reads those contracts rather than rebuilding verdicts blindly from raw rows.
|
||||
- reply-packaging mechanics are now explicitly split into `address_runtime/replyPackaging.ts` instead of staying fully in `composeStage.ts`.
|
||||
- named reply contracts and answer semantics presets now also live in `address_runtime/replyContracts.ts` instead of being rebuilt inline across major factual branches.
|
||||
- inventory answer construction now has an explicit owner in `address_runtime/inventoryReplyBuilders.ts` instead of staying inline inside `composeFactualReplyBody()`.
|
||||
|
||||
Remaining debt:
|
||||
|
||||
- `composeFactualReplyBody()` is still a major concentration point;
|
||||
- `composeFactualReplyBody()` is still a major concentration point, but its graph pressure is lower than in the previous snapshot;
|
||||
- humanized blocked/limited semantics are not yet fully separated from answer semantics across all paths;
|
||||
- `composeStage.ts` still remains too large even after packaging extraction.
|
||||
- `composeStage.ts` still remains too large even after packaging extraction and inventory-family extraction.
|
||||
|
||||
### Phase 5. AssistantService Extraction
|
||||
|
||||
|
|
@ -247,6 +249,8 @@ Compared with the pre-turnaround baseline, the system is now materially better i
|
|||
- organization data-scope probing is no longer owned only by coordinator-local helper bodies;
|
||||
- debug payload assembly is now further isolated from top-level turn coordination;
|
||||
- reply formatting and reply-type classification now have an explicit owner outside `composeStage.ts`;
|
||||
- confirmed-balance and heuristic-candidate reply contracts now have explicit builders instead of repeated inline `semantics` objects in major compose branches;
|
||||
- inventory factual replies are now owned by a dedicated module rather than embedded directly in the central compose body;
|
||||
- architecture regressions can now be localized to route, transition, truth gate, coverage/evidence, boundary, or meta/memory layers.
|
||||
|
||||
## What Still Remains The Main Architectural Debt
|
||||
|
|
@ -263,7 +267,7 @@ This means capability and contour growth still concentrate pressure there.
|
|||
|
||||
### 3. `composeFactualReplyBody()` is still too central
|
||||
|
||||
Truth contracts are now explicit, and reply packaging has started moving into its own owner, but final answer-shaping still retains too much architecture weight.
|
||||
Truth contracts are now explicit, and reply packaging, reply contracts, and the inventory answer family have all started moving into their own owners, but final answer-shaping still retains too much architecture weight.
|
||||
|
||||
This is the main remaining reason why user-facing humanization and limitation semantics are not completely isolated yet.
|
||||
|
||||
|
|
@ -284,7 +288,7 @@ But not every business family has reached the same contract maturity.
|
|||
The next honest architecture slice should be:
|
||||
|
||||
1. continue reducing `assistantService.ts` to a thinner coordinator;
|
||||
2. continue isolating answer semantics further away from `composeFactualReply()` now that reply packaging has its own owner seam;
|
||||
2. continue isolating answer semantics further away from `composeFactualReplyBody()` now that reply packaging and reply contracts have their own owner seams;
|
||||
3. keep extending AGENT packs with mixed business + meta + interruption patterns instead of single-family smoke tests;
|
||||
4. keep using scenario acceptance as the main sign-off rather than unit-test green status alone.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
|
||||
"domain": "inventory_answer_shape_and_continuity",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"description": "Targeted AGENT replay for the recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
|
||||
"bindings": {
|
||||
"target_date": "2021-03-31",
|
||||
"observed_organization": "ООО \\Альтернатива Плюс\\",
|
||||
"observed_warehouse": "Основной склад",
|
||||
"focus_item_current": "Столешница 600*3050*26 альмандин"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_inventory_root_requires_company",
|
||||
"title": "Inventory root asks to choose the company instead of hallucinating scope",
|
||||
"question": "какие остатки на складе на март 2021",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"company_clarification"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)компан|организац",
|
||||
"(?i)уточни|уточните|выбери|выберите|по какой"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json",
|
||||
"(?i)route[_ ]summary|trace_id|capability[_ ]id|intent",
|
||||
"(?i)selected_object|focus_object|answer_object"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_choose_company",
|
||||
"title": "User chooses the company inside the same session",
|
||||
"question": "давай по Альтернативе Плюс",
|
||||
"semantic_tags": [
|
||||
"meta_scope",
|
||||
"company_selection"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)альтернатива плюс|ООО \\\\Альтернатива Плюс\\\\|работаем по"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)tool_gate_reason|living_router_reason|hard_meta_mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inventory_root_after_company",
|
||||
"title": "Inventory root continues after the company choice",
|
||||
"question": "тогда покажи остатки на март 2021",
|
||||
"allowed_reply_types": [
|
||||
"factual"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2021-03-31",
|
||||
"period_from": "2021-03-01",
|
||||
"period_to": "2021-03-31"
|
||||
},
|
||||
"required_direct_answer_patterns_any": [
|
||||
"31\\.03\\.2021",
|
||||
"(?i)на складе",
|
||||
"(?i)столешница 600\\*3050\\*26 альмандин"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json",
|
||||
"(?i)route[_ ]summary|trace_id|capability[_ ]id|intent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_selected_item_supplier",
|
||||
"title": "Selected-object supplier follow-up stays business-first",
|
||||
"question": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"allowed_reply_types": [
|
||||
"factual"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_purchase_provenance_for_item"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)столешница 600\\*3050\\*26 альмандин",
|
||||
"(?i)поставщик|поставил|куплен",
|
||||
"(?i)союз|торговый дом"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^на 31\\.03\\.2021 на складе",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)selected_object|focus_object|answer_object",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_selected_item_purchase_date",
|
||||
"title": "Natural follow-up keeps the same selected item",
|
||||
"question": "а по этой позиции когда была закупка?",
|
||||
"allowed_reply_types": [
|
||||
"factual"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_purchase_provenance_for_item"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)столешница 600\\*3050\\*26 альмандин|по этой позиции",
|
||||
"(?i)дата|закупк|поступ"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^на 31\\.03\\.2021 на складе",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)selected_object|focus_object|answer_object",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_selected_item_documents",
|
||||
"title": "Purchase documents stay in the same contour without technical dump",
|
||||
"question": "покажи документы по этой позиции",
|
||||
"allowed_reply_types": [
|
||||
"factual"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_purchase_documents_for_item"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)столешница 600\\*3050\\*26 альмандин|по этой позиции",
|
||||
"(?i)документ"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)selected_object|focus_object|answer_object",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json",
|
||||
"(?i)route[_ ]summary|trace_id|capability[_ ]id|intent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_inventory_same_date_restore",
|
||||
"title": "Same-date restore returns to the March 2021 root snapshot",
|
||||
"question": "покажи еще раз остатки на эту же дату",
|
||||
"allowed_reply_types": [
|
||||
"factual"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2021-03-31",
|
||||
"period_from": "2021-03-01",
|
||||
"period_to": "2021-03-31"
|
||||
},
|
||||
"required_direct_answer_patterns_any": [
|
||||
"31\\.03\\.2021",
|
||||
"(?i)на складе"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)transition_not_supported_by_capability",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_memory_recap_business_only",
|
||||
"title": "Memory recap stays business-first after the provenance chain",
|
||||
"question": "а что мы уже выяснили по этой позиции?",
|
||||
"semantic_tags": [
|
||||
"meta_memory"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)столешница 600\\*3050\\*26 альмандин|по этой позиции",
|
||||
"(?i)поставщик|закупк|документ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)snapshot_items",
|
||||
"(?i)bank_operations?_by_",
|
||||
"(?i)selected_object|focus_object|answer_object",
|
||||
"(?i)technical_debug_payload|debug_payload_json|technical_breakdown_json",
|
||||
"(?i)route[_ ]summary|trace_id|capability[_ ]id|intent"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -944,6 +944,12 @@ function isLowQualityInventoryItemAnchorValue(rawValue) {
|
|||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
.filter((token) => !lowQualityTokens.has(token));
|
||||
if (/^(?:(?:\u043a\u043e\u0433\u0434\u0430|when)\s+)?(?:(?:\u0431\u044b\u043b(?:\u0430|\u0438|\u043e)?|was|were)\s+)?(?:\u0437\u0430\u043a\u0443\u043f[\p{L}\p{N}_-]*|purchase\s+date|purchase)$/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:(?:\u043a\u0442\u043e|who)\s+)?(?:(?:\u043d\u0430\u043c\s+)?(?:\u043f\u043e\u0441\u0442\u0430\u0432[\p{L}\p{N}_-]*|supplier)|supplier)$/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
return meaningfulTokens.length === 0;
|
||||
}
|
||||
function normalizeInventoryItemAnchorForComparison(rawValue) {
|
||||
|
|
|
|||
|
|
@ -739,6 +739,10 @@ function hasCounterpartyActivityLifecycleSignal(text) {
|
|||
return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb);
|
||||
}
|
||||
function hasCounterpartyShipmentItemFlowSignal(text) {
|
||||
const hasSelectedObjectInventoryCue = /(?:по\s+этой\s+позици(?:и|я|ю)|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|selected\s+object|по\s+выбранному\s+объекту)/iu.test(text);
|
||||
if (hasSelectedObjectInventoryCue) {
|
||||
return false;
|
||||
}
|
||||
const hasNamedTailAfterShipmentCue = /(?:отгруж(?:ал|али|ено)|постав(?:лял|ляли|ил|или)|привоз(?:ил|или)|продал)\s+[a-zа-яё][a-zа-яё0-9._-]{2,}/iu.test(text);
|
||||
const hasPartySignal = hasPartyAnchorMention(text) ||
|
||||
hasLooseByAnchorMention(text) ||
|
||||
|
|
@ -1397,8 +1401,9 @@ function hasInventoryProvenanceSignalV2(text) {
|
|||
return hasItemCue && hasSupplierCue && hasPurchaseCue;
|
||||
}
|
||||
function hasInventoryPurchaseDateSignal(text) {
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text);
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
|
||||
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text) ||
|
||||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
|
||||
return hasItemCue && hasPurchaseDateCue;
|
||||
}
|
||||
function hasInventoryPurchaseDocumentsSignalV2(text) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ exports.composeFactualReply = composeFactualReply;
|
|||
exports.inferReplyType = inferReplyType;
|
||||
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||
const replyPackaging_1 = require("./replyPackaging");
|
||||
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
|
||||
function uniqueStrings(values) {
|
||||
return Array.from(new Set(values
|
||||
.map((item) => item.trim())
|
||||
|
|
@ -2114,6 +2115,26 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
}
|
||||
function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
const joinLines = replyPackaging_1.joinComposeReplyLines;
|
||||
const inventoryReply = (0, inventoryReplyBuilders_1.composeInventoryReply)(intent, rows, options, {
|
||||
resolvePayablesAsOfDate,
|
||||
buildInventoryOnHandAggregate,
|
||||
uniqueStrings,
|
||||
formatDateRu,
|
||||
formatNumberWithDots,
|
||||
formatMoneyRub,
|
||||
isInventoryPurchaseMovement,
|
||||
summarizeInventoryTraceRows,
|
||||
formatInventoryTraceRows,
|
||||
hasInventoryPurchaseDateActionFocus,
|
||||
inventoryTraceDateLabel,
|
||||
extractInventoryCounterpartyCandidates,
|
||||
buildInventoryAgingByItemAggregate,
|
||||
formatInventoryAgingRows,
|
||||
isInventorySaleMovement
|
||||
});
|
||||
if (inventoryReply) {
|
||||
return inventoryReply;
|
||||
}
|
||||
if (intent === "document_type_and_account_section_profile") {
|
||||
const rowsByMarker = new Map();
|
||||
for (const row of rows) {
|
||||
|
|
@ -3308,370 +3329,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const positions = buildInventoryOnHandAggregate(rows, asOfDate);
|
||||
const uniqueItems = uniqueStrings(positions.map((item) => item.item));
|
||||
const uniqueWarehouses = uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
|
||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
const directAnswerLine = positions.length > 0
|
||||
? `На ${formatDateRu(asOfDate)} на складе подтверждено ${formatNumberWithDots(positions.length)} позиций с остатком на ${formatMoneyRub(totalAmount)}.`
|
||||
: `На ${formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (positions.length > 0) {
|
||||
lines.push("", "Позиции:");
|
||||
lines.push(...positions.slice(0, 20).map((item, index) => {
|
||||
const warehouseLabel = item.warehouse ?? "склад не определен";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${formatNumberWithDots(item.quantity, 3)} | стоимость: ${formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
}
|
||||
lines.push("", "Подтверждение:", `- Дата среза: ${formatDateRu(asOfDate)}.`, "- Контур: остатки по счету 41.01 «Товары на складах».", `- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`, `- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`, `- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Строк в подтвержденной выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
return {
|
||||
responseType: positions.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: positions.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_purchase_documents_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const directAnswerLine = purchaseRows.length <= 0
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
|
||||
: `По позиции ${itemLabel} найдено ${formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${formatDateRu(asOfDate)}.`;
|
||||
const lines = [directAnswerLine];
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Дата верхней границы: ${formatDateRu(asOfDate)}.`);
|
||||
lines.push(`- Операций поступления в выборке: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
lines.push("", "Документы:");
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(purchaseRows, 12));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_purchase_provenance_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const boundedAsOfLabel = asOfDate ? formatDateRu(asOfDate) : null;
|
||||
const purchaseDateActionFocus = hasInventoryPurchaseDateActionFocus(options.userMessage);
|
||||
if (purchaseDateActionFocus) {
|
||||
const firstPurchaseDate = inventoryTraceDateLabel(summary.firstPeriod);
|
||||
const lastPurchaseDate = inventoryTraceDateLabel(summary.lastPeriod);
|
||||
const directAnswerLine = purchaseRows.length <= 0 || !summary.firstPeriod
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
|
||||
if (summary.firstPeriod !== summary.lastPeriod) {
|
||||
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
|
||||
}
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
const directAnswerLine = purchaseRows.length <= 0
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} подтвержденные закупочные документы до ${boundedAsOfLabel} не найдены.`
|
||||
: `По позиции ${itemLabel} подтвержденные закупочные документы не найдены.`
|
||||
: summary.counterparties.length === 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: `По позиции ${itemLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По позиции ${itemLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`
|
||||
: `По позиции ${itemLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(`- Первая найденная дата закупки: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в найденных документах: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Поставщики в найденных документах: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
else {
|
||||
lines.push("- Закупочные документы найдены, но поставщик в них не выделен отдельным полем.");
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
|
||||
}
|
||||
}
|
||||
else if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const unresolvedRows = purchaseRows.filter((row) => extractInventoryCounterpartyCandidates(row).length === 0);
|
||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
|
||||
const lines = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез supplier overlap для складского остатка до ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
|
||||
"- Важно: без партионности этот контур показывает документально наблюдаемые supplier candidates, но не подменяет собой лот-level атрибуцию текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Первая найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Закупочных документов в выборке: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в выборке: ${formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
else {
|
||||
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно материализованного поставщика: ${formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...formatInventoryTraceRows(purchaseRows, 10));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const agingItems = buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||
const oldestAnswerPreview = agingItems
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.item} (${inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine = agingItems.length > 0
|
||||
? `К старым закупкам на ${formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
const lines = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез старых закупок для складского остатка на ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
|
||||
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
|
||||
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
`- Самая ранняя первая закупка среди позиций: ${inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`- Самая поздняя найденная закупка в наблюдаемом следе: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Позиции в aging-срезе: ${formatNumberWithDots(agingItems.length)}.`,
|
||||
`- Закупочных документов в наблюдаемом следе: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в наблюдаемом следе: ${formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
}
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", ...formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
}
|
||||
else {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: agingItems.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: agingItems.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_sale_trace_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
|
||||
const requestedItemHint = String(options.itemHint ?? "").trim();
|
||||
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
|
||||
const summary = summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
|
||||
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
||||
const excludedCounterpartyTokens = [itemLabel];
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата выбытия: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов выбытия: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций выбытия: ${formatNumberWithDots(saleRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
}
|
||||
return {
|
||||
responseType: saleRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: saleRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
|
||||
balance_confirmed: saleRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
|
||||
const purchaseSummary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const saleSummary = summarizeInventoryTraceRows(saleRows);
|
||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||
const directAnswerLine = purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Закупочных движений на 41.01: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
|
||||
}
|
||||
else if (saleRows.length > 0) {
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
|
||||
}
|
||||
else {
|
||||
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Закупка:", `- Первая дата: ${inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`, `- Последняя дата: ${inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`, ...formatInventoryTraceRows(purchaseRows, 6));
|
||||
}
|
||||
if (saleRows.length > 0) {
|
||||
lines.push("", "Продажа:", `- Первая дата: ${inventoryTraceDateLabel(saleSummary.firstPeriod)}.`, `- Последняя дата: ${inventoryTraceDateLabel(saleSummary.lastPeriod)}.`, ...formatInventoryTraceRows(saleRows, 6));
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 || saleRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 && saleRows.length > 0 ? "strong" : purchaseRows.length > 0 || saleRows.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: purchaseRows.length > 0 || saleRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
|||
return hasTemporalPatch;
|
||||
}
|
||||
function hasSelectedObjectInventorySignal(text) {
|
||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasSelectedObjectInlineSnapshotMetadata(text) {
|
||||
return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(String(text ?? ""));
|
||||
|
|
@ -489,7 +489,9 @@ function hasInventoryProfitabilityFollowupCue(text) {
|
|||
}
|
||||
function hasInventoryPurchaseDateFollowupCue(text) {
|
||||
const value = String(text ?? "");
|
||||
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(value) || (/когда/iu.test(value) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value));
|
||||
return (/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+была\s+закупк(?:а|и)|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(value) ||
|
||||
/(?:по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)[\s\S]{0,40}(?:когда|дата\s+закупки|закупк))/iu.test(value) ||
|
||||
(/когда/iu.test(value) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value)));
|
||||
}
|
||||
function hasBareInventoryPurchaseDateFollowupCue(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
|
|
@ -509,7 +511,7 @@ function hasAddressFollowupContextSignal(text) {
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(normalized)) {
|
||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (hasAllTimeHint(normalized)) {
|
||||
|
|
@ -1107,6 +1109,10 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
}
|
||||
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "list_documents_by_counterparty" ||
|
||||
detectedIntent.intent === "list_documents_by_contract" ||
|
||||
detectedIntent.intent === "bank_operations_by_counterparty" ||
|
||||
detectedIntent.intent === "bank_operations_by_contract" ||
|
||||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date") {
|
||||
|
|
|
|||
317
llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js
vendored
Normal file
317
llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js
vendored
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.composeInventoryReply = composeInventoryReply;
|
||||
const replyContracts_1 = require("./replyContracts");
|
||||
function composeInventoryReply(intent, rows, options, deps) {
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
|
||||
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
|
||||
const uniqueWarehouses = deps.uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
|
||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
const directAnswerLine = positions.length > 0
|
||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
|
||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (positions.length > 0) {
|
||||
lines.push("", "Позиции:");
|
||||
lines.push(...positions.slice(0, 20).map((item, index) => {
|
||||
const warehouseLabel = item.warehouse ?? "склад не определен";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
}
|
||||
lines.push("", "Подтверждение:", `- Дата среза: ${deps.formatDateRu(asOfDate)}.`, "- Контур: остатки по счету 41.01 «Товары на складах».", `- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`, `- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`, `- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
return positions.length > 0
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
|
||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium"));
|
||||
}
|
||||
if (intent === "inventory_purchase_documents_for_item") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const directAnswerLine = purchaseRows.length <= 0
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
|
||||
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
|
||||
const lines = [directAnswerLine];
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
|
||||
lines.push(`- Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
lines.push("", "Документы:");
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
|
||||
}
|
||||
return purchaseRows.length > 0
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong", true))
|
||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
||||
}
|
||||
if (intent === "inventory_purchase_provenance_for_item") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const boundedAsOfLabel = asOfDate ? deps.formatDateRu(asOfDate) : null;
|
||||
const purchaseDateActionFocus = deps.hasInventoryPurchaseDateActionFocus(options.userMessage);
|
||||
if (purchaseDateActionFocus) {
|
||||
const firstPurchaseDate = deps.inventoryTraceDateLabel(summary.firstPeriod);
|
||||
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
|
||||
const directAnswerLine = purchaseRows.length <= 0 || !summary.firstPeriod
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
|
||||
if (summary.firstPeriod !== summary.lastPeriod) {
|
||||
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
|
||||
}
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
|
||||
}
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? "strong" : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
const directAnswerLine = purchaseRows.length <= 0
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} подтвержденные закупочные документы до ${boundedAsOfLabel} не найдены.`
|
||||
: `По позиции ${itemLabel} подтвержденные закупочные документы не найдены.`
|
||||
: summary.counterparties.length === 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: `По позиции ${itemLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По позиции ${itemLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`
|
||||
: `По позиции ${itemLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(`- Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в найденных документах: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Поставщики в найденных документах: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
else {
|
||||
lines.push("- Закупочные документы найдены, но поставщик в них не выделен отдельным полем.");
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
|
||||
}
|
||||
}
|
||||
else if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
|
||||
const lines = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
|
||||
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Подтверждение",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
else {
|
||||
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||
const oldestAnswerPreview = agingItems
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine = agingItems.length > 0
|
||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
const lines = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
|
||||
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
|
||||
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
}
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
}
|
||||
else {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
|
||||
}
|
||||
if (intent === "inventory_sale_trace_for_item") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
const requestedItemHint = String(options.itemHint ?? "").trim();
|
||||
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
|
||||
const summary = deps.summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
|
||||
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
||||
const excludedCounterpartyTokens = [itemLabel];
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
}
|
||||
return saleRows.length > 0
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
||||
}
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||
const directAnswerLine = purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
|
||||
}
|
||||
else if (saleRows.length > 0) {
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
|
||||
}
|
||||
else {
|
||||
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Закупка:", `- Первая дата: ${deps.inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`, `- Последняя дата: ${deps.inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`, ...deps.formatInventoryTraceRows(purchaseRows, 6));
|
||||
}
|
||||
if (saleRows.length > 0) {
|
||||
lines.push("", "Продажа:", `- Первая дата: ${deps.inventoryTraceDateLabel(saleSummary.firstPeriod)}.`, `- Последняя дата: ${deps.inventoryTraceDateLabel(saleSummary.lastPeriod)}.`, ...deps.formatInventoryTraceRows(saleRows, 6));
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 && saleRows.length > 0
|
||||
? "strong"
|
||||
: purchaseRows.length > 0 || saleRows.length > 0
|
||||
? "medium"
|
||||
: "weak", purchaseRows.length > 0 || saleRows.length > 0));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toComposeReplyText = toComposeReplyText;
|
||||
exports.buildComposeReplyResult = buildComposeReplyResult;
|
||||
exports.buildFactualSummaryReply = buildFactualSummaryReply;
|
||||
exports.buildFactualListReply = buildFactualListReply;
|
||||
exports.buildConfirmedBalanceSemantics = buildConfirmedBalanceSemantics;
|
||||
exports.buildHeuristicCandidatesSemantics = buildHeuristicCandidatesSemantics;
|
||||
const replyPackaging_1 = require("./replyPackaging");
|
||||
function toComposeReplyText(text) {
|
||||
return Array.isArray(text) ? (0, replyPackaging_1.joinComposeReplyLines)(text) : String(text ?? "");
|
||||
}
|
||||
function buildComposeReplyResult(responseType, text, semantics) {
|
||||
return {
|
||||
responseType,
|
||||
text: toComposeReplyText(text),
|
||||
...(semantics ? { semantics } : {})
|
||||
};
|
||||
}
|
||||
function buildFactualSummaryReply(text, semantics) {
|
||||
return buildComposeReplyResult("FACTUAL_SUMMARY", text, semantics);
|
||||
}
|
||||
function buildFactualListReply(text, semantics) {
|
||||
return buildComposeReplyResult("FACTUAL_LIST", text, semantics);
|
||||
}
|
||||
function buildConfirmedBalanceSemantics(evidenceStrength = "strong", balanceConfirmed = true) {
|
||||
return {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: evidenceStrength,
|
||||
balance_confirmed: balanceConfirmed
|
||||
};
|
||||
}
|
||||
function buildHeuristicCandidatesSemantics(hasCandidates) {
|
||||
return {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: hasCandidates ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
};
|
||||
}
|
||||
|
|
@ -1071,6 +1071,20 @@ export function isLowQualityInventoryItemAnchorValue(rawValue: string): boolean
|
|||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
.filter((token) => !lowQualityTokens.has(token));
|
||||
if (
|
||||
/^(?:(?:\u043a\u043e\u0433\u0434\u0430|when)\s+)?(?:(?:\u0431\u044b\u043b(?:\u0430|\u0438|\u043e)?|was|were)\s+)?(?:\u0437\u0430\u043a\u0443\u043f[\p{L}\p{N}_-]*|purchase\s+date|purchase)$/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/^(?:(?:\u043a\u0442\u043e|who)\s+)?(?:(?:\u043d\u0430\u043c\s+)?(?:\u043f\u043e\u0441\u0442\u0430\u0432[\p{L}\p{N}_-]*|supplier)|supplier)$/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return meaningfulTokens.length === 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -861,6 +861,12 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasCounterpartyShipmentItemFlowSignal(text: string): boolean {
|
||||
const hasSelectedObjectInventoryCue = /(?:по\s+этой\s+позици(?:и|я|ю)|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|selected\s+object|по\s+выбранному\s+объекту)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasSelectedObjectInventoryCue) {
|
||||
return false;
|
||||
}
|
||||
const hasNamedTailAfterShipmentCue =
|
||||
/(?:отгруж(?:ал|али|ено)|постав(?:лял|ляли|ил|или)|привоз(?:ил|или)|продал)\s+[a-zа-яё][a-zа-яё0-9._-]{2,}/iu.test(
|
||||
text
|
||||
|
|
@ -1705,10 +1711,13 @@ function hasInventoryProvenanceSignalV2(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasInventoryPurchaseDateSignal(text: string): boolean {
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
const hasItemCue =
|
||||
/(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
|
||||
const hasPurchaseDateCue =
|
||||
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
text
|
||||
);
|
||||
) ||
|
||||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
|
||||
return hasItemCue && hasPurchaseDateCue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
type ComposeReplyResult,
|
||||
type ComposeReplySemantics
|
||||
} from "./replyPackaging";
|
||||
import { composeInventoryReply } from "./inventoryReplyBuilders";
|
||||
|
||||
export type { ComposeFactualReplyOptions, ComposeReplySemantics } from "./replyPackaging";
|
||||
|
||||
|
|
@ -2729,6 +2730,26 @@ function composeFactualReplyBody(
|
|||
options: ComposeFactualReplyOptions = {}
|
||||
): ComposeReplyResult {
|
||||
const joinLines = joinComposeReplyLines;
|
||||
const inventoryReply = composeInventoryReply(intent, rows, options, {
|
||||
resolvePayablesAsOfDate,
|
||||
buildInventoryOnHandAggregate,
|
||||
uniqueStrings,
|
||||
formatDateRu,
|
||||
formatNumberWithDots,
|
||||
formatMoneyRub,
|
||||
isInventoryPurchaseMovement,
|
||||
summarizeInventoryTraceRows,
|
||||
formatInventoryTraceRows,
|
||||
hasInventoryPurchaseDateActionFocus,
|
||||
inventoryTraceDateLabel,
|
||||
extractInventoryCounterpartyCandidates,
|
||||
buildInventoryAgingByItemAggregate,
|
||||
formatInventoryAgingRows,
|
||||
isInventorySaleMovement
|
||||
});
|
||||
if (inventoryReply) {
|
||||
return inventoryReply;
|
||||
}
|
||||
|
||||
if (intent === "document_type_and_account_section_profile") {
|
||||
const rowsByMarker = new Map<string, ComposeStageRow[]>();
|
||||
|
|
@ -4251,401 +4272,6 @@ function composeFactualReplyBody(
|
|||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const positions = buildInventoryOnHandAggregate(rows, asOfDate);
|
||||
const uniqueItems = uniqueStrings(positions.map((item) => item.item));
|
||||
const uniqueWarehouses = uniqueStrings(
|
||||
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
||||
);
|
||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
const directAnswerLine =
|
||||
positions.length > 0
|
||||
? `На ${formatDateRu(asOfDate)} на складе подтверждено ${formatNumberWithDots(positions.length)} позиций с остатком на ${formatMoneyRub(totalAmount)}.`
|
||||
: `На ${formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
|
||||
if (positions.length > 0) {
|
||||
lines.push("", "Позиции:");
|
||||
lines.push(
|
||||
...positions.slice(0, 20).map((item, index) => {
|
||||
const warehouseLabel = item.warehouse ?? "склад не определен";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${formatNumberWithDots(item.quantity, 3)} | стоимость: ${formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"",
|
||||
"Подтверждение:",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счету 41.01 «Товары на складах».",
|
||||
`- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`
|
||||
);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Строк в подтвержденной выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
responseType: positions.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: positions.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_purchase_documents_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
|
||||
: `По позиции ${itemLabel} найдено ${formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${formatDateRu(asOfDate)}.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Дата верхней границы: ${formatDateRu(asOfDate)}.`);
|
||||
lines.push(`- Операций поступления в выборке: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
lines.push("", "Документы:");
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(purchaseRows, 12));
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_purchase_provenance_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const boundedAsOfLabel = asOfDate ? formatDateRu(asOfDate) : null;
|
||||
const purchaseDateActionFocus = hasInventoryPurchaseDateActionFocus(options.userMessage);
|
||||
if (purchaseDateActionFocus) {
|
||||
const firstPurchaseDate = inventoryTraceDateLabel(summary.firstPeriod);
|
||||
const lastPurchaseDate = inventoryTraceDateLabel(summary.lastPeriod);
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0 || !summary.firstPeriod
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
|
||||
if (summary.firstPeriod !== summary.lastPeriod) {
|
||||
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
|
||||
}
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(
|
||||
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} подтвержденные закупочные документы до ${boundedAsOfLabel} не найдены.`
|
||||
: `По позиции ${itemLabel} подтвержденные закупочные документы не найдены.`
|
||||
: summary.counterparties.length === 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: `По позиции ${itemLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По позиции ${itemLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`
|
||||
: `По позиции ${itemLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(`- Первая найденная дата закупки: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в найденных документах: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Поставщики в найденных документах: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
} else {
|
||||
lines.push("- Закупочные документы найдены, но поставщик в них не выделен отдельным полем.");
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(
|
||||
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
|
||||
);
|
||||
}
|
||||
} else if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength:
|
||||
purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const unresolvedRows = purchaseRows.filter((row) => extractInventoryCounterpartyCandidates(row).length === 0);
|
||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||
const directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез supplier overlap для складского остатка до ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
|
||||
"- Важно: без партионности этот контур показывает документально наблюдаемые supplier candidates, но не подменяет собой лот-level атрибуцию текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Первая найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Закупочных документов в выборке: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в выборке: ${formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
} else {
|
||||
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно материализованного поставщика: ${formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...formatInventoryTraceRows(purchaseRows, 10));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
|
||||
balance_confirmed: purchaseRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const agingItems = buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||
const oldestAnswerPreview = agingItems
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.item} (${inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine =
|
||||
agingItems.length > 0
|
||||
? `К старым закупкам на ${formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез старых закупок для складского остатка на ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
|
||||
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
|
||||
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
`- Самая ранняя первая закупка среди позиций: ${inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`- Самая поздняя найденная закупка в наблюдаемом следе: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Позиции в aging-срезе: ${formatNumberWithDots(agingItems.length)}.`,
|
||||
`- Закупочных документов в наблюдаемом следе: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в наблюдаемом следе: ${formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
}
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", ...formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
} else {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: agingItems.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: agingItems.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_sale_trace_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
|
||||
const requestedItemHint = String(options.itemHint ?? "").trim();
|
||||
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
|
||||
const summary = summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
|
||||
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
||||
const excludedCounterpartyTokens = [itemLabel];
|
||||
const directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата выбытия: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов выбытия: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций выбытия: ${formatNumberWithDots(saleRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
}
|
||||
return {
|
||||
responseType: saleRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: saleRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
|
||||
balance_confirmed: saleRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
|
||||
const purchaseSummary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const saleSummary = summarizeInventoryTraceRows(saleRows);
|
||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||
const directAnswerLine =
|
||||
purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Закупочных движений на 41.01: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
|
||||
} else {
|
||||
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Закупка:",
|
||||
`- Первая дата: ${inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`,
|
||||
`- Последняя дата: ${inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`,
|
||||
...formatInventoryTraceRows(purchaseRows, 6)
|
||||
);
|
||||
}
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Продажа:",
|
||||
`- Первая дата: ${inventoryTraceDateLabel(saleSummary.firstPeriod)}.`,
|
||||
`- Последняя дата: ${inventoryTraceDateLabel(saleSummary.lastPeriod)}.`,
|
||||
...formatInventoryTraceRows(saleRows, 6)
|
||||
);
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 || saleRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: purchaseRows.length > 0 && saleRows.length > 0 ? "strong" : purchaseRows.length > 0 || saleRows.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: purchaseRows.length > 0 || saleRows.length > 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
|
||||
|
|
|
|||
|
|
@ -580,7 +580,9 @@ function shouldRestoreInventoryRootFrame(
|
|||
}
|
||||
|
||||
function hasSelectedObjectInventorySignal(text: string): boolean {
|
||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function hasSelectedObjectInlineSnapshotMetadata(text: string): boolean {
|
||||
|
|
@ -629,9 +631,13 @@ export function hasInventoryProfitabilityFollowupCue(text: string): boolean {
|
|||
|
||||
export function hasInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(
|
||||
return (
|
||||
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+была\s+закупк(?:а|и)|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(
|
||||
value
|
||||
) || (/когда/iu.test(value) && hasInventoryPurchaseStem(value));
|
||||
) ||
|
||||
/(?:по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)[\s\S]{0,40}(?:когда|дата\s+закупки|закупк))/iu.test(value) ||
|
||||
(/когда/iu.test(value) && hasInventoryPurchaseStem(value))
|
||||
);
|
||||
}
|
||||
|
||||
export function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
|
|
@ -657,7 +663,11 @@ export function hasAddressFollowupContextSignal(text: string): boolean {
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(normalized)) {
|
||||
if (
|
||||
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (hasAllTimeHint(normalized)) {
|
||||
|
|
@ -1373,6 +1383,10 @@ function deriveIntentWithFollowupContext(
|
|||
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
|
||||
if (
|
||||
detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "list_documents_by_counterparty" ||
|
||||
detectedIntent.intent === "list_documents_by_contract" ||
|
||||
detectedIntent.intent === "bank_operations_by_counterparty" ||
|
||||
detectedIntent.intent === "bank_operations_by_contract" ||
|
||||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,446 @@
|
|||
import type { AddressIntent } from "../../types/addressQuery";
|
||||
import {
|
||||
buildConfirmedBalanceSemantics,
|
||||
buildFactualListReply,
|
||||
buildFactualSummaryReply
|
||||
} from "./replyContracts";
|
||||
import type { ComposeReplyResult } from "./replyPackaging";
|
||||
import type { ComposeStageRow } from "./composeStage";
|
||||
|
||||
interface InventoryComposeOptions {
|
||||
userMessage?: string;
|
||||
itemHint?: string;
|
||||
asOfDate?: string;
|
||||
periodFrom?: string;
|
||||
periodTo?: string;
|
||||
}
|
||||
|
||||
interface InventoryOnHandAggregate {
|
||||
item: string;
|
||||
warehouse: string | null;
|
||||
organization: string | null;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
operations: number;
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
sourceRefs: string[];
|
||||
}
|
||||
|
||||
interface InventoryTraceSummary {
|
||||
item: string | null;
|
||||
warehouses: string[];
|
||||
organizations: string[];
|
||||
counterparties: string[];
|
||||
documents: string[];
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
interface InventoryAgingByItemAggregate {
|
||||
item: string;
|
||||
warehouse: string | null;
|
||||
organization: string | null;
|
||||
firstPurchasePeriod: string | null;
|
||||
lastPurchasePeriod: string | null;
|
||||
operations: number;
|
||||
documentCount: number;
|
||||
counterparties: string[];
|
||||
ageDays: number | null;
|
||||
}
|
||||
|
||||
interface InventoryReplyDeps {
|
||||
resolvePayablesAsOfDate: (options: InventoryComposeOptions) => string;
|
||||
buildInventoryOnHandAggregate: (rows: ComposeStageRow[], asOfDate: string) => InventoryOnHandAggregate[];
|
||||
uniqueStrings: (values: string[]) => string[];
|
||||
formatDateRu: (isoDate: string) => string;
|
||||
formatNumberWithDots: (value: number, fractionDigits?: number) => string;
|
||||
formatMoneyRub: (value: number) => string;
|
||||
isInventoryPurchaseMovement: (row: ComposeStageRow) => boolean;
|
||||
summarizeInventoryTraceRows: (rows: ComposeStageRow[], excludedCounterpartyTokens?: string[]) => InventoryTraceSummary;
|
||||
formatInventoryTraceRows: (rows: ComposeStageRow[], limit?: number, excludedCounterpartyTokens?: string[]) => string[];
|
||||
hasInventoryPurchaseDateActionFocus: (userMessage: string | null | undefined) => boolean;
|
||||
inventoryTraceDateLabel: (value: string | null) => string;
|
||||
extractInventoryCounterpartyCandidates: (row: ComposeStageRow, excludedTokens?: string[]) => string[];
|
||||
buildInventoryAgingByItemAggregate: (rows: ComposeStageRow[], asOfDate: string) => InventoryAgingByItemAggregate[];
|
||||
formatInventoryAgingRows: (items: InventoryAgingByItemAggregate[], asOfDate: string, limit?: number) => string[];
|
||||
isInventorySaleMovement: (row: ComposeStageRow) => boolean;
|
||||
}
|
||||
|
||||
export function composeInventoryReply(
|
||||
intent: AddressIntent,
|
||||
rows: ComposeStageRow[],
|
||||
options: InventoryComposeOptions,
|
||||
deps: InventoryReplyDeps
|
||||
): ComposeReplyResult | null {
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
|
||||
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
|
||||
const uniqueWarehouses = deps.uniqueStrings(
|
||||
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
||||
);
|
||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
const directAnswerLine =
|
||||
positions.length > 0
|
||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
|
||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
|
||||
if (positions.length > 0) {
|
||||
lines.push("", "Позиции:");
|
||||
lines.push(
|
||||
...positions.slice(0, 20).map((item, index) => {
|
||||
const warehouseLabel = item.warehouse ?? "склад не определен";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"",
|
||||
"Подтверждение:",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счету 41.01 «Товары на складах».",
|
||||
`- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
||||
);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
|
||||
return positions.length > 0
|
||||
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
|
||||
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium"));
|
||||
}
|
||||
|
||||
if (intent === "inventory_purchase_documents_for_item") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
|
||||
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
|
||||
lines.push(`- Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
lines.push("", "Документы:");
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
|
||||
}
|
||||
return purchaseRows.length > 0
|
||||
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong", true))
|
||||
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
|
||||
}
|
||||
|
||||
if (intent === "inventory_purchase_provenance_for_item") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const boundedAsOfLabel = asOfDate ? deps.formatDateRu(asOfDate) : null;
|
||||
const purchaseDateActionFocus = deps.hasInventoryPurchaseDateActionFocus(options.userMessage);
|
||||
if (purchaseDateActionFocus) {
|
||||
const firstPurchaseDate = deps.inventoryTraceDateLabel(summary.firstPeriod);
|
||||
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0 || !summary.firstPeriod
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
|
||||
if (summary.firstPeriod !== summary.lastPeriod) {
|
||||
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
|
||||
}
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(
|
||||
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(purchaseRows.length > 0 ? "strong" : "medium", purchaseRows.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} подтвержденные закупочные документы до ${boundedAsOfLabel} не найдены.`
|
||||
: `По позиции ${itemLabel} подтвержденные закупочные документы не найдены.`
|
||||
: summary.counterparties.length === 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: `По позиции ${itemLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По позиции ${itemLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`
|
||||
: `По позиции ${itemLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(`- Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в найденных документах: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Поставщики в найденных документах: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
} else {
|
||||
lines.push("- Закупочные документы найдены, но поставщик в них не выделен отдельным полем.");
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(
|
||||
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
|
||||
);
|
||||
}
|
||||
} else if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(
|
||||
purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium",
|
||||
purchaseRows.length > 0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||
const directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
|
||||
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Подтверждение",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
} else {
|
||||
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(
|
||||
purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
|
||||
purchaseRows.length > 0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||
const oldestAnswerPreview = agingItems
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine =
|
||||
agingItems.length > 0
|
||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
|
||||
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
|
||||
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
}
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
} else {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
if (intent === "inventory_sale_trace_for_item") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
const requestedItemHint = String(options.itemHint ?? "").trim();
|
||||
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
|
||||
const summary = deps.summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
|
||||
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
||||
const excludedCounterpartyTokens = [itemLabel];
|
||||
const directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
}
|
||||
return saleRows.length > 0
|
||||
? buildFactualListReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(summary.counterparties.length > 0 ? "strong" : "medium", true)
|
||||
)
|
||||
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
|
||||
}
|
||||
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||
const directAnswerLine =
|
||||
purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
|
||||
} else {
|
||||
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Закупка:",
|
||||
`- Первая дата: ${deps.inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`,
|
||||
`- Последняя дата: ${deps.inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`,
|
||||
...deps.formatInventoryTraceRows(purchaseRows, 6)
|
||||
);
|
||||
}
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Продажа:",
|
||||
`- Первая дата: ${deps.inventoryTraceDateLabel(saleSummary.firstPeriod)}.`,
|
||||
`- Последняя дата: ${deps.inventoryTraceDateLabel(saleSummary.lastPeriod)}.`,
|
||||
...deps.formatInventoryTraceRows(saleRows, 6)
|
||||
);
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(
|
||||
purchaseRows.length > 0 && saleRows.length > 0
|
||||
? "strong"
|
||||
: purchaseRows.length > 0 || saleRows.length > 0
|
||||
? "medium"
|
||||
: "weak",
|
||||
purchaseRows.length > 0 || saleRows.length > 0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { AddressEvidenceStrength, AddressResponseType } from "../../types/addressQuery";
|
||||
import { joinComposeReplyLines, type ComposeReplyResult, type ComposeReplySemantics } from "./replyPackaging";
|
||||
|
||||
export type ComposeReplyText = string | string[];
|
||||
|
||||
export function toComposeReplyText(text: ComposeReplyText): string {
|
||||
return Array.isArray(text) ? joinComposeReplyLines(text) : String(text ?? "");
|
||||
}
|
||||
|
||||
export function buildComposeReplyResult(
|
||||
responseType: AddressResponseType,
|
||||
text: ComposeReplyText,
|
||||
semantics?: ComposeReplySemantics
|
||||
): ComposeReplyResult {
|
||||
return {
|
||||
responseType,
|
||||
text: toComposeReplyText(text),
|
||||
...(semantics ? { semantics } : {})
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFactualSummaryReply(
|
||||
text: ComposeReplyText,
|
||||
semantics?: ComposeReplySemantics
|
||||
): ComposeReplyResult {
|
||||
return buildComposeReplyResult("FACTUAL_SUMMARY", text, semantics);
|
||||
}
|
||||
|
||||
export function buildFactualListReply(
|
||||
text: ComposeReplyText,
|
||||
semantics?: ComposeReplySemantics
|
||||
): ComposeReplyResult {
|
||||
return buildComposeReplyResult("FACTUAL_LIST", text, semantics);
|
||||
}
|
||||
|
||||
export function buildConfirmedBalanceSemantics(
|
||||
evidenceStrength: AddressEvidenceStrength = "strong",
|
||||
balanceConfirmed = true
|
||||
): ComposeReplySemantics {
|
||||
return {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: evidenceStrength,
|
||||
balance_confirmed: balanceConfirmed
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHeuristicCandidatesSemantics(hasCandidates: boolean): ComposeReplySemantics {
|
||||
return {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: hasCandidates ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
};
|
||||
}
|
||||
|
|
@ -4120,6 +4120,27 @@ describe("address decompose stage follow-up carryover", () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
it("promotes selected-item purchase-date wording 'а по этой позиции когда была закупка' into inventory provenance", () => {
|
||||
const result = runAddressDecomposeStage("а по этой позиции когда была закупка?", {
|
||||
previous_intent: "inventory_purchase_provenance_for_item",
|
||||
previous_filters: {
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31",
|
||||
item: "Столешница 600*3050*26 альмандин"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.filters.extracted_filters.item).toBe("Столешница 600*3050*26 альмандин");
|
||||
expect(
|
||||
result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") ||
|
||||
result?.intent.reasons.includes("inventory_purchase_date_signal_detected")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("promotes conversational buyer follow-up into inventory sale trace with inherited date context", () => {
|
||||
const result = runAddressDecomposeStage("кому в итоге мы продали этот товар?", {
|
||||
previous_intent: "inventory_purchase_provenance_for_item",
|
||||
|
|
@ -5141,6 +5162,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
|||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
});
|
||||
|
||||
it("keeps selected-object purchase-date pronoun wording out of generic counterparty docs intent", () => {
|
||||
const result = resolveAddressIntent("а по этой позиции когда была закупка?");
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
});
|
||||
|
||||
it("keeps direct item supplier questions in provenance intent even with current-stock tail", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад?"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildConfirmedBalanceSemantics,
|
||||
buildFactualListReply,
|
||||
buildFactualSummaryReply,
|
||||
buildHeuristicCandidatesSemantics
|
||||
} from "../src/services/address_runtime/replyContracts";
|
||||
|
||||
describe("replyContracts", () => {
|
||||
it("builds factual replies from line arrays", () => {
|
||||
const result = buildFactualListReply(["Первая строка", "Вторая строка"], buildConfirmedBalanceSemantics("strong"));
|
||||
|
||||
expect(result).toEqual({
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: "Первая строка\nВторая строка",
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: "strong",
|
||||
balance_confirmed: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("supports downgraded confirmed-balance semantics", () => {
|
||||
expect(buildFactualSummaryReply("Нет подтвержденных строк", buildConfirmedBalanceSemantics("medium", false)).semantics).toEqual({
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: "medium",
|
||||
balance_confirmed: false
|
||||
});
|
||||
});
|
||||
|
||||
it("builds heuristic candidate semantics from candidate presence", () => {
|
||||
expect(buildHeuristicCandidatesSemantics(true)).toEqual({
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: "medium",
|
||||
balance_confirmed: false
|
||||
});
|
||||
expect(buildHeuristicCandidatesSemantics(false)).toEqual({
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: "weak",
|
||||
balance_confirmed: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,49 @@
|
|||
[
|
||||
{
|
||||
"generation_id": "gen-ag04171508-760111",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"count": 8,
|
||||
"domain": "inventory_answer_shape_and_continuity",
|
||||
"questions": [
|
||||
"какие остатки на складе на март 2021",
|
||||
"давай по Альтернативе Плюс",
|
||||
"тогда покажи остатки на март 2021",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"а по этой позиции когда была закупка?",
|
||||
"покажи документы по этой позиции",
|
||||
"покажи еще раз остатки на эту же дату",
|
||||
"а что мы уже выяснили по этой позиции?"
|
||||
],
|
||||
"generated_by": "codex_agent",
|
||||
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260417150806_gen-ag04171508-760111.json",
|
||||
"context": {
|
||||
"llm_provider": null,
|
||||
"model": null,
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"autogen_personality_id": null,
|
||||
"autogen_personality_prompt": null,
|
||||
"source_session_id": null,
|
||||
"saved_session_file": "assistant_saved_session_20260417150806_gen-ag04171508-760111.json",
|
||||
"saved_case_set_kind": "agent_semantic_scenario",
|
||||
"agent_run": true,
|
||||
"agent_focus": "Targeted AGENT replay for the recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_inventory_answer_shape_continuity.json",
|
||||
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
|
||||
"semantic_tags": [
|
||||
"company_clarification",
|
||||
"company_selection",
|
||||
"inventory_root",
|
||||
"meta_memory",
|
||||
"meta_scope"
|
||||
],
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"generation_id": "gen-ag04171326-15a132",
|
||||
"created_at": "2026-04-17T13:26:00+00:00",
|
||||
|
|
@ -55,7 +100,8 @@
|
|||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
],
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -109,7 +155,8 @@
|
|||
"selected_object_documents",
|
||||
"selected_object_supplier",
|
||||
"settlements_receivables"
|
||||
]
|
||||
],
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -151,7 +198,10 @@
|
|||
"agent_run": true,
|
||||
"agent_focus": "mixed documents meta and cross-domain replay for turnaround 11",
|
||||
"architecture_phase": "turnaround_11_phase7",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json"
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json",
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -193,7 +243,10 @@
|
|||
"agent_run": true,
|
||||
"agent_focus": "mixed documents meta and cross-domain replay for turnaround 11",
|
||||
"architecture_phase": "turnaround_11_phase7",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json"
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json",
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -227,7 +280,10 @@
|
|||
"agent_run": true,
|
||||
"agent_focus": "scenario acceptance gate over root selected-object restore and human meta",
|
||||
"architecture_phase": "turnaround_11_phase7",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_acceptance_gate_mix.json"
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_acceptance_gate_mix.json",
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -260,7 +316,10 @@
|
|||
"agent_run": true,
|
||||
"agent_focus": "provider runtime axis hardening across chat meta and address boundaries",
|
||||
"architecture_phase": "turnaround_11_phase6",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase6_provider_axis_mix.json"
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase6_provider_axis_mix.json",
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -294,7 +353,10 @@
|
|||
"agent_run": true,
|
||||
"agent_focus": "meta and memory recap replay over interrupted address context",
|
||||
"architecture_phase": "turnaround_11_phase5",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_meta_memory_mix.json"
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_meta_memory_mix.json",
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -327,7 +389,10 @@
|
|||
"agent_run": true,
|
||||
"agent_focus": "coverage/evidence contract on factual, fallback, and root-reset branches",
|
||||
"architecture_phase": "turnaround_11_phase4",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_coverage_evidence_mix.json"
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_coverage_evidence_mix.json",
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -364,7 +429,14 @@
|
|||
"autogen_personality_prompt": null,
|
||||
"source_session_id": "asst-kBU5iS0mEt",
|
||||
"saved_session_file": "assistant_saved_session_20260417070448_gen-mo2kcds2-tlqmvng.json",
|
||||
"saved_case_set_kind": "assistant_session_scenario"
|
||||
"saved_case_set_kind": "assistant_session_scenario",
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -372,7 +444,7 @@
|
|||
"created_at": "2026-04-16T18:26:26.191Z",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||
"count": 31,
|
||||
"count": 30,
|
||||
"domain": null,
|
||||
"questions": [
|
||||
"приветик - че как там дела",
|
||||
|
|
@ -402,7 +474,6 @@
|
|||
"что нам отгружать чепурнов? какой товар или услугу?",
|
||||
"какие остатки на складе на сегодня",
|
||||
"остатки на март 2016",
|
||||
"это по общей базе уже нужен вывод не по чепурнову",
|
||||
"хвосты покажи по счету 60 на август 2022",
|
||||
"Есть ли остатки товара, которые закупались очень давно",
|
||||
"Какие конкретно номенклатуры формируют остаток по складу на май 2020"
|
||||
|
|
@ -419,7 +490,14 @@
|
|||
"autogen_personality_prompt": null,
|
||||
"source_session_id": "asst-NhPaZfbRYr",
|
||||
"saved_session_file": "assistant_saved_session_20260416182626_gen-mo1t93wq-jy0453e.json",
|
||||
"saved_case_set_kind": "assistant_session_scenario"
|
||||
"saved_case_set_kind": "assistant_session_scenario",
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -444,7 +522,14 @@
|
|||
"autogen_personality_prompt": null,
|
||||
"source_session_id": "asst-iIpNheKZpP",
|
||||
"saved_session_file": "assistant_saved_session_20260416175150_gen-mo1s0m9z-ndf56a3.json",
|
||||
"saved_case_set_kind": "assistant_session_scenario"
|
||||
"saved_case_set_kind": "assistant_session_scenario",
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -483,7 +568,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -522,7 +614,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -556,7 +655,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -590,7 +696,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -634,7 +747,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов \\ нсд \\ счета \\ общий ваывод по компании - контрагенты заказчиким скока денег кто принес и тп",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -678,7 +798,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов \\ нсд \\ счета \\ общий ваывод по компании - контрагенты заказчиким скока денег кто принес и тп",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -707,7 +834,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -736,7 +870,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -770,7 +911,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -804,7 +952,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -848,7 +1003,14 @@
|
|||
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -874,7 +1036,14 @@
|
|||
"autogen_personality_prompt": "????????? ??????? ??????? ?? ????????.",
|
||||
"source_session_id": null,
|
||||
"saved_session_file": null,
|
||||
"saved_case_set_kind": null
|
||||
"saved_case_set_kind": null,
|
||||
"agent_run": null,
|
||||
"agent_focus": null,
|
||||
"architecture_phase": null,
|
||||
"source_spec_file": null,
|
||||
"scenario_id": null,
|
||||
"semantic_tags": null,
|
||||
"latest_acceptance": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
"saved_at": "2026-04-17T15:08:06+00:00",
|
||||
"generation_id": "gen-ag04171508-760111",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"agent_run": true,
|
||||
"questions": [
|
||||
"какие остатки на складе на март 2021",
|
||||
"давай по Альтернативе Плюс",
|
||||
"тогда покажи остатки на март 2021",
|
||||
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"а по этой позиции когда была закупка?",
|
||||
"покажи документы по этой позиции",
|
||||
"покажи еще раз остатки на эту же дату",
|
||||
"а что мы уже выяснили по этой позиции?"
|
||||
],
|
||||
"metadata": {
|
||||
"assistant_prompt_version": null,
|
||||
"decomposition_prompt_version": null,
|
||||
"prompt_fingerprint": null,
|
||||
"agent_focus": "Targeted AGENT replay for the recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_inventory_answer_shape_continuity.json",
|
||||
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
|
||||
"semantic_tags": [
|
||||
"company_clarification",
|
||||
"company_selection",
|
||||
"inventory_root",
|
||||
"meta_memory",
|
||||
"meta_scope"
|
||||
]
|
||||
},
|
||||
"source_session_id": null,
|
||||
"session": {
|
||||
"session_id": null,
|
||||
"mode": "agent_semantic_run",
|
||||
"items": [
|
||||
{
|
||||
"message_id": "agent-user-001",
|
||||
"role": "user",
|
||||
"text": "какие остатки на складе на март 2021",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-002",
|
||||
"role": "user",
|
||||
"text": "давай по Альтернативе Плюс",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-003",
|
||||
"role": "user",
|
||||
"text": "тогда покажи остатки на март 2021",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-004",
|
||||
"role": "user",
|
||||
"text": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-005",
|
||||
"role": "user",
|
||||
"text": "а по этой позиции когда была закупка?",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-006",
|
||||
"role": "user",
|
||||
"text": "покажи документы по этой позиции",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-007",
|
||||
"role": "user",
|
||||
"text": "покажи еще раз остатки на эту же дату",
|
||||
"created_at": "2026-04-17T15:08:06+00:00",
|
||||
"reply_type": null,
|
||||
"trace_id": null,
|
||||
"debug": null
|
||||
},
|
||||
{
|
||||
"message_id": "agent-user-008",
|
||||
"role": "user",
|
||||
"text": "а что мы уже выяснили по этой позиции?",
|
||||
"created_at": "2026-04-17T15:08:06+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 recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
|
||||
"architecture_phase": "turnaround_11",
|
||||
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_inventory_answer_shape_continuity.json",
|
||||
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
|
||||
"semantic_tags": [
|
||||
"company_clarification",
|
||||
"company_selection",
|
||||
"inventory_root",
|
||||
"meta_memory",
|
||||
"meta_scope"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"suite_id": "assistant_saved_session_gen-mo1t93wq-jy0453e",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||
"generated_at": "2026-04-16T18:26:26.186Z",
|
||||
"generated_at": "2026-04-17T15:22:18.956Z",
|
||||
"generation_id": "gen-mo1t93wq-jy0453e",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||
|
|
@ -99,9 +99,6 @@
|
|||
{
|
||||
"user_message": "остатки на март 2016"
|
||||
},
|
||||
{
|
||||
"user_message": "это по общей базе уже нужен вывод не по чепурнову"
|
||||
},
|
||||
{
|
||||
"user_message": "хвосты покажи по счету 60 на август 2022"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_gen-ag04171508-760111",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||
"generated_at": "2026-04-17T15:08:06+00:00",
|
||||
"generation_id": "gen-ag04171508-760111",
|
||||
"mode": "saved_user_sessions",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"domain": "inventory_answer_shape_and_continuity",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "agent_saved_user_sessions",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "какие остатки на складе на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "давай по Альтернативе Плюс"
|
||||
},
|
||||
{
|
||||
"user_message": "тогда покажи остатки на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
|
||||
},
|
||||
{
|
||||
"user_message": "а по этой позиции когда была закупка?"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи документы по этой позиции"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи еще раз остатки на эту же дату"
|
||||
},
|
||||
{
|
||||
"user_message": "а что мы уже выяснили по этой позиции?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_runtime_job-XDhN0VgANV",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "saved_user_sessions_runtime",
|
||||
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "какие остатки на складе на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "давай по Альтернативе Плюс"
|
||||
},
|
||||
{
|
||||
"user_message": "тогда покажи остатки на март 2021"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
|
||||
},
|
||||
{
|
||||
"user_message": "а по этой позиции когда была закупка?"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи документы по этой позиции"
|
||||
},
|
||||
{
|
||||
"user_message": "покажи еще раз остатки на эту же дату"
|
||||
},
|
||||
{
|
||||
"user_message": "а что мы уже выяснили по этой позиции?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"suite_id": "assistant_saved_session_runtime_job-az4ZDEQptK",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||
"scenario_count": 1,
|
||||
"case_ids": [
|
||||
"SAVED-001"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "SAVED-001",
|
||||
"scenario_tag": "saved_user_sessions_runtime",
|
||||
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||
"question_type": "followup",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "приветик - че как там дела"
|
||||
},
|
||||
{
|
||||
"user_message": "расскажи что можешь интересного"
|
||||
},
|
||||
{
|
||||
"user_message": "кайф - что там на складе по остаткам?"
|
||||
},
|
||||
{
|
||||
"user_message": "а исторические остатки на другие даты умеешь?"
|
||||
},
|
||||
{
|
||||
"user_message": "давай на июль 2017"
|
||||
},
|
||||
{
|
||||
"user_message": "март 2016"
|
||||
},
|
||||
{
|
||||
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
|
||||
},
|
||||
{
|
||||
"user_message": "а кому продали?"
|
||||
},
|
||||
{
|
||||
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
|
||||
},
|
||||
{
|
||||
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
|
||||
},
|
||||
{
|
||||
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
|
||||
},
|
||||
{
|
||||
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
|
||||
},
|
||||
{
|
||||
"user_message": "кто у нас самый доходный клиент за все время"
|
||||
},
|
||||
{
|
||||
"user_message": "кто нам должен денег на май 2017"
|
||||
},
|
||||
{
|
||||
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
|
||||
},
|
||||
{
|
||||
"user_message": "мы должны комуто денег на сегодня?"
|
||||
},
|
||||
{
|
||||
"user_message": "а нам?"
|
||||
},
|
||||
{
|
||||
"user_message": "какой у нас самый доходный год"
|
||||
},
|
||||
{
|
||||
"user_message": "а за 2017 мы скок заработали?"
|
||||
},
|
||||
{
|
||||
"user_message": "сколько вообще денег мы заработали за все время?"
|
||||
},
|
||||
{
|
||||
"user_message": "ты умеешь считать дельту по договорам?"
|
||||
},
|
||||
{
|
||||
"user_message": "по чепурнову покажи все доки"
|
||||
},
|
||||
{
|
||||
"user_message": "а по свк"
|
||||
},
|
||||
{
|
||||
"user_message": "а сейчас у нас есть что на складе?"
|
||||
},
|
||||
{
|
||||
"user_message": "что нам отгружать чепурнов? какой товар или услугу?"
|
||||
},
|
||||
{
|
||||
"user_message": "какие остатки на складе на сегодня"
|
||||
},
|
||||
{
|
||||
"user_message": "остатки на март 2016"
|
||||
},
|
||||
{
|
||||
"user_message": "это по общей базе уже нужен вывод не по чепурнову"
|
||||
},
|
||||
{
|
||||
"user_message": "хвосты покажи по счету 60 на август 2022"
|
||||
},
|
||||
{
|
||||
"user_message": "Есть ли остатки товара, которые закупались очень давно"
|
||||
},
|
||||
{
|
||||
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue