Spaces:
Sleeping
Sleeping
| """Heuristic AI involvement transparency for section payloads. | |
| Percentages are *estimates* derived from mode + AI level, not cryptographic | |
| proof. They communicate how much of the output is attributable to user | |
| bullets, retrieved chunks from the tenant's own uploads, versus LLM synthesis. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Any | |
| from app.models.schemas import GenerationMode | |
| logger = logging.getLogger(__name__) | |
| def _mode_value(mode: str | GenerationMode) -> str: | |
| if isinstance(mode, GenerationMode): | |
| return mode.value | |
| return str(mode).lower() | |
| def compute_ai_transparency( | |
| mode: str | GenerationMode, | |
| ai_level: int, | |
| ai_percent: int | None = None, | |
| measured_ai_percent: int | None = None, | |
| ) -> dict[str, Any]: | |
| """Return a JSON-serialisable transparency dict for API responses. | |
| Args: | |
| mode: ``generate`` | ``proofread`` | ``enhance``. | |
| ai_level: 1β5 (RAG-only β¦ full AI) β used when ``ai_percent`` is not set. | |
| ai_percent: If provided for **generate** mode, ``ai_involvement_percent`` | |
| is aligned to the user slider (0β100) so the UI matches behaviour. | |
| Returns: | |
| Dict with ``ai_involvement_percent``, ``breakdown``, ``summary``, | |
| ``transformations``, and ``plain_language``. | |
| """ | |
| level = max(1, min(5, int(ai_level))) | |
| mv = _mode_value(mode) | |
| if mv == GenerationMode.proofread.value: | |
| # Mostly your text; AI edits language only. | |
| ai_pct = 8 + (level - 1) * 2 | |
| user_pct = 100 - ai_pct | |
| return { | |
| "ai_involvement_percent": ai_pct, | |
| "breakdown": { | |
| "user_original_text_percent": user_pct, | |
| "ai_language_review_percent": ai_pct, | |
| "retrieved_context_percent": 0, | |
| }, | |
| "summary": ( | |
| "Proofread mode: your section text is preserved; the AI applies " | |
| "grammar, clarity, and style adjustments only." | |
| ), | |
| "transformations": [ | |
| "Grammar and spelling corrections", | |
| "Clarity and readability improvements", | |
| "Alignment with your detected writing style", | |
| ], | |
| "plain_language": ( | |
| f"About {ai_pct}% of the wording may be adjusted by the AI for " | |
| "readability; meaning and facts should stay aligned with your bullets." | |
| ), | |
| } | |
| if mv == GenerationMode.enhance.value: | |
| # Original text + retrieved evidence + expansion. | |
| base_ai = 28 + (level - 1) * 6 | |
| retrieval_pct = min(35, 22 + level * 2) | |
| user_pct = max(0, 100 - base_ai - retrieval_pct) | |
| return { | |
| "ai_involvement_percent": base_ai, | |
| "breakdown": { | |
| "user_original_and_bullets_percent": user_pct, | |
| "your_documents_retrieval_percent": retrieval_pct, | |
| "ai_technical_expansion_percent": base_ai, | |
| }, | |
| "summary": ( | |
| "Enhance mode: your existing text is expanded using additional " | |
| "passages retrieved *only from documents you uploaded* for this tenant." | |
| ), | |
| "transformations": [ | |
| "Semantic search across your uploaded RICS / survey documents", | |
| "Technical depth and professional phrasing added", | |
| "missing-information phrasing when facts cannot be confirmed", | |
| ], | |
| "plain_language": ( | |
| f"Roughly {retrieval_pct}% of the signal comes from retrieved " | |
| f"snippets from your own files; about {base_ai}% reflects AI synthesis " | |
| "and bridging prose." | |
| ), | |
| } | |
| # generate β align headline % with the user's ai_percent slider when present. | |
| logger.debug( | |
| "compute_ai_transparency: mode=%r ai_level=%r ai_percent=%r (type=%s)", | |
| mv, | |
| level, | |
| ai_percent, | |
| type(ai_percent).__name__, | |
| ) | |
| if ai_percent is not None: | |
| try: | |
| slider = max(0, min(100, int(ai_percent))) | |
| except Exception: # noqa: BLE001 | |
| slider = ai_level_to_approx_percent(level) | |
| # Split remainder between bullets vs retrieval heuristically (not additive with slider). | |
| bullet_pct = max(10, round(42 - slider * 0.22)) | |
| retrieval_pct = max(15, round(48 - slider * 0.18)) | |
| synth_pct = max(0, 100 - bullet_pct - retrieval_pct) | |
| total = bullet_pct + retrieval_pct + synth_pct | |
| bullet_pct = round(100 * bullet_pct / total) | |
| retrieval_pct = round(100 * retrieval_pct / total) | |
| synth_pct = 100 - bullet_pct - retrieval_pct | |
| low_mode = slider <= 12 | |
| measured: int | None = None | |
| if measured_ai_percent is not None: | |
| try: | |
| measured = max(0, min(100, int(measured_ai_percent))) | |
| except Exception: # noqa: BLE001 | |
| measured = None | |
| # Prefer measured involvement when available (it is computed from verbatim overlap), | |
| # but keep the user's requested slider value alongside it. | |
| headline = measured if measured is not None else slider | |
| return { | |
| "ai_involvement_percent": headline, | |
| "requested_ai_involvement_percent": slider, | |
| "measured_ai_involvement_percent": measured, | |
| "breakdown": { | |
| "user_bullets_percent": bullet_pct, | |
| "your_documents_retrieval_percent": retrieval_pct, | |
| "ai_synthesis_and_style_percent": synth_pct, | |
| }, | |
| "summary": ( | |
| "Low AI involvement: assembly from your notes and retrieved standard " | |
| "wording with minimal new prose." | |
| if low_mode | |
| else "Generate mode: your bullets and retrieved firm/standard passages are primary; " | |
| "AI contribution scales with the involvement slider." | |
| ), | |
| "transformations": [ | |
| "Vector similarity search over your ingested uploads (retrieval-first)", | |
| "Section skeleton + non-fictional notes", | |
| "LLM task controlled by AI involvement: verbatim assembly (0β12%) β full drafting (75β100%)", | |
| ], | |
| "plain_language": ( | |
| f"Your slider is {slider}% AI involvement. " | |
| f"About {bullet_pct}% of the signal is attributed to your notes, " | |
| f"{retrieval_pct}% to retrieved text from your documents, " | |
| f"and {synth_pct}% to AI synthesis and connectors β exact split is an estimate." | |
| ), | |
| } | |
| # generate (legacy): infer from 1β5 level only | |
| bullet_pct = max(18, 32 - level * 2) | |
| retrieval_pct = max(22, 38 - (level - 3) * 3) | |
| ai_pct = max(5, min(55, 100 - bullet_pct - retrieval_pct)) | |
| total = bullet_pct + retrieval_pct + ai_pct | |
| bullet_pct = round(100 * bullet_pct / total) | |
| retrieval_pct = round(100 * retrieval_pct / total) | |
| ai_pct = 100 - bullet_pct - retrieval_pct | |
| return { | |
| "ai_involvement_percent": ai_pct, | |
| "breakdown": { | |
| "user_bullets_percent": bullet_pct, | |
| "your_documents_retrieval_percent": retrieval_pct, | |
| "ai_synthesis_and_style_percent": ai_pct, | |
| }, | |
| "summary": ( | |
| "Generate mode: your fact bullets drive content; similar text is " | |
| "retrieved from *your uploaded documents only* (same tenant), then " | |
| "the AI adapts prose to RICS style and your writing profile." | |
| ), | |
| "transformations": [ | |
| "Notes interpretation / expansion (unless AI level 1 β RAG only)", | |
| "Vector similarity search over your ingested uploads", | |
| "Reranking for relevance", | |
| "LLM adaptation to section skeleton + style profile", | |
| ], | |
| "plain_language": ( | |
| f"Approximately {bullet_pct}% derives from your bullets, " | |
| f"{retrieval_pct}% from retrieved excerpts of your own files, " | |
| f"and {ai_pct}% from AI wording and structure." | |
| ), | |
| } | |
| def ai_level_to_approx_percent(level: int) -> int: | |
| """Map legacy 1β5 scale to a midpoint percent for fallback messaging.""" | |
| return min(100, max(0, (max(1, min(5, int(level))) - 1) * 25 + 12)) | |