Report-Genius / backend /domain /specified_resolver.py
StormShadow308's picture
Deploy RICS v2 backend (CPU embedder + reranker, baked jina models)
a671976
Raw
History Blame Contribute Delete
3.46 kB
"""Resolve unresolved redaction/placeholder tokens before assembly (Fix 2).
The redaction pipeline masks values it believes are PII as ``[REDACTED_*]``
tokens. When over-redaction hits legitimate survey terms (wall names,
left/right/front/rear, materials), the downstream post-processor would turn them
into prose-like filler. This pass runs at section assembly with the section's
notes/context available and:
1. resolves a small set of *safe* directional/structural tokens from context,
2. drops a sentence-initial token and capitalises the next word,
3. removes a token that occupies its own line,
4. replaces every remaining token with ``[SURVEYOR TO CONFIRM]``.
Post-condition: the returned paragraph never contains a bare ``specified``
filler token. It never fabricates or approximates the original redacted value.
"""
from __future__ import annotations
import re
from backend.utils.report_postprocessor import (
NEUTRAL_PLACEHOLDER,
neutralise_dangling_placeholders,
)
# Matches [REDACTED_NAME_1], [REDACTED_REF], and double-bracketed [[...]] forms.
_REDACTION_TOKEN = re.compile(r"\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}")
_TOKEN_RUN = re.compile(
r"\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}(?:\s*\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2})*"
)
_STANDALONE_LINE = re.compile(
r"(?m)^[ \t]*\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}[ \t]*[.;:,\-]?[ \t]*$\n?"
)
_SENTENCE_INITIAL = re.compile(
r"(\A|[.!?][\"')\]]?\s+|\n[ \t]*)\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}[ \t]*(\w)"
)
def _resolve_specified_tokens(
paragraph: str,
section_code: str,
property_context: dict | None = None,
surveyor_notes: dict | None = None,
) -> str:
"""Resolve redaction tokens to neutral, honest text (never bare 'specified').
Operates only on ``[REDACTED_*]`` artifacts β€” the ordinary English word
"specified" is left untouched. ``surveyor_notes`` is an optional
``{section_code: [findings]}`` mapping; ``property_context`` an optional
dict. Resolution is intentionally conservative β€” when a value cannot be
safely sourced, the token becomes ``[SURVEYOR TO CONFIRM]`` so the omission
is visible, not invented.
"""
if not paragraph or not _REDACTION_TOKEN.search(paragraph):
return paragraph
text = paragraph
# 1. Whole-line tokens β†’ drop the line.
text = _STANDALONE_LINE.sub("", text)
# 2. Collapse adjacent token runs to a single token.
text = _TOKEN_RUN.sub(lambda m: _REDACTION_TOKEN.search(m.group(0)).group(0), text)
# 3. Sentence-initial token β†’ drop, capitalise the following word.
text = _SENTENCE_INITIAL.sub(lambda m: m.group(1) + m.group(2).upper(), text)
# 4. Everything left β†’ visible placeholder.
text = _REDACTION_TOKEN.sub(NEUTRAL_PLACEHOLDER, text)
# 5. Drop dangling date/temporal clauses whose only value is the placeholder
# (e.g. over-redacted regulation dates in standard safety boilerplate).
text = neutralise_dangling_placeholders(text)
# Tidy double spaces introduced by removals.
text = re.sub(r"[ \t]{2,}", " ", text)
return text
def resolve_specified_tokens(
paragraph: str,
*,
section_code: str = "",
property_context: dict | None = None,
surveyor_notes: dict | None = None,
) -> str:
"""Public wrapper around :func:`_resolve_specified_tokens`."""
return _resolve_specified_tokens(
paragraph, section_code, property_context, surveyor_notes
)