Запустить Open-World Breadth через бизнес-обзор компании

This commit is contained in:
2026-05-02 00:09:28 +03:00
parent 6df2018086
commit 284b201912
9 changed files with 477 additions and 17 deletions
@@ -291,6 +291,136 @@ function collectRecentRecapFacts(input) {
}
return facts.reverse();
}
function parseHumanMoney(value) {
const text = String(value ?? "")
.replace(/[^\d,.\-]/g, "")
.replace(/\s+/g, "")
.replace(",", ".");
if (!text) {
return null;
}
const parsed = Number(text);
return Number.isFinite(parsed) ? parsed : null;
}
function pushBusinessLine(target, value) {
const text = String(value ?? "").trim();
if (text && !target.includes(text)) {
target.push(text);
}
}
function collectBusinessEvaluationEvidence(input) {
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
const currentOrganizationKey = normalizeRecapIdentity(input.organization);
const confirmedLines = [];
const interpretationLines = [];
let hasRanking = false;
let hasNet = false;
let moneySignalCount = 0;
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
const item = sessionItems[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
if (!(0, assistantContinuityPolicy_1.isGroundedAddressDebug)(item.debug, input.toNonEmptyString)) {
continue;
}
const debugContext = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(item.debug, input.toNonEmptyString);
const debugOrganizationKey = normalizeRecapIdentity(debugContext.organization);
if (currentOrganizationKey && debugOrganizationKey && debugOrganizationKey !== currentOrganizationKey) {
continue;
}
const discoveryEntry = toRecordObject(item.debug.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(item.debug, input.toNonEmptyString);
if (!pilot) {
continue;
}
const rankedFlow = toRecordObject(pilot.derived_ranked_value_flow);
if (rankedFlow) {
const rankedValues = Array.isArray(rankedFlow.ranked_values) ? rankedFlow.ranked_values : [];
const leader = toRecordObject(rankedValues[0]);
const leaderName = input.toNonEmptyString(leader?.axis_value);
const leaderAmount = input.toNonEmptyString(leader?.total_amount_human_ru);
const period = input.toNonEmptyString(rankedFlow.period_scope);
const periodPart = period ? ` за ${period}` : "";
if (leaderName && leaderAmount) {
pushBusinessLine(confirmedLines, `Топ-контрагент по подтвержденному денежному срезу${periodPart}: ${leaderName} - ${leaderAmount}.`);
hasRanking = true;
moneySignalCount += 1;
}
}
const bidirectionalFlow = toRecordObject(pilot.derived_bidirectional_value_flow);
if (bidirectionalFlow) {
const incoming = toRecordObject(bidirectionalFlow.incoming_customer_revenue);
const outgoing = toRecordObject(bidirectionalFlow.outgoing_supplier_payout);
const incomingAmount = input.toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = input.toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = input.toNonEmptyString(bidirectionalFlow.net_amount_human_ru);
const period = input.toNonEmptyString(bidirectionalFlow.period_scope);
const periodPart = period ? ` за ${period}` : "";
if (incomingAmount && outgoingAmount && netAmount) {
pushBusinessLine(confirmedLines, `Денежный поток${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}.`);
const incomingNumber = parseHumanMoney(incomingAmount);
const outgoingNumber = parseHumanMoney(outgoingAmount);
const netNumber = parseHumanMoney(netAmount);
if (incomingNumber && outgoingNumber !== null && netNumber !== null) {
const spreadPercent = Math.abs(netNumber) / Math.max(Math.abs(incomingNumber), 1);
const spreadLabel = `${Math.round(spreadPercent * 100)}%`;
if (spreadPercent < 0.1) {
pushBusinessLine(interpretationLines, `Обороты есть, но денежный спред узкий: нетто около ${spreadLabel} от входящего потока. Это не прибыль, но сигнал, что маржу надо проверять отдельно.`);
}
else if (netNumber > 0) {
pushBusinessLine(interpretationLines, `Денежный поток в проверенном срезе положительный: нетто около ${spreadLabel} от входящего потока. Это хороший cash-flow сигнал, но не доказанная прибыль.`);
}
else {
pushBusinessLine(interpretationLines, `Денежный поток в проверенном срезе отрицательный: исходящие платежи выше входящих примерно на ${spreadLabel} от входящего потока. Нужна проверка причин и структуры расходов.`);
}
}
hasNet = true;
moneySignalCount += 1;
}
}
const valueFlow = toRecordObject(pilot.derived_value_flow);
if (valueFlow) {
const amount = input.toNonEmptyString(valueFlow.total_amount_human_ru);
const period = input.toNonEmptyString(valueFlow.period_scope);
const direction = String(valueFlow.value_flow_direction ?? "");
const periodPart = period ? ` за ${period}` : "";
if (amount) {
pushBusinessLine(confirmedLines, direction === "outgoing_supplier_payout"
? `Исходящий денежный поток${periodPart}: ${amount}.`
: `Входящий денежный поток${periodPart}: ${amount}.`);
moneySignalCount += 1;
}
}
const activityPeriod = toRecordObject(pilot.derived_activity_period);
if (pilotScope === "counterparty_lifecycle_query_documents_v1" && activityPeriod) {
const duration = input.toNonEmptyString(activityPeriod.duration_human_ru);
const first = input.toNonEmptyString(activityPeriod.first_activity_date);
const latest = input.toNonEmptyString(activityPeriod.latest_activity_date);
if (duration) {
pushBusinessLine(confirmedLines, `Подтвержденная активность в 1С: примерно ${duration}${first && latest ? ` (${first} - ${latest})` : ""}.`);
}
}
if (pilotScope === "inventory_route_template_v1") {
pushBusinessLine(confirmedLines, "Есть проверенный складской/товарный срез; его можно использовать как операционный контекст, но не как финансовую прибыль.");
}
}
if (hasRanking) {
pushBusinessLine(interpretationLines, "По клиентской базе уже виден лидер, но концентрацию выручки надо проверять отдельным рейтингом и долями, а не одной строкой.");
}
if (moneySignalCount > 0 && !hasNet) {
pushBusinessLine(interpretationLines, "Денежный контур частично подтвержден, но без входящие-вс-исходящие нельзя честно говорить о чистом денежном эффекте.");
}
return {
confirmedLines: confirmedLines.slice(0, 6),
interpretationLines: interpretationLines.slice(0, 5),
hasRanking,
hasNet,
moneySignalCount
};
}
function buildAddressMemoryRecapReply(input) {
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
const item = contextFacts.item;
@@ -363,10 +493,17 @@ function buildBroadBusinessEvaluationReply(input) {
limit: 5
});
const organizationPart = organization ? ` по компании «${organization}»` : "";
if (recapFacts.length > 0) {
const moneyFactCount = recapFacts.filter((fact) => /(?:денежн|нетто|поступлен|платеж|рейтинг|клиент|выруч|оборот|заплатили|получили)/iu.test(fact)).length;
const hasRankingFact = recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
const hasNetFact = recapFacts.some((fact) => /нетто/iu.test(fact));
const businessEvidence = collectBusinessEvaluationEvidence({
sessionItems: input.sessionItems,
organization,
toNonEmptyString: input.toNonEmptyString
});
const hasBusinessEvidence = businessEvidence.confirmedLines.length > 0;
if (recapFacts.length > 0 || hasBusinessEvidence) {
const moneyFactCount = recapFacts.filter((fact) => /(?:денежн|нетто|поступлен|платеж|рейтинг|клиент|выруч|оборот|заплатили|получили)/iu.test(fact)).length + businessEvidence.moneySignalCount;
const hasRankingFact = businessEvidence.hasRanking ||
recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
const hasNetFact = businessEvidence.hasNet || recapFacts.some((fact) => /нетто/iu.test(fact));
const auditLines = [
moneyFactCount > 0
? "- Денежный контур уже выглядит операционно значимым: есть подтвержденные поступления, платежи или клиентские срезы."
@@ -382,7 +519,11 @@ function buildBroadBusinessEvaluationReply(input) {
`Коротко: по уже подтвержденным срезам 1С${organizationPart} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
"Что уже видно:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
...(businessEvidence.confirmedLines.length > 0
? ["Подтвержденные метрики:", ...businessEvidence.confirmedLines.map((fact) => `- ${ensureSentence(fact)}`)]
: []),
"Предварительный LLM-аудит:",
...businessEvidence.interpretationLines.map((fact) => `- ${ensureSentence(fact)}`),
...auditLines,
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
].join("\n");
@@ -117,7 +117,7 @@ function detectBroadBusinessEvaluation(text) {
if (!normalized) {
return null;
}
if (/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(normalized)) {
if (/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|оцени\s+(?:компан|бизнес|деятельност)|(?:полный|сводный|нормальн\w*|взросл\w*)\s+анализ\s+(?:компан|бизнес|деятельност)|проанализируй\s+(?:компан|бизнес|деятельност)|(?:что\s+думаешь|какое\s+мнение)\s+(?:о|по)\s+(?:компан|бизнес)|(?:llm[-\s]?)?аудит\s+(?:компан|бизнес)|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(normalized)) {
return {
family: "broad_business_evaluation"
};
@@ -378,6 +378,186 @@ function collectRecentRecapFacts(input: {
return facts.reverse();
}
function parseHumanMoney(value: unknown): number | null {
const text = String(value ?? "")
.replace(/[^\d,.\-]/g, "")
.replace(/\s+/g, "")
.replace(",", ".");
if (!text) {
return null;
}
const parsed = Number(text);
return Number.isFinite(parsed) ? parsed : null;
}
function pushBusinessLine(target: string[], value: string | null): void {
const text = String(value ?? "").trim();
if (text && !target.includes(text)) {
target.push(text);
}
}
function collectBusinessEvaluationEvidence(input: {
sessionItems?: unknown[];
organization: string | null;
toNonEmptyString: (value: unknown) => string | null;
}): {
confirmedLines: string[];
interpretationLines: string[];
hasRanking: boolean;
hasNet: boolean;
moneySignalCount: number;
} {
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
const currentOrganizationKey = normalizeRecapIdentity(input.organization);
const confirmedLines: string[] = [];
const interpretationLines: string[] = [];
let hasRanking = false;
let hasNet = false;
let moneySignalCount = 0;
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
const item = sessionItems[index] as { role?: string; debug?: Record<string, unknown> } | null;
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
if (!isGroundedAddressDebug(item.debug, input.toNonEmptyString)) {
continue;
}
const debugContext = resolveAddressDebugContextFacts(item.debug, input.toNonEmptyString);
const debugOrganizationKey = normalizeRecapIdentity(debugContext.organization);
if (currentOrganizationKey && debugOrganizationKey && debugOrganizationKey !== currentOrganizationKey) {
continue;
}
const discoveryEntry = toRecordObject(item.debug.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const pilotScope = readAssistantMcpDiscoveryPilotScope(item.debug, input.toNonEmptyString);
if (!pilot) {
continue;
}
const rankedFlow = toRecordObject(pilot.derived_ranked_value_flow);
if (rankedFlow) {
const rankedValues = Array.isArray(rankedFlow.ranked_values) ? rankedFlow.ranked_values : [];
const leader = toRecordObject(rankedValues[0]);
const leaderName = input.toNonEmptyString(leader?.axis_value);
const leaderAmount = input.toNonEmptyString(leader?.total_amount_human_ru);
const period = input.toNonEmptyString(rankedFlow.period_scope);
const periodPart = period ? ` за ${period}` : "";
if (leaderName && leaderAmount) {
pushBusinessLine(
confirmedLines,
`Топ-контрагент по подтвержденному денежному срезу${periodPart}: ${leaderName} - ${leaderAmount}.`
);
hasRanking = true;
moneySignalCount += 1;
}
}
const bidirectionalFlow = toRecordObject(pilot.derived_bidirectional_value_flow);
if (bidirectionalFlow) {
const incoming = toRecordObject(bidirectionalFlow.incoming_customer_revenue);
const outgoing = toRecordObject(bidirectionalFlow.outgoing_supplier_payout);
const incomingAmount = input.toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = input.toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = input.toNonEmptyString(bidirectionalFlow.net_amount_human_ru);
const period = input.toNonEmptyString(bidirectionalFlow.period_scope);
const periodPart = period ? ` за ${period}` : "";
if (incomingAmount && outgoingAmount && netAmount) {
pushBusinessLine(
confirmedLines,
`Денежный поток${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}.`
);
const incomingNumber = parseHumanMoney(incomingAmount);
const outgoingNumber = parseHumanMoney(outgoingAmount);
const netNumber = parseHumanMoney(netAmount);
if (incomingNumber && outgoingNumber !== null && netNumber !== null) {
const spreadPercent = Math.abs(netNumber) / Math.max(Math.abs(incomingNumber), 1);
const spreadLabel = `${Math.round(spreadPercent * 100)}%`;
if (spreadPercent < 0.1) {
pushBusinessLine(
interpretationLines,
`Обороты есть, но денежный спред узкий: нетто около ${spreadLabel} от входящего потока. Это не прибыль, но сигнал, что маржу надо проверять отдельно.`
);
} else if (netNumber > 0) {
pushBusinessLine(
interpretationLines,
`Денежный поток в проверенном срезе положительный: нетто около ${spreadLabel} от входящего потока. Это хороший cash-flow сигнал, но не доказанная прибыль.`
);
} else {
pushBusinessLine(
interpretationLines,
`Денежный поток в проверенном срезе отрицательный: исходящие платежи выше входящих примерно на ${spreadLabel} от входящего потока. Нужна проверка причин и структуры расходов.`
);
}
}
hasNet = true;
moneySignalCount += 1;
}
}
const valueFlow = toRecordObject(pilot.derived_value_flow);
if (valueFlow) {
const amount = input.toNonEmptyString(valueFlow.total_amount_human_ru);
const period = input.toNonEmptyString(valueFlow.period_scope);
const direction = String(valueFlow.value_flow_direction ?? "");
const periodPart = period ? ` за ${period}` : "";
if (amount) {
pushBusinessLine(
confirmedLines,
direction === "outgoing_supplier_payout"
? `Исходящий денежный поток${periodPart}: ${amount}.`
: `Входящий денежный поток${periodPart}: ${amount}.`
);
moneySignalCount += 1;
}
}
const activityPeriod = toRecordObject(pilot.derived_activity_period);
if (pilotScope === "counterparty_lifecycle_query_documents_v1" && activityPeriod) {
const duration = input.toNonEmptyString(activityPeriod.duration_human_ru);
const first = input.toNonEmptyString(activityPeriod.first_activity_date);
const latest = input.toNonEmptyString(activityPeriod.latest_activity_date);
if (duration) {
pushBusinessLine(
confirmedLines,
`Подтвержденная активность в 1С: примерно ${duration}${first && latest ? ` (${first} - ${latest})` : ""}.`
);
}
}
if (pilotScope === "inventory_route_template_v1") {
pushBusinessLine(
confirmedLines,
"Есть проверенный складской/товарный срез; его можно использовать как операционный контекст, но не как финансовую прибыль."
);
}
}
if (hasRanking) {
pushBusinessLine(
interpretationLines,
"По клиентской базе уже виден лидер, но концентрацию выручки надо проверять отдельным рейтингом и долями, а не одной строкой."
);
}
if (moneySignalCount > 0 && !hasNet) {
pushBusinessLine(
interpretationLines,
"Денежный контур частично подтвержден, но без входящие-вс-исходящие нельзя честно говорить о чистом денежном эффекте."
);
}
return {
confirmedLines: confirmedLines.slice(0, 6),
interpretationLines: interpretationLines.slice(0, 5),
hasRanking,
hasNet,
moneySignalCount
};
}
export function buildAddressMemoryRecapReply(input: {
organization: string | null;
addressDebug: Record<string, unknown> | null;
@@ -466,13 +646,21 @@ export function buildBroadBusinessEvaluationReply(input: {
limit: 5
});
const organizationPart = organization ? ` по компании «${organization}»` : "";
const businessEvidence = collectBusinessEvaluationEvidence({
sessionItems: input.sessionItems,
organization,
toNonEmptyString: input.toNonEmptyString
});
const hasBusinessEvidence = businessEvidence.confirmedLines.length > 0;
if (recapFacts.length > 0) {
if (recapFacts.length > 0 || hasBusinessEvidence) {
const moneyFactCount = recapFacts.filter((fact) =>
/(?:денежн|нетто|поступлен|платеж|рейтинг|клиент|выруч|оборот|заплатили|получили)/iu.test(fact)
).length;
const hasRankingFact = recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
const hasNetFact = recapFacts.some((fact) => /нетто/iu.test(fact));
).length + businessEvidence.moneySignalCount;
const hasRankingFact =
businessEvidence.hasRanking ||
recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
const hasNetFact = businessEvidence.hasNet || recapFacts.some((fact) => /нетто/iu.test(fact));
const auditLines = [
moneyFactCount > 0
? "- Денежный контур уже выглядит операционно значимым: есть подтвержденные поступления, платежи или клиентские срезы."
@@ -488,7 +676,11 @@ export function buildBroadBusinessEvaluationReply(input: {
`Коротко: по уже подтвержденным срезам 1С${organizationPart} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
"Что уже видно:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
...(businessEvidence.confirmedLines.length > 0
? ["Подтвержденные метрики:", ...businessEvidence.confirmedLines.map((fact) => `- ${ensureSentence(fact)}`)]
: []),
"Предварительный LLM-аудит:",
...businessEvidence.interpretationLines.map((fact) => `- ${ensureSentence(fact)}`),
...auditLines,
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
].join("\n");
@@ -123,7 +123,7 @@ function detectBroadBusinessEvaluation(text) {
return null;
}
if (
/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(
/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|оцени\s+(?:компан|бизнес|деятельност)|(?:полный|сводный|нормальн\w*|взросл\w*)\s+анализ\s+(?:компан|бизнес|деятельност)|проанализируй\s+(?:компан|бизнес|деятельност)|(?:что\s+думаешь|какое\s+мнение)\s+(?:о|по)\s+(?:компан|бизнес)|(?:llm[-\s]?)?аудит\s+(?:компан|бизнес)|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(
normalized
)
) {
@@ -214,6 +214,9 @@ describe("assistant living chat runtime adapter", () => {
expect(output.chatText).toContain("ООО Альтернатива Плюс");
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("нетто");
expect(output.chatText).toContain("Подтвержденные метрики");
expect(output.chatText).toContain("Денежный поток");
expect(output.chatText).toContain("маржу");
expect(output.debug?.living_chat_response_source).toBe("deterministic_broad_business_evaluation_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
@@ -110,4 +110,20 @@ describe("assistantTurnMeaningPolicy", () => {
expect(meaning.stale_replay_forbidden).toBe(true);
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
it("recognizes broad company analysis wording as the same bounded business overview ask", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" })
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage: "Дай полный анализ компании и LLM-аудит бизнеса"
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("business_summary");
expect(meaning.asked_action_family).toBe("broad_evaluation");
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
});