RICS / app /services /ai_transparency.py
StormShadow308's picture
feat: enhance embedding model configuration and retrieval logic
0b42403
Raw
History Blame Contribute Delete
8.35 kB
"""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))