АДРЕСНЫЙ РЕЖИМ - M2.3c микро набор вопросов отрабатывается коректно
This commit is contained in:
parent
a2886faed6
commit
4060a5e575
|
|
@ -0,0 +1,225 @@
|
|||
# Assistant conversation export
|
||||
session_id: asst-6FlZY4h6vE
|
||||
export_mode: technical
|
||||
exported_at: 2026-03-29T18:54:42.757Z
|
||||
|
||||
## 1. user
|
||||
message_id: msg-ytF_ybdXTx
|
||||
created_at: 2026-03-29T18:52:46.296Z
|
||||
reply_type: n/a
|
||||
|
||||
Какой остаток по счету 60 на дату 2020-07-31
|
||||
|
||||
## 2. assistant
|
||||
message_id: msg-lqh9qULwpD
|
||||
created_at: 2026-03-29T18:52:47.102Z
|
||||
reply_type: partial_coverage
|
||||
trace_id: address-u6vpJC9cQq
|
||||
|
||||
Не удалось выдать полноценный адресный factual-ответ в V1.
|
||||
Причина: по выбранному recipe нет строк в live-выборке.
|
||||
Что нужно уточнить: уточнить период или фильтры.
|
||||
|
||||
### technical_debug_payload_json
|
||||
```json
|
||||
{
|
||||
"trace_id": "address-u6vpJC9cQq",
|
||||
"prompt_version": "address_query_runtime_v1",
|
||||
"schema_version": "address_query_runtime_v1",
|
||||
"fallback_type": "partial",
|
||||
"route_summary": null,
|
||||
"fragments": [],
|
||||
"requirements_extracted": [],
|
||||
"coverage_report": {
|
||||
"requirements_total": 0,
|
||||
"requirements_covered": 0,
|
||||
"requirements_uncovered": [],
|
||||
"requirements_partially_covered": [],
|
||||
"clarification_needed_for": [],
|
||||
"out_of_scope_requirements": []
|
||||
},
|
||||
"routes": [],
|
||||
"retrieval_status": [],
|
||||
"retrieval_results": [],
|
||||
"answer_grounding_check": {
|
||||
"status": "partial",
|
||||
"route_subject_match": true,
|
||||
"missing_requirements": [],
|
||||
"reasons": [
|
||||
"address_action_detected",
|
||||
"address_entity_detected",
|
||||
"account_balance_signal_detected"
|
||||
],
|
||||
"why_included_summary": [],
|
||||
"selection_reason_summary": []
|
||||
},
|
||||
"dropped_intent_segments": [],
|
||||
"detected_mode": "address_query",
|
||||
"detected_mode_confidence": "high",
|
||||
"detected_intent": "account_balance_snapshot",
|
||||
"detected_intent_confidence": "high",
|
||||
"extracted_filters": {
|
||||
"sort": "period_desc",
|
||||
"limit": 20,
|
||||
"account": "60",
|
||||
"as_of_date": "2031-07-20"
|
||||
},
|
||||
"missing_required_filters": [],
|
||||
"selected_recipe": "address_movements_account_snapshot_v1",
|
||||
"mcp_call_status": "empty",
|
||||
"rows_fetched": 20,
|
||||
"rows_matched": 0,
|
||||
"response_type": "LIMITED_WITH_REASON",
|
||||
"answer_structure_v11": null,
|
||||
"investigation_state_snapshot": null,
|
||||
"normalized": null,
|
||||
"normalizer_output": null
|
||||
}
|
||||
```
|
||||
|
||||
## 3. user
|
||||
message_id: msg-SHZVsxgJlv
|
||||
created_at: 2026-03-29T18:53:30.732Z
|
||||
reply_type: n/a
|
||||
|
||||
Какие договоры не закрыты на дату 2020-07-31
|
||||
|
||||
## 4. assistant
|
||||
message_id: msg-YXWwlMLzBZ
|
||||
created_at: 2026-03-29T18:53:31.811Z
|
||||
reply_type: partial_coverage
|
||||
trace_id: address-4ozG_gIFqA
|
||||
|
||||
Не удалось выдать полноценный адресный factual-ответ в V1.
|
||||
Причина: в live строках нет явных договорных аналитик для уверенного списка незакрытых договоров.
|
||||
Что нужно уточнить: сузить запрос по контрагенту или добавить номер договора.
|
||||
|
||||
### technical_debug_payload_json
|
||||
```json
|
||||
{
|
||||
"trace_id": "address-4ozG_gIFqA",
|
||||
"prompt_version": "address_query_runtime_v1",
|
||||
"schema_version": "address_query_runtime_v1",
|
||||
"fallback_type": "partial",
|
||||
"route_summary": null,
|
||||
"fragments": [],
|
||||
"requirements_extracted": [],
|
||||
"coverage_report": {
|
||||
"requirements_total": 0,
|
||||
"requirements_covered": 0,
|
||||
"requirements_uncovered": [],
|
||||
"requirements_partially_covered": [],
|
||||
"clarification_needed_for": [],
|
||||
"out_of_scope_requirements": []
|
||||
},
|
||||
"routes": [],
|
||||
"retrieval_status": [],
|
||||
"retrieval_results": [],
|
||||
"answer_grounding_check": {
|
||||
"status": "partial",
|
||||
"route_subject_match": true,
|
||||
"missing_requirements": [],
|
||||
"reasons": [
|
||||
"address_action_detected",
|
||||
"address_entity_detected",
|
||||
"open_contract_signal_detected"
|
||||
],
|
||||
"why_included_summary": [],
|
||||
"selection_reason_summary": []
|
||||
},
|
||||
"dropped_intent_segments": [],
|
||||
"detected_mode": "address_query",
|
||||
"detected_mode_confidence": "high",
|
||||
"detected_intent": "list_open_contracts",
|
||||
"detected_intent_confidence": "medium",
|
||||
"extracted_filters": {
|
||||
"sort": "period_desc",
|
||||
"limit": 20,
|
||||
"as_of_date": "2031-07-20"
|
||||
},
|
||||
"missing_required_filters": [],
|
||||
"selected_recipe": "address_open_contracts_candidates_v1",
|
||||
"mcp_call_status": "empty",
|
||||
"rows_fetched": 20,
|
||||
"rows_matched": 0,
|
||||
"response_type": "LIMITED_WITH_REASON",
|
||||
"answer_structure_v11": null,
|
||||
"investigation_state_snapshot": null,
|
||||
"normalized": null,
|
||||
"normalizer_output": null
|
||||
}
|
||||
```
|
||||
|
||||
## 5. user
|
||||
message_id: msg-Z2N2uECEpu
|
||||
created_at: 2026-03-29T18:54:31.437Z
|
||||
reply_type: n/a
|
||||
|
||||
Какие документы формируют остаток по счету 62 на дату 2020-07-31
|
||||
|
||||
## 6. assistant
|
||||
message_id: msg-qsm4s1CpVs
|
||||
created_at: 2026-03-29T18:54:31.793Z
|
||||
reply_type: partial_coverage
|
||||
trace_id: address-zx8NZ3oPYI
|
||||
|
||||
Не удалось выдать полноценный адресный factual-ответ в V1.
|
||||
Причина: по выбранному recipe нет строк в live-выборке.
|
||||
Что нужно уточнить: уточнить период или фильтры.
|
||||
|
||||
### technical_debug_payload_json
|
||||
```json
|
||||
{
|
||||
"trace_id": "address-zx8NZ3oPYI",
|
||||
"prompt_version": "address_query_runtime_v1",
|
||||
"schema_version": "address_query_runtime_v1",
|
||||
"fallback_type": "partial",
|
||||
"route_summary": null,
|
||||
"fragments": [],
|
||||
"requirements_extracted": [],
|
||||
"coverage_report": {
|
||||
"requirements_total": 0,
|
||||
"requirements_covered": 0,
|
||||
"requirements_uncovered": [],
|
||||
"requirements_partially_covered": [],
|
||||
"clarification_needed_for": [],
|
||||
"out_of_scope_requirements": []
|
||||
},
|
||||
"routes": [],
|
||||
"retrieval_status": [],
|
||||
"retrieval_results": [],
|
||||
"answer_grounding_check": {
|
||||
"status": "partial",
|
||||
"route_subject_match": true,
|
||||
"missing_requirements": [],
|
||||
"reasons": [
|
||||
"address_action_detected",
|
||||
"address_entity_detected",
|
||||
"account_balance_signal_detected"
|
||||
],
|
||||
"why_included_summary": [],
|
||||
"selection_reason_summary": []
|
||||
},
|
||||
"dropped_intent_segments": [],
|
||||
"detected_mode": "address_query",
|
||||
"detected_mode_confidence": "high",
|
||||
"detected_intent": "account_balance_snapshot",
|
||||
"detected_intent_confidence": "high",
|
||||
"extracted_filters": {
|
||||
"sort": "period_desc",
|
||||
"limit": 20,
|
||||
"account": "62",
|
||||
"as_of_date": "2031-07-20"
|
||||
},
|
||||
"missing_required_filters": [],
|
||||
"selected_recipe": "address_movements_account_snapshot_v1",
|
||||
"mcp_call_status": "empty",
|
||||
"rows_fetched": 20,
|
||||
"rows_matched": 0,
|
||||
"response_type": "LIMITED_WITH_REASON",
|
||||
"answer_structure_v11": null,
|
||||
"investigation_state_snapshot": null,
|
||||
"normalized": null,
|
||||
"normalizer_output": null
|
||||
}
|
||||
```
|
||||
|
|
@ -78,11 +78,29 @@ function cleanupAnchorValue(value) {
|
|||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
||||
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
||||
const periodTailPattern = /\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu;
|
||||
if (periodTailPattern.test(normalized)) {
|
||||
return normalized.replace(periodTailPattern, "").trim();
|
||||
}
|
||||
const allTimeTailPattern = /\s+за\s+вс[её]\s+время(?:\s+|$)[\s\S]*$/iu;
|
||||
if (allTimeTailPattern.test(normalized)) {
|
||||
return normalized.replace(allTimeTailPattern, "").trim();
|
||||
}
|
||||
const allTimeTailPatternEn = /\s+(?:for\s+all\s+time|all\s+time)(?:\s+|$)[\s\S]*$/iu;
|
||||
if (allTimeTailPatternEn.test(normalized)) {
|
||||
return normalized.replace(allTimeTailPatternEn, "").trim();
|
||||
}
|
||||
return normalized
|
||||
.replace(/\s+(?:from|to|between|and)\b[\s\S]*$/i, "")
|
||||
.replace(/\s+(?:с|по|за)\b[\s\S]*$/i, "")
|
||||
.replace(/\s+(?:from|to|between|and)(?:\s+|$)[\s\S]*$/iu, "")
|
||||
.replace(/\s+(?:с|по|за)(?:\s+|$)[\s\S]*$/iu, "")
|
||||
.trim();
|
||||
}
|
||||
function hasAllTimeHint(text) {
|
||||
const value = String(text ?? "");
|
||||
return /(?:за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(value);
|
||||
}
|
||||
function shiftDaysIso(baseIso, deltaDays) {
|
||||
const date = new Date(`${baseIso}T00:00:00.000Z`);
|
||||
date.setUTCDate(date.getUTCDate() + deltaDays);
|
||||
|
|
@ -140,7 +158,8 @@ function extractAddressFilters(userMessage, intent) {
|
|||
// For document/bank lists we default to a short recent window if no explicit period was provided.
|
||||
if ((intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") &&
|
||||
!filters.period_from &&
|
||||
!filters.period_to) {
|
||||
!filters.period_to &&
|
||||
!hasAllTimeHint(text)) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
filters.period_to = today;
|
||||
filters.period_from = shiftDaysIso(today, -90);
|
||||
|
|
|
|||
|
|
@ -162,6 +162,16 @@ function buildWhereClause(filters, fieldPath) {
|
|||
}
|
||||
return "";
|
||||
}
|
||||
function shouldBoostLimitForAllTimeCounterparty(filters) {
|
||||
const hasCounterparty = typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0;
|
||||
if (!hasCounterparty) {
|
||||
return false;
|
||||
}
|
||||
const hasPeriod = Boolean((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0) ||
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0));
|
||||
return !hasPeriod;
|
||||
}
|
||||
function selectAddressRecipe(intent, filters) {
|
||||
const recipe = BASE_RECIPES.find((item) => item.intent === intent) ?? null;
|
||||
if (!recipe) {
|
||||
|
|
@ -182,9 +192,14 @@ function selectAddressRecipe(intent, filters) {
|
|||
};
|
||||
}
|
||||
function buildAddressRecipePlan(recipe, filters) {
|
||||
const resolvedLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(200, Math.trunc(filters.limit)))
|
||||
: recipe.default_limit;
|
||||
const boostedLimit = (recipe.intent === "list_documents_by_counterparty" || recipe.intent === "bank_operations_by_counterparty") &&
|
||||
shouldBoostLimitForAllTimeCounterparty(filters)
|
||||
? Math.max(baseLimit, 200)
|
||||
: baseLimit;
|
||||
const resolvedLimit = Math.max(1, Math.min(200, boostedLimit));
|
||||
const accountScope = (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && filters.account
|
||||
? [String(filters.account)]
|
||||
: Array.isArray(recipe.account_scope)
|
||||
|
|
|
|||
|
|
@ -86,12 +86,35 @@ function cleanupAnchorValue(value: string): string {
|
|||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
||||
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
||||
const periodTailPattern =
|
||||
/\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu;
|
||||
if (periodTailPattern.test(normalized)) {
|
||||
return normalized.replace(periodTailPattern, "").trim();
|
||||
}
|
||||
|
||||
const allTimeTailPattern = /\s+за\s+вс[её]\s+время(?:\s+|$)[\s\S]*$/iu;
|
||||
if (allTimeTailPattern.test(normalized)) {
|
||||
return normalized.replace(allTimeTailPattern, "").trim();
|
||||
}
|
||||
const allTimeTailPatternEn = /\s+(?:for\s+all\s+time|all\s+time)(?:\s+|$)[\s\S]*$/iu;
|
||||
if (allTimeTailPatternEn.test(normalized)) {
|
||||
return normalized.replace(allTimeTailPatternEn, "").trim();
|
||||
}
|
||||
|
||||
return normalized
|
||||
.replace(/\s+(?:from|to|between|and)\b[\s\S]*$/i, "")
|
||||
.replace(/\s+(?:с|по|за)\b[\s\S]*$/i, "")
|
||||
.replace(/\s+(?:from|to|between|and)(?:\s+|$)[\s\S]*$/iu, "")
|
||||
.replace(/\s+(?:с|по|за)(?:\s+|$)[\s\S]*$/iu, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function hasAllTimeHint(text: string): boolean {
|
||||
const value = String(text ?? "");
|
||||
return /(?:за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(value);
|
||||
}
|
||||
|
||||
function shiftDaysIso(baseIso: string, deltaDays: number): string {
|
||||
const date = new Date(`${baseIso}T00:00:00.000Z`);
|
||||
date.setUTCDate(date.getUTCDate() + deltaDays);
|
||||
|
|
@ -159,7 +182,8 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
if (
|
||||
(intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") &&
|
||||
!filters.period_from &&
|
||||
!filters.period_to
|
||||
!filters.period_to &&
|
||||
!hasAllTimeHint(text)
|
||||
) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
filters.period_to = today;
|
||||
|
|
|
|||
|
|
@ -183,6 +183,19 @@ function buildWhereClause(filters: AddressFilterSet, fieldPath: string): string
|
|||
return "";
|
||||
}
|
||||
|
||||
function shouldBoostLimitForAllTimeCounterparty(filters: AddressFilterSet): boolean {
|
||||
const hasCounterparty = typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0;
|
||||
if (!hasCounterparty) {
|
||||
return false;
|
||||
}
|
||||
const hasPeriod = Boolean(
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0) ||
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0)
|
||||
);
|
||||
return !hasPeriod;
|
||||
}
|
||||
|
||||
export function selectAddressRecipe(intent: AddressIntent, filters: AddressFilterSet): AddressRecipeSelection {
|
||||
const recipe = BASE_RECIPES.find((item) => item.intent === intent) ?? null;
|
||||
if (!recipe) {
|
||||
|
|
@ -209,10 +222,16 @@ export function buildAddressRecipePlan(
|
|||
recipe: AddressRecipeDefinition,
|
||||
filters: AddressFilterSet
|
||||
): AddressRecipeExecutionPlan {
|
||||
const resolvedLimit =
|
||||
const baseLimit =
|
||||
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(200, Math.trunc(filters.limit)))
|
||||
: recipe.default_limit;
|
||||
const boostedLimit =
|
||||
(recipe.intent === "list_documents_by_counterparty" || recipe.intent === "bank_operations_by_counterparty") &&
|
||||
shouldBoostLimitForAllTimeCounterparty(filters)
|
||||
? Math.max(baseLimit, 200)
|
||||
: baseLimit;
|
||||
const resolvedLimit = Math.max(1, Math.min(200, boostedLimit));
|
||||
|
||||
const accountScope =
|
||||
(recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && filters.account
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
|||
import { classifyAddressQueryShape } from "../src/services/addressQueryShapeClassifier";
|
||||
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
||||
import { AddressQueryService } from "../src/services/addressQueryService";
|
||||
import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog";
|
||||
|
||||
describe("address query shape classifier", () => {
|
||||
it("classifies explain question as deep-shape", () => {
|
||||
|
|
@ -46,6 +47,27 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
expect(result.extracted_filters.as_of_date).toBeDefined();
|
||||
expect(result.missing_required_filters).toEqual([]);
|
||||
});
|
||||
|
||||
it("cuts period tail from counterparty anchor", () => {
|
||||
const result = extractAddressFilters(
|
||||
"Покажи документы по контрагенту test_cp с 2020-07-01 по 2020-07-31",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
expect(result.extracted_filters.counterparty).toBe("test_cp");
|
||||
expect(result.extracted_filters.period_from).toBe("2020-07-01");
|
||||
expect(result.extracted_filters.period_to).toBe("2020-07-31");
|
||||
});
|
||||
|
||||
it("cuts all-time tail from counterparty anchor and skips 90-day default window", () => {
|
||||
const result = extractAddressFilters(
|
||||
"Покажи документы по контрагенту тестовый за все время",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
expect(result.extracted_filters.counterparty).toBe("тестовый");
|
||||
expect(result.extracted_filters.period_from).toBeUndefined();
|
||||
expect(result.extracted_filters.period_to).toBeUndefined();
|
||||
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
|
||||
});
|
||||
});
|
||||
|
||||
describe("address query limited taxonomy and stage diagnostics", () => {
|
||||
|
|
@ -101,3 +123,40 @@ describe("address query limited taxonomy and stage diagnostics", () => {
|
|||
expect(result?.debug.account_scope_drop_reason).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("address recipe catalog counterparty filtering", () => {
|
||||
it("boosts limit for all-time counterparty queries", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Покажи документы по контрагенту тестовый за все время",
|
||||
"list_documents_by_counterparty"
|
||||
).extracted_filters;
|
||||
const selected = selectAddressRecipe("list_documents_by_counterparty", filters);
|
||||
expect(selected.selected_recipe).toBeTruthy();
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
|
||||
|
||||
expect(plan.limit).toBe(200);
|
||||
});
|
||||
|
||||
it("boosts limit for english all-time counterparty queries", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"show documents by counterparty test_cp for all time",
|
||||
"list_documents_by_counterparty"
|
||||
).extracted_filters;
|
||||
const selected = selectAddressRecipe("list_documents_by_counterparty", filters);
|
||||
expect(selected.selected_recipe).toBeTruthy();
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
|
||||
|
||||
expect(plan.limit).toBe(200);
|
||||
});
|
||||
|
||||
it("cuts english all-time tail from counterparty anchor", () => {
|
||||
const result = extractAddressFilters(
|
||||
"show documents by counterparty test_cp for all time",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
expect(result.extracted_filters.counterparty).toBe("test_cp");
|
||||
expect(result.extracted_filters.period_from).toBeUndefined();
|
||||
expect(result.extracted_filters.period_to).toBeUndefined();
|
||||
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue