АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С

This commit is contained in:
2026-04-17 16:40:08 +03:00
parent 7be037e225
commit cd0b78d1de
31 changed files with 1909 additions and 1187 deletions
@@ -3735,6 +3735,17 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
}
});
it("routes company activity-age wording into lifecycle aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("по Альтернативе Плюс сколько лет активности в базе 1С?", {
activeOrganization: 'ООО "Альтернатива Плюс"'
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("routes debt-longevity wording into receivables lane with factual reply", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -4226,6 +4237,127 @@ describe("address decompose stage follow-up carryover", () => {
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("restores inventory root follow-up after company selection for repeated stock request", () => {
const result = runAddressDecomposeStage("тогда покажи остатки на март 2021", {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Альтернатива Плюс",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
current_frame_kind: "inventory_root"
} as any);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.filters.extracted_filters.period_from).toBe("2021-03-01");
expect(result?.filters.extracted_filters.period_to).toBe("2021-03-31");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("composes direct activity-age answer from lifecycle aggregate for focused counterparty", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2019-02-14",
registrator: "CP_CUSTOMER_ACTIVITY_FIRST",
account_dt: "",
account_kt: "",
amount: 4,
analytics: ["Альтернатива Плюс"]
},
{
period: "2019-01-01",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 4,
analytics: ["Альтернатива Плюс"]
},
{
period: "2020-01-01",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 7,
analytics: ["Альтернатива Плюс"]
},
{
period: "2024-11-30",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 11,
analytics: ["Альтернатива Плюс"]
}
],
{
userMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
counterpartyHint: "Альтернатива Плюс"
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Альтернатива Плюс");
expect(reply.text).toContain("Первая подтвержденная активность");
expect(reply.text).toContain("Последняя подтвержденная активность");
expect(reply.text).toContain("не дата регистрации юрлица");
});
it("composes organization activity-age answer when company name is the selected organization scope", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2018-03-12",
registrator: "CP_CUSTOMER_ACTIVITY_FIRST",
account_dt: "",
account_kt: "",
amount: 3,
analytics: ["Торговый дом Союз"]
},
{
period: "2018-01-01",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 3,
analytics: ["Торговый дом Союз"]
},
{
period: "2024-12-20",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 9,
analytics: ["МебельТорг"]
}
],
{
userMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
counterpartyHint: "Альтернатива Плюс",
organizationHint: 'ООО "Альтернатива Плюс"'
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain('По активности организации ООО "Альтернатива Плюс"');
expect(reply.text).toContain("Первая подтвержденная активность");
expect(reply.text).toContain("Последняя подтвержденная активность");
expect(reply.text).toContain("не дата регистрации юрлица");
});
it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => {
const result = runAddressDecomposeStage("а теперь только за май 2020", {
previous_intent: "list_documents_by_counterparty",
@@ -285,6 +285,117 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.followup_context_detected).toBe(true);
});
it("blocks organization age lookup from re-entering inventory lane on follow-up context", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \"Альтернатива Плюс\"",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "item",
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
},
llmPreDecomposeMeta: {
applied: true,
reason: "normalized_fragment_applied",
predecomposeContract: buildAddressLlmPredecomposeContractV1({
sourceMessage: "а какой возраст у Альтернативы Плюс?",
canonicalMessage: "Какой возраст у контрагента Альтернатива Плюс?",
mode: "address_query",
modeConfidence: "high",
intent: "unknown",
intentConfidence: "low",
entities: {
counterparty: "Альтернатива Плюс"
}
}),
semanticExtractionContract: buildAddressSemanticExtractionContractV1({
valid: true,
applyCanonicalRecommended: true
}),
llmCanonicalCandidateDetected: true
} as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
});
it("keeps organization activity-age lookup in address lane when the question is about 1C evidence", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
effectiveAddressUserMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Альтернатива Плюс"
},
llmPreDecomposeMeta: null,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "followup_context_detected"]).toContain(
String(decision.toolGateReason)
);
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps inventory root restatement after company selection in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "тогда покажи остатки на март 2021",
effectiveAddressUserMessage: "тогда покажи остатки на март 2021",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Альтернатива Плюс",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
current_frame_kind: "inventory_root",
root_context_only: true
},
llmPreDecomposeMeta: null,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(["followup_context_detected", "address_mode_classifier_detected"]).toContain(String(decision.toolGateReason));
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps VAT payable forecast query in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
@@ -62,6 +62,8 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
: null,
isMetaFollowupOverGroundedAnswer: () => false,
hasDataRetrievalRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
hasAggregateBusinessAnalyticsSignal: () => false,
hasStandaloneAddressTopicSignal: () => false,
hasOpenContractsAddressSignal: () => false,
@@ -187,6 +189,43 @@ describe("assistantRoutePolicy", () => {
expect(decision.livingReason).toBe("memory_recap_followup_detected");
});
it("routes organization fact lookup away from address lane even with follow-up context", () => {
const policy = buildPolicy({
hasDataRetrievalRequestSignal: () => true,
hasStrongDataIntentSignal: () => true,
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "low" }),
hasOrganizationFactLookupSignal: (text: unknown) =>
/возраст.*альтернатива плюс/i.test(String(text ?? "")),
resolveAddressToolGateDecision: () => ({
runAddressLane: true,
decision: "run_address_lane",
reason: "address_mode_classifier_detected"
})
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \"Альтернатива Плюс\"",
as_of_date: "2021-03-31"
},
previous_anchor_type: "item",
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
},
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
});
it("routes explicit recap wording with selected-object phrasing to chat even when address-like cues exist", () => {
const policy = buildPolicy({
hasStrongDataIntentSignal: () => true,