Spaces:
Runtime error
Runtime error
File size: 5,893 Bytes
c893230 aad7814 c893230 aad7814 c893230 | 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 | """Normalize messy inspector notes into bullet lines for RAG + generation."""
from __future__ import annotations
import re
from dataclasses import dataclass
from app.chunking.splitter import count_tokens
_ENCODING = None
def _encoding():
global _ENCODING
if _ENCODING is None:
import tiktoken
_ENCODING = tiktoken.get_encoding("cl100k_base")
return _ENCODING
def explode_note_lines(bullets: list[str]) -> list[str]:
"""Split dense note blobs into separate lines (semicolons, pipes, long sentences)."""
out: list[str] = []
for raw in bullets:
text = str(raw or "").strip()
if not text:
continue
if len(text) <= 320 and text.count(";") + text.count("|") == 0:
out.append(text)
continue
chunks = re.split(r"[;\n|]+", text)
if len(chunks) == 1 and len(text) > 320:
chunks = re.split(r"(?<=[.!?])\s+(?=[A-Z0-9£])", text)
for piece in chunks:
p = piece.strip()
if len(p) >= 2:
out.append(p)
return out
def clean_and_clamp_bullets(bullets: list[str], *, max_items: int) -> list[str]:
"""Trim, de-dupe (exact), and cap bullet count."""
if not bullets:
return []
out: list[str] = []
seen: set[str] = set()
for raw in explode_note_lines(bullets):
t = str(raw or "").strip()
if not t:
continue
t = re.sub(r"^[-•*]+\s*", "", t).strip()
if not t:
continue
key = t.casefold()
if key in seen:
continue
seen.add(key)
out.append(t)
if len(out) >= max_items:
break
return out
@dataclass(frozen=True, slots=True)
class BulletClampReport:
"""Audit record describing what ``clean_and_clamp_bullets_with_report`` dropped.
Used by ``_generate_section_text`` (standard path) and
``run_inspector_tool_loop`` (agentic path) so we can surface the warning
in section metadata instead of silently losing content. Without this the
user reports "data from messy notes didn't show up in the final report",
but the cause is the silent clamp here — not the LLM.
"""
input_count: int
cleaned_count: int
duplicate_dropped: int
overflow_dropped: int
overflow_examples: tuple[str, ...]
max_items: int
@property
def total_dropped(self) -> int:
return self.duplicate_dropped + self.overflow_dropped
@property
def has_clamps(self) -> bool:
return self.total_dropped > 0
def to_dict(self) -> dict:
return {
"input_count": self.input_count,
"cleaned_count": self.cleaned_count,
"duplicate_dropped": self.duplicate_dropped,
"overflow_dropped": self.overflow_dropped,
"overflow_examples": list(self.overflow_examples),
"max_items": self.max_items,
}
def clean_and_clamp_bullets_with_report(
bullets: list[str], *, max_items: int, overflow_example_limit: int = 6
) -> tuple[list[str], BulletClampReport]:
"""Same behaviour as ``clean_and_clamp_bullets`` plus an audit report.
We return the dropped overflow examples (first few that didn't fit) so
the section metadata can show the user *which* observations were
silently truncated. This is what powers the bullet-clamp warning surfaced
on the report API response.
"""
out: list[str] = []
seen: set[str] = set()
duplicate_dropped = 0
overflow_examples: list[str] = []
exploded = explode_note_lines(bullets or [])
input_count = len(exploded)
overflow_count = 0
for raw in exploded:
t = str(raw or "").strip()
if not t:
continue
t = re.sub(r"^[-•*]+\s*", "", t).strip()
if not t:
continue
key = t.casefold()
if key in seen:
duplicate_dropped += 1
continue
if len(out) >= max_items:
overflow_count += 1
if len(overflow_examples) < overflow_example_limit:
overflow_examples.append(t)
continue
seen.add(key)
out.append(t)
report = BulletClampReport(
input_count=input_count,
cleaned_count=len(out),
duplicate_dropped=duplicate_dropped,
overflow_dropped=overflow_count,
overflow_examples=tuple(overflow_examples),
max_items=max_items,
)
return out, report
def format_bullets_for_prompt(bullets: list[str], max_tokens: int) -> str:
"""Format bullets for the LLM user prompt within a token budget.
Notes are pre-processed (typo normalisation + unverified-term flagging) at
this boundary so the LLM never sees a raw transcription artefact, while the
grounding pass continues to compare against the untouched bullet list.
"""
if not bullets:
return "(none)"
try:
from app.config import settings
from app.generator.term_glossary import preprocess_bullets
if getattr(settings, "enable_unverified_term_flagging", True):
bullets = preprocess_bullets(bullets)
except Exception: # noqa: BLE001 — never block prompt assembly on preprocessing
pass
if max_tokens < 40:
max_tokens = 40
parts: list[str] = []
used = 0
for b in bullets:
line = f"- {b}"
need = count_tokens(line)
if parts and used + need > max_tokens:
remaining = len(bullets) - len(parts)
if remaining > 0:
parts.append(f"- … ({remaining} further note line(s) still used for retrieval)")
break
if need > max_tokens:
enc = _encoding().encode(line)[:max_tokens]
parts.append(_encoding().decode(enc))
break
parts.append(line)
used += need
return "\n".join(parts)
|