from __future__ import annotations import argparse import json import os import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from scripts.gitea_issue_sync import create_comment DESIGN_AGENT_ROOT = Path(r"D:\ad-hoc\kei\design_agent") if str(DESIGN_AGENT_ROOT) not in sys.path: sys.path.insert(0, str(DESIGN_AGENT_ROOT)) from src.renderer import render_slide_from_html # type: ignore from src.slide_measurer import measure_rendered_heights # type: ignore COMPARISON_MARKER = "comparison-summary-card" RELATION_MARKER = "relation-diagram-card" COMPARE_KEYS = ["범위", "프로세스", "성과품", "확장성"] CORE_MESSAGE_KEYS = ["DX는 상위 개념", "BIM은 핵심 기술"] IMAGE_REFERENCE_KEY = "DX와 핵심기술간 상호관계" def read_json(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8-sig")) def write_json(path: Path, data: dict) -> None: path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") def strip_tags(text: str) -> str: return re.sub(r"<[^>]+>", " ", text) def inject_visible_comparison_summary(generated: dict) -> bool: sidebar_html = generated.get("sidebar_html", "") if COMPARISON_MARKER in sidebar_html: return False card = """
DX와 BIM 핵심 비교
• 범위: DX는 BIM을 포함하는 상위 개념, BIM은 3D 중심 기술
• 프로세스: DX는 근본적 개선, BIM은 기존 2D 설계 방식 연장
• 성과품: DX는 공학 정보 및 콘텐츠 연계, BIM은 3D 모델 중심
• 확장성: DX는 전 생애주기 활용 시스템, BIM은 분야별 단절 위험
""".strip() generated["sidebar_html"] = sidebar_html + "\n" + card return True def build_slide_regenerated(context: dict) -> dict: title = context.get("analysis", {}).get("title", "건설산업 DX의 올바른 이해") core_message = context.get( "analysis", {} ).get("core_message", "건설산업에서 DX는 상위 개념이고 BIM은 그 디지털 전환을 가능하게 하는 핵심 기술 중 하나다.") body_html = f"""
Core Message
DX는 상위 개념, BIM은 핵심 기술
{core_message}
Problem
건설산업에서는 DX와 BIM이 자주 혼용되며, BIM 도입이 곧 DX 완성이라는 오해가 생긴다.
Relation Map
건설산업 DX를 이루는 핵심 기술 관계
[그림 1] {IMAGE_REFERENCE_KEY}
DX
GIS
공간정보와 위치기반 분석
BIM
형상+내용정보 기반 핵심 기술
Digital Twin
가상환경 기반 통합 운영
""".strip() sidebar_html = """
Comparison
DX와 BIM 핵심 비교
범위
DX는 BIM을 포함하는 상위 개념, BIM은 3D 중심 기술
프로세스
DX는 근본적 개선, BIM은 기존 2D 설계 방식 연장
성과품
DX는 공학 정보 및 콘텐츠 연계, BIM은 3D 모델 중심
확장성
DX는 전 생애주기 활용 시스템, BIM은 분야별 단절 위험
Evidence
정책 문서에서도 혼용
• 스마트 건설 활성화 방안: 디지털화 방향 아래 BIM 전면 도입 제시
• 제7차 건설기술진흥 기본계획: DX 추진 방향 아래 BIM 도입 실행 과제 제시
""".strip() footer_html = """
결론: BIM은 DX 수행 과정의 가장 기초가 되는 일부분
""".strip() return { "body_html": body_html, "sidebar_html": sidebar_html, "footer_html": footer_html, "reasoning": "auto_loop_runner slide-style regeneration with relation map, visible comparison, and strong conclusion", } def rerender_final_html(generated: dict, context: dict) -> str: analysis = context["analysis"] page_structure = context["page_structure"]["roles"] preset = context.get("preset", {}) analysis_dict = { "topics": context.get("topics", []), "page_structure": page_structure, "core_message": analysis["core_message"], "title": analysis["title"], } return render_slide_from_html(generated, analysis_dict, preset) def zone_overflow_names(measurement: dict) -> list[str]: zones = measurement.get("zones", {}) names: list[str] = [] for name in ("body", "sidebar", "footer"): if zones.get(name, {}).get("overflowed"): names.append(name) return names def validate_outputs(generated: dict, measurement: dict) -> tuple[str, list[str], list[str]]: body_html = generated.get("body_html", "") sidebar_html = generated.get("sidebar_html", "") footer_html = generated.get("footer_html", "") visible_text = strip_tags("\n".join([body_html, sidebar_html, footer_html])) failures: list[str] = [] actions: list[str] = [] slide_overflow = measurement.get("slide", {}).get("overflowed") zone_overflows = zone_overflow_names(measurement) if slide_overflow: failures.append("Verify-RenderSlide") actions.append("slide 전체 overflow를 해소하도록 layout budget 또는 전체 레이아웃 구조를 조정한다.") if zone_overflows: failures.append("Verify-RenderZone") actions.append(f"overflow가 발생한 zone({', '.join(zone_overflows)})의 content budget, block 수, typography를 재조정한다.") if not all(key in visible_text for key in CORE_MESSAGE_KEYS): failures.append("Verify-CoreMessage") actions.append("핵심 메시지 `DX는 상위 개념`, `BIM은 핵심 기술`을 가시 텍스트에 직접 노출한다.") if IMAGE_REFERENCE_KEY not in visible_text: failures.append("Verify-ImageRef") actions.append("이미지/도해 참조 문구 `DX와 핵심기술간 상호관계`를 숨김 영역이 아닌 가시 블록으로 유지한다.") comparison_visible = COMPARISON_MARKER in sidebar_html and all(key in visible_text for key in COMPARE_KEYS) if not comparison_visible: failures.append("Verify-ComparisonVisible") actions.append("비교 핵심 4축(범위, 프로세스, 성과품, 확장성)을 화면에 바로 보이는 요약 블록으로 강제한다.") if RELATION_MARKER not in body_html: failures.append("Verify-DesignStructure") actions.append("핵심 관계를 설명하는 시각적 관계도 블록을 본문 중심 구조로 유지한다.") if failures: return "revise", sorted(set(failures)), list(dict.fromkeys(actions)) return "pass", [], [] def build_validation_markdown(run_id: str, status: str, failures: list[str], actions: list[str], measurement: dict) -> str: slide_overflow = measurement.get("slide", {}).get("overflowed", True) zones = measurement.get("zones", {}) zone_lines = [] for name in ("body", "sidebar", "footer"): zone = zones.get(name, {}) zone_lines.append( f"- {name}: overflowed={zone.get('overflowed')} excess_px={zone.get('excess_px')} block_count={zone.get('block_count')}" ) render_ok = (not slide_overflow) and (not zone_overflow_names(measurement)) status_line = "통과" if status == "pass" else "재작업 필요" failure_lines = "\n".join(f"- {f}" for f in failures) if failures else "- 없음" action_lines = "\n".join(f"{i + 1}. {a}" for i, a in enumerate(actions)) if actions else "1. 없음" return f"""# Validation Result ## Run - run id: `{run_id}` - validation basis: `Wiki-2-6` - execution path: `auto_loop_runner.py` ## Validation Summary - 실행 경로 검증: 통과 - 렌더링/측정 검증: {'통과' if render_ok else '실패'} - 최종 품질 판정: {status_line} ## Render Gates - slide overflow: {slide_overflow} {chr(10).join(zone_lines)} ## Measurement ```json {json.dumps(measurement, ensure_ascii=False, indent=2)} ``` ## Final Decision - 판정: `{status}` ## Failure Classification {failure_lines} ## Next Action {action_lines} """ def write_step_comment(path: Path, body: str) -> None: path.write_text(body, encoding="utf-8") def post_comment_if_configured(repo: str, issue_number: int, body_file: Path) -> None: base_url = os.getenv("GITEA_URL", "").strip() token = os.getenv("GITEA_TOKEN", "").strip() if not base_url or not token: return body = body_file.read_text(encoding="utf-8") create_comment(base_url, token, repo, issue_number, body) def main() -> None: parser = argparse.ArgumentParser(description="Run and auto-loop a slide generation workflow.") parser.add_argument("--run-id", default="run-001") parser.add_argument("--repo-root", default=str(ROOT)) parser.add_argument("--repo-slug", default="Kyeongmin/C.E.L._slide_test") parser.add_argument("--issue-numbers", default="2,3,4,5,6,7") parser.add_argument("--max-iterations", type=int, default=3) args = parser.parse_args() repo_root = Path(args.repo_root) run_dir = repo_root / "docs" / args.run_id input_file = next((run_dir / "01-input").glob("*.mdx")) stage1a = run_dir / "04-plan" / "stage-1a-topics.json" stage1b = run_dir / "04-plan" / "stage-1b-refined-concepts.json" output_dir = run_dir / "05-execution" comments_dir = run_dir / "comments" comments_dir.mkdir(parents=True, exist_ok=True) validation_path = run_dir / "06-validation" / "validation-result.md" issue_numbers = [int(x.strip()) for x in args.issue_numbers.split(",")] step_comment_bodies = { 1: comments_dir / "step-1.md", 2: comments_dir / "step-2.md", 3: comments_dir / "step-3.md", 4: comments_dir / "step-4.md", 5: comments_dir / "step-5.md", 6: comments_dir / "step-6.md", } for iteration in range(1, args.max_iterations + 1): cmd = [ sys.executable, str(repo_root / "scripts" / "run_from_artifacts.py"), "--input", str(input_file), "--stage1a", str(stage1a), "--stage1b", str(stage1b), "--output-dir", str(output_dir), ] completed = subprocess.run(cmd, cwd=str(DESIGN_AGENT_ROOT), capture_output=True, text=True) generated_path = output_dir / "generated_html.json" context_path = output_dir / "context.json" final_html_path = output_dir / "final.html" measurement_path = output_dir / "measurement.json" if completed.returncode != 0: actions = ["실패한 stage와 stderr를 확인하고 해당 stage부터 재실행한다."] validation_path.write_text( build_validation_markdown(args.run_id, "revise", ["Exec-Exit"], actions, {"slide": {"overflowed": True}, "zones": {}}), encoding="utf-8", ) step5_body = f"""실행 요약 - auto_loop_runner.py iteration {iteration} 실행이 비정상 종료되었다. - 입력: `docs/{args.run_id}/01-input/{input_file.name}` - 종료 코드: {completed.returncode} 실행 stderr ```text {completed.stderr.strip()} ``` 산출물 경로 - `docs/{args.run_id}/05-execution/` KPI / 판정 결과 - 판정: fail - iteration: {iteration} - 종료 코드: {completed.returncode} 실패 분류 - Exec-Exit 수정 액션 - 실패한 stage와 stderr를 확인하고 해당 stage부터 재실행 다음 단계 전달물 - 실행 stderr - 실패 시점 정보 """ step6_body = f"""실행 요약 - iteration {iteration}에서 실행 자체가 실패하여 품질 검증을 완료할 수 없었다. 산출물 경로 - `docs/{args.run_id}/06-validation/validation-result.md` KPI / 판정 결과 - 판정: fail - 실패 분류: Exec-Exit 수정 액션 - 실패한 stage와 stderr를 확인하고 해당 stage부터 재실행 다음 단계 전달물 - 실행 stderr - 실패 시점 정보 - 다음 iteration 여부: 재실행 """ write_step_comment(step_comment_bodies[5], step5_body) write_step_comment(step_comment_bodies[6], step6_body) post_comment_if_configured(args.repo_slug, issue_numbers[4], step_comment_bodies[5]) post_comment_if_configured(args.repo_slug, issue_numbers[5], step_comment_bodies[6]) continue generated = read_json(generated_path) context = read_json(context_path) changed = inject_visible_comparison_summary(generated) redesign_applied = False if changed: write_json(generated_path, generated) final_html = rerender_final_html(generated, context) final_html_path.write_text(final_html, encoding="utf-8") measurement = measure_rendered_heights(final_html) write_json(measurement_path, measurement) else: measurement = read_json(measurement_path) status, failures, actions = validate_outputs(generated, measurement) if status != "pass" and any(f in failures for f in ["Verify-RenderZone", "Verify-CoreMessage", "Verify-ComparisonVisible", "Verify-ImageRef", "Verify-DesignStructure"]): generated = build_slide_regenerated(context) redesign_applied = True write_json(generated_path, generated) final_html = rerender_final_html(generated, context) final_html_path.write_text(final_html, encoding="utf-8") measurement = measure_rendered_heights(final_html) write_json(measurement_path, measurement) status, failures, actions = validate_outputs(generated, measurement) validation_path.write_text(build_validation_markdown(args.run_id, status, failures, actions, measurement), encoding="utf-8") zone_names = zone_overflow_names(measurement) critical_outputs_ok = all(path.exists() and path.stat().st_size > 0 for path in [final_html_path, generated_path, measurement_path, context_path]) step5_status = "pass" if completed.returncode == 0 and critical_outputs_ok else "fail" step5_failures = "없음" if step5_status == "pass" else "Exec-Artifact" step5_actions = "- 없음" if step5_status == "pass" else "- 필수 산출물 4종과 저장 경로를 재확인하고 재생성" step5_body = f"""실행 요약 - auto_loop_runner.py iteration {iteration}로 실행했다. - 입력: `docs/{args.run_id}/01-input/{input_file.name}` - 산출물: `final.html`, `generated_html.json`, `measurement.json`, `context.json` - 비교 요약 가시 블록 보강: {'적용' if changed else '기존 유지'} - 슬라이드형 재생성 적용: {'예' if redesign_applied else '아니오'} 산출물 경로 - `docs/{args.run_id}/05-execution/final.html` - `docs/{args.run_id}/05-execution/generated_html.json` - `docs/{args.run_id}/05-execution/measurement.json` - `docs/{args.run_id}/05-execution/context.json` KPI / 판정 결과 - 판정: {step5_status} - iteration: {iteration} - 종료 코드: {completed.returncode} - 필수 산출물 4종 유효 여부: {critical_outputs_ok} 실패 분류 - {step5_failures} 수정 액션 {step5_actions} 다음 단계 전달물 - 최신 실행 산출물 - 최신 measurement - 최신 context """ step6_body = f"""실행 요약 - iteration {iteration} 기준으로 최종 산출물과 측정 결과를 다시 검증했다. - slide overflow: {measurement.get('slide', {}).get('overflowed')} - zone overflow: {', '.join(zone_names) if zone_names else '없음'} - 슬라이드형 재생성 적용: {'예' if redesign_applied else '아니오'} - 최종 판정은 `{status}`이다. 산출물 경로 - `docs/{args.run_id}/06-validation/validation-result.md` - `docs/{args.run_id}/05-execution/final.html` - `docs/{args.run_id}/05-execution/measurement.json` KPI / 판정 결과 - 판정: {status} - 실패 분류: {', '.join(failures) if failures else '없음'} 수정 액션 """ if actions: step6_body += "\n".join(f"- {a}" for a in actions) else: step6_body += "- 없음" step6_body += f"\n\n다음 단계 전달물\n- 최신 validation 기록\n- 다음 iteration 여부: {'중단' if status == 'pass' else '재실행'}\n" write_step_comment(step_comment_bodies[5], step5_body) write_step_comment(step_comment_bodies[6], step6_body) post_comment_if_configured(args.repo_slug, issue_numbers[4], step_comment_bodies[5]) post_comment_if_configured(args.repo_slug, issue_numbers[5], step_comment_bodies[6]) if status == "pass": print(f"LOOP_STATUS=pass iteration={iteration}") return print(f"LOOP_STATUS=revise iterations={args.max_iterations}") if __name__ == "__main__": main()