"""Cost-controlled, single-flight Intelligence Center model generation.""" from __future__ import annotations import hashlib import json import logging import uuid from dataclasses import dataclass from datetime import UTC, datetime, timedelta from typing import Any, cast from zoneinfo import ZoneInfo from sqlalchemy import delete, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import AppException from app.domains.ai_spending.service import ( SETTING_LLM_API_KEY, SETTING_LLM_MODEL, SETTING_LLM_REGION, ) from app.domains.bailian.llm_client import ( DEFAULT_MODEL, DEFAULT_REGION, BailianLLMClient, ) from app.integrations.models import PlatformSetting from app.intelligence.contracts import ( FLAT_KEY_SEPARATOR, IntelligenceContractError, IntelligenceOutput, validate_intelligence_output, ) from app.intelligence.models import ( IntelligenceAnalysis, IntelligenceGenerationLease, IntelligenceSnapshot, ) from app.intelligence.privacy import UnsafeFactPackage, assert_llm_safe from app.intelligence.scoring import canonical_json, json_value from app.intelligence.settings import ( DEFAULT_CLOUD_CACHE_DAYS, DEFAULT_CLOUD_COOLDOWN_MINUTES, DEFAULT_DAILY_LIMIT, SETTING_CLOUD_CACHE_DAYS, SETTING_CLOUD_COOLDOWN_MINUTES, SETTING_DAILY_LIMIT, bounded_int_setting, setting_value, ) _TZ = ZoneInfo("Asia/Shanghai") _LEASE_TTL = timedelta(minutes=5) logger = logging.getLogger(__name__) _SYSTEM_PROMPT = """\ You are the bilingual intelligence analyst for Helix, a company IT operating platform. The user payload is an anonymous aggregate fact package calculated by Helix. It is the only source of truth. Return JSON matching the supplied schema exactly. Every narrative item must cite scalar facts in fact_refs, and every fact_refs value must be COPIED VERBATIM from the citable_fact_refs list in the payload. Never compose, shorten or guess a reference; if no listed reference supports a sentence, drop the sentence. Every number written in Chinese or English must occur in those cited scalar facts. Use only anonymous evidence_refs present in the package; never invent an object. Do not infer or request names, emails, accounts, hosts, IPs, domains, credentials, policies or other identifiers. Estimated, confirmed and realized savings are distinct. Never describe an estimate as achieved. When insufficient_data is true, use only low/medium confidence and include concrete data_gaps. Chinese and English must express the same facts. Be concise and decision-useful. Citations are examples, not inventories: at most 12 fact_refs and at most 20 evidence_refs per item, and fewer is better. The full populations are already listed elsewhere in the product; cite the few that carry the point. For cloud/network ownership, distinguish stored ownership from effective ownership. A server with no direct owner field may inherit the current owner of its cloud account/subscription. Treat owner_coverage_basis and the directly_owned_server_count / inherited_owner_server_count / owner_gap_count facts as authoritative. Never describe all resources without a stored owner as ownerless, and never recommend assigning owners to inherited resources. """ # Subscription analyses often begin with incomplete inventory or monitoring. # Keep the grounding boundary strict, but tell the model not to turn every # dashboard value into an uncited numeric claim. This avoids an automatic # second, cost-bearing request merely to recover from an avoidable rejection. _CLOUD_SCOPE_PROMPT = _SYSTEM_PROMPT + """ For a cloud subscription, prefer a short qualitative conclusion over a numeric recap. Do not write any numeral, percentage, date, score, count or 30/60/90 horizon unless that exact scalar is named in the item's fact_refs. When confidence is low, return concrete data_gaps first and do not claim that missing inventory means the subscription has no resources. """ # The posture summary kept being refused in production for numbers the # model had derived CORRECTLY: handed a ratio of 0.4545 it wrote the # obvious "45.45%", which the grounding check — a literal token # comparison that understands no arithmetic — reported as an uncited # number. Three rounds of publishing yet another pre-computed form # (integer score, then each dimension, then the percentage) each bought # one deploy before the model found a fourth way to phrase it. # # So the instruction is bounded instead: copy, never compute. Same # posture as _CLOUD_SCOPE_PROMPT, which exists for this same guard. _POSTURE_PROMPT = _SYSTEM_PROMPT + """ This is a monthly compliance posture letter for auditors. Copy numbers verbatim from the cited scalar facts — never compute, convert, round or re-express them. Do not turn a ratio into a percentage, do not sum or subtract facts, and do not restate a figure in words ("about half", "two in five"). If the form you want is not present as its own scalar fact, describe it qualitatively instead. Name the reporting period only by copying period_start / period_end. """ # The cross-domain brief was the ONLY subject whose prompt never told it # not to do arithmetic — and it is the only subject that has never # published. ``compliance_posture`` (the one subject with a success to its # name) and ``cloud_scope`` each carry a numeric-discipline paragraph; # ``platform`` inherited the bare base prompt and, once every reference # spelling finally resolved, fell at exactly the predicted hurdle: # "Narrative contains uncited numbers: ['2148']" — a total the model # computed rather than copied. # # The guard is right to refuse it. Its package publishes 1390 numeric # facts, so admitting "sums of two facts" would admit almost any number a # model could write (a spurious pair adds to 2148 already); the honest fix # is to stop the model computing, not to widen what counts as grounded. _PLATFORM_BRIEF_PROMPT = _SYSTEM_PROMPT + """ This is a cross-domain weekly brief. Copy every figure verbatim from a citable fact — never add, subtract, total, average, convert or round. Do not sum counts across domains, and do not compute a share or a percentage that is not published as its own fact. If the figure you want does not exist as one scalar, describe the situation qualitatively instead, or name the two published figures separately. Keep the weekly brief deliberately short: at most 4 conclusions, 5 risks, 3 opportunities, 3 forecasts, 5 recommendations and 3 data gaps. Prefer an empty list to a weakly supported item. This keeps every claim reviewable and avoids a long, partially grounded response being discarded as a whole. """ @dataclass(frozen=True) class GenerationResult: analysis: IntelligenceAnalysis cache_reused: bool # A pointer the model can form must resolve to a scalar. The contract # refuses one that lands on a dict or a list, and production proved a # nested package makes forming a correct pointer an error-prone act rather # than a mechanical one: the cloud subscription package nests 7 groups and # 12 lists, and the model cited # ``/facts/costs/data_quality/sources/2/available_count`` — concatenating # two paths, since ``data_quality`` is a TOP-LEVEL group and not under # ``costs``. The whole narrative was thrown away for it. # # So the model is handed one flat level whose keys carry the full path. # ``costs.top_resources.0.amount`` cannot be mis-navigated: there is # nothing to navigate. This is the same lesson the posture summary learned # by hand, applied once at the only seam every subject passes through. _FLAT_SEP = FLAT_KEY_SEPARATOR def _flatten_for_citation( node: Any, prefix: str = "", out: dict[str, Any] | None = None, *, item_cap: int | None = None, ) -> dict[str, Any]: """One flat level of citable scalars, keys carrying the original path. List items stay individually addressable and each list also publishes its ``count`` — the citable fact a narrative usually wants, and the only one that must never be sampled away. ``item_cap`` bounds how many members of a list are addressed individually. It is not a display preference: a provider-emitted list scales with the roster, and the payload cost of one member is roughly tripled here (a long flattened key, its value, and the citable pointer published for it). ``count`` above is still the TRUE length, and a truncated list also publishes ``shown_count`` so the model can never read the sample as the population — see ``_FACT_LIST_ITEMS_SHOWN``. """ if out is None: out = {} if isinstance(node, dict): for key, value in node.items(): child = f"{prefix}{_FLAT_SEP}{key}" if prefix else str(key) _flatten_for_citation(value, child, out, item_cap=item_cap) elif isinstance(node, (list, tuple)): count_key = f"{prefix}{_FLAT_SEP}count" if prefix else "count" _assign_flat(out, count_key, len(node)) shown = node if item_cap is None else node[:item_cap] if len(shown) != len(node): shown_key = f"{prefix}{_FLAT_SEP}shown_count" if prefix else "shown_count" _assign_flat(out, shown_key, len(shown)) used: set[str] = set() for index, value in enumerate(shown): segment = _list_item_segment(value, index, prefix, used) used.add(segment) # The empty-prefix case matters: ``dimensions`` IS a list, so # without this branch every dimension key is published with a # leading separator — ``.0.metrics.1.value``. Production, 2026-08-17: # the model cited exactly that, character for character as it was # handed over, and the brief was refused for "must be a JSON # pointer". The address the model is given has to be an address it # can use. (``count`` above already had this branch; the item keys # did not.) child = f"{prefix}{_FLAT_SEP}{segment}" if prefix else segment _flatten_for_citation(value, child, out, item_cap=item_cap) else: _assign_flat(out, prefix or "value", node) return out def _list_item_segment( item: Any, index: int, prefix: str, used: set[str], ) -> str: """Address a list item by its own name when it has one. A positional address is one nobody can guess. ``dimensions`` was published as ``0.metrics.1.value`` while every record in it carries a stable ``key`` (``cloud_network``, and the metric's own ``cloud_network.resource_waste_rate``), and production, 2026-08-17, showed the model writing what a human would write — ``cloud_network.metrics.1.value`` — against a package that only answered to the index. Naming the record makes the natural address the real one. Two details earn their keep. A record's key may already carry its parent's namespace (the metric key repeats its dimension), so the namespace is not published twice. And a repeated or missing key falls back to the index rather than colliding — two facts must never share one citable address (see ``_assign_flat``). """ if isinstance(item, dict): raw = item.get("key") if isinstance(raw, str) and raw.strip(): segment = raw.strip() head = prefix.split(_FLAT_SEP)[0] if prefix else "" if head and segment.startswith(f"{head}{_FLAT_SEP}"): segment = segment[len(head) + 1:] if segment and segment not in used: return segment return str(index) def _assign_flat(out: dict[str, Any], key: str, value: Any) -> None: """Refuse to let two different facts share one citable key. Only reachable if a source key already contains a dot, which would make ``a.b`` ambiguous between the nested ``{"a": {"b": ...}}`` and the literal ``"a.b"``. Silently keeping one would hand the model a number filed under someone else's name, so this fails loudly in tests instead. """ if key in out and out[key] != value: raise ValueError( f"Fact key collision while flattening for citation: {key!r}. " "A fact key must not contain a dot." ) out[key] = value # How many anonymous evidence refs of each kind the model is shown # (2026-08-18). The platform snapshot's ``reference_map`` holds **1976** # refs across 29 kinds (624 `access_*`, 279 `peer_*`, 148 `server_*`, …). # Handing all of them to a model told to "cite the evidence" produced # exactly what you would expect: single narrative items citing 42 and then # 124 refs against a per-item cap of 20, and the whole brief — every # correctly grounded number in it — was discarded for the count. # # An evidence ref anchors a claim to an example object a human can drill # into; it is not an inventory, and the deterministic read model already # lists every population in full. Sorted and per-kind, so one snapshot # always shows the same sample and two runs stay comparable. Validation # still accepts ANY ref in ``reference_map`` (``allowed_evidence_refs``), # so narrowing what is SHOWN can never turn a legitimate citation into a # rejection. _EVIDENCE_REFS_PER_KIND = 6 # How many members of one provider-emitted fact list the model is shown # individually (2026-09-01). Not a display preference — an input-window # invariant, learned from a production outage. # # On 2026-08-31 seventeen transfer events landed inside the 30-day # forecast horizon where the week before there had been one. # ``lifecycle.transfer_forecast`` publishes a record per transfer, each # carrying its own per-resource ``deltas`` list, and the same records are # published again under each of the 30/60/90 horizons. That one roster # event took ``aggregate_facts`` from 44 KB to 128 KB, and the model-facing # package — where every leaf costs a long flattened key, its value, AND # the citable pointer published for it — from 445 K chars to 1.05 M. # Measured against the live endpoint: qwen3-max answered # ``HTTP 400 InvalidParameter: Range of input length should be # [1, 258048]``, and the weekly brief failed. The last good run was # 114,689 input tokens for 447 K chars, i.e. ~3.9 chars/token, so 1.05 M # chars is ~270 K tokens — over the window by 5%. # # Nothing about that was a one-off: every one of these lists is a record # per person, per leaver, per transfer, per resource, and grows with the # company. A brief narrative quotes POPULATIONS (``count``, rates, the # dimension scores) and names a FEW examples; it has never had a use for # the 17th transfer's 6th resource delta. So the aggregate stays exact # (``count`` is the true length, and ``shown_count`` appears whenever a # list is sampled) and the individual records are capped. # # Deterministic prefix rather than a ranked sample: the flattener is # generic and has no way to know which field of an unknown provider's # record means "worst". Provider order is already stable (the two large # lists order by process / transfer id), so one snapshot always shows the # same members and two runs stay comparable — the same property # ``_EVIDENCE_REFS_PER_KIND`` is chosen for. # # 8 is sized against the real package, not chosen by taste. Rebuilt from # the 2026-09-01 production snapshot it yields a 441,966-character prompt # that the live endpoint answered with HTTP 200 and counted as **116,545 # input tokens** — 2.2x under the window, and within 2% of the last run # that ever succeeded (114,689 on 2026-08-29). Every dimension score and # every aggregate count is still exact. Note the payload does NOT fall to # a third of the pre-fix size, because the cap applies per list and the # nesting is multiplicative: 3 horizons x 8 transfers x 8 deltas is still # the bulk of it. See ``test_model_fact_package_input_budget_2026_09_01``. _FACT_LIST_ITEMS_SHOWN = 8 #: How many model replies one generation may validate before giving up #: (2026-09-01). Sized from the measured per-attempt rate, not from taste. #: #: Six live runs against the real production package, with every other fix #: in this change applied, validated **4 of 6**. The two survivors were the #: guard working correctly — one narrative wrote ``76000`` with no fact #: within 38,000 of it, the other wrote ``75`` for a published ``75.5`` #: (truncation is a misstatement, not an abbreviation) — so the residual is #: single-reply slips, which is exactly what re-asking clears. At a 1/3 #: per-attempt rejection rate the job's own failure probability is #: ``(1/3)^attempts``: one ask fails 33% of weeks, two 11%, three **3.7%**. #: #: Three, because the marginal ask is only ever paid on the 11% of runs #: where two have already failed, and the thing it buys is not a nicer #: number — it is not paging an operator with a P1 for a report that would #: have published on the next sentence the model wrote. A fourth is where #: this stops being insurance and starts being a retry loop against a #: package the model genuinely cannot narrate. #: #: Only ``IntelligenceContractError`` is retried — a too-large or truncated #: prompt is a property of the package and re-earns itself at full cost. _VALIDATION_ATTEMPTS = 3 def _evidence_ref_kind(ref: str) -> str: """``access_017`` → ``access``. Unsuffixed refs are their own kind.""" head, sep, tail = ref.rpartition("_") return head if sep and tail.isdigit() else ref def _sampled_evidence_refs(reference_map: dict[str, Any] | None) -> list[str]: """Up to ``_EVIDENCE_REFS_PER_KIND`` refs per kind, deterministically.""" per_kind: dict[str, list[str]] = {} for ref in sorted((reference_map or {}).keys()): bucket = per_kind.setdefault(_evidence_ref_kind(ref), []) if len(bucket) < _EVIDENCE_REFS_PER_KIND: bucket.append(ref) return sorted(ref for bucket in per_kind.values() for ref in bucket) def _citable_pointers(package: dict[str, Any]) -> list[str]: """Every legal fact reference in the package, derived not hand-listed. The first version of this listed ``/facts/`` only, and production immediately showed the cost: the model wanted to cite the score and found no legal address for it, so it constructed ``/facts/overall_score`` — where ``overall_score`` is a TOP-LEVEL member. An incomplete list of legal addresses is an invitation to invent one, and the score, grade and confidence are exactly what a narrative wants to cite. So the list is computed from the package itself: every scalar member, plus every scalar inside the already-flattened ``facts`` and ``dimensions``. Complete by construction, which is the only way it stays complete when a member is added later. One exclusion, and it is a caliber question rather than a completeness hole: an address under an ``evidence_refs`` segment holds an anonymous drill-down anchor (``server_79``), not a magnitude. The contract gives those their own channel — the per-item ``evidence_refs`` field, checked against the whole ``reference_map`` — and this list is what a narrative copies when it needs a NUMBER. Flattening turned each anchor into a scalar and so quietly promoted 693 of the platform package's 3505 addresses into "facts you may cite", two thirds of everything published under ``dimensions``. They are still resolvable if cited, so nothing legal became illegal; they are simply no longer advertised as facts. """ refs: list[str] = [] for key, value in package.items(): if isinstance(value, dict): refs.extend( f"/{key}/{child}" for child in value if not _is_evidence_ref_address(child) ) elif not isinstance(value, list): refs.append(f"/{key}") # Lists are skipped, which also excludes every vocabulary member — # ``evidence_refs``, ``horizon_days`` and this list itself are all # lists. An explicit name-based exclusion was written first and # removed: a mutation showed it could never be the rule that fired, # so it was a branch no test could hold. return sorted(refs) def _is_evidence_ref_address(flat_key: str) -> bool: """True for a flattened address that lands inside an ``evidence_refs`` list. ``cloud_network.metrics.resource_waste_rate.evidence_refs.7`` yes; its sibling ``.evidence_refs.count`` no — a count IS a magnitude a narrative may quote, and it is the fact the list-flattener publishes precisely so that the narrative does not have to count anchors itself. """ head, sep, leaf = flat_key.rpartition(_FLAT_SEP) # Segment-exact: a fact legitimately named ``prior_evidence_refs`` is not # this, and ``endswith`` would have swallowed it. return ( bool(sep) and head.rpartition(_FLAT_SEP)[2] == "evidence_refs" and leaf != "count" ) def build_model_fact_package(snapshot: IntelligenceSnapshot) -> dict[str, Any]: """Create the only payload shape permitted to cross the LLM boundary.""" package = json_value({ "score_version": snapshot.score_version, "overall_score": snapshot.overall_score, "confidence_score": snapshot.confidence_score, "insufficient_data": snapshot.confidence_score < 70, "grade": snapshot.grade, "dimensions": snapshot.dimensions, "facts": snapshot.aggregate_facts, "evidence_refs": _sampled_evidence_refs(snapshot.reference_map), "horizon_days": [30, 60, 90], "data_as_of": snapshot.data_as_of, }, decimal_as_string=True) package = cast(dict[str, Any], package) # Guard the NESTED shape first. ``assert_llm_safe`` matches denied KEY # NAMES exactly (``name``, ``email``, ``hostname``, ``ip``, ``policy``, …), # and flattening rewrites every key to ``.`` — after which # not one of those exact names can ever match again, leaving only the value # regexes on the largest part of the payload. Nothing reaches the model # today that would trip it (the providers publish counts and bounded # enums), so this is defence-in-depth rather than a live leak — but this # function's docstring calls itself the only permitted payload shape, and a # future subject assembling a package here would otherwise inherit a # boundary with half its net missing. Checked before AND after: before for # the key names, after for whatever flattening and citation-listing # produced. assert_llm_safe(package) # ``facts`` and ``dimensions`` are the two members narratives cite; # ``evidence_refs`` and ``horizon_days`` stay lists because they are # vocabularies the contract reads, not magnitudes to quote. # # Only ``facts`` is capped. ``dimensions`` is the score model itself — # a closed vocabulary defined in ``catalog.py`` and held to a weight # total of 1.00 per dimension by ``validate_metric_weight_totals`` — # and it is the headline content of every brief, so a cap there would # silently drop a dimension the day a ninth one ships rather than # dropping the 9th of an unbounded population. ``facts`` is # provider-emitted and scales with the roster; that is the list that # broke the input window. for citable, cap in (("facts", _FACT_LIST_ITEMS_SHOWN), ("dimensions", None)): current = package.get(citable) if isinstance(current, (dict, list)): package[citable] = _flatten_for_citation(current, item_cap=cap) # The legal addresses, as a copyable list. ``evidence_refs`` has always # been published this way and has never once been invented; fact refs # were left for the model to COMPOSE from dict keys, and it composed # them wrong. Production evidence for the difference this makes: the # subject that works publishes 23 citable keys, the subject that kept # failing publishes 124, and its last three rejections were an # unresolvable path, a missing prefix, and a wholly invented # ``.0.metrics.0.value``. Copying a listed address cannot go wrong the # way composing one does. # Scan the real content FIRST, then attach the derived vocabulary. # # Publishing the list before the scan promoted fact KEY NAMES into # scanned VALUES, and dict keys were never scanned here. Real # production data: the cloud subscription package was rejected outright # with "domain-like value" on eight of its own reference strings — # ``/dimensions/0.score`` and friends read as hostnames. Nothing unsafe # had entered the payload; the same key names were already present as # keys of the members just validated. # # Not fixed by changing the separator: the dot is load-bearing in the # resolver's tolerant spellings (``FLAT_KEY_SEPARATOR`` in contracts). # Restoring the original values-only boundary is the smaller change and # the honest one. # # ``keys_are_flat_paths`` because the keys above are no longer names — # they are addresses this function just joined. Scanning a joined # address with the value regexes rejected every cloud-subscription # package outright: all 19 of that subject's metric keys start with # ``cloud.``, so the flattener emits ``...metrics.cloud.<...>`` and # ``metrics.cloud`` reads as ``