diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 794b61e..91407dc 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -2382,6 +2382,25 @@ Implemented in current pass (Stage 3.2 semantic route arbitration): - Stage 3 focused suite: `10` files / `76` tests passed. - Type build: `npm --prefix llm_normalizer/backend run build` passed. +Implemented in current pass (Stage 3.3 soft-refusal + anti-template limited replies): +1. Reworked address-lane limited reply composer from fixed template to contextual soft-refusal: + - Added deterministic phrasing variants (stable, non-random) to reduce repeated boilerplate. + - Kept concise "Коротко" structure while replacing hard repeated wording. +2. Added context-aware explanation and recovery offers for limited responses: + - Response now injects request scope hints when available (organization, as-of date, period window). + - Added "Что могу сделать сейчас" with nearest supported scenarios (documents/payments by anchor, open contracts/tails, account-balance drilldown). + - Added weak-anchor suppression for user-facing hints to avoid low-value auto-substitutions in suggestions. +3. Improved unsupported aggregate handling UX: + - For aggregate/ranking style queries, reply now proposes evidence-first path (collect factual base -> continue in extended analysis) instead of hard static fallback phrase. +4. Improved missing-anchor UX: + - Missing anchor tokens are normalized to user-facing terms (e.g., `counterparty_or_contract` -> "контрагент или договор"). + - Added concrete reformulation hint for underspecified anchor cases. +5. Regression updates: + - Updated `addressQueryRuntimeM23.test.ts` soft out-of-scope assertion to validate contextual soft-refusal structure. +6. Validation snapshot: + - Extended regression pack: `11` files / `344` tests passed. + - Type build: `npm --prefix llm_normalizer/backend run build` passed. + Acceptance (Stage 3): 1. LLM outputs strictly validated schema for extraction/decomposition (no free-form). 2. Deterministic guards can block or downgrade answers when evidence insufficient. diff --git a/llm_normalizer/backend/dist/services/addressQueryService.js b/llm_normalizer/backend/dist/services/addressQueryService.js index 18a7426..d06b0f7 100644 --- a/llm_normalizer/backend/dist/services/addressQueryService.js +++ b/llm_normalizer/backend/dist/services/addressQueryService.js @@ -835,29 +835,173 @@ function toLegacyMcpStatus(status) { } return status; } -function composeLimitedReply(category, reason, nextStep) { - const heading = category === "empty_match" - ? "По текущим условиям в доступном срезе данных совпадений не нашлось." - : category === "missing_anchor" - ? "Чтобы ответить надежно, нужен более точный ориентир в запросе." - : category === "recipe_visibility_gap" - ? "Запрос понятен, но текущий режим не дает нужной детализации." - : category === "unsupported" - ? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима." - : "Не удалось завершить проверку в адресном режиме."; - const reasonLine = category === "unsupported" - ? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре." - : category === "missing_anchor" - ? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)." - : category === "recipe_visibility_gap" - ? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки." - : `Коротко: ${normalizeLimitedReason(reason)}.`; - const lines = [ - heading, - reasonLine - ]; +function pickDeterministicVariant(seed, variants) { + if (variants.length === 0) { + return ""; + } + let score = 0; + for (const char of String(seed ?? "")) { + score = (score + char.charCodeAt(0)) % 104_729; + } + return variants[score % variants.length]; +} +function toNonEmptyFilterValue(value) { + if (typeof value !== "string") { + return null; + } + const normalized = value.trim(); + return normalized.length > 0 ? normalized : null; +} +function isWeakOfferAnchorValue(value) { + const normalized = String(value ?? "") + .toLowerCase() + .replace(/\s+/gu, " ") + .trim(); + if (!normalized) { + return true; + } + if (normalized.length < 3) { + return true; + } + if (/^\d+$/u.test(normalized)) { + return true; + } + return /^(?:контрагент(?:ы|а|у|ом)?|договор(?:ы|а|у|ом)?|контракт(?:ы|а|у|ом)?|документ(?:ы|а|у|ом)?|оплат(?:а|ы|у|ой|ам)?|плат[её]ж(?:и|а|у|ом)?|операц(?:ия|ии|ий|ию|иями)?|период|данные|база|компания|организация)$/iu.test(normalized); +} +function normalizeMissingAnchorLabel(anchor) { + if (anchor === "counterparty_or_contract") { + return "контрагент или договор"; + } + if (anchor === "counterparty") { + return "контрагент"; + } + if (anchor === "contract") { + return "договор"; + } + if (anchor === "account") { + return "счет"; + } + if (anchor === "document_ref") { + return "документ"; + } + if (anchor === "organization") { + return "организация"; + } + if (anchor === "period" || anchor === "period_from" || anchor === "period_to" || anchor === "as_of_date") { + return "период/дата"; + } + return anchor.replace(/_/gu, " "); +} +function buildLimitedScopeLine(filters) { + const organization = toNonEmptyFilterValue(filters.organization); + const asOfDate = toNonEmptyFilterValue(filters.as_of_date); + const periodFrom = toNonEmptyFilterValue(filters.period_from); + const periodTo = toNonEmptyFilterValue(filters.period_to); + const scopeParts = []; + if (organization) { + scopeParts.push(`организация ${organization}`); + } + if (asOfDate) { + scopeParts.push(`срез на ${asOfDate}`); + } + else if (periodFrom || periodTo) { + scopeParts.push(`период ${periodFrom ?? "..."}..${periodTo ?? "..."}`); + } + if (scopeParts.length === 0) { + return null; + } + return `Контекст запроса: ${scopeParts.join(", ")}.`; +} +function buildLimitedOffers(input) { + const counterpartyRaw = toNonEmptyFilterValue(input.filters.counterparty); + const contractRaw = toNonEmptyFilterValue(input.filters.contract); + const counterparty = counterpartyRaw && !isWeakOfferAnchorValue(counterpartyRaw) ? counterpartyRaw : null; + const contract = contractRaw && !isWeakOfferAnchorValue(contractRaw) ? contractRaw : null; + const account = toNonEmptyFilterValue(input.filters.account); + const offers = []; + if (input.category === "missing_anchor") { + const missingAnchors = Array.from(new Set((Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : []) + .map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim())) + .filter((item) => item.length > 0))); + if (missingAnchors.length > 0) { + offers.push(`уточнить ориентир: ${missingAnchors.join(", ")}`); + } + if (missingAnchors.includes("контрагент или договор")) { + offers.push("пример: «покажи документы по договору <номер> за 2020 год»"); + } + } + if (counterparty) { + offers.push(`показать документы и платежи по контрагенту ${counterparty}`); + } + else if (contract) { + offers.push(`показать документы и платежи по договору ${contract}`); + } + else { + offers.push("показать документы/платежи по контрагенту или договору"); + } + if (account) { + offers.push(`проверить остаток и документы, формирующие остаток по счету ${account}`); + } + else { + offers.push("показать незакрытые договоры или хвосты на дату"); + } + const aggregateIntentSignal = input.shape.shape === "AGGREGATE_LOOKUP" || + /(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin)/iu.test(String(input.reason ?? "")); + if (input.category === "unsupported" && aggregateIntentSignal) { + offers.unshift("собрать фактическую базу по периоду, после чего посчитать метрику в расширенном анализе"); + } + const nextStep = normalizeLimitedNextStep(input.nextStep ?? ""); if (nextStep) { - lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`); + offers.push(nextStep); + } + return Array.from(new Set(offers)).slice(0, 3); +} +function composeLimitedReply(input) { + const reason = normalizeLimitedReason(input.reason); + const headingSeed = `${input.category}|${input.shape.shape}|${reason}`; + const heading = input.category === "empty_match" + ? pickDeterministicVariant(headingSeed, [ + "По текущим условиям в доступном срезе данных совпадений не нашлось.", + "В текущем срезе данных по этому запросу совпадения не найдены." + ]) + : input.category === "missing_anchor" + ? pickDeterministicVariant(headingSeed, [ + "Чтобы ответ был точным, нужно чуть сильнее заякорить запрос.", + "Запрос понятен, но для надежного ответа не хватает опорного ориентира." + ]) + : input.category === "recipe_visibility_gap" + ? pickDeterministicVariant(headingSeed, [ + "Запрос понятен, но текущий сценарий выборки не дает нужной детализации.", + "Смысл запроса ясен, но в этом контуре не хватает глубины выборки." + ]) + : input.category === "unsupported" + ? pickDeterministicVariant(headingSeed, [ + "По этому вопросу в текущем адресном контуре пока нет надежного маршрута ответа.", + "Сейчас в адресном режиме такой сценарий не закрыт без риска ошибочного вывода." + ]) + : "Не удалось завершить проверку в адресном режиме."; + const reasonLine = input.category === "unsupported" + ? "Коротко: сценарий пока не покрыт текущими адресными маршрутами." + : input.category === "missing_anchor" + ? "Коротко: не хватает конкретного ориентира (контрагент, договор, счет или период)." + : input.category === "recipe_visibility_gap" + ? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки." + : `Коротко: ${reason}.`; + const lines = [heading, reasonLine]; + const scopeLine = buildLimitedScopeLine(input.filters); + if (scopeLine) { + lines.push(scopeLine); + } + const offers = buildLimitedOffers({ + category: input.category, + shape: input.shape, + filters: input.filters, + missingRequiredFilters: input.missingRequiredFilters, + reason: input.reason, + nextStep: input.nextStep + }); + if (offers.length > 0) { + lines.push(`Что могу сделать сейчас: ${offers.join("; ")}.`); } return lines.join("\n"); } @@ -865,7 +1009,14 @@ function buildLimitedExecutionResult(input) { const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters); return { handled: true, - reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep), + reply_text: composeLimitedReply({ + category: input.category, + reason: input.reasonText, + nextStep: input.nextStep, + shape: input.shape, + filters: input.filters, + missingRequiredFilters: input.missingRequiredFilters + }), reply_type: "partial_coverage", response_type: "LIMITED_WITH_REASON", debug: { diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index 555d8d6..5fb95ab 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -1025,32 +1025,211 @@ function toLegacyMcpStatus( return status; } -function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string { - const heading = - category === "empty_match" - ? "По текущим условиям в доступном срезе данных совпадений не нашлось." - : category === "missing_anchor" - ? "Чтобы ответить надежно, нужен более точный ориентир в запросе." - : category === "recipe_visibility_gap" - ? "Запрос понятен, но текущий режим не дает нужной детализации." - : category === "unsupported" - ? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима." - : "Не удалось завершить проверку в адресном режиме."; - const reasonLine = - category === "unsupported" - ? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре." - : category === "missing_anchor" - ? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)." - : category === "recipe_visibility_gap" - ? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки." - : `Коротко: ${normalizeLimitedReason(reason)}.`; - const lines = [ - heading, - reasonLine - ]; - if (nextStep) { - lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`); +function pickDeterministicVariant(seed: string, variants: string[]): string { + if (variants.length === 0) { + return ""; } + let score = 0; + for (const char of String(seed ?? "")) { + score = (score + char.charCodeAt(0)) % 104_729; + } + return variants[score % variants.length]; +} + +function toNonEmptyFilterValue(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + const normalized = value.trim(); + return normalized.length > 0 ? normalized : null; +} + +function isWeakOfferAnchorValue(value: string): boolean { + const normalized = String(value ?? "") + .toLowerCase() + .replace(/\s+/gu, " ") + .trim(); + if (!normalized) { + return true; + } + if (normalized.length < 3) { + return true; + } + if (/^\d+$/u.test(normalized)) { + return true; + } + return /^(?:контрагент(?:ы|а|у|ом)?|договор(?:ы|а|у|ом)?|контракт(?:ы|а|у|ом)?|документ(?:ы|а|у|ом)?|оплат(?:а|ы|у|ой|ам)?|плат[её]ж(?:и|а|у|ом)?|операц(?:ия|ии|ий|ию|иями)?|период|данные|база|компания|организация)$/iu.test( + normalized + ); +} + +function normalizeMissingAnchorLabel(anchor: string): string { + if (anchor === "counterparty_or_contract") { + return "контрагент или договор"; + } + if (anchor === "counterparty") { + return "контрагент"; + } + if (anchor === "contract") { + return "договор"; + } + if (anchor === "account") { + return "счет"; + } + if (anchor === "document_ref") { + return "документ"; + } + if (anchor === "organization") { + return "организация"; + } + if (anchor === "period" || anchor === "period_from" || anchor === "period_to" || anchor === "as_of_date") { + return "период/дата"; + } + return anchor.replace(/_/gu, " "); +} + +function buildLimitedScopeLine(filters: AddressFilterSet): string | null { + const organization = toNonEmptyFilterValue(filters.organization); + const asOfDate = toNonEmptyFilterValue(filters.as_of_date); + const periodFrom = toNonEmptyFilterValue(filters.period_from); + const periodTo = toNonEmptyFilterValue(filters.period_to); + const scopeParts: string[] = []; + if (organization) { + scopeParts.push(`организация ${organization}`); + } + if (asOfDate) { + scopeParts.push(`срез на ${asOfDate}`); + } else if (periodFrom || periodTo) { + scopeParts.push(`период ${periodFrom ?? "..."}..${periodTo ?? "..."}`); + } + if (scopeParts.length === 0) { + return null; + } + return `Контекст запроса: ${scopeParts.join(", ")}.`; +} + +function buildLimitedOffers(input: { + category: AddressLimitedReasonCategory; + shape: AddressQueryShapeDetection; + filters: AddressFilterSet; + missingRequiredFilters: string[]; + reason: string; + nextStep?: string; +}): string[] { + const counterpartyRaw = toNonEmptyFilterValue(input.filters.counterparty); + const contractRaw = toNonEmptyFilterValue(input.filters.contract); + const counterparty = counterpartyRaw && !isWeakOfferAnchorValue(counterpartyRaw) ? counterpartyRaw : null; + const contract = contractRaw && !isWeakOfferAnchorValue(contractRaw) ? contractRaw : null; + const account = toNonEmptyFilterValue(input.filters.account); + const offers: string[] = []; + + if (input.category === "missing_anchor") { + const missingAnchors = Array.from( + new Set( + (Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : []) + .map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim())) + .filter((item) => item.length > 0) + ) + ); + if (missingAnchors.length > 0) { + offers.push(`уточнить ориентир: ${missingAnchors.join(", ")}`); + } + if (missingAnchors.includes("контрагент или договор")) { + offers.push("пример: «покажи документы по договору <номер> за 2020 год»"); + } + } + + if (counterparty) { + offers.push(`показать документы и платежи по контрагенту ${counterparty}`); + } else if (contract) { + offers.push(`показать документы и платежи по договору ${contract}`); + } else { + offers.push("показать документы/платежи по контрагенту или договору"); + } + + if (account) { + offers.push(`проверить остаток и документы, формирующие остаток по счету ${account}`); + } else { + offers.push("показать незакрытые договоры или хвосты на дату"); + } + + const aggregateIntentSignal = + input.shape.shape === "AGGREGATE_LOOKUP" || + /(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin)/iu.test( + String(input.reason ?? "") + ); + if (input.category === "unsupported" && aggregateIntentSignal) { + offers.unshift("собрать фактическую базу по периоду, после чего посчитать метрику в расширенном анализе"); + } + + const nextStep = normalizeLimitedNextStep(input.nextStep ?? ""); + if (nextStep) { + offers.push(nextStep); + } + + return Array.from(new Set(offers)).slice(0, 3); +} + +function composeLimitedReply(input: { + category: AddressLimitedReasonCategory; + reason: string; + nextStep?: string; + shape: AddressQueryShapeDetection; + filters: AddressFilterSet; + missingRequiredFilters: string[]; +}): string { + const reason = normalizeLimitedReason(input.reason); + const headingSeed = `${input.category}|${input.shape.shape}|${reason}`; + const heading = + input.category === "empty_match" + ? pickDeterministicVariant(headingSeed, [ + "По текущим условиям в доступном срезе данных совпадений не нашлось.", + "В текущем срезе данных по этому запросу совпадения не найдены." + ]) + : input.category === "missing_anchor" + ? pickDeterministicVariant(headingSeed, [ + "Чтобы ответ был точным, нужно чуть сильнее заякорить запрос.", + "Запрос понятен, но для надежного ответа не хватает опорного ориентира." + ]) + : input.category === "recipe_visibility_gap" + ? pickDeterministicVariant(headingSeed, [ + "Запрос понятен, но текущий сценарий выборки не дает нужной детализации.", + "Смысл запроса ясен, но в этом контуре не хватает глубины выборки." + ]) + : input.category === "unsupported" + ? pickDeterministicVariant(headingSeed, [ + "По этому вопросу в текущем адресном контуре пока нет надежного маршрута ответа.", + "Сейчас в адресном режиме такой сценарий не закрыт без риска ошибочного вывода." + ]) + : "Не удалось завершить проверку в адресном режиме."; + + const reasonLine = + input.category === "unsupported" + ? "Коротко: сценарий пока не покрыт текущими адресными маршрутами." + : input.category === "missing_anchor" + ? "Коротко: не хватает конкретного ориентира (контрагент, договор, счет или период)." + : input.category === "recipe_visibility_gap" + ? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки." + : `Коротко: ${reason}.`; + + const lines = [heading, reasonLine]; + const scopeLine = buildLimitedScopeLine(input.filters); + if (scopeLine) { + lines.push(scopeLine); + } + + const offers = buildLimitedOffers({ + category: input.category, + shape: input.shape, + filters: input.filters, + missingRequiredFilters: input.missingRequiredFilters, + reason: input.reason, + nextStep: input.nextStep + }); + if (offers.length > 0) { + lines.push(`Что могу сделать сейчас: ${offers.join("; ")}.`); + } + return lines.join("\n"); } @@ -1091,7 +1270,14 @@ function buildLimitedExecutionResult(input: { const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters); return { handled: true, - reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep), + reply_text: composeLimitedReply({ + category: input.category, + reason: input.reasonText, + nextStep: input.nextStep, + shape: input.shape, + filters: input.filters, + missingRequiredFilters: input.missingRequiredFilters + }), reply_type: "partial_coverage", response_type: "LIMITED_WITH_REASON", debug: { diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index 12a08a1..66c7739 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -2352,7 +2352,8 @@ describe("address query limited taxonomy and stage diagnostics", () => { expect(result?.debug.detected_intent).toBe("unknown"); expect(result?.debug.limited_reason_category).toBe("unsupported"); const reply = String(result?.reply_text ?? ""); - expect(reply.toLowerCase()).toContain("вне поддерживаемого контура"); + expect(reply.toLowerCase()).toContain("адресн"); + expect(reply).toContain("Что могу сделать сейчас:"); expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu); }); diff --git a/llm_normalizer/data/eval_cases/assistant_autogen_runtime_job-0tckNFdleX.json b/llm_normalizer/data/eval_cases/assistant_autogen_runtime_job-0tckNFdleX.json new file mode 100644 index 0000000..2a04efa --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_autogen_runtime_job-0tckNFdleX.json @@ -0,0 +1,190 @@ +{ + "suite_id": "assistant_autogen_runtime_job-0tckNFdleX", + "suite_version": "0.1.0", + "schema_version": "assistant_autogen_runtime_v0_1", + "scenario_count": 15, + "case_ids": [ + "AUTO-001", + "AUTO-002", + "AUTO-003", + "AUTO-004", + "AUTO-005", + "AUTO-006", + "AUTO-007", + "AUTO-008", + "AUTO-009", + "AUTO-010", + "AUTO-011", + "AUTO-012", + "AUTO-013", + "AUTO-014", + "AUTO-015" + ], + "cases": [ + { + "case_id": "AUTO-001", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Кому из контрагентов мы уже месяц отдаем товары, но на счетах все еще красуется минусовое сальдо - это реально зеленый свет для ручного вмешательства?" + } + ] + }, + { + "case_id": "AUTO-002", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?" + } + ] + }, + { + "case_id": "AUTO-003", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Покажи контрагентов, по которым сальдо у нас выглядит так, будто оно врет - ну точно не совпадает с тем, что они нам прислали. Это уже критично для сверки." + } + ] + }, + { + "case_id": "AUTO-004", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?" + } + ] + }, + { + "case_id": "AUTO-005", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "В каких случаях мы видим ситуацию, когда документы есть, а денег - нет и пока не предвидится?" + } + ] + }, + { + "case_id": "AUTO-006", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Какие контрагенты висят с закрытыми отгрузками, но с открытыми документами оплаты, что явно выглядит как кейс для ручной проверки?" + } + ] + }, + { + "case_id": "AUTO-007", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Покажи контрагентов, у которых есть неоплаченные задолженности по договорам на конец месяца - это уже красный свет для бухгалтера." + } + ] + }, + { + "case_id": "AUTO-008", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "По каким заказчикам мы можем выделить непростую картину: сальдо нулевое, а история платежей явно говорит о том, что все не так просто?" + } + ] + }, + { + "case_id": "AUTO-009", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?" + } + ] + }, + { + "case_id": "AUTO-010", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности." + } + ] + }, + { + "case_id": "AUTO-011", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Покажи контрагентов, по которым на конец месяца сальдо выглядит так, будто документы собраны криво и их нужно перепроверить." + } + ] + }, + { + "case_id": "AUTO-012", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Какие у нас зависшие авансы или предоплаты уже давно пора либо закрыть, либо хотя бы проверить - это уже не просто вопрос времени?" + } + ] + }, + { + "case_id": "AUTO-013", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "По каким контрагентам мы можем заметить такую картину: оплачено меньше, чем отгружено, и это явно требует вмешательства бухгалтера." + } + ] + }, + { + "case_id": "AUTO-014", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Какие незакрытые документы по договорам у нас уже давно пора проверить - это грозит серьезными проблемами?" + } + ] + }, + { + "case_id": "AUTO-015", + "scenario_tag": "autogen_runtime", + "question_type": "direct", + "broadness_level": "medium", + "turns": [ + { + "user_message": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться." + } + ] + } + ] +} \ No newline at end of file