myreport-space / scripts /test_sp_decompose_generate.py
Chaudhry Waleed
Deploy RICS v2 (senior baseline, CPU Spaces Dockerfile)
c1708ae
Raw
History Blame Contribute Delete
7.68 kB
"""E2E: LLM decompose issues → per-issue SP retrieve → generate for one subsection.
Bypasses the note-parser heading trap by using the same per-section observations
as ``test_sp_decompose_notes.py``. Does not require
``STANDARD_PARAGRAPHS_DECOMPOSE_NOTES=true`` (uses force_decompose).
Examples:
python scripts/test_sp_decompose_generate.py --section D1 --force-llm
python scripts/test_sp_decompose_generate.py --section F6 --force-llm --all-sps
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
_ROOT = Path(__file__).resolve().parents[1]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
from backend.config import settings
from backend.domain import template_discoverer
from backend.standard_paragraphs.generate import generate_from_standard_paragraphs
from backend.standard_paragraphs.note_issues_manifest import record_note_issues
from backend.storage import retrieval_manifest
def _load_decompose_cases() -> tuple[list[dict], str]:
path = _ROOT / "scripts" / "test_sp_decompose_notes.py"
spec = importlib.util.spec_from_file_location("test_sp_decompose_notes", path)
assert spec and spec.loader
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return list(mod.CASES), str(mod.TENANT)
CASES, DEFAULT_TENANT = _load_decompose_cases()
def _case_for(section_id: str) -> dict:
sid = section_id.strip().upper()
for case in CASES:
if case["section_id"].upper() == sid:
return case
known = [c["section_id"] for c in CASES]
raise SystemExit(f"Unknown section {section_id!r}. Known: {known}")
def main() -> int:
parser = argparse.ArgumentParser(
description="E2E SP: decompose → retrieve → generate (one subsection)"
)
parser.add_argument("--section", required=True, help="e.g. D1, D8, F6")
parser.add_argument(
"--force-llm",
action="store_true",
help="Force LLM decompose even for short notes",
)
parser.add_argument(
"--all-sps",
action="store_true",
help=(
"Pass EVERY standard paragraph for this subsection from FAISS "
"(no Top-K). Still decomposes notes into findings for the prompt."
),
)
parser.add_argument(
"--style-samples",
action="store_true",
help=(
"Force-inject past REFERENCE subsection samples into the SP prompt "
"(overrides STANDARD_PARAGRAPHS_STYLE_SAMPLES_ENABLED for this run)."
),
)
parser.add_argument("--tenant", default=DEFAULT_TENANT)
parser.add_argument(
"--draft-id",
default="",
help="Retrieval manifest id (default: sp-e2e-<section>-<timestamp>)",
)
args = parser.parse_args()
case = _case_for(args.section)
sid = case["section_id"]
title = case["section_title"]
observations = list(case["observations"])
suffix = "allsps" if args.all_sps else "e2e"
draft_id = args.draft_id.strip() or (
f"sp-{suffix}-{sid.lower()}-"
+ datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
)
print("tenant=", args.tenant)
print("draft=", draft_id)
print("section=", sid, title)
print("force_decompose=True force_llm=", args.force_llm)
print("use_all_section_sps=", args.all_sps)
print("style_samples=", args.style_samples)
print("decompose_flag_in_env=", settings.standard_paragraphs_decompose_notes)
print("observations:")
for o in observations:
print(" -", o)
schema = template_discoverer.ensure_canonical_schema(args.tenant)
text, hits, messages, guidance, issues, llm_usage, style_sample_count = (
generate_from_standard_paragraphs(
tenant_id=args.tenant,
schema=schema,
section_id=sid,
section_title=title,
observations=observations,
candidate_ids=[sid],
force_decompose=True,
force_decompose_llm=args.force_llm,
use_all_section_sps=args.all_sps,
style_samples_enabled=True if args.style_samples else None,
)
)
issues_path = record_note_issues(
args.tenant,
draft_id,
section_id=sid,
section_title=title,
observations=observations,
issues=list(issues),
source="e2e_decompose_generate",
used_llm=True if args.force_llm else None,
)
retrieval_manifest.record_section_retrieval(
args.tenant,
draft_id,
section_id=sid,
section_title=title,
observations=observations,
baseline_text=guidance,
hits=hits,
status="MAPPED" if text.strip() and hits else "NO_RAG_MATCH",
prompt_messages=messages,
retrieved_count=len(hits),
prompt_chunk_count=len(hits),
knowledge_source="standard_paragraph",
generated_text=text,
retrieval_issues=list(issues),
requested_top_k=len(hits) if args.all_sps else None,
llm_usage=llm_usage,
style_sample_count=style_sample_count,
)
ret_path = retrieval_manifest.retrieval_manifest_path(args.tenant, draft_id)
print("\n=== ISSUES (LLM / heuristic) ===")
for i, issue in enumerate(issues, 1):
print(f" {i}. {issue}")
print("note_issues=", issues_path)
# Show how the user prompt was grouped (findings ↔ candidates).
user_prompt = ""
if messages:
for msg in messages:
if msg.get("role") == "user":
user_prompt = msg.get("content") or ""
break
if "CURRENT INSPECTION FINDINGS" in user_prompt or "CURRENT FINDINGS" in user_prompt:
print("\n=== PROMPT FINDINGS / CANDIDATES (excerpt) ===")
# Print from findings header through first ~2500 chars of that block.
start = user_prompt.find("CURRENT INSPECTION FINDINGS")
if start < 0:
start = user_prompt.find("CURRENT FINDINGS")
excerpt = user_prompt[start : start + 2500]
print(excerpt)
if len(user_prompt) - start > 2500:
print("... [truncated]")
print("\n=== RETRIEVED SPs (flat manifest hits) ===", len(hits), ("(ALL section SPs)" if args.all_sps else ""))
for i, h in enumerate(hits[:20], 1):
preview = (h.text or "").replace("\n", " ")[:120]
if args.all_sps:
print(f" {i}. idx={h.paragraph_index} {preview!r}")
else:
print(f" {i}. cosine={getattr(h, 'score', 0) or 0:.3f} fusion={getattr(h, 'fusion_score', 0) or 0:.4f} {preview!r}")
if len(hits) > 20:
print(f" ... +{len(hits) - 20} more")
print("\n=== GENERATED ===")
print("style_sample_count=", style_sample_count)
print("llm_usage=", llm_usage)
print(text or "(empty)")
print("\nretrieval=", ret_path)
sample = (
_ROOT
/ "backend"
/ "standard_paragraphs"
/ "samples"
/ f"{draft_id}.e2e.json"
)
payload = {
"draft_id": draft_id,
"section_id": sid,
"section_title": title,
"observations": observations,
"retrieval_issues": issues,
"retrieved_chunk_count": len(hits),
"use_all_section_sps": args.all_sps,
"generated_text": text,
"note_issues_path": str(issues_path),
"retrieval_path": str(ret_path),
}
sample.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
print("sample copy=", sample)
return 0 if text.strip() else 2
if __name__ == "__main__":
raise SystemExit(main())