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)를 추가했습니다.
|
- 테스트로 [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 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
|
- 검증: `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` 추가
|
- 테스트: `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 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
|
- 검증: `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.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -155,6 +155,27 @@ public class PptxSkill : IAgentTool
|
|||||||
Description = "Slide aspect ratio. Default: widescreen (16:9)",
|
Description = "Slide aspect ratio. Default: widescreen (16:9)",
|
||||||
Enum = ["widescreen", "standard"]
|
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"]
|
Required = ["slides"]
|
||||||
};
|
};
|
||||||
@@ -506,9 +527,15 @@ public class PptxSkill : IAgentTool
|
|||||||
if (safe.Length > 60) safe = safe[..60].TrimEnd();
|
if (safe.Length > 60) safe = safe[..60].TrimEnd();
|
||||||
path = (string.IsNullOrWhiteSpace(safe) ? "presentation" : safe) + ".pptx";
|
path = (string.IsNullOrWhiteSpace(safe) ? "presentation" : safe) + ".pptx";
|
||||||
}
|
}
|
||||||
var theme = args.SafeTryGetProperty("theme", out var th) ? th.SafeGetString() ?? "basic100" : "basic100";
|
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 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 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;
|
Dictionary<string, string>? globalReplacements = null;
|
||||||
@@ -526,6 +553,20 @@ public class PptxSkill : IAgentTool
|
|||||||
if (!hasSlidesArray && !cloneAll)
|
if (!hasSlidesArray && !cloneAll)
|
||||||
return ToolResult.Fail("slides 배열이 필요합니다.");
|
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);
|
var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
||||||
if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
||||||
if (!fullPath.EndsWith(".pptx", StringComparison.OrdinalIgnoreCase))
|
if (!fullPath.EndsWith(".pptx", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -923,9 +964,23 @@ public class PptxSkill : IAgentTool
|
|||||||
: templatePptxPath != null
|
: templatePptxPath != null
|
||||||
? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})"
|
? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})"
|
||||||
: theme;
|
: theme;
|
||||||
return ToolResult.Ok(
|
var deckReview = hasSlidesArray
|
||||||
$"✅ PPTX 생성 완료: {fullPath} ({slideCount_final}슬라이드, {themeLabel}, {(isWide ? "16:9" : "4:3")})",
|
? DeckQualityReviewService.ReviewDeck(
|
||||||
fullPath);
|
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)
|
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
|
name: pptx-creator
|
||||||
label: PPT 프레젠테이션 생성
|
label: PPT deck creator
|
||||||
description: AX 기본 PPT 엔진으로 고급 제안서, 보고서, 발표 자료를 생성합니다. 작업 폴더의 양식 파일과 프로젝트 자료를 함께 활용합니다.
|
description: Create consulting-style PowerPoint decks with AX native PPT generation.
|
||||||
icon: \uE7BE
|
icon: \uE7BE
|
||||||
when_to_use: 제안서 deck, 경영보고, 임원 보고자료, 프로젝트 현황 발표, 교육용 슬라이드, 회의 발표 자료를 새로 만들어야 하거나 기존 양식 PPT를 활용해야 할 때
|
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: <주제 또는 문서 목적>
|
argument-hint: <topic or deck objective>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- folder_map
|
- folder_map
|
||||||
- document_plan
|
- document_plan
|
||||||
@@ -14,96 +14,68 @@ allowed-tools:
|
|||||||
tabs: cowork
|
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.
|
||||||
|
|
||||||
## 기본 원칙
|
## Default workflow
|
||||||
- 기본 경로는 항상 `pptx_create`입니다. Python 스크립트 생성은 예외 상황에서만 고려하세요.
|
1. Review the workspace for existing presentation files, reports, or templates.
|
||||||
- 슬라이드마다 메시지는 하나만 두고, 제목은 설명형 문장이 아니라 결론형 headline으로 작성하세요.
|
2. Use `document_plan` with `document_type: presentation` to define the storyline first.
|
||||||
- bullet은 슬라이드당 3~5개 이내의 짧고 강한 문장으로 유지하세요. 공간을 채우기 위한 장문 bullet은 금지합니다.
|
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.
|
||||||
|
|
||||||
## 권장 작업 순서
|
## Quality rules
|
||||||
1. `folder_map`으로 작업 폴더를 확인하고 기존 `.pptx`, 보고서, 분석 문서, 회의록이 있는지 찾으세요.
|
- Keep one message per slide.
|
||||||
2. 참고할 양식 PPT가 있으면 파일명을 기억하고, 필요 시 `document_read`로 슬라이드 구조를 파악하세요.
|
- Prefer 3-5 concise bullets, not long paragraphs.
|
||||||
3. 먼저 `document_plan`을 `document_type: presentation`으로 호출해서 스토리라인을 정리하세요.
|
- Use a message headline, not a vague topic label.
|
||||||
4. 그 결과를 바탕으로 `pptx_create`용 슬라이드 배열을 설계하세요.
|
- Include `Executive Summary`, `Recommendation`, and `Roadmap` in most business decks.
|
||||||
5. 생성 후 결과 파일 경로와 핵심 구성(슬라이드 수, 사용 템플릿/테마, 주요 레이아웃)을 간단히 안내하세요.
|
- Add `Appendix` or `Evidence` when the deck is 6 slides or longer.
|
||||||
|
- Use comparison, chart, KPI, or evidence slides when data exists.
|
||||||
|
|
||||||
## 컨설팅형 스토리라인 규칙
|
## Useful layouts
|
||||||
- 기본 구조는 가능하면 아래 순서를 따르세요.
|
|
||||||
- `Executive Summary`
|
|
||||||
- `Situation & Imperative`
|
|
||||||
- `Key Findings`
|
|
||||||
- `Options & Recommendation`
|
|
||||||
- `Implementation Roadmap`
|
|
||||||
- `Impact & Ask`
|
|
||||||
|
|
||||||
## 레이아웃 선택 기준
|
|
||||||
- `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`
|
- `title`
|
||||||
- `executive_summary`
|
- `executive_summary`
|
||||||
- `comparison`
|
- `comparison`
|
||||||
- `recommendation`
|
- `recommendation`
|
||||||
- `roadmap`
|
- `roadmap`
|
||||||
- `kpi_dashboard`
|
- `kpi_dashboard`
|
||||||
- `section`
|
- `chart`
|
||||||
- `content`
|
- `table`
|
||||||
|
- `issue_tree`
|
||||||
|
- `before_after`
|
||||||
|
- `decision_matrix`
|
||||||
|
- `risk_heatmap`
|
||||||
|
- `benefit_waterfall`
|
||||||
|
- `operating_model`
|
||||||
|
- `appendix_evidence`
|
||||||
|
|
||||||
## 예시 파라미터 스케치
|
## Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"path": "strategy-deck.pptx",
|
"path": "strategy-deck.pptx",
|
||||||
"template": "basic100",
|
"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": [
|
"slides": [
|
||||||
{
|
|
||||||
"layout": "title",
|
|
||||||
"title": "2026 사업 전략 제안",
|
|
||||||
"subtitle": "성장 가속과 운영 효율 동시 달성"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"layout": "executive_summary",
|
"layout": "executive_summary",
|
||||||
"title": "Executive Summary",
|
"title": "Executive Summary",
|
||||||
"headline": "핵심 투자 우선순위를 재배치하면 2개 분기 내 수익성을 개선할 수 있습니다.",
|
"headline": "Focus on retention and CRM automation to restore growth within two quarters.",
|
||||||
"summary_points": [
|
"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 고도화와 운영 자동화를 병행하고, 저효율 캠페인은 즉시 축소합니다.",
|
"recommendation": "Approve phase-1 CRM upgrade and retention program launch."
|
||||||
"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": "핵심 세그먼트" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
한국어로 안내하고, 결과 파일은 작업 폴더에 저장하세요.
|
|
||||||
|
|||||||
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