96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""IMP-33 u7 — AI fallback router (fallback path only).
|
|
|
|
Composes the IMP-33 fallback flow:
|
|
|
|
1. flag gate (``settings.ai_fallback_enabled`` default OFF)
|
|
2. V4 route gate (route must equal ``ai_adaptation_required``)
|
|
3. cache read (u6 stub returns ``None`` until IMP-46 lands)
|
|
4. build prompt (u3)
|
|
5. call client (u4 ``request_proposal``)
|
|
6. validate (u5 ``validate_proposal``)
|
|
|
|
Returns the validated ``AiFallbackProposal``. Save to cache is NOT
|
|
performed here — it is caller-driven AFTER ``visual_check_passed=True``
|
|
AND ``user_approved=True``, per the u6 IMP-46 gate. The router does not
|
|
import ``save_proposal``; this is the structural guarantee that the
|
|
router cannot persist a proposal before the caller's visual + user
|
|
checks (`feedback_artifact_status_naming`).
|
|
|
|
Guardrails:
|
|
|
|
* PZ-1 — normal-path AI call count stays 0: flag-off OR route-mismatch
|
|
short-circuits BEFORE the prompt builder or client are touched.
|
|
* ``feedback_ai_isolation_contract`` — MDX READ-ONLY (u3 enforces in
|
|
prompt; this module never reads or writes MDX).
|
|
* ``feedback_phase_z_spacing_direction`` — V4 rank-1 protected (u5
|
|
enforces; router only forwards the contract).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from src.config import settings
|
|
from src.phase_z2_ai_fallback.cache import read_proposal
|
|
from src.phase_z2_ai_fallback.client import AiFallbackClient
|
|
from src.phase_z2_ai_fallback.prompts import (
|
|
V4_ROUTE_AI_ADAPTATION,
|
|
build_ai_fallback_prompt,
|
|
)
|
|
from src.phase_z2_ai_fallback.schema import AiFallbackProposal
|
|
from src.phase_z2_ai_fallback.validate import validate_proposal
|
|
|
|
|
|
def route_ai_fallback(
|
|
*,
|
|
cache_key: str,
|
|
v4_result: dict[str, Any],
|
|
frame_contract: dict[str, Any],
|
|
frame_visual_html: str,
|
|
figma_partial_json: dict[str, Any],
|
|
internal_region: dict[str, Any],
|
|
mdx_text: str,
|
|
client: AiFallbackClient | None = None,
|
|
fingerprints: dict | None = None,
|
|
) -> AiFallbackProposal | None:
|
|
"""Route a fallback request through cache → prompt → client → validate.
|
|
|
|
Returns ``None`` when the master flag is OFF or when the V4 route is
|
|
not ``ai_adaptation_required`` — both gates short-circuit BEFORE any
|
|
prompt/client work, so the normal-path AI call count stays at 0
|
|
(PZ-1).
|
|
|
|
``fingerprints`` is forwarded into ``read_proposal`` so that
|
|
contract / partial / catalog SHA mismatches invalidate stale cache
|
|
entries (IMP-46 #62 Axis R). When ``None`` the cache layer skips
|
|
fingerprint comparison (legacy behaviour).
|
|
"""
|
|
if not settings.ai_fallback_enabled:
|
|
return None
|
|
route = v4_result.get("route") or v4_result.get("imp05_route_hint")
|
|
if route != V4_ROUTE_AI_ADAPTATION:
|
|
return None
|
|
cached = read_proposal(cache_key, fingerprints=fingerprints)
|
|
if cached is not None:
|
|
validate_proposal(
|
|
cached,
|
|
frame_contract=frame_contract,
|
|
internal_region=internal_region,
|
|
)
|
|
return cached
|
|
prompt = build_ai_fallback_prompt(
|
|
v4_result=v4_result,
|
|
frame_contract=frame_contract,
|
|
frame_visual_html=frame_visual_html,
|
|
figma_partial_json=figma_partial_json,
|
|
internal_region=internal_region,
|
|
mdx_text=mdx_text,
|
|
)
|
|
active_client = client if client is not None else AiFallbackClient()
|
|
proposal = active_client.request_proposal(prompt)
|
|
validate_proposal(
|
|
proposal,
|
|
frame_contract=frame_contract,
|
|
internal_region=internal_region,
|
|
)
|
|
return proposal
|