АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user