RICS / app /generator /section_scope.py
StormShadow308's picture
Add demo documentation and Docker setup for v2 report generation system
aad7814
Raw
History Blame Contribute Delete
4.74 kB
"""Per-section SCOPE FENCES for RICS Home Survey report generation.
Each section is generated in isolation, so without an explicit scope the LLM
"dumps everything it knows" into whichever section it is writing β€” Section D
becomes a summary of the entire report, defects bleed across elements, and
legal/hazard matters (knotweed, asbestos) land in the wrong place.
A scope fence states, per RICS section code, what is IN scope and what is OUT
of scope (with the correct destination code). The fence is injected into the
section's drafting prompt. This is deterministic, free, and the single most
effective lever against cross-section contamination.
Standard RICS Home Survey numbering is the source of truth (E1 = Chimney
stacks, E2 = Roof coverings, F1 = Roof structure, …).
"""
from __future__ import annotations
# Element-level sections share one rule: write ONLY about their own element and
# push everything else to its home section. Section D and the legal/risk
# sections get bespoke fences because they are the worst offenders.
_DESTINATIONS = (
"Roof coverings/tiles/slate β†’ E2; roof structure/timbers/insulation β†’ F1; "
"chimney/flashing/stack β†’ E1; gutters/downpipes/rainwater β†’ E3; "
"external walls/cracking/cavity β†’ E4; windows β†’ E5; external doors β†’ E6; "
"ceilings β†’ F2; internal walls/partitions β†’ F3; floors β†’ F4; "
"fireplaces/flues β†’ F5; bathroom/kitchen/mould β†’ F8; electrics β†’ G1; "
"gas β†’ G2; water supply β†’ G3; heating/boiler/radiators β†’ G4; "
"drainage/inspection chamber/soil stack β†’ G6; garages/outbuildings β†’ H1/H2; "
"grounds/trees β†’ H3; regulations/listed/conservation/alterations β†’ I1; "
"guarantees β†’ I2; other legal matters β†’ I3; knotweed/asbestos/hazards β†’ I3/J4."
)
# Bespoke fences for the high-risk sections.
_SCOPE_OVERRIDES: dict[str, str] = {
"D": (
"IN SCOPE for Section D (ABOUT THE PROPERTY β€” INTRODUCTION ONLY):\n"
" + Property type and tenure (house/flat/bungalow; freehold/leasehold)\n"
" + Address and date of inspection (only if present in notes)\n"
" + Legal status that frames the whole report (listed building, "
"conservation area) β€” state prominently, one sentence\n"
" + One sentence on overall character\n"
" + Reference to key alterations (one sentence, NO detail)\n"
"OUT OF SCOPE for Section D (these have their own sections β€” do NOT "
"describe them here):\n"
" - Roof condition β†’ E2/F1 - Damp β†’ E3/F1 - Wall defects β†’ E4\n"
" - Floor construction β†’ F4 - Services β†’ G1\u2013G8\n"
" - Legal detail β†’ I1\u2013I3 - Grounds β†’ H1\u2013H3\n"
"Write 2\u20133 sentences maximum. If tempted to mention a defect or "
"repair, stop and instead reference the relevant section. "
'End Section D with exactly: "Full details of each element are '
'provided in sections E through K of this report."'
),
"I1": (
"IN SCOPE for Section I1 (REGULATIONS): planning permission, building "
"regulations, listed-building/conservation-area constraints, unknown "
"approval status for alterations/conversions.\n"
"OUT OF SCOPE: element condition/defects (E/F/G sections), guarantees "
"(I2). " + _DESTINATIONS
),
"I3": (
"IN SCOPE for Section I3 (OTHER LEGAL MATTERS): hazardous materials "
"(asbestos), invasive plants (Japanese knotweed), tree/TPO legal "
"matters, other solicitor referrals.\n"
"OUT OF SCOPE: element condition (E/F/G), regulations detail (I1). "
+ _DESTINATIONS
),
}
_ELEMENT_FENCE_TEMPLATE = (
"IN SCOPE for Section {code} ({title}): observations, condition, defects "
"and recommendations for {title_lower} ONLY.\n"
"OUT OF SCOPE: anything about a different building element or a legal/"
"hazard matter β€” do NOT describe it here, it belongs elsewhere. {dest}"
)
def section_scope_block(code: str, title: str | None = None) -> str:
"""Return the scope-fence text for a RICS section code (never raises)."""
c = (code or "").strip().upper()
if c in _SCOPE_OVERRIDES:
fence = _SCOPE_OVERRIDES[c]
else:
t = (title or c).strip()
fence = _ELEMENT_FENCE_TEMPLATE.format(
code=c or "this section",
title=t,
title_lower=t.lower(),
dest=_DESTINATIONS,
)
return "SECTION SCOPE (mandatory β€” keep content within this fence):\n" + fence
def is_introduction_section(code: str) -> bool:
"""Section D is an introduction; it must never carry element-level detail."""
return (code or "").strip().upper() == "D"