"""Render practical answer drafts from source-backed retrieval contracts.""" from __future__ import annotations from typing import Any def _as_list(value: Any) -> list[Any]: return value if isinstance(value, list) else [] def _first_text(*values: Any) -> str: for value in values: text = str(value or "").strip() if text: return text return "" def _status_label(status: str) -> str: if status == "covered": return "근거 확인" if status == "missing": return "추가 확인 필요" return "상태 미확인" def _markdown_link(label: str, url: str | None) -> str: if url and str(url).startswith("https://www.kcsc.re.kr/"): return f"[{label}]({url})" return label def _numbered(items: list[str], *, empty: str) -> list[str]: if not items: return [f"- {empty}"] return [f"{index}. {item}" for index, item in enumerate(items, start=1)] def _bullets(items: list[str], *, empty: str) -> list[str]: if not items: return [f"- {empty}"] return [f"- {item}" for item in items] def build_practical_answer_sections(query: str, draft: dict[str, Any]) -> dict[str, Any]: """Build stable user-facing sections from a response draft. This intentionally does not infer new technical conclusions. It only restructures the concept contract and retrieved evidence into a readable answer shape. """ outline = draft.get("answer_outline") or {} abstention = draft.get("abstention") or {} should_abstain = bool(abstention.get("should_abstain")) standards = [] for item in _as_list(draft.get("basis_evidence"))[:8]: code = _first_text(item.get("code")) role = _first_text(item.get("role"), item.get("title")) status = _first_text(item.get("evidence_status"), "unknown") revision_note = f", {item.get('revision_date')}" if item.get("revision_date") else "" standards.append({ "code": code, "title": _first_text(item.get("title")), "role": role, "evidence_status": status, "status_label": _status_label(status), "official_url": item.get("official_url"), "revision_date": item.get("revision_date"), "link_status": item.get("link_status"), "line": ( f"{_markdown_link(code, item.get('official_url'))} - {role} ({_status_label(status)}{revision_note})" if role else f"{_markdown_link(code, item.get('official_url'))} ({_status_label(status)}{revision_note})" ), }) evidence = [] for item in _as_list(draft.get("evidence_summary"))[:5]: code = _first_text(item.get("code")) title = _first_text(item.get("title")) section = _first_text(item.get("section")) preview = _first_text(item.get("evidence_preview")) label = " / ".join(part for part in (code, section) if part) evidence.append({ "label": label or code or title or "검색 근거", "title": title, "preview": preview, "official_url": item.get("official_url"), "revision_date": item.get("revision_date"), "link_status": item.get("link_status"), }) required_inputs = [str(item) for item in _as_list(outline.get("required_inputs"))[:8]] compliance_workflow = [str(item) for item in _as_list(outline.get("compliance_workflow"))[:8]] cautions = [str(item) for item in _as_list(outline.get("evidence_cautions"))[:4]] if should_abstain: summary = "검색된 조항 근거가 부족해 자동 답변을 보류합니다. 기준명, 수치, 적합성 판단은 추가 검색 또는 수동 검토 후 확정해야 합니다." else: summary = "검색 근거와 실무 concept 매핑을 바탕으로 작성한 기준 검토 초안입니다. 구조 안전, 품질 판정, 보수 공법 확정은 책임기술자 검토가 필요합니다." return { "query": query, "summary": summary, "standard_priority": standards, "required_inputs": required_inputs, "compliance_workflow": compliance_workflow, "evidence_summary": evidence, "cautions": cautions, "should_abstain": should_abstain, "review_required": bool(draft.get("review_required")), "confidence": draft.get("confidence"), } def render_practical_answer_markdown(query: str, draft: dict[str, Any]) -> str: """Render a concise Markdown answer from the structured draft.""" sections = build_practical_answer_sections(query, draft) heading = "검토 보류" if sections["should_abstain"] else "검토 초안" lines: list[str] = [ f"**{heading}**", sections["summary"], "", "**기준 우선순위**", ] standard_lines = [item["line"] for item in sections["standard_priority"] if item.get("line")] lines.extend(_numbered(standard_lines, empty="검색 근거가 없어 우선 적용 기준을 확정하지 않습니다.")) lines.extend(["", "**필수 확인 조건**"]) lines.extend(_bullets(sections["required_inputs"], empty="현장 조건과 설계 입력값을 추가 확인해야 합니다.")) lines.extend(["", "**준수 체크리스트**"]) lines.extend(_bullets(sections["compliance_workflow"], empty="검색된 조항 근거 확보 후 체크리스트를 확정합니다.")) lines.extend(["", "**근거 요약**"]) if sections["evidence_summary"]: for item in sections["evidence_summary"]: label = _markdown_link(item["label"], item.get("official_url")) preview = f": {item['preview']}" if item.get("preview") else "" lines.append(f"- {label}{preview}") else: lines.append("- 검색된 조항 근거가 없습니다.") lines.extend(["", "**주의**"]) caution_lines = sections["cautions"] or [ "검색 trace가 없는 기준은 최종 수치나 적합성 판단에 사용하지 않습니다.", ] lines.extend(_bullets(caution_lines, empty="검색 trace가 없는 기준은 최종 수치나 적합성 판단에 사용하지 않습니다.")) return "\n".join(lines)