PPT 생성 고도화 3차를 반영하고 deck planning·quality gate를 추가
- DeckPlanningService와 DeckQualityReviewService를 추가해 deck brief 정규화, consulting storyline 보강, 누락된 Executive Summary/Recommendation/Roadmap/Appendix 자동 보강, deck-level 품질 점수와 경고 계산을 지원합니다. - PptxSkill에 audience/objective/decision_ask/storyline 파라미터를 추가하고, issue_tree/before_after/decision_matrix/risk_heatmap/benefit_waterfall/operating_model/appendix_evidence 레이아웃을 네이티브 슬라이드 타입으로 정규화한 뒤 planning summary와 quality summary를 함께 반환하도록 보강했습니다. - pptx-creator 및 strategy-deck/board-update/pmo-steering/sales-review-deck/operating-model-deck 번들 스킬을 추가·정리하고, DeckPlanningServiceTests/DeckQualityReviewServiceTests/PptxSkillAutoRepairTests로 회귀 검증을 보강했습니다. - README.md와 docs/DEVELOPMENT.md에 2026-04-14 21:50, 22:00 (KST) 기준 변경 이력과 검증 결과를 반영했습니다. - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_ppt_phase3\ -p:IntermediateOutputPath=obj\verify_ppt_phase3\ (경고 0 / 오류 0) - 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter 'DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests' -p:OutputPath=bin\verify_ppt_phase3_tests\ -p:IntermediateOutputPath=obj\verify_ppt_phase3_tests\ (통과 5)
This commit is contained in:
12
README.md
12
README.md
@@ -1782,3 +1782,15 @@ MIT License
|
||||
- 테스트로 [DocumentAssemblerSemanticTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerSemanticTests.cs), [ExcelSkillSummarySheetTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillSummarySheetTests.cs), [HtmlSkillConsultingSectionsTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs), [DocumentPlannerBusinessDocumentTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentPlannerBusinessDocumentTests.cs)를 추가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase1\\ -p:IntermediateOutputPath=obj\\verify_doc_phase1\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerSemanticTests|ExcelSkillSummarySheetTests|HtmlSkillConsultingSectionsTests|DocumentPlannerBusinessDocumentTests|DocumentPlannerPresentationTests" -p:OutputPath=bin\\verify_doc_phase1_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase1_tests\\` 통과 5
|
||||
- 업데이트: 2026-04-14 21:50 (KST)
|
||||
- PPT 생성 고도화 3차를 반영했습니다. [DeckPlanningService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckPlanningService.cs)는 `audience`, `objective`, `decision_ask`, `storyline` 힌트를 바탕으로 슬라이드를 생성 전에 정규화하고, 누락된 `title`, `Executive Summary`, `Recommendation`, `Roadmap`, `Appendix`를 자동 보강합니다.
|
||||
- [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 deck 단위 품질 점수를 계산해 `Executive Summary`, `Recommendation`, 근거 슬라이드, 텍스트 과밀, placeholder 잔존 여부를 점검합니다.
|
||||
- [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)는 `issue_tree`, `before_after`, `decision_matrix`, `risk_heatmap`, `benefit_waterfall`, `operating_model`, `appendix_evidence` 같은 컨설팅형 레이아웃을 생성 전에 네이티브 레이아웃으로 정규화하고, 결과 메시지에 deck planning/quality review 요약을 함께 반환합니다.
|
||||
- [pptx-creator.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pptx-creator.skill.md)는 `document_plan -> deck brief -> pptx_create` 흐름을 중심으로 재작성했고, [strategy-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/strategy-deck.skill.md), [board-update.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/board-update.skill.md), [pmo-steering.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pmo-steering.skill.md), [sales-review-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/sales-review-deck.skill.md), [operating-model-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/operating-model-deck.skill.md)를 추가해 목적형 deck 진입점을 늘렸습니다.
|
||||
- 테스트로 [DeckPlanningServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [PptxSkillAutoRepairTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillAutoRepairTests.cs)를 추가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\` 통과 5
|
||||
????: 2026-04-14 22:00 (KST)
|
||||
- PPT 3? ?? ??? ??????. DeckPlanningService, DeckQualityReviewService, PptxSkill? planning/quality summary ??? ??? PPT ?? ?? ??? ???? ?? ??? ?? ?? ??? ??? ?????.
|
||||
- ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\ ?? 0 / ?? 0
|
||||
- ??: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\ ?? 5
|
||||
|
||||
@@ -852,3 +852,13 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
||||
- 테스트: `DocumentAssemblerSemanticTests`, `ExcelSkillSummarySheetTests`, `HtmlSkillConsultingSectionsTests`, `DocumentPlannerBusinessDocumentTests` 추가
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase1\\ -p:IntermediateOutputPath=obj\\verify_doc_phase1\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerSemanticTests|ExcelSkillSummarySheetTests|HtmlSkillConsultingSectionsTests|DocumentPlannerBusinessDocumentTests|DocumentPlannerPresentationTests" -p:OutputPath=bin\\verify_doc_phase1_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase1_tests\\` 통과 5
|
||||
|
||||
업데이트: 2026-04-14 21:50 (KST)
|
||||
- PPT 생성 고도화 3차를 반영했습니다. `DeckPlanningService`를 추가해 deck brief 정규화, consulting storyline 보강, 누락된 `Executive Summary`/`Recommendation`/`Roadmap`/`Appendix` 자동 삽입, 레이아웃 alias 정규화를 내부 파이프라인으로 처리합니다.
|
||||
- `DeckQualityReviewService`를 추가해 deck-level 품질 점수와 경고를 계산합니다. 템플릿 사용 여부, 레이아웃 다양성, executive summary/recommendation 유무, 텍스트 과밀, 근거 슬라이드 부족, placeholder 잔존을 함께 점검합니다.
|
||||
- `PptxSkill`은 `audience`, `objective`, `decision_ask`, `storyline` 파라미터를 추가했고, `issue_tree`, `before_after`, `decision_matrix`, `risk_heatmap`, `benefit_waterfall`, `operating_model`, `appendix_evidence` 같은 상위 deck 레이아웃을 네이티브 슬라이드 타입으로 자동 정규화해 렌더링 전에 보정합니다.
|
||||
- 결과 메시지도 고도화했습니다. `pptx_create` 실행 후 파일 경로만 반환하던 흐름에서, 이제 planning summary와 deck quality summary를 함께 반환해 모델과 사용자가 결과물 완성도를 바로 확인할 수 있습니다.
|
||||
- `pptx-creator.skill.md`를 deck planning 중심으로 재작성했고, `strategy-deck`, `board-update`, `pmo-steering`, `sales-review-deck`, `operating-model-deck` 번들 스킬을 추가해 목적형 deck 생성 진입점을 늘렸습니다.
|
||||
- 테스트로 `DeckPlanningServiceTests`, `DeckQualityReviewServiceTests`, `PptxSkillAutoRepairTests`를 추가했고, 기존 `PptxSkillConsultingDeckTests`와 함께 검증했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\` 통과 5
|
||||
|
||||
54
src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs
Normal file
54
src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class DeckPlanningServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Prepare_ShouldInjectConsultingStructure_WhenKeySlidesAreMissing()
|
||||
{
|
||||
using var args = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"title": "Growth Plan",
|
||||
"objective": "growth strategy recommendation",
|
||||
"decision_ask": "approve phase-1 funding",
|
||||
"slides": [
|
||||
{
|
||||
"layout": "content",
|
||||
"title": "Current State",
|
||||
"body": [
|
||||
"Top-line growth has slowed in the enterprise segment.",
|
||||
"Retention is declining in two priority cohorts.",
|
||||
"Manual CRM operations drive avoidable cost."
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "comparison",
|
||||
"title": "Options",
|
||||
"options": [
|
||||
{ "name": "Option A", "pros": "Fast", "cons": "Shallow", "verdict": "Fastest" },
|
||||
{ "name": "Option B", "pros": "Balanced", "cons": "Requires alignment", "verdict": "Recommended" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
using var result = DeckPlanningService.Prepare(args.RootElement, args.RootElement.GetProperty("slides"), "Growth Plan");
|
||||
var slides = result.Slides.EnumerateArray().ToList();
|
||||
var layouts = slides.Select(slide => slide.GetProperty("layout").GetString()).ToList();
|
||||
var titles = slides.Select(slide => slide.GetProperty("title").GetString()).ToList();
|
||||
|
||||
layouts.Should().Contain("title");
|
||||
layouts.Should().Contain("executive_summary");
|
||||
layouts.Should().Contain("recommendation");
|
||||
layouts.Should().Contain("roadmap");
|
||||
titles.Should().Contain(title => title != null && title.Contains("Appendix", StringComparison.OrdinalIgnoreCase));
|
||||
result.AutoRepairCount.Should().BeGreaterThan(0);
|
||||
result.Storyline.Should().NotBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class DeckQualityReviewServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReviewDeck_ShouldFlagMissingExecutiveSummaryAndRecommendation()
|
||||
{
|
||||
using var slides = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"layout": "content",
|
||||
"title": "Current State",
|
||||
"body": "Point 1\nPoint 2\nPoint 3\nPoint 4\nPoint 5\nPoint 6\nPoint 7"
|
||||
},
|
||||
{
|
||||
"layout": "content",
|
||||
"title": "Findings",
|
||||
"body": "Observation A\nObservation B\nObservation C"
|
||||
}
|
||||
]
|
||||
""");
|
||||
|
||||
var review = DeckQualityReviewService.ReviewDeck("Weak Deck", slides.RootElement, hasTemplate: false, autoRepairCount: 0);
|
||||
|
||||
review.Score.Should().BeLessThan(75);
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("Executive Summary"));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("Recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReviewDeck_ShouldRewardBalancedConsultingDeck()
|
||||
{
|
||||
using var slides = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{ "layout": "title", "title": "Growth Strategy" },
|
||||
{ "layout": "executive_summary", "title": "Executive Summary", "headline": "Headline", "summary_points": ["A","B","C"], "recommendation": "Do B" },
|
||||
{ "layout": "comparison", "title": "Options", "headline": "Compare", "options": [{ "name": "A", "pros": "Fast", "cons": "Shallow", "verdict": "Fastest" }, { "name": "B", "pros": "Balanced", "cons": "Needs alignment", "verdict": "Recommended" }] },
|
||||
{ "layout": "roadmap", "title": "Roadmap", "headline": "Execute in three phases" },
|
||||
{ "layout": "recommendation", "title": "Recommendation", "recommendation": "Approve phase-1", "summary_points": ["Reason 1", "Reason 2"] },
|
||||
{ "layout": "table", "title": "Appendix & Evidence", "headers": ["Evidence","Detail"], "rows": [["Metric","Value"]] }
|
||||
]
|
||||
""");
|
||||
|
||||
var review = DeckQualityReviewService.ReviewDeck("Strong Deck", slides.RootElement, hasTemplate: true, autoRepairCount: 2, storyline: ["Executive Summary", "Options", "Roadmap"]);
|
||||
|
||||
review.Score.Should().BeGreaterThan(80);
|
||||
review.Strengths.Should().Contain(strength => strength.Contains("Executive Summary"));
|
||||
review.Issues.Should().NotContain(issue => issue.Severity == DeckReviewSeverity.Critical);
|
||||
}
|
||||
}
|
||||
79
src/AxCopilot.Tests/Services/PptxSkillAutoRepairTests.cs
Normal file
79
src/AxCopilot.Tests/Services/PptxSkillAutoRepairTests.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class PptxSkillAutoRepairTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ShouldAugmentDeckAndReturnQualitySummary()
|
||||
{
|
||||
var workDir = Path.Combine(Path.GetTempPath(), "ax-pptx-repair-" + 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": "auto-repair-deck.pptx",
|
||||
"title": "Operating Improvement",
|
||||
"objective": "operating model proposal",
|
||||
"decision_ask": "approve phase-1 launch",
|
||||
"slides": [
|
||||
{
|
||||
"layout": "before_after",
|
||||
"title": "Current vs Future",
|
||||
"before": ["Manual coordination", "Slow approvals", "Fragmented KPI tracking"],
|
||||
"after": ["Standard workflow", "Faster approvals", "Single KPI view"]
|
||||
},
|
||||
{
|
||||
"layout": "benefit_waterfall",
|
||||
"title": "Benefits",
|
||||
"benefits": [
|
||||
{ "label": "Cost", "value": 8 },
|
||||
{ "label": "Speed", "value": 12 },
|
||||
{ "label": "Quality", "value": 6 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""").RootElement;
|
||||
|
||||
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||
var outputPath = Path.Combine(workDir, "auto-repair-deck.pptx");
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Output.Should().Contain("PPT quality");
|
||||
result.Output.Should().Contain("Storyline:");
|
||||
File.Exists(outputPath).Should().BeTrue();
|
||||
|
||||
using var doc = PresentationDocument.Open(outputPath, false);
|
||||
doc.PresentationPart!.Presentation.SlideIdList!.Count().Should().BeGreaterThanOrEqualTo(6);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(workDir))
|
||||
Directory.Delete(workDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
806
src/AxCopilot/Services/Agent/DeckPlanningService.cs
Normal file
806
src/AxCopilot/Services/Agent/DeckPlanningService.cs
Normal file
@@ -0,0 +1,806 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public sealed class DeckPreparationResult : IDisposable
|
||||
{
|
||||
public required JsonDocument SlidesDocument { get; init; }
|
||||
public required IReadOnlyList<string> Storyline { get; init; }
|
||||
public required int AutoRepairCount { get; init; }
|
||||
public string? Audience { get; init; }
|
||||
public string? Objective { get; init; }
|
||||
public string? DecisionAsk { get; init; }
|
||||
public string? SuggestedTheme { get; init; }
|
||||
public bool InjectedTitleSlide { get; init; }
|
||||
public bool InjectedExecutiveSummary { get; init; }
|
||||
public bool InjectedRecommendation { get; init; }
|
||||
public bool InjectedRoadmap { get; init; }
|
||||
public bool InjectedAppendix { get; init; }
|
||||
|
||||
public JsonElement Slides => SlidesDocument.RootElement;
|
||||
|
||||
public string ToToolSummary()
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(Audience))
|
||||
parts.Add($"Audience: {Audience}");
|
||||
if (!string.IsNullOrWhiteSpace(Objective))
|
||||
parts.Add($"Objective: {Objective}");
|
||||
if (!string.IsNullOrWhiteSpace(DecisionAsk))
|
||||
parts.Add($"Ask: {DecisionAsk}");
|
||||
var injected = new List<string>();
|
||||
if (InjectedTitleSlide) injected.Add("Title");
|
||||
if (InjectedExecutiveSummary) injected.Add("Executive Summary");
|
||||
if (InjectedRecommendation) injected.Add("Recommendation");
|
||||
if (InjectedRoadmap) injected.Add("Roadmap");
|
||||
if (InjectedAppendix) injected.Add("Appendix");
|
||||
if (injected.Count > 0)
|
||||
parts.Add("Added: " + string.Join(", ", injected));
|
||||
if (AutoRepairCount > 0)
|
||||
parts.Add($"Auto-repair: {AutoRepairCount}");
|
||||
if (Storyline.Count > 0)
|
||||
parts.Add("Storyline: " + string.Join(" -> ", Storyline.Take(6)));
|
||||
return string.Join(" | ", parts);
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
SlidesDocument.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeckPlanningService
|
||||
{
|
||||
public static DeckPreparationResult Prepare(
|
||||
JsonElement args,
|
||||
JsonElement slides,
|
||||
string presentationTitle)
|
||||
{
|
||||
var audience = ReadString(args, "audience");
|
||||
var objective = ReadString(args, "objective");
|
||||
var decisionAsk = ReadString(args, "decision_ask");
|
||||
var storylineHint = ReadHintList(args, "storyline", "storyline_hint", "deck_outline");
|
||||
|
||||
var slideNodes = ParseSlides(slides);
|
||||
var autoRepairCount = 0;
|
||||
|
||||
for (var i = 0; i < slideNodes.Count; i++)
|
||||
autoRepairCount += NormalizeSlide(slideNodes[i], i, presentationTitle, objective, decisionAsk);
|
||||
|
||||
var hasTitle = slideNodes.Any(slide => string.Equals(ReadString(slide, "layout"), "title", StringComparison.OrdinalIgnoreCase));
|
||||
var hasExecutiveSummary = slideNodes.Any(slide => string.Equals(ReadString(slide, "layout"), "executive_summary", StringComparison.OrdinalIgnoreCase));
|
||||
var hasRecommendation = slideNodes.Any(slide => string.Equals(ReadString(slide, "layout"), "recommendation", StringComparison.OrdinalIgnoreCase));
|
||||
var hasRoadmap = slideNodes.Any(slide => string.Equals(ReadString(slide, "layout"), "roadmap", StringComparison.OrdinalIgnoreCase));
|
||||
var hasAppendix = slideNodes.Any(slide =>
|
||||
{
|
||||
var title = ReadString(slide, "title");
|
||||
return title.Contains("appendix", StringComparison.OrdinalIgnoreCase) ||
|
||||
title.Contains("\uBD80\uB85D", StringComparison.OrdinalIgnoreCase) ||
|
||||
title.Contains("evidence", StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
var injectedTitle = false;
|
||||
var injectedExecutiveSummary = false;
|
||||
var injectedRecommendation = false;
|
||||
var injectedRoadmap = false;
|
||||
var injectedAppendix = false;
|
||||
|
||||
if (!hasTitle && slideNodes.Count > 0)
|
||||
{
|
||||
slideNodes.Insert(0, BuildTitleSlide(presentationTitle, audience, objective));
|
||||
injectedTitle = true;
|
||||
autoRepairCount++;
|
||||
}
|
||||
|
||||
if (!hasExecutiveSummary)
|
||||
{
|
||||
var insertAt = injectedTitle || (slideNodes.Count > 0 && string.Equals(ReadString(slideNodes[0], "layout"), "title", StringComparison.OrdinalIgnoreCase))
|
||||
? 1
|
||||
: 0;
|
||||
slideNodes.Insert(insertAt, BuildExecutiveSummarySlide(slideNodes, objective, decisionAsk));
|
||||
injectedExecutiveSummary = true;
|
||||
autoRepairCount++;
|
||||
}
|
||||
|
||||
if (!hasRecommendation && slideNodes.Count >= 3)
|
||||
{
|
||||
slideNodes.Add(BuildRecommendationSlide(slideNodes, objective, decisionAsk));
|
||||
injectedRecommendation = true;
|
||||
autoRepairCount++;
|
||||
}
|
||||
|
||||
if (!hasRoadmap && slideNodes.Count >= 4)
|
||||
{
|
||||
slideNodes.Add(BuildRoadmapSlide(decisionAsk));
|
||||
injectedRoadmap = true;
|
||||
autoRepairCount++;
|
||||
}
|
||||
|
||||
if (!hasAppendix && slideNodes.Count >= 6)
|
||||
{
|
||||
slideNodes.Add(BuildAppendixSlide(slideNodes));
|
||||
injectedAppendix = true;
|
||||
autoRepairCount++;
|
||||
}
|
||||
|
||||
for (var i = 0; i < slideNodes.Count; i++)
|
||||
autoRepairCount += NormalizeSlide(slideNodes[i], i, presentationTitle, objective, decisionAsk);
|
||||
|
||||
var storyline = BuildStoryline(slideNodes, storylineHint);
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(ToJsonArray(slideNodes));
|
||||
var document = JsonDocument.Parse(json);
|
||||
|
||||
return new DeckPreparationResult
|
||||
{
|
||||
SlidesDocument = document,
|
||||
Storyline = storyline,
|
||||
AutoRepairCount = autoRepairCount,
|
||||
Audience = audience,
|
||||
Objective = objective,
|
||||
DecisionAsk = decisionAsk,
|
||||
SuggestedTheme = SuggestTheme(objective, audience),
|
||||
InjectedTitleSlide = injectedTitle,
|
||||
InjectedExecutiveSummary = injectedExecutiveSummary,
|
||||
InjectedRecommendation = injectedRecommendation,
|
||||
InjectedRoadmap = injectedRoadmap,
|
||||
InjectedAppendix = injectedAppendix,
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizeSlide(
|
||||
JsonObject slide,
|
||||
int slideIndex,
|
||||
string presentationTitle,
|
||||
string? objective,
|
||||
string? decisionAsk)
|
||||
{
|
||||
var repairs = 0;
|
||||
var layout = ReadString(slide, "layout");
|
||||
if (string.IsNullOrWhiteSpace(layout))
|
||||
{
|
||||
slide["layout"] = "content";
|
||||
layout = "content";
|
||||
repairs++;
|
||||
}
|
||||
|
||||
switch (layout.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "issue_tree":
|
||||
slide["layout"] = "two_column";
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Issue Tree");
|
||||
slide["left"] = ToMultiline(slide, "issues", "left", "body");
|
||||
slide["right"] = ToMultiline(slide, "implications", "next_steps", "right");
|
||||
repairs++;
|
||||
break;
|
||||
|
||||
case "before_after":
|
||||
slide["layout"] = "two_column";
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Before / After");
|
||||
slide["left"] = Coalesce(ToMultiline(slide, "before", "current_state", "left"), "Before state");
|
||||
slide["right"] = Coalesce(ToMultiline(slide, "after", "future_state", "right"), "After state");
|
||||
repairs++;
|
||||
break;
|
||||
|
||||
case "decision_matrix":
|
||||
slide["layout"] = "comparison";
|
||||
if (!slide.ContainsKey("options") && slide.TryGetPropertyValue("choices", out var choicesNode))
|
||||
slide["options"] = choicesNode?.DeepClone();
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Decision Matrix");
|
||||
repairs++;
|
||||
break;
|
||||
|
||||
case "risk_heatmap":
|
||||
slide["layout"] = "table";
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Risk Overview");
|
||||
EnsureRiskRows(slide);
|
||||
repairs++;
|
||||
break;
|
||||
|
||||
case "benefit_waterfall":
|
||||
slide["layout"] = "chart";
|
||||
slide["chart_type"] = "bar";
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Benefit Waterfall");
|
||||
EnsureChartFromBenefits(slide);
|
||||
repairs++;
|
||||
break;
|
||||
|
||||
case "operating_model":
|
||||
slide["layout"] = "two_column";
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Operating Model");
|
||||
slide["left"] = Coalesce(ToMultiline(slide, "current_state", "capabilities", "left"), "Current model");
|
||||
slide["right"] = Coalesce(ToMultiline(slide, "target_state", "changes", "right"), "Target model");
|
||||
repairs++;
|
||||
break;
|
||||
|
||||
case "appendix_evidence":
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Appendix");
|
||||
if (slide.TryGetPropertyValue("evidence", out var evidenceNode) && evidenceNode is JsonArray evidence && evidence.Count > 0)
|
||||
{
|
||||
slide["layout"] = "table";
|
||||
slide["headers"] = new JsonArray("Evidence", "Detail", "Source");
|
||||
slide["rows"] = BuildEvidenceRows(evidence);
|
||||
}
|
||||
else
|
||||
{
|
||||
slide["layout"] = "content";
|
||||
slide["body"] = ToMultiline(slide, "summary_points", "body");
|
||||
}
|
||||
repairs++;
|
||||
break;
|
||||
}
|
||||
|
||||
repairs += ClampArray(slide, "summary_points", 4);
|
||||
repairs += ClampArray(slide, "next_steps", 3);
|
||||
repairs += ClampArray(slide, "options", 3);
|
||||
repairs += ClampArray(slide, "kpis", 4);
|
||||
repairs += ClampArray(slide, "phases", 4);
|
||||
repairs += ClampArray(slide, "rows", 6);
|
||||
|
||||
var normalizedLayout = ReadString(slide, "layout").ToLowerInvariant();
|
||||
if (normalizedLayout is "content" or "title" or "section" or "two_column" or "quote")
|
||||
repairs += NormalizeMultilineText(slide, normalizedLayout);
|
||||
|
||||
if (normalizedLayout == "chart" && !HasChartData(slide))
|
||||
{
|
||||
slide["layout"] = "comparison";
|
||||
slide["title"] = Coalesce(ReadString(slide, "title"), "Decision Comparison");
|
||||
slide["headline"] = Coalesce(ReadString(slide, "headline"), Coalesce(objective, "?듭떖 ??덉쓣 鍮꾧탳?⑸땲??"));
|
||||
EnsureFallbackOptions(slide);
|
||||
repairs++;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ReadString(slide, "headline")) &&
|
||||
normalizedLayout is "executive_summary" or "comparison" or "recommendation" or "roadmap" or "kpi_dashboard")
|
||||
{
|
||||
slide["headline"] = BuildHeadline(slide, slideIndex, presentationTitle, objective, decisionAsk);
|
||||
repairs++;
|
||||
}
|
||||
|
||||
if (normalizedLayout == "recommendation" && string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
|
||||
{
|
||||
slide["recommendation"] = Coalesce(decisionAsk, objective, "利됱떆 ?ㅽ뻾??沅뚭퀬?덉쓣 ??臾몄옣?쇰줈 ?쒖떆?⑸땲??");
|
||||
repairs++;
|
||||
}
|
||||
|
||||
if (normalizedLayout == "title" && string.IsNullOrWhiteSpace(ReadString(slide, "subtitle")))
|
||||
{
|
||||
slide["subtitle"] = Coalesce(objective, decisionAsk, "?듭떖 硫붿떆吏? ?섏궗寃곗젙 ?ъ씤?몃? ?뺣━?⑸땲??");
|
||||
repairs++;
|
||||
}
|
||||
|
||||
return repairs;
|
||||
}
|
||||
|
||||
private static int NormalizeMultilineText(JsonObject slide, string normalizedLayout)
|
||||
{
|
||||
var repairs = 0;
|
||||
if (normalizedLayout == "content")
|
||||
{
|
||||
var lines = ReadTextLines(slide, "body");
|
||||
if (lines.Count > 0)
|
||||
{
|
||||
var compact = string.Join("\n", lines.Take(5).Select(TrimBullet));
|
||||
if (!string.Equals(compact, ReadString(slide, "body"), StringComparison.Ordinal))
|
||||
{
|
||||
slide["body"] = compact;
|
||||
repairs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedLayout == "two_column")
|
||||
{
|
||||
foreach (var key in new[] { "left", "right" })
|
||||
{
|
||||
var lines = ReadTextLines(slide, key);
|
||||
if (lines.Count == 0)
|
||||
continue;
|
||||
|
||||
var compact = string.Join("\n", lines.Take(5).Select(TrimBullet));
|
||||
if (!string.Equals(compact, ReadString(slide, key), StringComparison.Ordinal))
|
||||
{
|
||||
slide[key] = compact;
|
||||
repairs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return repairs;
|
||||
}
|
||||
|
||||
private static JsonObject BuildTitleSlide(string title, string? audience, string? objective)
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["layout"] = "title",
|
||||
["title"] = Coalesce(title, "Presentation"),
|
||||
["subtitle"] = Coalesce(objective, audience, "Core message and decision ask"),
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject BuildExecutiveSummarySlide(IReadOnlyList<JsonObject> slides, string? objective, string? decisionAsk)
|
||||
{
|
||||
var findings = ExtractEvidenceLines(slides, 3);
|
||||
var kpis = ExtractMetrics(slides, 3);
|
||||
return new JsonObject
|
||||
{
|
||||
["layout"] = "executive_summary",
|
||||
["title"] = "Executive Summary",
|
||||
["headline"] = Coalesce(decisionAsk, objective, "?듭떖 ?ㅽ뻾?덉쓣 ?곗꽑 ?곸슜?섎㈃ ?④린 ?깃낵? 以묒옣湲??뺤옣?깆쓣 ?숈떆???뺣낫?????덉뒿?덈떎."),
|
||||
["summary_points"] = ToJsonArray(findings.Count > 0
|
||||
? findings
|
||||
: ["?듭떖 ?댁뒋? ?ㅽ뻾 ?곗꽑?쒖쐞瑜????덉뿉 ?뺣━?덉뒿?덈떎.", "利됱떆 ?ㅽ뻾 怨쇱젣? 以묎린 怨쇱젣瑜?援щ텇?덉뒿?덈떎.", "寃쎌쁺吏??섏궗寃곗젙???꾪븳 ?듭떖 洹쇨굅瑜??뺤텞?덉뒿?덈떎."]),
|
||||
["recommendation"] = Coalesce(decisionAsk, "?곗꽑?쒖쐞媛 ?믪? 媛쒖꽑?덉쓣 癒쇱? ?ㅽ뻾?섍퀬, 蹂묓뻾 怨쇱젣???④퀎?곸쑝濡??뺤옣?⑸땲??"),
|
||||
["kpis"] = ToMetricArray(kpis),
|
||||
["next_steps"] = ToJsonArray(new[]
|
||||
{
|
||||
"?듭떖 ?섏궗寃곗젙 1嫄댁쓣 ?뺤젙?⑸땲??",
|
||||
"1李??ㅽ뻾 踰붿쐞? 梨낆엫 二쇱껜瑜??뺥빀?덈떎.",
|
||||
"?깃낵 異붿쟻 KPI瑜??붽컙 ?⑥쐞濡?由щ럭?⑸땲??",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject BuildRecommendationSlide(IReadOnlyList<JsonObject> slides, string? objective, string? decisionAsk)
|
||||
{
|
||||
var reasons = ExtractEvidenceLines(slides, 4);
|
||||
return new JsonObject
|
||||
{
|
||||
["layout"] = "recommendation",
|
||||
["title"] = "Recommendation",
|
||||
["headline"] = Coalesce(decisionAsk, objective, "媛???④낵媛 ???ㅽ뻾?덉쓣 癒쇱? ?좏깮?댁빞 ?⑸땲??"),
|
||||
["recommendation"] = Coalesce(decisionAsk, "媛移섍? ?믪? ?곗꽑 怨쇱젣??吏묒쨷?섍퀬, ??⑥쑉 ??ぉ? 異뺤냼 ?먮뒗 ?⑥닚?뷀빀?덈떎."),
|
||||
["summary_points"] = ToJsonArray(reasons.Count > 0
|
||||
? reasons
|
||||
: ["?④린 ?④낵? ?ㅽ뻾 媛?μ꽦???④퍡 怨좊젮?덉뒿?덈떎.", "由ъ뒪???鍮??④낵媛 ?믪? ??덉쓣 ?곗꽑?덉뒿?덈떎.", "?섏궗寃곗젙 ?댄썑 諛붾줈 ?ㅽ뻾 媛?ν븳 ?≪뀡???꾩텧?덉뒿?덈떎."]),
|
||||
["next_steps"] = ToJsonArray(new[]
|
||||
{
|
||||
"?뱀씤 踰붿쐞瑜??뺤젙?⑸땲??",
|
||||
"梨낆엫?먯? ?쇱젙 湲곗??좎쓣 ?ㅼ젙?⑸땲??",
|
||||
"珥덇린 ?깃낵瑜?2~4二??⑥쐞濡??먭??⑸땲??",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject BuildRoadmapSlide(string? decisionAsk)
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["layout"] = "roadmap",
|
||||
["title"] = "Implementation Roadmap",
|
||||
["headline"] = Coalesce(decisionAsk, "?④퀎蹂??곗꽑?쒖쐞? ?곗텧臾쇱쓣 湲곗??쇰줈 ?ㅽ뻾?⑸땲??"),
|
||||
["phases"] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["title"] = "Phase 1",
|
||||
["detail"] = "Diagnose and lock the phase-1 scope",
|
||||
["timeline"] = "0-30 days",
|
||||
["owner"] = "PM / Business",
|
||||
},
|
||||
new JsonObject
|
||||
{
|
||||
["title"] = "Phase 2",
|
||||
["detail"] = "Build and validate early outcomes",
|
||||
["timeline"] = "30-60 days",
|
||||
["owner"] = "Delivery",
|
||||
},
|
||||
new JsonObject
|
||||
{
|
||||
["title"] = "Phase 3",
|
||||
["detail"] = "Scale and stabilize the operating model",
|
||||
["timeline"] = "60-90 days",
|
||||
["owner"] = "Business / Ops",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject BuildAppendixSlide(IReadOnlyList<JsonObject> slides)
|
||||
{
|
||||
var lines = ExtractEvidenceLines(slides, 4);
|
||||
return new JsonObject
|
||||
{
|
||||
["layout"] = "content",
|
||||
["title"] = "Appendix & Evidence",
|
||||
["body"] = string.Join("\n", (lines.Count > 0 ? lines : new List<string>
|
||||
{
|
||||
"洹쇨굅 ?곗씠?곗? ?곸꽭 ?댁꽍??遺濡앹쑝濡??뺣━?⑸땲??",
|
||||
"?듭떖 ?섏튂??異쒖쿂? 怨꾩궛 湲곗???遺?고빀?덈떎.",
|
||||
"?ㅽ뻾 ?④퀎?먯꽌 李멸퀬???곸꽭 硫붾え瑜??ы븿?⑸땲??",
|
||||
}).Select(line => "- " + TrimBullet(line))),
|
||||
};
|
||||
}
|
||||
|
||||
private static void EnsureRiskRows(JsonObject slide)
|
||||
{
|
||||
slide["headers"] ??= new JsonArray("Risk", "Impact", "Likelihood", "Mitigation");
|
||||
if (slide.ContainsKey("rows"))
|
||||
return;
|
||||
|
||||
if (slide.TryGetPropertyValue("risks", out var risksNode) && risksNode is JsonArray risks && risks.Count > 0)
|
||||
{
|
||||
var rows = new JsonArray();
|
||||
foreach (var risk in risks.OfType<JsonObject>().Take(6))
|
||||
{
|
||||
rows.Add(new JsonArray(
|
||||
Coalesce(ReadString(risk, "title"), ReadString(risk, "risk"), "Key risk"),
|
||||
Coalesce(ReadString(risk, "impact"), "Medium"),
|
||||
Coalesce(ReadString(risk, "likelihood"), "Medium"),
|
||||
Coalesce(ReadString(risk, "mitigation"), ReadString(risk, "action"), "Mitigation required")));
|
||||
}
|
||||
slide["rows"] = rows;
|
||||
return;
|
||||
}
|
||||
|
||||
slide["rows"] = new JsonArray
|
||||
{
|
||||
new JsonArray("Execution delay", "High", "Medium", "Lock owners and weekly steering"),
|
||||
new JsonArray("Budget overrun", "Medium", "Medium", "Stage-gate investment decisions"),
|
||||
new JsonArray("Change adoption", "High", "Low", "Sponsor-led change management"),
|
||||
};
|
||||
}
|
||||
|
||||
private static void EnsureChartFromBenefits(JsonObject slide)
|
||||
{
|
||||
if (slide.ContainsKey("chart_labels") && slide.ContainsKey("chart_values"))
|
||||
return;
|
||||
|
||||
if (slide.TryGetPropertyValue("benefits", out var benefitsNode) && benefitsNode is JsonArray benefits && benefits.Count > 0)
|
||||
{
|
||||
var labels = new JsonArray();
|
||||
var values = new JsonArray();
|
||||
foreach (var benefit in benefits.OfType<JsonObject>().Take(6))
|
||||
{
|
||||
labels.Add(Coalesce(ReadString(benefit, "label"), ReadString(benefit, "title"), "Benefit"));
|
||||
values.Add(ReadDouble(benefit, "value", 0));
|
||||
}
|
||||
slide["chart_labels"] = labels;
|
||||
slide["chart_values"] = values;
|
||||
slide["chart_series_name"] ??= "Benefit";
|
||||
return;
|
||||
}
|
||||
|
||||
slide["chart_labels"] = new JsonArray("Revenue uplift", "Cost reduction", "Retention");
|
||||
slide["chart_values"] = new JsonArray(12, 8, 6);
|
||||
slide["chart_series_name"] ??= "Impact";
|
||||
}
|
||||
|
||||
private static void EnsureFallbackOptions(JsonObject slide)
|
||||
{
|
||||
if (slide.ContainsKey("options"))
|
||||
return;
|
||||
|
||||
slide["options"] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["name"] = "Option A",
|
||||
["pros"] = "鍮좊Ⅸ ?곸슜",
|
||||
["cons"] = "?뺤옣???쒗븳",
|
||||
["verdict"] = "Fastest",
|
||||
},
|
||||
new JsonObject
|
||||
{
|
||||
["name"] = "Option B",
|
||||
["pros"] = "?④낵? ?ㅽ뻾?깆쓽 洹좏삎",
|
||||
["cons"] = "珥덇린 ?뺣젹 ?꾩슂",
|
||||
["verdict"] = "Recommended",
|
||||
},
|
||||
new JsonObject
|
||||
{
|
||||
["name"] = "Option C",
|
||||
["pros"] = "High upside",
|
||||
["cons"] = "由щ뱶???湲몄쓬",
|
||||
["verdict"] = "Strategic",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasChartData(JsonObject slide)
|
||||
{
|
||||
return slide.TryGetPropertyValue("chart_labels", out var labelsNode) &&
|
||||
slide.TryGetPropertyValue("chart_values", out var valuesNode) &&
|
||||
labelsNode is JsonArray labels &&
|
||||
valuesNode is JsonArray values &&
|
||||
labels.Count > 0 &&
|
||||
values.Count > 0;
|
||||
}
|
||||
|
||||
private static int ClampArray(JsonObject slide, string propertyName, int maxCount)
|
||||
{
|
||||
if (!slide.TryGetPropertyValue(propertyName, out var node) || node is not JsonArray array || array.Count <= maxCount)
|
||||
return 0;
|
||||
|
||||
while (array.Count > maxCount)
|
||||
array.RemoveAt(array.Count - 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static List<string> BuildStoryline(IReadOnlyList<JsonObject> slides, IReadOnlyList<string> hint)
|
||||
{
|
||||
if (hint.Count > 0)
|
||||
return hint.ToList();
|
||||
|
||||
return slides
|
||||
.Select(slide => ReadString(slide, "title"))
|
||||
.Where(title => !string.IsNullOrWhiteSpace(title))
|
||||
.Take(8)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string? SuggestTheme(string? objective, string? audience)
|
||||
{
|
||||
var source = (objective + " " + audience).Trim();
|
||||
if (source.Contains("board", StringComparison.OrdinalIgnoreCase) ||
|
||||
source.Contains("?꾩썝", StringComparison.OrdinalIgnoreCase) ||
|
||||
source.Contains("executive", StringComparison.OrdinalIgnoreCase))
|
||||
return "corporate";
|
||||
|
||||
if (source.Contains("finance", StringComparison.OrdinalIgnoreCase) ||
|
||||
source.Contains("?щТ", StringComparison.OrdinalIgnoreCase))
|
||||
return "slate";
|
||||
|
||||
if (source.Contains("strategy", StringComparison.OrdinalIgnoreCase) ||
|
||||
source.Contains("?꾨왂", StringComparison.OrdinalIgnoreCase))
|
||||
return "professional";
|
||||
|
||||
if (source.Contains("training", StringComparison.OrdinalIgnoreCase) ||
|
||||
source.Contains("援먯쑁", StringComparison.OrdinalIgnoreCase))
|
||||
return "modern";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<JsonObject> ParseSlides(JsonElement slides)
|
||||
{
|
||||
var results = new List<JsonObject>();
|
||||
if (slides.ValueKind != JsonValueKind.Array)
|
||||
return results;
|
||||
|
||||
foreach (var slide in slides.EnumerateArray())
|
||||
{
|
||||
var node = JsonNode.Parse(slide.GetRawText()) as JsonObject;
|
||||
if (node != null)
|
||||
results.Add(node);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static JsonArray ToJsonArray(IEnumerable<JsonObject> slides)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var slide in slides)
|
||||
array.Add(slide);
|
||||
return array;
|
||||
}
|
||||
|
||||
private static JsonArray ToJsonArray(IEnumerable<string> values)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var value in values)
|
||||
array.Add(value);
|
||||
return array;
|
||||
}
|
||||
|
||||
private static JsonArray ToMetricArray(IReadOnlyList<(string Label, string Value, string Trend, string Note)> metrics)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var metric in metrics)
|
||||
{
|
||||
array.Add(new JsonObject
|
||||
{
|
||||
["label"] = metric.Label,
|
||||
["value"] = metric.Value,
|
||||
["trend"] = metric.Trend,
|
||||
["note"] = metric.Note,
|
||||
});
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static JsonArray BuildEvidenceRows(JsonArray evidence)
|
||||
{
|
||||
var rows = new JsonArray();
|
||||
foreach (var item in evidence.OfType<JsonObject>().Take(6))
|
||||
{
|
||||
rows.Add(new JsonArray(
|
||||
Coalesce(ReadString(item, "title"), ReadString(item, "label"), "Evidence"),
|
||||
Coalesce(ReadString(item, "detail"), ReadString(item, "summary"), ReadString(item, "value"), "-"),
|
||||
Coalesce(ReadString(item, "source"), ReadString(item, "note"), "-")));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<string> ExtractEvidenceLines(IReadOnlyList<JsonObject> slides, int maxCount)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
foreach (var slide in slides)
|
||||
{
|
||||
foreach (var line in ReadTextLines(slide, "summary_points"))
|
||||
{
|
||||
if (lines.Count >= maxCount)
|
||||
return lines;
|
||||
lines.Add(TrimBullet(line));
|
||||
}
|
||||
|
||||
foreach (var line in ReadTextLines(slide, "body"))
|
||||
{
|
||||
if (lines.Count >= maxCount)
|
||||
return lines;
|
||||
lines.Add(TrimBullet(line));
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static List<(string Label, string Value, string Trend, string Note)> ExtractMetrics(IReadOnlyList<JsonObject> slides, int maxCount)
|
||||
{
|
||||
var metrics = new List<(string Label, string Value, string Trend, string Note)>();
|
||||
foreach (var slide in slides)
|
||||
{
|
||||
if (!slide.TryGetPropertyValue("kpis", out var node) || node is not JsonArray items)
|
||||
continue;
|
||||
|
||||
foreach (var item in items.OfType<JsonObject>())
|
||||
{
|
||||
metrics.Add((
|
||||
Coalesce(ReadString(item, "label"), ReadString(item, "title"), "Metric"),
|
||||
Coalesce(ReadString(item, "value"), "-"),
|
||||
Coalesce(ReadString(item, "trend"), string.Empty),
|
||||
Coalesce(ReadString(item, "note"), string.Empty)));
|
||||
|
||||
if (metrics.Count >= maxCount)
|
||||
return metrics;
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics.Count == 0)
|
||||
{
|
||||
metrics.Add(("Value", "Impact", "Near term", "Priority"));
|
||||
metrics.Add(("Risk", "Managed", "Execution", "Tracked"));
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private static string BuildHeadline(JsonObject slide, int slideIndex, string presentationTitle, string? objective, string? decisionAsk)
|
||||
{
|
||||
var title = ReadString(slide, "title");
|
||||
var recommendation = ReadString(slide, "recommendation");
|
||||
var bodyLine = ReadTextLines(slide, "body").FirstOrDefault();
|
||||
|
||||
return Coalesce(
|
||||
recommendation,
|
||||
bodyLine,
|
||||
objective,
|
||||
decisionAsk,
|
||||
title,
|
||||
slideIndex == 0 ? presentationTitle : "?듭떖 硫붿떆吏瑜???臾몄옣?쇰줈 ?뺣━?⑸땲??");
|
||||
}
|
||||
|
||||
private static List<string> ReadHintList(JsonElement args, params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!args.SafeTryGetProperty(key, out var value))
|
||||
continue;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return value.EnumerateArray()
|
||||
.Select(item => item.ValueKind == JsonValueKind.String ? item.SafeGetString() ?? string.Empty : string.Empty)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return (value.SafeGetString() ?? string.Empty)
|
||||
.Split(new[] { '\n', '/', '>', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static List<string> ReadTextLines(JsonObject slide, string propertyName)
|
||||
{
|
||||
if (!slide.TryGetPropertyValue(propertyName, out var node) || node == null)
|
||||
return [];
|
||||
|
||||
return node switch
|
||||
{
|
||||
JsonArray array => array
|
||||
.Select(item => item?.ToJsonString())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(item => UnquoteJson(item ?? string.Empty))
|
||||
.Select(TrimBullet)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList(),
|
||||
JsonValue => (node.ToJsonString().Trim('"'))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(TrimBullet)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToMultiline(JsonObject slide, params string[] propertyNames)
|
||||
{
|
||||
foreach (var propertyName in propertyNames)
|
||||
{
|
||||
var lines = ReadTextLines(slide, propertyName);
|
||||
if (lines.Count > 0)
|
||||
return string.Join("\n", lines.Take(5));
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement element, string propertyName)
|
||||
{
|
||||
return element.SafeTryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.SafeGetString() ?? string.Empty
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonObject obj, string propertyName)
|
||||
{
|
||||
if (!obj.TryGetPropertyValue(propertyName, out var node) || node == null)
|
||||
return string.Empty;
|
||||
|
||||
return node switch
|
||||
{
|
||||
JsonValue value => UnquoteJson(value.ToJsonString()),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static double ReadDouble(JsonObject obj, string propertyName, double fallback)
|
||||
{
|
||||
if (!obj.TryGetPropertyValue(propertyName, out var node) || node == null)
|
||||
return fallback;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<double>(node.ToJsonString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Coalesce(params string?[] values)
|
||||
{
|
||||
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string TrimBullet(string line)
|
||||
{
|
||||
return line.Trim().TrimStart('-', '*', '\u2022', ' ', '\t');
|
||||
}
|
||||
|
||||
private static string UnquoteJson(string value)
|
||||
{
|
||||
if (value.Length >= 2 && value[0] == '"' && value[^1] == '"')
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<string>(value) ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value.Trim('"');
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
218
src/AxCopilot/Services/Agent/DeckQualityReviewService.cs
Normal file
218
src/AxCopilot/Services/Agent/DeckQualityReviewService.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public enum DeckReviewSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
public sealed record DeckReviewIssue(string Message, DeckReviewSeverity Severity);
|
||||
|
||||
public sealed record DeckQualityReport(
|
||||
int Score,
|
||||
IReadOnlyList<string> Strengths,
|
||||
IReadOnlyList<DeckReviewIssue> Issues,
|
||||
IReadOnlyList<string> Storyline)
|
||||
{
|
||||
public string ToToolSummary()
|
||||
{
|
||||
var strengths = Strengths.Take(3).ToList();
|
||||
var issues = Issues
|
||||
.OrderByDescending(issue => issue.Severity)
|
||||
.Take(3)
|
||||
.Select(issue => issue.Message)
|
||||
.ToList();
|
||||
var parts = new List<string> { $"PPT quality {Score}/100" };
|
||||
if (strengths.Count > 0)
|
||||
parts.Add("Strengths: " + string.Join(", ", strengths));
|
||||
if (issues.Count > 0)
|
||||
parts.Add("Needs work: " + string.Join(", ", issues));
|
||||
else
|
||||
parts.Add("Needs work: none");
|
||||
return string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeckQualityReviewService
|
||||
{
|
||||
public static DeckQualityReport ReviewDeck(
|
||||
string title,
|
||||
JsonElement slides,
|
||||
bool hasTemplate,
|
||||
int autoRepairCount,
|
||||
IEnumerable<string>? storyline = null)
|
||||
{
|
||||
var strengths = new List<string>();
|
||||
var issues = new List<DeckReviewIssue>();
|
||||
var storylineSteps = (storyline ?? []).Where(step => !string.IsNullOrWhiteSpace(step)).ToList();
|
||||
const string KoreanAppendix = "\uBD80\uB85D";
|
||||
if (slides.ValueKind != JsonValueKind.Array)
|
||||
return new DeckQualityReport(40, strengths, [new("Slides must be an array.", DeckReviewSeverity.Critical)], storylineSteps);
|
||||
var slidesList = slides.EnumerateArray().ToList();
|
||||
var slideCount = slidesList.Count;
|
||||
var titleSlides = 0;
|
||||
var executiveSlides = 0;
|
||||
var recommendationSlides = 0;
|
||||
var roadmapSlides = 0;
|
||||
var appendixSlides = 0;
|
||||
var chartSlides = 0;
|
||||
var tableSlides = 0;
|
||||
var comparisonSlides = 0;
|
||||
var textHeavySlides = 0;
|
||||
var duplicateHeadlines = 0;
|
||||
var placeholderCount = 0;
|
||||
var headlines = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var layoutCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var slide in slidesList)
|
||||
{
|
||||
var layout = ReadString(slide, "layout", "content");
|
||||
var normalizedLayout = layout.ToLowerInvariant();
|
||||
layoutCounts[normalizedLayout] = layoutCounts.GetValueOrDefault(normalizedLayout) + 1;
|
||||
switch (normalizedLayout)
|
||||
{
|
||||
case "title":
|
||||
titleSlides++;
|
||||
break;
|
||||
case "executive_summary":
|
||||
executiveSlides++;
|
||||
break;
|
||||
case "recommendation":
|
||||
recommendationSlides++;
|
||||
break;
|
||||
case "roadmap":
|
||||
roadmapSlides++;
|
||||
break;
|
||||
case "comparison":
|
||||
comparisonSlides++;
|
||||
break;
|
||||
case "chart":
|
||||
chartSlides++;
|
||||
break;
|
||||
case "table":
|
||||
tableSlides++;
|
||||
break;
|
||||
}
|
||||
var titleText = ReadString(slide, "title");
|
||||
if (titleText.Contains("appendix", StringComparison.OrdinalIgnoreCase) ||
|
||||
titleText.Contains(KoreanAppendix, StringComparison.OrdinalIgnoreCase) ||
|
||||
titleText.Contains("evidence", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
appendixSlides++;
|
||||
}
|
||||
var headline = ReadString(slide, "headline");
|
||||
if (!string.IsNullOrWhiteSpace(headline) && !headlines.Add(headline.Trim()))
|
||||
duplicateHeadlines++;
|
||||
var bulletCount =
|
||||
ReadStringList(slide, "summary_points").Count +
|
||||
ReadStringList(slide, "next_steps").Count +
|
||||
ReadStringList(slide, "body").Count;
|
||||
var bodyText = ReadString(slide, "body");
|
||||
var textLength = bodyText.Length +
|
||||
headline.Length +
|
||||
ReadString(slide, "recommendation").Length +
|
||||
ReadString(slide, "left").Length +
|
||||
ReadString(slide, "right").Length;
|
||||
if (bulletCount >= 7 || textLength >= 420)
|
||||
textHeavySlides++;
|
||||
placeholderCount += CountPlaceholders(slide.GetRawText());
|
||||
}
|
||||
if (hasTemplate) strengths.Add("Uses template or branded theme");
|
||||
if (titleSlides > 0) strengths.Add("Includes title slide");
|
||||
if (executiveSlides > 0) strengths.Add("Includes Executive Summary");
|
||||
if (recommendationSlides > 0) strengths.Add("Includes Recommendation");
|
||||
if (roadmapSlides > 0) strengths.Add("Includes Roadmap");
|
||||
if (chartSlides + tableSlides + comparisonSlides >= 2)
|
||||
strengths.Add("Contains evidence-oriented comparison, table, or chart slides");
|
||||
if (layoutCounts.Count >= 4)
|
||||
strengths.Add($"Uses {layoutCounts.Count} distinct layouts");
|
||||
if (autoRepairCount > 0)
|
||||
strengths.Add($"Auto-repair applied {autoRepairCount} time(s)");
|
||||
if (slideCount < 4)
|
||||
issues.Add(new("Slide count may be too low for an executive deck.", DeckReviewSeverity.Warning));
|
||||
if (titleSlides == 0)
|
||||
issues.Add(new("Title slide is missing.", DeckReviewSeverity.Warning));
|
||||
if (executiveSlides == 0)
|
||||
issues.Add(new("Executive Summary slide is missing.", DeckReviewSeverity.Critical));
|
||||
if (recommendationSlides == 0)
|
||||
issues.Add(new("Recommendation or decision request slide is missing.", DeckReviewSeverity.Critical));
|
||||
if (roadmapSlides == 0 && slideCount >= 5)
|
||||
issues.Add(new("Execution roadmap slide is missing.", DeckReviewSeverity.Warning));
|
||||
if (appendixSlides == 0 && slideCount >= 6)
|
||||
issues.Add(new("Appendix or evidence slide is limited.", DeckReviewSeverity.Info));
|
||||
if (duplicateHeadlines > 0)
|
||||
issues.Add(new($"Found {duplicateHeadlines} duplicate headline(s).", DeckReviewSeverity.Warning));
|
||||
if (textHeavySlides > Math.Max(1, slideCount / 2))
|
||||
issues.Add(new("Too many slides are text-heavy.", DeckReviewSeverity.Warning));
|
||||
if (chartSlides + tableSlides + comparisonSlides == 0 && slideCount >= 4)
|
||||
issues.Add(new("Evidence slides such as charts, tables, or comparisons are limited.", DeckReviewSeverity.Warning));
|
||||
if (placeholderCount > 0)
|
||||
issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", DeckReviewSeverity.Critical));
|
||||
var score = 70;
|
||||
score += Math.Min(18, strengths.Count * 3);
|
||||
score -= issues.Sum(issue => issue.Severity switch
|
||||
{
|
||||
DeckReviewSeverity.Critical => 12,
|
||||
DeckReviewSeverity.Warning => 6,
|
||||
_ => 2,
|
||||
});
|
||||
score = Math.Clamp(score, 30, 98);
|
||||
return new DeckQualityReport(score, strengths, issues, storylineSteps);
|
||||
}
|
||||
private static string ReadString(JsonElement element, string propertyName, string fallback = "")
|
||||
{
|
||||
if (!element.SafeTryGetProperty(propertyName, out var value))
|
||||
return fallback;
|
||||
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => value.SafeGetString() ?? fallback,
|
||||
JsonValueKind.Number => value.ToString(),
|
||||
_ => fallback,
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> ReadStringList(JsonElement element, string propertyName)
|
||||
{
|
||||
if (!element.SafeTryGetProperty(propertyName, out var value))
|
||||
return [];
|
||||
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array => value.EnumerateArray()
|
||||
.Select(item => item.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => item.SafeGetString() ?? string.Empty,
|
||||
JsonValueKind.Number => item.ToString(),
|
||||
JsonValueKind.Object => item.ToString(),
|
||||
_ => string.Empty,
|
||||
})
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList(),
|
||||
JsonValueKind.String => (value.SafeGetString() ?? string.Empty)
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
||||
private static int CountPlaceholders(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return 0;
|
||||
|
||||
var patterns = new[]
|
||||
{
|
||||
@"\bTBD\b",
|
||||
@"\bTODO\b",
|
||||
@"\[(placeholder|fill me|todo)\]",
|
||||
@"lorem ipsum",
|
||||
};
|
||||
|
||||
return patterns.Sum(pattern => Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -155,6 +155,27 @@ public class PptxSkill : IAgentTool
|
||||
Description = "Slide aspect ratio. Default: widescreen (16:9)",
|
||||
Enum = ["widescreen", "standard"]
|
||||
},
|
||||
["audience"] = new()
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Target audience such as executives, steering committee, PMO, sales leadership, or team leads.",
|
||||
},
|
||||
["objective"] = new()
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Presentation objective such as strategy recommendation, board update, PMO steering, operating model proposal, or KPI review.",
|
||||
},
|
||||
["decision_ask"] = new()
|
||||
{
|
||||
Type = "string",
|
||||
Description = "The exact decision or approval being requested from the audience.",
|
||||
},
|
||||
["storyline"] = new()
|
||||
{
|
||||
Type = "array",
|
||||
Description = "Optional storyline or ordered section hints.",
|
||||
Items = new() { Type = "string" }
|
||||
},
|
||||
},
|
||||
Required = ["slides"]
|
||||
};
|
||||
@@ -506,9 +527,15 @@ public class PptxSkill : IAgentTool
|
||||
if (safe.Length > 60) safe = safe[..60].TrimEnd();
|
||||
path = (string.IsNullOrWhiteSpace(safe) ? "presentation" : safe) + ".pptx";
|
||||
}
|
||||
var theme = args.SafeTryGetProperty("theme", out var th) ? th.SafeGetString() ?? "basic100" : "basic100";
|
||||
var aspect = args.SafeTryGetProperty("aspect", out var asp) ? asp.SafeGetString() ?? "widescreen" : "widescreen";
|
||||
var hasExplicitTheme = args.SafeTryGetProperty("theme", out var th) &&
|
||||
!string.IsNullOrWhiteSpace(th.SafeGetString());
|
||||
var theme = hasExplicitTheme ? th.SafeGetString() ?? "basic100" : "basic100";
|
||||
var aspect = args.SafeTryGetProperty("aspect", out var asp) ? asp.SafeGetString() ?? "widescreen" : "widescreen";
|
||||
var cloneAll = args.SafeTryGetProperty("clone_slides", out var cloneEl) && cloneEl.ValueKind == JsonValueKind.True;
|
||||
var hasThemeFile = args.SafeTryGetProperty("theme_file", out var themeFileEl) &&
|
||||
!string.IsNullOrWhiteSpace(themeFileEl.SafeGetString());
|
||||
var hasExplicitTemplate = args.SafeTryGetProperty("template", out var templateArgEl) &&
|
||||
!string.IsNullOrWhiteSpace(templateArgEl.SafeGetString());
|
||||
|
||||
// 전체 복제용 텍스트 교체 맵
|
||||
Dictionary<string, string>? globalReplacements = null;
|
||||
@@ -526,6 +553,20 @@ public class PptxSkill : IAgentTool
|
||||
if (!hasSlidesArray && !cloneAll)
|
||||
return ToolResult.Fail("slides 배열이 필요합니다.");
|
||||
|
||||
using var preparedDeck = hasSlidesArray
|
||||
? DeckPlanningService.Prepare(args, slidesEl, presTitle)
|
||||
: null;
|
||||
|
||||
if (preparedDeck != null)
|
||||
{
|
||||
slidesEl = preparedDeck.Slides;
|
||||
if (!hasExplicitTheme && !hasExplicitTemplate && !hasThemeFile &&
|
||||
!string.IsNullOrWhiteSpace(preparedDeck.SuggestedTheme))
|
||||
{
|
||||
theme = preparedDeck.SuggestedTheme!;
|
||||
}
|
||||
}
|
||||
|
||||
var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
||||
if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
||||
if (!fullPath.EndsWith(".pptx", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -923,9 +964,23 @@ public class PptxSkill : IAgentTool
|
||||
: templatePptxPath != null
|
||||
? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})"
|
||||
: theme;
|
||||
return ToolResult.Ok(
|
||||
$"✅ PPTX 생성 완료: {fullPath} ({slideCount_final}슬라이드, {themeLabel}, {(isWide ? "16:9" : "4:3")})",
|
||||
fullPath);
|
||||
var deckReview = hasSlidesArray
|
||||
? DeckQualityReviewService.ReviewDeck(
|
||||
presTitle,
|
||||
slidesEl,
|
||||
!string.IsNullOrWhiteSpace(templateName) || templatePptxPath != null,
|
||||
preparedDeck?.AutoRepairCount ?? 0,
|
||||
preparedDeck?.Storyline)
|
||||
: null;
|
||||
var outputParts = new List<string>
|
||||
{
|
||||
$"PPTX created: {fullPath} ({slideCount_final} slides, {themeLabel}, {(isWide ? "16:9" : "4:3")})"
|
||||
};
|
||||
if (preparedDeck != null)
|
||||
outputParts.Add(preparedDeck.ToToolSummary());
|
||||
if (deckReview != null)
|
||||
outputParts.Add(deckReview.ToToolSummary());
|
||||
return ToolResult.Ok(string.Join("\n", outputParts), fullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
21
src/AxCopilot/skills/board-update.skill.md
Normal file
21
src/AxCopilot/skills/board-update.skill.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: board-update
|
||||
label: Board update deck
|
||||
description: Create a concise board or executive update deck.
|
||||
icon: \uE7BE
|
||||
when_to_use: Use for board reporting, steering committee updates, quarterly executive reviews, or decision update packs.
|
||||
argument-hint: <update topic>
|
||||
allowed-tools:
|
||||
- document_plan
|
||||
- document_read
|
||||
- file_read
|
||||
- pptx_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
Create a concise board update deck with `pptx_create`.
|
||||
|
||||
- Set `audience` to `board` or `executives`.
|
||||
- Set `objective` to `board update`.
|
||||
- Aim for 6-8 slides when possible.
|
||||
- Include `Executive Summary`, `Key Findings`, `Recommendation`, `Roadmap`, and `Impact & Ask`.
|
||||
20
src/AxCopilot/skills/operating-model-deck.skill.md
Normal file
20
src/AxCopilot/skills/operating-model-deck.skill.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: operating-model-deck
|
||||
label: Operating model deck
|
||||
description: Create a deck that explains current and target operating models with transition steps.
|
||||
icon: \uE7BE
|
||||
when_to_use: Use for operating model proposals, process redesign decks, governance changes, or target-state transformation materials.
|
||||
argument-hint: <operating model topic>
|
||||
allowed-tools:
|
||||
- document_plan
|
||||
- document_read
|
||||
- file_read
|
||||
- pptx_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
Create an operating model deck with `pptx_create`.
|
||||
|
||||
- Set `objective` to `operating model proposal`.
|
||||
- Prefer `before_after`, `operating_model`, `recommendation`, and `roadmap`.
|
||||
- Separate current state, target state, transition actions, and governance implications.
|
||||
20
src/AxCopilot/skills/pmo-steering.skill.md
Normal file
20
src/AxCopilot/skills/pmo-steering.skill.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: pmo-steering
|
||||
label: PMO steering deck
|
||||
description: Create a project or program steering deck with risks, status, and decisions.
|
||||
icon: \uE7BE
|
||||
when_to_use: Use for PMO weekly or monthly updates, project steering meetings, or risk and decision review decks.
|
||||
argument-hint: <project or program name>
|
||||
allowed-tools:
|
||||
- document_plan
|
||||
- document_read
|
||||
- file_read
|
||||
- pptx_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
Create a PMO steering deck with `pptx_create`.
|
||||
|
||||
- Set `objective` to `program steering update`.
|
||||
- Prefer `risk_heatmap`, `roadmap`, `kpi_dashboard`, and `recommendation`.
|
||||
- Separate current status, key risks, decisions needed, and next actions.
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: pptx-creator
|
||||
label: PPT 프레젠테이션 생성
|
||||
description: AX 기본 PPT 엔진으로 고급 제안서, 보고서, 발표 자료를 생성합니다. 작업 폴더의 양식 파일과 프로젝트 자료를 함께 활용합니다.
|
||||
label: PPT deck creator
|
||||
description: Create consulting-style PowerPoint decks with AX native PPT generation.
|
||||
icon: \uE7BE
|
||||
when_to_use: 제안서 deck, 경영보고, 임원 보고자료, 프로젝트 현황 발표, 교육용 슬라이드, 회의 발표 자료를 새로 만들어야 하거나 기존 양식 PPT를 활용해야 할 때
|
||||
argument-hint: <주제 또는 문서 목적>
|
||||
when_to_use: Use for executive decks, board updates, PMO steering materials, operating model proposals, KPI review decks, or any request that needs a polished PPTX deliverable.
|
||||
argument-hint: <topic or deck objective>
|
||||
allowed-tools:
|
||||
- folder_map
|
||||
- document_plan
|
||||
@@ -14,96 +14,68 @@ allowed-tools:
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
사용자의 요구에 맞는 PowerPoint 프레젠테이션을 AX 기본 `pptx_create` 도구로 생성하세요.
|
||||
Use `pptx_create` as the default path. Do not start with ad-hoc slide arrays unless the user already gave a very detailed deck structure.
|
||||
|
||||
## 기본 원칙
|
||||
- 기본 경로는 항상 `pptx_create`입니다. Python 스크립트 생성은 예외 상황에서만 고려하세요.
|
||||
- 슬라이드마다 메시지는 하나만 두고, 제목은 설명형 문장이 아니라 결론형 headline으로 작성하세요.
|
||||
- bullet은 슬라이드당 3~5개 이내의 짧고 강한 문장으로 유지하세요. 공간을 채우기 위한 장문 bullet은 금지합니다.
|
||||
- 숫자, 비교, 일정, 권고안은 각각 맞는 레이아웃으로 분리하세요.
|
||||
## Default workflow
|
||||
1. Review the workspace for existing presentation files, reports, or templates.
|
||||
2. Use `document_plan` with `document_type: presentation` to define the storyline first.
|
||||
3. Build a deck brief with `audience`, `objective`, `decision_ask`, and optional `storyline`.
|
||||
4. Generate the deck with `pptx_create`.
|
||||
5. Return the file path together with the storyline and quality summary.
|
||||
|
||||
## 권장 작업 순서
|
||||
1. `folder_map`으로 작업 폴더를 확인하고 기존 `.pptx`, 보고서, 분석 문서, 회의록이 있는지 찾으세요.
|
||||
2. 참고할 양식 PPT가 있으면 파일명을 기억하고, 필요 시 `document_read`로 슬라이드 구조를 파악하세요.
|
||||
3. 먼저 `document_plan`을 `document_type: presentation`으로 호출해서 스토리라인을 정리하세요.
|
||||
4. 그 결과를 바탕으로 `pptx_create`용 슬라이드 배열을 설계하세요.
|
||||
5. 생성 후 결과 파일 경로와 핵심 구성(슬라이드 수, 사용 템플릿/테마, 주요 레이아웃)을 간단히 안내하세요.
|
||||
## Quality rules
|
||||
- Keep one message per slide.
|
||||
- Prefer 3-5 concise bullets, not long paragraphs.
|
||||
- Use a message headline, not a vague topic label.
|
||||
- Include `Executive Summary`, `Recommendation`, and `Roadmap` in most business decks.
|
||||
- Add `Appendix` or `Evidence` when the deck is 6 slides or longer.
|
||||
- Use comparison, chart, KPI, or evidence slides when data exists.
|
||||
|
||||
## 컨설팅형 스토리라인 규칙
|
||||
- 기본 구조는 가능하면 아래 순서를 따르세요.
|
||||
- `Executive Summary`
|
||||
- `Situation & Imperative`
|
||||
- `Key Findings`
|
||||
- `Options & Recommendation`
|
||||
- `Implementation Roadmap`
|
||||
- `Impact & Ask`
|
||||
## Useful layouts
|
||||
- `title`
|
||||
- `executive_summary`
|
||||
- `comparison`
|
||||
- `recommendation`
|
||||
- `roadmap`
|
||||
- `kpi_dashboard`
|
||||
- `chart`
|
||||
- `table`
|
||||
- `issue_tree`
|
||||
- `before_after`
|
||||
- `decision_matrix`
|
||||
- `risk_heatmap`
|
||||
- `benefit_waterfall`
|
||||
- `operating_model`
|
||||
- `appendix_evidence`
|
||||
|
||||
## 레이아웃 선택 기준
|
||||
- `executive_summary`: 경영진 요약, 한 줄 권고안, 핵심 takeaways, KPI 요약
|
||||
- `recommendation`: 단일 권고안과 근거, 즉시 실행 항목
|
||||
- `comparison`: 대안 비교, 옵션별 pros/risks, 추천안 강조
|
||||
- `roadmap`: 단계별 일정, 소유자, 주요 산출물
|
||||
- `kpi_dashboard`: 핵심 수치, 추세, 시사점
|
||||
- `chart`: 정량 데이터 시각화
|
||||
- `table`: 비교표, 상세 데이터표
|
||||
- `section`: 장 구분
|
||||
- `content`: 일반 설명 슬라이드
|
||||
|
||||
## 양식 활용
|
||||
- 작업 폴더에 기존 양식 PPT가 있으면 `template` 또는 `theme_file`로 우선 활용하세요.
|
||||
- 양식 후보 예시:
|
||||
- 파일명에 `양식`, `template`, `표준`, `기본`, `보고`, `proposal`, `deck` 포함
|
||||
- 사용자가 특정 `.pptx` 파일명을 직접 언급
|
||||
- 양식이 명확하지 않으면 기본 `template: basic100` 또는 문서 성격에 맞는 `corporate`, `professional`, `modern` 중 하나를 사용하세요.
|
||||
|
||||
## 슬라이드 작성 규칙
|
||||
- 제목은 `무엇을 다룰지`가 아니라 `무슨 결론인지`를 말해야 합니다.
|
||||
- 각 슬라이드는 headline, supporting evidence, takeaway의 3요소를 갖추세요.
|
||||
- 데이터가 없으면 억지 차트를 만들지 말고, 비교 카드나 권고안 슬라이드로 전환하세요.
|
||||
- 표는 설명보다 비교에 유리할 때만 사용하세요.
|
||||
- speaker notes가 유용하면 `notes`를 넣어 발표 포인트를 남기세요.
|
||||
|
||||
## 예시 흐름
|
||||
- `document_plan`으로 발표 구조 생성
|
||||
- `pptx_create`에서 아래와 같은 레이아웃 조합 사용
|
||||
- `title`
|
||||
- `executive_summary`
|
||||
- `comparison`
|
||||
- `recommendation`
|
||||
- `roadmap`
|
||||
- `kpi_dashboard`
|
||||
- `section`
|
||||
- `content`
|
||||
|
||||
## 예시 파라미터 스케치
|
||||
## Example
|
||||
```json
|
||||
{
|
||||
"path": "strategy-deck.pptx",
|
||||
"template": "basic100",
|
||||
"audience": "executive committee",
|
||||
"objective": "customer growth strategy recommendation",
|
||||
"decision_ask": "approve phase-1 CRM and retention program funding",
|
||||
"storyline": [
|
||||
"Executive Summary",
|
||||
"Current State",
|
||||
"Options",
|
||||
"Recommendation",
|
||||
"Roadmap",
|
||||
"Impact & Ask"
|
||||
],
|
||||
"slides": [
|
||||
{
|
||||
"layout": "title",
|
||||
"title": "2026 사업 전략 제안",
|
||||
"subtitle": "성장 가속과 운영 효율 동시 달성"
|
||||
},
|
||||
{
|
||||
"layout": "executive_summary",
|
||||
"title": "Executive Summary",
|
||||
"headline": "핵심 투자 우선순위를 재배치하면 2개 분기 내 수익성을 개선할 수 있습니다.",
|
||||
"headline": "Focus on retention and CRM automation to restore growth within two quarters.",
|
||||
"summary_points": [
|
||||
"핵심 비용 3개 영역에서 구조적 비효율이 확인되었습니다.",
|
||||
"고객 유지율 개선이 신규 확보보다 더 큰 수익 기회를 만듭니다.",
|
||||
"선행 투자 없이 가능한 빠른 실행과제도 존재합니다."
|
||||
"Three priority cohorts explain most of the growth gap.",
|
||||
"Workflow automation reduces manual operating cost.",
|
||||
"Phase-1 actions and medium-term changes are clearly separated."
|
||||
],
|
||||
"recommendation": "우선 CRM 고도화와 운영 자동화를 병행하고, 저효율 캠페인은 즉시 축소합니다.",
|
||||
"kpis": [
|
||||
{ "label": "매출총이익", "value": "+4.2%p", "trend": "2Q forecast", "note": "product mix 개선" },
|
||||
{ "label": "CAC", "value": "-11%", "trend": "target", "note": "퍼널 최적화" },
|
||||
{ "label": "Retention", "value": "+6pt", "trend": "12M", "note": "핵심 세그먼트" }
|
||||
]
|
||||
"recommendation": "Approve phase-1 CRM upgrade and retention program launch."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
한국어로 안내하고, 결과 파일은 작업 폴더에 저장하세요.
|
||||
|
||||
20
src/AxCopilot/skills/sales-review-deck.skill.md
Normal file
20
src/AxCopilot/skills/sales-review-deck.skill.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: sales-review-deck
|
||||
label: Sales review deck
|
||||
description: Create a sales review deck with KPI, pipeline, and growth actions.
|
||||
icon: \uE7BE
|
||||
when_to_use: Use for sales performance reviews, pipeline reviews, or growth action meetings.
|
||||
argument-hint: <review topic>
|
||||
allowed-tools:
|
||||
- document_plan
|
||||
- document_read
|
||||
- file_read
|
||||
- pptx_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
Create a sales review deck with `pptx_create`.
|
||||
|
||||
- Set `objective` to `sales review`.
|
||||
- Prefer `kpi_dashboard`, `comparison`, `recommendation`, and `roadmap`.
|
||||
- Use numeric KPI, segment insight, and pipeline evidence when available.
|
||||
21
src/AxCopilot/skills/strategy-deck.skill.md
Normal file
21
src/AxCopilot/skills/strategy-deck.skill.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: strategy-deck
|
||||
label: Strategy deck
|
||||
description: Create a consulting-style strategy recommendation deck for leadership.
|
||||
icon: \uE7BE
|
||||
when_to_use: Use for business strategy recommendations, growth agenda reviews, priority-setting decks, or executive decision materials.
|
||||
argument-hint: <strategy topic>
|
||||
allowed-tools:
|
||||
- document_plan
|
||||
- document_read
|
||||
- file_read
|
||||
- pptx_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
Create a strategy recommendation deck with `pptx_create`.
|
||||
|
||||
- Set `audience` to `executive committee` or `leadership team`.
|
||||
- Set `objective` to `strategy recommendation`.
|
||||
- Prefer `Executive Summary -> Situation -> Key Findings -> Options -> Recommendation -> Roadmap -> Impact & Ask`.
|
||||
- Prefer `professional` or `corporate` themes unless a branded template exists.
|
||||
Reference in New Issue
Block a user