코워크 PPT 생성 경로를 공통 품질 루프로 고도화하고 자동 보정 단계를 추가

- Cowork 프롬프트와 direct-creation 탐색 정책에서 PPT 요청은 document_plan을 무조건 선행하지 않고 pptx_create를 우선하도록 조정했습니다.

- DeckPlanningService에 RefineForQuality 루프를 추가해 executive summary, comparison, roadmap, chart, KPI dashboard 슬라이드의 headline, takeaway, verdict, timeline/owner, KPI narrative를 자동 보강하도록 했습니다.

- PptxSkill이 초기 deck review가 약할 때 한 번 더 refined deck을 만들고 실제로 점수와 경고가 개선된 경우에만 최종 렌더링에 반영하도록 수정했습니다.

- DeckPlanningServiceTests와 PptxSkillAutoRepairTests를 확장해 generic PPT 품질 보정 회귀를 고정했습니다.

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_generic_quality\\ -p:IntermediateOutputPath=obj\\verify_ppt_generic_quality\\ (경고 0 / 오류 0)

- 검증: deck planning, pptx auto repair, golden deck, deck quality review 테스트 통과 14
This commit is contained in:
2026-04-15 15:17:12 +09:00
parent 99990b9778
commit 791d172850
8 changed files with 790 additions and 19 deletions

View File

@@ -98,4 +98,97 @@ public class DeckPlanningServiceTests
layouts.Should().Contain("comparison");
layouts.Should().Contain("roadmap");
}
[Fact]
public void RefineForQuality_ShouldStrengthenWeakSpecializedSlides()
{
using var args = JsonDocument.Parse(
"""
{
"title": "Executive Operating Review",
"objective": "improve execution quality",
"decision_ask": "approve the first delivery wave",
"slides": [
{
"layout": "executive_summary",
"title": "Executive Summary",
"body": [
"Improve execution quality",
"Approve the first delivery wave"
]
},
{
"layout": "comparison",
"title": "Execution Options",
"options": [
{ "name": "Pilot" },
{ "name": "Wave rollout" }
]
},
{
"layout": "roadmap",
"title": "Roadmap",
"phases": [
{ "title": "Kickoff", "owner": "담당 미정" }
]
},
{
"layout": "chart",
"title": "Trend",
"chart_labels": ["Q1", "Q2", "Q3"],
"chart_values": [100, 120, 138]
},
{
"layout": "kpi_dashboard",
"title": "KPI Snapshot",
"kpis": [
{ "label": "Revenue", "value": "+12%" },
{ "label": "Cycle Time", "value": "-8%" },
{ "label": "Quality", "value": "97%" }
]
}
]
}
""");
using var prepared = DeckPlanningService.Prepare(args.RootElement, args.RootElement.GetProperty("slides"), "Executive Operating Review");
var review = DeckQualityReviewService.ReviewDeck(
"Executive Operating Review",
prepared.Slides,
hasTemplate: false,
prepared.AutoRepairCount,
prepared.Storyline);
using var refined = DeckPlanningService.RefineForQuality(prepared, review, "Executive Operating Review");
var slides = refined.Slides.EnumerateArray().ToList();
var executive = slides.First(slide => slide.GetProperty("layout").GetString() == "executive_summary");
executive.TryGetProperty("summary_points", out var executivePoints).Should().BeTrue();
executivePoints.GetArrayLength().Should().BeGreaterThan(0);
executive.TryGetProperty("kpis", out var executiveKpis).Should().BeTrue();
executiveKpis.GetArrayLength().Should().BeGreaterThan(0);
var comparison = slides.First(slide => slide.GetProperty("layout").GetString() == "comparison");
comparison.TryGetProperty("options", out var options).Should().BeTrue();
options[0].GetProperty("verdict").GetString().Should().NotBeNullOrWhiteSpace();
options[0].GetProperty("summary").GetString().Should().NotBeNullOrWhiteSpace();
var roadmap = slides.First(slide => slide.GetProperty("layout").GetString() == "roadmap");
var firstPhase = roadmap.GetProperty("phases")[0];
firstPhase.GetProperty("owner").GetString().Should().NotContain("미정");
firstPhase.GetProperty("timeline").GetString().Should().NotBeNullOrWhiteSpace();
firstPhase.GetProperty("detail").GetString().Should().NotBeNullOrWhiteSpace();
var chart = slides.First(slide => slide.GetProperty("layout").GetString() == "chart");
chart.TryGetProperty("summary_points", out var chartPoints).Should().BeTrue();
chartPoints.GetArrayLength().Should().BeGreaterThan(0);
var kpi = slides.First(slide => slide.GetProperty("layout").GetString() == "kpi_dashboard");
kpi.TryGetProperty("summary_points", out var kpiPoints).Should().BeTrue();
kpiPoints.GetArrayLength().Should().BeGreaterThan(0);
kpi.GetProperty("kpis")[0].GetProperty("trend").GetString().Should().NotBeNullOrWhiteSpace();
kpi.GetProperty("kpis")[0].GetProperty("note").GetString().Should().NotBeNullOrWhiteSpace();
refined.AutoRepairCount.Should().BeGreaterThan(prepared.AutoRepairCount);
}
}

View File

@@ -75,5 +75,91 @@ public class PptxSkillAutoRepairTests
}
}
}
[Fact]
public async Task ExecuteAsync_ShouldRefineWeakDeckBeforeExport()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-pptx-refine-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var tool = new PptxSkill();
var args = JsonDocument.Parse(
"""
{
"path": "refined-deck.pptx",
"title": "Quarterly Steering Deck",
"objective": "approve the next delivery wave",
"slides": [
{
"layout": "executive_summary",
"title": "Executive Summary",
"body": [
"Approve the next delivery wave",
"The delivery plan should move forward"
]
},
{
"layout": "comparison",
"title": "Options",
"options": [
{ "name": "Conservative" },
{ "name": "Balanced" }
]
},
{
"layout": "roadmap",
"title": "Roadmap",
"phases": [
{ "title": "Kickoff", "owner": "담당 미정" }
]
},
{
"layout": "chart",
"title": "Trend",
"chart_labels": ["Jan", "Feb", "Mar"],
"chart_values": [10, 13, 16]
},
{
"layout": "kpi_dashboard",
"title": "KPI Snapshot",
"kpis": [
{ "label": "Revenue", "value": "+9%" },
{ "label": "Cycle Time", "value": "-6%" },
{ "label": "Stability", "value": "96%" }
]
}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
result.Output.Should().Contain("PPT quality");
result.Output.Should().Contain("Needs work: none");
result.Output.Should().Contain("Auto-repair:");
File.Exists(Path.Combine(workDir, "refined-deck.pptx")).Should().BeTrue();
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}