Persist stage context snapshots for run workflows

This commit is contained in:
2026-04-02 10:24:38 +09:00
parent 9cf20c02a5
commit b864872d1a
14 changed files with 3729 additions and 31 deletions

File diff suppressed because one or more lines are too long

View File

@@ -201,8 +201,8 @@
<div style="width:100%; height:100%; box-sizing:border-box; font-family:'Segoe UI',sans-serif; display:flex; flex-direction:column; gap:12px;"> <div style="width:100%; height:100%; box-sizing:border-box; font-family:'Segoe UI',sans-serif; display:flex; flex-direction:column; gap:12px;">
<div class="comparison-summary-card" style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; overflow:hidden; box-shadow:0 10px 22px rgba(15,23,42,0.08);"> <div class="comparison-summary-card" style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; overflow:hidden; box-shadow:0 10px 22px rgba(15,23,42,0.08);">
<div style="padding:12px 14px; background:linear-gradient(135deg,#eff6ff 0%,#dbeafe 100%); border-bottom:1px solid #bfdbfe;"> <div style="padding:12px 14px; background:linear-gradient(135deg,#eff6ff 0%,#dbeafe 100%); border-bottom:1px solid #bfdbfe;">
<div style="font-size:11px; font-weight:700; color:#1d4ed8; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:4px;">Comparison</div> <div style="font-size:10.0px; font-weight:700; color:#1d4ed8; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:4px;">Comparison</div>
<div style="font-size:16px; font-weight:800; color:#0f172a;">DX와 BIM 핵심 비교</div> <div style="font-size:10.0px; font-weight:800; color:#0f172a;">DX와 BIM 핵심 비교</div>
</div> </div>
<div style="padding:10px 14px 12px; display:grid; grid-template-columns:80px 1fr; row-gap:8px; column-gap:10px; font-size:10px; line-height:1.45; color:#334155;"> <div style="padding:10px 14px 12px; display:grid; grid-template-columns:80px 1fr; row-gap:8px; column-gap:10px; font-size:10px; line-height:1.45; color:#334155;">
<div style="font-weight:800; color:#0f172a;">범위</div><div>DX는 BIM을 포함하는 상위 개념, BIM은 3D 중심 기술</div> <div style="font-weight:800; color:#0f172a;">범위</div><div>DX는 BIM을 포함하는 상위 개념, BIM은 3D 중심 기술</div>
@@ -213,8 +213,8 @@
</div> </div>
<div style="background:#f8fafc; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box;"> <div style="background:#f8fafc; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box;">
<div style="font-size:11px; font-weight:700; color:#475569; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:6px;">Evidence</div> <div style="font-size:10.0px; font-weight:700; color:#475569; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:6px;">Evidence</div>
<div style="font-size:14px; font-weight:800; color:#0f172a; margin-bottom:8px;">정책 문서에서도 혼용</div> <div style="font-size:10.0px; font-weight:800; color:#0f172a; margin-bottom:8px;">정책 문서에서도 혼용</div>
<div style="font-size:10px; line-height:1.55; color:#475569;">• 스마트 건설 활성화 방안: 디지털화 방향 아래 BIM 전면 도입 제시</div> <div style="font-size:10px; line-height:1.55; color:#475569;">• 스마트 건설 활성화 방안: 디지털화 방향 아래 BIM 전면 도입 제시</div>
<div style="font-size:10px; line-height:1.55; color:#475569;">• 제7차 건설기술진흥 기본계획: DX 추진 방향 아래 BIM 도입 실행 과제 제시</div> <div style="font-size:10px; line-height:1.55; color:#475569;">• 제7차 건설기술진흥 기본계획: DX 추진 방향 아래 BIM 도입 실행 과제 제시</div>
</div> </div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
{
"body_bg": {
"passed": false,
"score": 0.7666666666666666,
"errors": [
"누락 문장 (7/10):",
" - \"* 이로인해 BIM기술의 도입을 DX의 완성으로 오인하거나, DX를 BIM 기술 도입 수준으로 한정하는 인식...\"",
" - \"* **[스마트 건설 활성화 방안(2022.07)]**\"",
" - \"* 추진과제 : 건설산업 디지털화\"",
" - \"* 실행과제 : BIM 전면 도입, BIM 전문인력 양성\"",
" - \"* **[제7차 건설기술진흥 기본계획(2023.12)]**\""
]
},
"body_core": {
"passed": false,
"score": 0.8854166666666666,
"errors": [
"누락 문장 (11/32):",
" - \"| **BIM &lt;&lt; DX**(Engineering + Management 통합) | **범위** ...\"",
" - \"| **제작 및 운영**(상용 + 전용 40~80개)[Rhino, Sketchup, Blender..] + ...\"",
" - \"| **공학 정보 및 콘텐츠 연계에 집중****도면, 수량, 시공계획 등 일식** | **성과품** | **...\"",
" - \"| **설계/시공 생산성 혁신**(개념의 재정립) | **활용** | **3D 모델에 의한 일반적 이해 향상...\"",
" - \"| **전 생애주기 활용 시스템** | **확장성** | **(설계/시공/운영) 분야별 단절** |\""
]
},
"sidebar": {
"passed": true,
"score": 1.0,
"errors": []
},
"footer": {
"passed": true,
"score": 1.0,
"errors": []
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,8 @@ import asyncio
import json import json
import sys import sys
from pathlib import Path from pathlib import Path
DESIGN_AGENT_ROOT = Path(r'D:\\ad-hoc\\kei\\design_agent')
DESIGN_AGENT_ROOT = Path(r'D:\ad-hoc\kei\design_agent')
if str(DESIGN_AGENT_ROOT) not in sys.path: if str(DESIGN_AGENT_ROOT) not in sys.path:
sys.path.insert(0, str(DESIGN_AGENT_ROOT)) sys.path.insert(0, str(DESIGN_AGENT_ROOT))
@@ -30,6 +31,7 @@ from src.pipeline_context import (
) )
from src.renderer import render_slide_from_html from src.renderer import render_slide_from_html
from src.slide_measurer import capture_slide_screenshot, measure_rendered_heights from src.slide_measurer import capture_slide_screenshot, measure_rendered_heights
if not hasattr(html_generator, 'SIDEBAR_PROMPT') and hasattr(html_generator, '_LEGACY_SIDEBAR_PROMPT'): if not hasattr(html_generator, 'SIDEBAR_PROMPT') and hasattr(html_generator, '_LEGACY_SIDEBAR_PROMPT'):
html_generator.SIDEBAR_PROMPT = html_generator._LEGACY_SIDEBAR_PROMPT html_generator.SIDEBAR_PROMPT = html_generator._LEGACY_SIDEBAR_PROMPT
if not hasattr(html_generator, 'FOOTER_PROMPT') and hasattr(html_generator, '_LEGACY_FOOTER_PROMPT'): if not hasattr(html_generator, 'FOOTER_PROMPT') and hasattr(html_generator, '_LEGACY_FOOTER_PROMPT'):
@@ -48,10 +50,12 @@ def _load_json(path: Path) -> dict:
return json.loads(path.read_text(encoding='utf-8-sig')) return json.loads(path.read_text(encoding='utf-8-sig'))
def _build_context(content: str, base_path: str, stage1a: dict, stage1b: dict) -> PipelineContext: def _write_json(path: Path, data: dict) -> None:
ctx = create_context(content, base_path) path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
normalized = normalize_mdx_content(content)
def _stage_0(ctx: PipelineContext) -> PipelineContext:
normalized = normalize_mdx_content(ctx.raw_content)
ctx.normalized = NormalizedContent( ctx.normalized = NormalizedContent(
clean_text=normalized['clean_text'], clean_text=normalized['clean_text'],
title=normalized['title'], title=normalized['title'],
@@ -60,7 +64,11 @@ def _build_context(content: str, base_path: str, stage1a: dict, stage1b: dict) -
tables=normalized['tables'], tables=normalized['tables'],
sections=normalized['sections'], sections=normalized['sections'],
) )
ctx.save_snapshot('stage_0')
return ctx
def _stage_1a(ctx: PipelineContext, stage1a: dict) -> PipelineContext:
analysis_raw = stage1a['analysis'] analysis_raw = stage1a['analysis']
ctx.analysis = Analysis( ctx.analysis = Analysis(
core_message=analysis_raw['core_message'], core_message=analysis_raw['core_message'],
@@ -68,15 +76,21 @@ def _build_context(content: str, base_path: str, stage1a: dict, stage1b: dict) -
total_pages=analysis_raw.get('total_pages', 1), total_pages=analysis_raw.get('total_pages', 1),
) )
ctx.page_structure = PageStructure(roles=stage1a['page_structure']) ctx.page_structure = PageStructure(roles=stage1a['page_structure'])
ctx.topics = [Topic(**raw) for raw in stage1a['topics']]
ctx.save_snapshot('stage_1a')
return ctx
def _stage_1b(ctx: PipelineContext, stage1b: dict) -> PipelineContext:
refined_map = {item['topic_id']: item for item in stage1b['concepts']} refined_map = {item['topic_id']: item for item in stage1b['concepts']}
topics = [] topics = []
for raw in stage1a['topics']: for raw in ctx.topics:
merged = dict(raw) merged = raw.model_dump()
if raw['id'] in refined_map: if raw.id in refined_map:
merged.update(refined_map[raw['id']]) merged.update(refined_map[raw.id])
topics.append(Topic(**merged)) topics.append(Topic(**merged))
ctx.topics = topics ctx.topics = topics
ctx.save_snapshot('stage_1b')
return ctx return ctx
@@ -138,6 +152,7 @@ def _stage_1_5a(ctx: PipelineContext) -> PipelineContext:
}) })
ctx.slide_images = slide_images ctx.slide_images = slide_images
ctx.analysis = ctx.analysis.model_copy(update={'image_sizes': image_sizes or {}}) ctx.analysis = ctx.analysis.model_copy(update={'image_sizes': image_sizes or {}})
ctx.save_snapshot('stage_1_5a')
return ctx return ctx
@@ -157,6 +172,7 @@ def _stage_1_7(ctx: PipelineContext) -> PipelineContext:
) )
for role, ref in refs_raw.items() for role, ref in refs_raw.items()
} }
ctx.save_snapshot('stage_1_7')
return ctx return ctx
@@ -184,6 +200,7 @@ def _stage_1_5b(ctx: PipelineContext) -> PipelineContext:
) )
}) })
ctx.containers = updated ctx.containers = updated
ctx.save_snapshot('stage_1_5b')
return ctx return ctx
@@ -218,7 +235,7 @@ async def _stage_2(ctx: PipelineContext) -> PipelineContext:
for role, ci in ctx.containers.items() for role, ci in ctx.containers.items()
}, },
} }
generated, _verification = await generate_with_retry( generated, verification = await generate_with_retry(
content=ctx.raw_content, content=ctx.raw_content,
analysis=analysis_dict, analysis=analysis_dict,
container_specs=container_specs_dict, container_specs=container_specs_dict,
@@ -226,6 +243,16 @@ async def _stage_2(ctx: PipelineContext) -> PipelineContext:
images=ctx.slide_images, images=ctx.slide_images,
) )
ctx.generated_html = generated ctx.generated_html = generated
verification_path = ctx.get_run_dir() / 'stage_2_verification.json'
_write_json(verification_path, {
area: {
'passed': result.passed,
'score': result.score,
'errors': result.errors,
}
for area, result in verification.items()
})
ctx.save_snapshot('stage_2')
return ctx return ctx
@@ -239,15 +266,17 @@ def _stage_3(ctx: PipelineContext) -> PipelineContext:
ctx.rendered_html = render_slide_from_html(ctx.generated_html, analysis_dict, ctx.preset) ctx.rendered_html = render_slide_from_html(ctx.generated_html, analysis_dict, ctx.preset)
if ctx.base_path: if ctx.base_path:
ctx.rendered_html = embed_images(ctx.rendered_html, ctx.base_path) ctx.rendered_html = embed_images(ctx.rendered_html, ctx.base_path)
ctx.save_snapshot('stage_3')
return ctx return ctx
def _stage_4_lite(ctx: PipelineContext) -> PipelineContext: def _stage_4(ctx: PipelineContext) -> PipelineContext:
ctx.measurement = measure_rendered_heights(ctx.rendered_html) ctx.measurement = measure_rendered_heights(ctx.rendered_html)
ctx.screenshot_b64 = capture_slide_screenshot(ctx.rendered_html) or '' ctx.screenshot_b64 = capture_slide_screenshot(ctx.rendered_html) or ''
ctx.quality_score = 100 if not any( ctx.quality_score = 100 if not any(
zone.get('overflowed') for zone in ctx.measurement.get('zones', {}).values() zone.get('overflowed') for zone in ctx.measurement.get('zones', {}).values()
) else 60 ) else 60
ctx.save_snapshot('stage_4')
return ctx return ctx
@@ -264,16 +293,22 @@ async def main() -> None:
stage1a = _load_json(Path(args.stage1a)) stage1a = _load_json(Path(args.stage1a))
stage1b = _load_json(Path(args.stage1b)) stage1b = _load_json(Path(args.stage1b))
ctx = _build_context(content, args.base_path, stage1a, stage1b) out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
ctx = create_context(content, args.base_path)
ctx.run_dir = str(out_dir)
ctx = _stage_0(ctx)
ctx = _stage_1a(ctx, stage1a)
ctx = _stage_1b(ctx, stage1b)
ctx = _stage_1_5a(ctx) ctx = _stage_1_5a(ctx)
ctx = _stage_1_7(ctx) ctx = _stage_1_7(ctx)
ctx = _stage_1_5b(ctx) ctx = _stage_1_5b(ctx)
ctx = await _stage_2(ctx) ctx = await _stage_2(ctx)
ctx = _stage_3(ctx) ctx = _stage_3(ctx)
ctx = _stage_4_lite(ctx) ctx = _stage_4(ctx)
out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / 'generated_html.json').write_text( (out_dir / 'generated_html.json').write_text(
json.dumps(ctx.generated_html, ensure_ascii=False, indent=2), json.dumps(ctx.generated_html, ensure_ascii=False, indent=2),
encoding='utf-8', encoding='utf-8',
@@ -287,10 +322,11 @@ async def main() -> None:
ctx.model_dump_json(indent=2, exclude={'screenshot_b64', 'rendered_html'}), ctx.model_dump_json(indent=2, exclude={'screenshot_b64', 'rendered_html'}),
encoding='utf-8', encoding='utf-8',
) )
(out_dir / 'final_context.json').write_text(
ctx.model_dump_json(indent=2, exclude={'screenshot_b64', 'rendered_html'}),
encoding='utf-8',
)
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(main()) asyncio.run(main())