Semantic Gate: закрепить бизнес-аудит и executive summary
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
|
||||
buildBroadBusinessEvaluationReply as buildBroadBusinessEvaluationReplyFromPolicy,
|
||||
buildConversationExecutiveSummaryReply as buildConversationExecutiveSummaryReplyFromPolicy,
|
||||
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
|
||||
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
|
||||
resolveAssistantLivingChatMemoryContext
|
||||
@@ -81,6 +82,17 @@ function buildDeterministicSmalltalkLeadReply(): string {
|
||||
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
|
||||
}
|
||||
|
||||
function hasConversationExecutiveSummarySignal(value: unknown): boolean {
|
||||
const normalized = String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/\u0451/gu, "\u0435")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return /(?:executive\s+summary|финальн\w*\s+собери|итогов\w*\s+(?:резюм|summary|вывод)|по\s+всему\s+диалогу|где\s+ответы\s+были\s+подтвержден|где\s+proxy|где\s+прокси|не\s+хватил\w*\s+доказательств|ручн\w*\s+(?:смотр|провер|контрол))/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
@@ -246,15 +258,25 @@ export async function runAssistantLivingChatRuntime(
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_inventory_history_capability_contract";
|
||||
} else if (contextualMemoryRecapFollowup) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = buildAddressMemoryRecapReplyFromPolicy({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: lastMemoryAddressDebug,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
|
||||
const executiveSummaryFollowup = hasConversationExecutiveSummarySignal(userMessage);
|
||||
chatText = executiveSummaryFollowup
|
||||
? buildConversationExecutiveSummaryReplyFromPolicy({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: lastMemoryAddressDebug,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
})
|
||||
: buildAddressMemoryRecapReplyFromPolicy({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: lastMemoryAddressDebug,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_memory_recap_contract";
|
||||
livingChatSource = executiveSummaryFollowup
|
||||
? "deterministic_conversation_executive_summary_contract"
|
||||
: "deterministic_memory_recap_contract";
|
||||
} else if (contextualAnswerInspectionFollowup) {
|
||||
chatText = buildSelectedObjectAnswerInspectionReplyFromPolicy({
|
||||
addressDebug: lastAnswerInspectionAddressDebug,
|
||||
|
||||
@@ -185,6 +185,10 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasConversationExecutiveSummarySignal(sample) {
|
||||
return /(?:executive\s+summary|финальн\w*\s+собери|итогов\w*\s+(?:резюм|summary|вывод)|по\s+всему\s+диалогу|где\s+ответы\s+были\s+подтвержден|где\s+proxy|где\s+прокси|не\s+хватил\w*\s+доказательств|ручн\w*\s+(?:смотр|провер|контрол))/iu.test(sample);
|
||||
}
|
||||
|
||||
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||
@@ -196,7 +200,8 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
|
||||
}
|
||||
const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample));
|
||||
const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample));
|
||||
const hasExplicitRecapPrompt = samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|напомни\s+что\s+мы|what\s+we\s+already\s+(?:discussed|figured\s+out))/iu.test(sample));
|
||||
const hasExplicitRecapPrompt = samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|напомни\s+что\s+мы|what\s+we\s+already\s+(?:discussed|figured\s+out))/iu.test(sample) ||
|
||||
hasConversationExecutiveSummarySignal(sample));
|
||||
if (!(hasExplicitRecapPrompt || (hasMemoryCue && hasDiscussionCue))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ function hasSignalAcrossSamples(
|
||||
|
||||
function hasExplicitRecapPromptSignal(samples: string[]): boolean {
|
||||
return samples.some((sample) =>
|
||||
/(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|что\s+уже\s+поняли|напомни\s+что\s+мы)/iu.test(
|
||||
/(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|что\s+уже\s+поняли|напомни\s+что\s+мы|executive\s+summary|финальн\w*\s+собери|итогов\w*\s+(?:резюм|summary|вывод)|по\s+всему\s+диалогу|где\s+ответы\s+были\s+подтвержден|где\s+proxy|где\s+прокси|не\s+хватил\w*\s+доказательств|ручн\w*\s+(?:смотр|провер|контрол))/iu.test(
|
||||
sample
|
||||
)
|
||||
);
|
||||
@@ -268,6 +268,41 @@ function normalizeRecapIdentity(value: unknown): string {
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function isLowQualityRecapCounterparty(value: string | null): boolean {
|
||||
const normalized = normalizeRecapIdentity(value);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const stopwordOnlyCounterparty = new Set([
|
||||
"без",
|
||||
"в",
|
||||
"во",
|
||||
"для",
|
||||
"до",
|
||||
"за",
|
||||
"из",
|
||||
"к",
|
||||
"ко",
|
||||
"на",
|
||||
"от",
|
||||
"по",
|
||||
"с",
|
||||
"со",
|
||||
"у"
|
||||
]);
|
||||
if (stopwordOnlyCounterparty.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/^(?:и\s+)?(?:кто|что|где|какой|какие)\b/iu.test(normalized) ||
|
||||
/(?:главн|основн|крупн|поставщик|клиент|контрагент|покупател|документ|движени|операци)/iu.test(normalized) &&
|
||||
!/(?<!\p{L})(?:ооо|ип|ао|пао|зао|llc|inc|corp)(?!\p{L})/iu.test(normalized)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return /(?:\s+в|\s+по|\s+для)$/iu.test(normalized);
|
||||
}
|
||||
|
||||
function buildRecapFactLine(input: {
|
||||
debug: Record<string, unknown> | null;
|
||||
item: string | null;
|
||||
@@ -276,9 +311,10 @@ function buildRecapFactLine(input: {
|
||||
}): string | null {
|
||||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||||
const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate;
|
||||
const counterparty = isLowQualityRecapCounterparty(input.counterparty) ? null : input.counterparty;
|
||||
const discoveryFact = buildDiscoveryRecapFactLine({
|
||||
debug: input.debug,
|
||||
counterparty: input.counterparty,
|
||||
counterparty,
|
||||
organization: input.organization,
|
||||
scopedDate
|
||||
});
|
||||
@@ -286,7 +322,7 @@ function buildRecapFactLine(input: {
|
||||
return discoveryFact;
|
||||
}
|
||||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||||
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
|
||||
const counterpartyPart = counterparty ? `по контрагенту «${counterparty}»` : null;
|
||||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||
@@ -673,7 +709,7 @@ export function buildBroadBusinessEvaluationReply(input: {
|
||||
: "- Прибыль, маржа и качество операционки пока не доказаны: нужны расходы, себестоимость и задолженность."
|
||||
];
|
||||
return [
|
||||
`Коротко: по уже подтвержденным срезам 1С${organizationPart} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
|
||||
`Коротко: по уже подтвержденным срезам 1С${organizationPart} бизнес выглядит операционно живым; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
|
||||
"Что уже видно:",
|
||||
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
|
||||
...(businessEvidence.confirmedLines.length > 0
|
||||
@@ -692,6 +728,84 @@ export function buildBroadBusinessEvaluationReply(input: {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export function buildConversationExecutiveSummaryReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
sessionItems?: unknown[];
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
}): string {
|
||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||
const organization = input.organization ?? contextFacts.organization;
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const recapFacts = collectRecentRecapFacts({
|
||||
sessionItems: input.sessionItems,
|
||||
item: null,
|
||||
organization,
|
||||
toNonEmptyString: input.toNonEmptyString,
|
||||
limit: 8
|
||||
});
|
||||
const businessEvidence = collectBusinessEvaluationEvidence({
|
||||
sessionItems: input.sessionItems,
|
||||
organization,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const confirmedLines: string[] = [];
|
||||
for (const fact of recapFacts) {
|
||||
pushBusinessLine(confirmedLines, ensureSentence(fact));
|
||||
}
|
||||
for (const fact of businessEvidence.confirmedLines) {
|
||||
pushBusinessLine(confirmedLines, ensureSentence(fact));
|
||||
}
|
||||
const proxyLines: string[] = [];
|
||||
for (const line of businessEvidence.interpretationLines) {
|
||||
pushBusinessLine(proxyLines, ensureSentence(line));
|
||||
}
|
||||
if (businessEvidence.hasNet) {
|
||||
pushBusinessLine(
|
||||
proxyLines,
|
||||
"Нетто по деньгам можно использовать как cash-flow proxy, но это не бухгалтерская прибыль и не маржа."
|
||||
);
|
||||
}
|
||||
if (businessEvidence.hasRanking) {
|
||||
pushBusinessLine(
|
||||
proxyLines,
|
||||
"Крупнейшие клиенты/контрагенты видны как операционный сигнал концентрации, но это не полноценный CRM-аудит."
|
||||
);
|
||||
}
|
||||
const missingLines = [
|
||||
"Чистая прибыль, финрезультат и маржа не доказаны без отдельной проверки себестоимости, расходов и закрытия периода.",
|
||||
"Просрочка, качество долга и due-date aging не доказаны без сроков оплаты и отдельного долгового контура.",
|
||||
"Ликвидность склада, резервы, списания и устаревание нельзя считать подтвержденными без специальных складских доказательств.",
|
||||
"Vendor-risk, качество закупок и юридическая надежность контрагентов остаются вне подтвержденного контура."
|
||||
];
|
||||
const manualLines = [
|
||||
"Сверить ОСВ/финрезультат и управленческие расходы за ключевые годы.",
|
||||
"Отдельно проверить старые открытые расчеты, крупные долги и документы закрытия.",
|
||||
"Посмотреть концентрацию клиентов и поставщиков не только по сумме, но и по доле в периоде.",
|
||||
"Сверить НДС, склад и договоры там, где ответ опирался на proxy, а не на прямой учетный факт."
|
||||
];
|
||||
const confirmedSection =
|
||||
confirmedLines.length > 0
|
||||
? confirmedLines.slice(0, 10).map((line) => `- ${line}`)
|
||||
: ["- Есть grounded-контекст диалога, но для строгого executive summary не хватает подтвержденных метрик в текущем окне."];
|
||||
const proxySection =
|
||||
proxyLines.length > 0
|
||||
? proxyLines.slice(0, 6).map((line) => `- ${line}`)
|
||||
: ["- Proxy-выводы пока слабые: можно говорить только о направлении проверки, не о зрелой оценке бизнеса."];
|
||||
|
||||
return [
|
||||
`Executive summary${organizationPart}: по диалогу уже можно собрать рабочую карту подтвержденного, proxy и ручного контроля, но не стоит выдавать это за полный аудит компании.`,
|
||||
"Подтверждено по данным/ответам 1С:",
|
||||
...confirmedSection,
|
||||
"Proxy и осторожная аналитика:",
|
||||
...proxySection,
|
||||
"Где не хватило доказательств:",
|
||||
...missingLines.map((line) => `- ${line}`),
|
||||
"Что директору смотреть руками в первую очередь:",
|
||||
...manualLines.map((line) => `- ${line}`)
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildSelectedObjectAnswerInspectionReply(input: {
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
|
||||
@@ -662,6 +662,14 @@ export function createAssistantRoutePolicy(deps) {
|
||||
hasOrganizationFactFollowupSignal(repairedRawUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(effectiveAddressUserMessage, sessionItems ?? []) ||
|
||||
hasOrganizationFactFollowupSignal(repairedEffectiveAddressUserMessage, sessionItems ?? []);
|
||||
const broadBusinessMeaningBoundary = Boolean(
|
||||
assistantTurnMeaning?.unsupported_but_understood_family === "broad_business_evaluation" &&
|
||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||
!turnMeaningIntentCandidate &&
|
||||
!dataScopeMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
@@ -697,6 +705,41 @@ export function createAssistantRoutePolicy(deps) {
|
||||
};
|
||||
}
|
||||
if (hardMetaMode === "capability") {
|
||||
if (broadBusinessMeaningBoundary) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "unsupported_current_turn_meaning_boundary",
|
||||
livingMode: "chat",
|
||||
livingReason: "unsupported_current_turn_meaning_boundary",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: null,
|
||||
provider_execution: providerExecution,
|
||||
assistant_turn_meaning: assistantTurnMeaning,
|
||||
address_mode: resolvedModeDetection.mode,
|
||||
address_mode_confidence: resolvedModeDetection.confidence,
|
||||
address_intent: resolvedIntentResolution.intent,
|
||||
address_intent_confidence: resolvedIntentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext),
|
||||
unsupported_current_turn_meaning_boundary: true,
|
||||
unsupported_current_turn_family: assistantTurnMeaning.unsupported_but_understood_family,
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "unsupported_current_turn_meaning_boundary",
|
||||
living_mode: "chat",
|
||||
living_reason: "unsupported_current_turn_meaning_boundary"
|
||||
},
|
||||
reason_codes: [
|
||||
"business_overview_meaning_overrides_capability_meta_noise"
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
if (contextualHistoricalCapabilityFollowupDetected) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
@@ -784,7 +827,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||
};
|
||||
}
|
||||
const unsupportedCurrentTurnMeaningBoundary = Boolean(
|
||||
assistantTurnMeaning?.unsupported_but_understood_family &&
|
||||
(broadBusinessMeaningBoundary || assistantTurnMeaning?.unsupported_but_understood_family) &&
|
||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||
!turnMeaningIntentCandidate &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
|
||||
Reference in New Issue
Block a user