АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия

This commit is contained in:
2026-04-09 12:34:10 +03:00
parent df29798fa2
commit edfa09c9af
31 changed files with 5261 additions and 2392 deletions
@@ -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({