Spaces:
Runtime error
Runtime error
File size: 8,345 Bytes
eda3d74 3f6016e faa8fb3 eda3d74 3f6016e eda3d74 295512e 0b42403 295512e eda3d74 295512e eda3d74 b76f199 eda3d74 295512e 3f6016e 295512e 0b42403 295512e 0b42403 295512e eda3d74 295512e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | """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))
|