АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия
This commit is contained in:
@@ -2696,6 +2696,24 @@ describe("address decompose stage follow-up carryover", () => {
|
||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("inherits organization scope from follow-up context when organization is omitted in user text", () => {
|
||||
const result = runAddressDecomposeStage("покажи документы по свк за 2020", {
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "свк",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "свк"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
|
||||
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.baseReasons).toContain("organization_from_followup_context");
|
||||
});
|
||||
|
||||
it("inherits as_of_date from previous period for same-date balance follow-up", () => {
|
||||
const result = runAddressDecomposeStage("а по счету 60.01 на ту же дату", {
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
|
||||
@@ -672,4 +672,61 @@ describe("assistant address follow-up carryover", () => {
|
||||
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it("passes active organization scope into address lane follow-up context", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
return buildAddressLaneResult();
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-org-scope-${Date.now()}`;
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-seed-org",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "Data scope organizations are available.",
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: "chat-org-seed",
|
||||
debug: {
|
||||
trace_id: "chat-org-seed",
|
||||
living_chat_data_scope_probe_status: "resolved",
|
||||
living_chat_data_scope_probe_organizations: ["Alternative Plus LLC", "Lacewood LLC", "RIME"],
|
||||
assistant_active_organization: "Alternative Plus LLC"
|
||||
}
|
||||
} as any);
|
||||
|
||||
const response = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: "show docs by svk for 2020",
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual");
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
const scopedCall = calls.find((entry) => Boolean(entry.options?.followupContext?.previous_filters?.organization));
|
||||
expect(scopedCall).toBeTruthy();
|
||||
expect(scopedCall?.options?.followupContext?.previous_filters?.organization).toBe("Alternative Plus LLC");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,201 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AssistantService } from "../src/services/assistantService";
|
||||
import { AssistantService, resolveSessionOrganizationScopeContextForTests } from "../src/services/assistantService";
|
||||
import { AssistantSessionStore } from "../src/services/assistantSessionStore";
|
||||
|
||||
describe("assistant living chat mode", () => {
|
||||
it("resolves active organization from slang mention using previously discovered organization list", () => {
|
||||
const items = [
|
||||
{
|
||||
role: "assistant",
|
||||
text: "Сейчас в активном MCP-канале `default` доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ.",
|
||||
debug: {
|
||||
trace_id: "chat-org-scope",
|
||||
living_chat_data_scope_probe_status: "resolved",
|
||||
living_chat_data_scope_probe_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]
|
||||
}
|
||||
} as any
|
||||
];
|
||||
|
||||
const context = resolveSessionOrganizationScopeContextForTests("га альтернативу тогда обсудим", items as any);
|
||||
expect(context.knownOrganizations).toContain("ООО Альтернатива Плюс");
|
||||
expect(context.selectedOrganization).toBe("ООО Альтернатива Плюс");
|
||||
expect(context.activeOrganization).toBe("ООО Альтернатива Плюс");
|
||||
});
|
||||
|
||||
it("repairs mojibake user text before persisting conversation", async () => {
|
||||
const normalizer = {
|
||||
normalize: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
trace_id: "norm-mojibake-user",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
normalized: null,
|
||||
validation: { passed: false, errors: ["mock"] },
|
||||
route_hint_summary: null,
|
||||
raw_model_output: {},
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
||||
latency_ms: 1,
|
||||
request_count_for_case: 1
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn().mockResolvedValue({ handled: false })
|
||||
} as any;
|
||||
const chatClient = {
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
raw: { id: "chat-mojibake-user" },
|
||||
outputText: "Привет. Я на связи.",
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
||||
})
|
||||
} as any;
|
||||
|
||||
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
|
||||
const response = await service.handleMessage({
|
||||
session_id: "asst-living-chat-mojibake-user",
|
||||
user_message: "че там",
|
||||
llmProvider: "local",
|
||||
model: "qwen2.5",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual_with_explanation");
|
||||
expect(response.conversation?.[0]?.role).toBe("user");
|
||||
expect(response.conversation?.[0]?.text).toBe("че там");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not treat organization fact lookup as scope-selection confirmation", async () => {
|
||||
const normalizer = {
|
||||
normalize: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
trace_id: "norm-org-fact-boundary",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
normalized: null,
|
||||
validation: { passed: false, errors: ["mock"] },
|
||||
route_hint_summary: null,
|
||||
raw_model_output: {},
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
||||
latency_ms: 1,
|
||||
request_count_for_case: 1
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const sessionId = "asst-living-chat-org-fact-boundary";
|
||||
sessions.ensureSession(sessionId);
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-seed-org-scope",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "Сейчас в активном MCP-канале `default` доступны организации (3): ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ.",
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: "chat-seed-org-scope",
|
||||
debug: {
|
||||
living_chat_data_scope_probe_status: "resolved",
|
||||
living_chat_data_scope_probe_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
|
||||
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
|
||||
assistant_active_organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
} as any);
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn().mockResolvedValue({ handled: false })
|
||||
} as any;
|
||||
const chatClient = {
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
raw: { id: "chat-should-not-run-org-fact-boundary" },
|
||||
outputText: "Для ООО Альтернатива Плюс дата регистрации 01.07.2015, возраст 8 лет.",
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
||||
})
|
||||
} as any;
|
||||
|
||||
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
|
||||
const response = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: "какой возраст у альтернативы?",
|
||||
llmProvider: "local",
|
||||
model: "qwen2.5",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual_with_explanation");
|
||||
expect(response.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary");
|
||||
expect(String(response.assistant_reply).toLowerCase()).toContain("не буду называть");
|
||||
expect(String(response.assistant_reply)).not.toContain("01.07.2015");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("keeps organization fact follow-up in deterministic boundary mode on short confirmation", async () => {
|
||||
const normalizer = {
|
||||
normalize: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
trace_id: "norm-org-fact-followup",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
normalized: null,
|
||||
validation: { passed: false, errors: ["mock"] },
|
||||
route_hint_summary: null,
|
||||
raw_model_output: {},
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
||||
latency_ms: 1,
|
||||
request_count_for_case: 1
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const sessionId = "asst-living-chat-org-fact-followup";
|
||||
sessions.ensureSession(sessionId);
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-seed-org-fact-boundary",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "По организации ООО Альтернатива Плюс не буду называть дату/возраст без live-подтвержденного источника.",
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: "chat-seed-org-fact-boundary",
|
||||
debug: {
|
||||
living_chat_response_source: "deterministic_organization_fact_boundary",
|
||||
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
|
||||
assistant_active_organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
} as any);
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn().mockResolvedValue({ handled: false })
|
||||
} as any;
|
||||
const chatClient = {
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
raw: { id: "chat-should-not-run-org-fact-followup" },
|
||||
outputText: "Дата регистрации: 2005-12-08",
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
||||
})
|
||||
} as any;
|
||||
|
||||
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
|
||||
const response = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: "давай",
|
||||
llmProvider: "local",
|
||||
model: "qwen2.5",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual_with_explanation");
|
||||
expect(response.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary_followup");
|
||||
expect(String(response.assistant_reply).toLowerCase()).toContain("не буду называть");
|
||||
expect(String(response.assistant_reply)).not.toContain("2005-12-08");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("handles casual greeting in chat mode without deep-pipeline pass", async () => {
|
||||
const normalizer = {
|
||||
normalize: vi.fn().mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user