Запустить Open-World Breadth через бизнес-обзор компании
This commit is contained in:
parent
6df2018086
commit
284b201912
|
|
@ -13,7 +13,8 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now
|
|||
- `Post-F Semantic Integrity Hardening`: `99%`, operationally closed as a hardening slice and now used as a regression gate.
|
||||
- `Inventory Stock Open-World Breadth Proof`: `100%` for the declared inventory-stock scenario pack, not for arbitrary inventory questions.
|
||||
- `Planner Autonomy Consolidation`: `100%` for the declared phase83 planner-brain slice, including catalog alignment, live-readiness gating, checked-source sanitation, and accepted mixed replay.
|
||||
- Active next pressure: broader `Open-World Bounded Autonomy Breadth` over unfamiliar 1C asks, while keeping Post-F and phase83 as regression gates.
|
||||
- Active next module: broader `Open-World Bounded Autonomy Breadth` over unfamiliar 1C asks, while keeping Post-F and phase83 as regression gates.
|
||||
- First active slice: `Business Overview Evidence Fusion`, tracked in `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`.
|
||||
|
||||
## Reporting Rule
|
||||
|
||||
|
|
@ -70,9 +71,10 @@ For current planning, read:
|
|||
|
||||
1. `README.md`
|
||||
2. this document
|
||||
3. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
4. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||
5. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
6. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
|
||||
3. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
4. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
5. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||
6. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
7. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
|
||||
|
||||
Documents `01` through `15` remain valuable, but mostly as the historical architecture trail.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
# 22 - Open-World Bounded Autonomy Breadth (2026-05-01)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note opens the next active module after:
|
||||
|
||||
- Post-F Semantic Integrity Hardening operational closure;
|
||||
- inventory-stock bounded breadth proof;
|
||||
- Planner Autonomy Consolidation phase83 closure.
|
||||
|
||||
The goal is not to reopen those slices.
|
||||
|
||||
The goal is to grow the assistant from reviewed route families toward broader bounded 1C autonomy:
|
||||
|
||||
`user business ask -> data-need graph -> catalog route fabric -> reviewed MCP evidence -> truth gate -> useful analyst answer`
|
||||
|
||||
## Entry Baseline
|
||||
|
||||
Already closed and kept as regression gates:
|
||||
|
||||
- Post-F semantic integrity: explicit current-turn meaning beats stale scope, stale focus object, and wrong post-pivot arbitration.
|
||||
- Planner phase83: selected chain must align with reviewed catalog templates and replay artifacts must expose that alignment.
|
||||
- Inventory breadth proof: a domain scenario tree can be accepted only through user-facing business correctness, not route labels alone.
|
||||
|
||||
The current active pressure is:
|
||||
|
||||
- unfamiliar or broad human 1C asks still need more bounded breadth;
|
||||
- broad business questions must become evidence-guided analyst answers, not generic chat summaries;
|
||||
- new capabilities must grow through reusable data-need and catalog surfaces, not one-off prompt patches.
|
||||
|
||||
## Slice 1 - Business Overview Evidence Fusion
|
||||
|
||||
User-facing trigger examples:
|
||||
|
||||
- `Как ты оценишь деятельность компании?`
|
||||
- `Дай полный анализ компании`
|
||||
- `Сделай LLM-аудит бизнеса`
|
||||
- `Что думаешь о компании в целом?`
|
||||
|
||||
### Problem
|
||||
|
||||
Before this slice, broad business evaluation was protected from stale lifecycle replay, but it could still be too thin:
|
||||
|
||||
- it summarized context, but did not consistently surface confirmed metrics;
|
||||
- it did not clearly separate cash-flow signals from profit/margin claims;
|
||||
- it could under-answer when the user expected a mature analyst-style company overview.
|
||||
|
||||
### Current Implementation Boundary
|
||||
|
||||
The first implementation step is intentionally bounded:
|
||||
|
||||
- it does not invent fresh facts;
|
||||
- it fuses already-confirmed MCP/session evidence from recent grounded answers;
|
||||
- it recognizes broader company-analysis wording as `broad_business_evaluation`;
|
||||
- it extracts useful analyst signals from confirmed value-flow, bidirectional net-flow, ranking, lifecycle, and inventory evidence;
|
||||
- it states that profit, margin, debt quality, and full company health remain unproved unless corresponding evidence exists.
|
||||
|
||||
This is not yet the final automatic multi-probe company-analysis chain.
|
||||
|
||||
It is the safe first slice: better user-facing business overview without bypassing evidence gates.
|
||||
|
||||
### Runtime Contract
|
||||
|
||||
When this bridge answers, it must:
|
||||
|
||||
- start with a direct business summary;
|
||||
- list confirmed metrics separately from interpretation;
|
||||
- call out cash-flow direction and net spread only as cash evidence, not as profit;
|
||||
- mention top counterparty/customer only when a ranked value-flow evidence slice exists;
|
||||
- keep profit/margin/debt/VAT as explicit missing evidence when not checked;
|
||||
- avoid raw MCP, planner, catalog, route, primitive, or debug wording.
|
||||
|
||||
### Next Slice
|
||||
|
||||
Promote this bridge into a real planner route:
|
||||
|
||||
- add a reviewed `business_overview` catalog chain template;
|
||||
- let the data-need graph model broad evaluation as a composed evidence need;
|
||||
- run bounded fresh probes for year turnover, top customers, incoming/outgoing/net flow, debt, VAT, and inventory context where available;
|
||||
- return a layered analyst answer with exact evidence, bounded inference, unknowns, and recommended next probes.
|
||||
|
||||
## Acceptance Signals
|
||||
|
||||
The slice is healthy when:
|
||||
|
||||
- broad analysis wording lands on the business-overview contour;
|
||||
- the answer is materially more informative than a generic recap;
|
||||
- confirmed metrics are visibly separated from LLM-style interpretation;
|
||||
- profit/margin are not claimed without supporting evidence;
|
||||
- Post-F stale-scope and phase83 catalog-alignment canaries remain green.
|
||||
|
||||
## Validation
|
||||
|
||||
Initial local validation:
|
||||
|
||||
- `npm.cmd test -- assistantTurnMeaningPolicy.test.ts assistantLivingChatRuntimeAdapter.test.ts`: passed `20/20`.
|
||||
- `npm.cmd test -- assistantTurnMeaningPolicy.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantRoutePolicy.test.ts assistantMcpDiscoveryResponsePolicy.test.ts`: passed `56/56`.
|
||||
- `npm.cmd run build`: passed.
|
||||
- graphify rebuild: `5977 nodes`, `12983 edges`, `137 communities`.
|
||||
|
||||
Graphify must be rebuilt after this code/doc slice before commit.
|
||||
|
|
@ -39,6 +39,7 @@ This package answers the next question:
|
|||
19. [19 - inventory_stock_open_world_breadth_proof_2026-05-01.md](./19%20-%20inventory_stock_open_world_breadth_proof_2026-05-01.md)
|
||||
20. [20 - planner_autonomy_consolidation_2026-05-01.md](./20%20-%20planner_autonomy_consolidation_2026-05-01.md)
|
||||
21. [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
|
||||
22. [22 - open_world_bounded_autonomy_breadth_2026-05-01.md](./22%20-%20open_world_bounded_autonomy_breadth_2026-05-01.md)
|
||||
|
||||
## Current Status Snapshot (2026-05-01)
|
||||
|
||||
|
|
@ -48,7 +49,8 @@ Status canon for planning:
|
|||
|
||||
- Post-F Semantic Integrity Hardening is operationally closed at `99%` and should now be used as a regression gate, not as the active module denominator.
|
||||
- Planner Autonomy Consolidation is closed at `100%` for the declared phase83 planner-brain slice.
|
||||
- The active next pressure is broader `Open-World Bounded Autonomy Breadth` over unfamiliar 1C asks, with Post-F and phase83 retained as semantic canaries.
|
||||
- The active next module is now `Open-World Bounded Autonomy Breadth` over unfamiliar 1C asks, with Post-F and phase83 retained as semantic canaries.
|
||||
- The first active slice is `Business Overview Evidence Fusion`: broad company-analysis wording now produces a richer evidence-grounded business overview from confirmed MCP/session facts instead of a thin generic summary.
|
||||
- The short source of truth for status wording is [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md).
|
||||
|
||||
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved through bounded MCP autonomy, Post-F hardening, inventory breadth proof, and the declared Planner Autonomy slice:
|
||||
|
|
@ -113,10 +115,11 @@ Current honest status:
|
|||
- pre-multidomain readiness: `~90%`
|
||||
- bounded-autonomy foundation readiness: `~89%`
|
||||
- open-world bounded-autonomy readiness: `~85%`
|
||||
- active Open-World Bounded Autonomy Breadth progress: `~12%`, with the first business-overview evidence-fusion slice started and locally tested; fresh multi-probe planner route is still pending
|
||||
- Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice
|
||||
- active inventory-stock breadth slice progress: `100%` for the declared scenario pack, not for arbitrary inventory questions
|
||||
- Planner Autonomy Consolidation progress: `100%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, unambiguous metadata-surface lane inference, catalog chain-template scoring, structured chain-match contract exposure, runtime/debug propagation, subject-aware bidirectional comparison arbitration, structured catalog-alignment verdicts, representative alignment regression guard, catalog-alignment reason-code telemetry, explicit `alignment_status` propagation, truth-harness/acceptance-matrix surfacing, soft divergence warning, `catalog_alignment_ok` acceptance invariant, step-level expected catalog-alignment assertions, phase66 and phase32 spec alignment expectations, AGENT source-catalog surfacing, generated phase83 mixed planner-brain replay spec, checked-source user-facing error sanitation, surface-grounded catalog promotion, and guarded live phase83 acceptance validated. Broader unfamiliar 1C asks are now next-module breadth work rather than an open blocker inside this declared slice
|
||||
- graph snapshot after latest rebuild: `5974 nodes`, `12974 edges`, `138 communities`
|
||||
- graph snapshot after latest rebuild: `5977 nodes`, `12983 edges`, `137 communities`
|
||||
- current regression-gate breakpoint:
|
||||
- the validated hot paths are no longer structurally broken;
|
||||
- flagship continuity collapse is no longer the primary risk;
|
||||
|
|
@ -205,6 +208,7 @@ For the detailed audit, current percentages, and remaining debt, read:
|
|||
- [19 - inventory_stock_open_world_breadth_proof_2026-05-01.md](./19%20-%20inventory_stock_open_world_breadth_proof_2026-05-01.md)
|
||||
- [20 - planner_autonomy_consolidation_2026-05-01.md](./20%20-%20planner_autonomy_consolidation_2026-05-01.md)
|
||||
- [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
|
||||
- [22 - open_world_bounded_autonomy_breadth_2026-05-01.md](./22%20-%20open_world_bounded_autonomy_breadth_2026-05-01.md)
|
||||
|
||||
## Architectural Objects Of Planning
|
||||
|
||||
|
|
@ -244,6 +248,7 @@ Read in this order:
|
|||
20. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||
21. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
22. `21 - current_status_canon_2026-05-01.md`
|
||||
23. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
|
||||
## Planning Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue