문서 대시보드·DOCX 스타일맵·PPT 골든 회귀를 고도화하고 검증 추가

- ExcelSkill에 summary_sheet 기반 Dashboard 시트 생성을 추가해 Summary -> Dashboard -> Detail 시트 흐름을 지원
- ArtifactQualityReviewService workbook 리뷰에 dashboard 존재 여부를 반영하고 multi-sheet workbook 보완 포인트를 강화
- DocumentAssemblerTool style_map 범위를 cover_subtitle/callout/table_header까지 확장해 템플릿 기반 DOCX 조립 품질을 개선
- Excel/DOCX/PPT 회귀 테스트를 확장하고 PptxSkillGoldenDeckTests를 추가해 strong deck 품질 기준을 고정
- README.md와 docs/DEVELOPMENT.md에 2026-04-14 23:25 (KST) 기준 작업 이력과 검증 명령을 반영

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next4\\ -p:IntermediateOutputPath=obj\\verify_doc_next4\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|PptxSkillGoldenDeckTests -p:OutputPath=bin\\verify_doc_next4_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next4_tests\\ : 통과 20
This commit is contained in:
2026-04-14 23:26:59 +09:00
parent 2e36f2fef1
commit 116c420bf6
9 changed files with 369 additions and 35 deletions

View File

@@ -1838,3 +1838,12 @@ MIT License
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [HtmlSkillPrintFrameTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillPrintFrameTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)를 확장해 trend dashboard, default print frame, slide-level alert를 회귀 검증했습니다. - 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [HtmlSkillPrintFrameTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillPrintFrameTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)를 확장해 trend dashboard, default print frame, slide-level alert를 회귀 검증했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\` 통과 13 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\` 통과 13
업데이트: 2026-04-14 23:25 (KST)
- 문서 고도화 다음 단계를 반영했습니다. [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `summary_sheet.dashboard_sheet_name``trend_series`를 기반으로 summary 시트 앞뒤에 별도 `Dashboard` 시트를 생성합니다. decision summary, scorecards, trend dashboard, detail-sheet links를 실제 워크북 시트로 분리해 multi-sheet 보고서형 workbook 완성도를 높였습니다.
- 같은 파일의 workbook review 입력과 [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 `HasDashboardSheet`를 인식하도록 확장됐습니다. summary sheet만 있고 dashboard가 없는 multi-sheet workbook은 개선 포인트로 잡고, dashboard가 있으면 강점으로 점수화합니다.
- [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)는 `style_map``cover_subtitle`, `callout`, `table_header`까지 확장했습니다. 이제 템플릿 기반 DOCX 조립에서 cover subtitle, 강조 블록, 표 헤더 문단도 사내 Word 스타일을 직접 따를 수 있습니다.
- 테스트로 [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 확장해 subtitle/callout/table header style 적용을 검증했고, [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs)는 실제 `Dashboard` 시트 생성과 링크 텍스트까지 확인하도록 보강했습니다.
- [PptxSkillGoldenDeckTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillGoldenDeckTests.cs)를 추가해 strong board deck이 `PPT quality` 요약을 안정적으로 반환하고 불필요한 `Slide alerts` 없이 통과하는지 golden 회귀로 고정했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next4\\ -p:IntermediateOutputPath=obj\\verify_doc_next4\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|PptxSkillGoldenDeckTests" -p:OutputPath=bin\\verify_doc_next4_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next4_tests\\` 통과 20

View File

@@ -910,3 +910,13 @@ UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [HtmlSkillPrintFrameTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillPrintFrameTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)를 확장했습니다. - 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [HtmlSkillPrintFrameTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillPrintFrameTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)를 확장했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\` 통과 13 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\` 통과 13
업데이트: 2026-04-14 23:25 (KST)
- [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `summary_sheet.dashboard_sheet_name`과 `trend_series`를 바탕으로 별도 `Dashboard` worksheet를 생성합니다. summary sheet에서 decision summary, scorecards, trend dashboard, detail sheet links를 요약하고, dashboard sheet에서는 이를 한 장의 workbook dashboard로 다시 정리해 summary-only workbook보다 분석/보고 밀도를 높입니다.
- 같은 파일의 single-sheet / multi-sheet workbook 생성 경로는 dashboard sheet가 있으면 시트 순서를 `Summary -> Dashboard -> Detail...`로 정렬하고, summary hyperlink 수와 total sheet count를 품질 리뷰 입력에 정확히 전달합니다.
- [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)의 `WorkbookReviewInput`은 `HasDashboardSheet`를 새로 받습니다. dashboard가 있는 workbook은 강점으로 계산하고, detail sheet가 여러 장인데 dashboard가 없는 경우는 보완 포인트로 돌려줍니다.
- [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)는 `style_map` 지원 범위를 `cover_subtitle`, `callout`, `table_header`까지 넓혔습니다. cover subtitle 문단, 강조 블록 paragraph, 표 header cell paragraph가 사내 DOCX 템플릿 스타일을 실제로 타도록 조립 경로를 연결했습니다.
- [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)는 title/heading/body뿐 아니라 subtitle/callout/table header 스타일까지 회귀 검증하도록 보강했습니다.
- [PptxSkillGoldenDeckTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillGoldenDeckTests.cs)를 추가해 strong board deck이 `PPT quality` 요약을 안정적으로 반환하고 불필요한 `Slide alerts` 없이 통과하는지 golden regression으로 고정했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next4\\ -p:IntermediateOutputPath=obj\\verify_doc_next4\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|PptxSkillGoldenDeckTests" -p:OutputPath=bin\\verify_doc_next4_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next4_tests\\` 통과 20

View File

@@ -71,6 +71,7 @@ public class ArtifactQualityReviewServiceTests
false, false,
false, false,
false, false,
false,
false)); false));
review.Issues.Should().Contain(issue => issue.Message.Contains("summary sheet", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("summary sheet", StringComparison.OrdinalIgnoreCase));

View File

@@ -28,7 +28,10 @@ public class DocumentAssemblerStyleMapTests
stylePart.Styles = new Styles( stylePart.Styles = new Styles(
new Style { Type = StyleValues.Paragraph, StyleId = "DeckTitle", CustomStyle = true }, new Style { Type = StyleValues.Paragraph, StyleId = "DeckTitle", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckHeading1", CustomStyle = true }, new Style { Type = StyleValues.Paragraph, StyleId = "DeckHeading1", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckBody", CustomStyle = true }); new Style { Type = StyleValues.Paragraph, StyleId = "DeckBody", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckSubtitle", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckCallout", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckTableHeader", CustomStyle = true });
stylePart.Styles.Save(); stylePart.Styles.Save();
mainPart.Document.Save(); mainPart.Document.Save();
} }
@@ -48,13 +51,17 @@ public class DocumentAssemblerStyleMapTests
"title": "Board Update", "title": "Board Update",
"format": "docx", "format": "docx",
"template_path": "style-template.docx", "template_path": "style-template.docx",
"cover_subtitle": "Quarterly Steering Pack",
"style_map": { "style_map": {
"title": "DeckTitle", "title": "DeckTitle",
"heading1": "DeckHeading1", "heading1": "DeckHeading1",
"body": "DeckBody" "body": "DeckBody",
"cover_subtitle": "DeckSubtitle",
"callout": "DeckCallout",
"table_header": "DeckTableHeader"
}, },
"sections": [ "sections": [
{ "heading": "1. Executive Summary", "level": 1, "content": "<p>Margins improved across key accounts.</p>" } { "heading": "1. Executive Summary", "level": 1, "content": "<p>Margins improved across key accounts.</p><div class=\"callout-info\">Focus on margin recovery</div><table><tr><th>Metric</th><th>Value</th></tr><tr><td>Margin</td><td>18%</td></tr></table>" }
] ]
} }
""").RootElement; """).RootElement;
@@ -69,12 +76,28 @@ public class DocumentAssemblerStyleMapTests
paragraphs.First(paragraph => paragraph.InnerText == "Board Update") paragraphs.First(paragraph => paragraph.InnerText == "Board Update")
.ParagraphProperties?.ParagraphStyleId?.Val?.Value .ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckTitle"); .Should().Be("DeckTitle");
paragraphs.First(paragraph => paragraph.InnerText == "Quarterly Steering Pack")
.ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckSubtitle");
paragraphs.First(paragraph => paragraph.InnerText == "1. Executive Summary") paragraphs.First(paragraph => paragraph.InnerText == "1. Executive Summary")
.ParagraphProperties?.ParagraphStyleId?.Val?.Value .ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckHeading1"); .Should().Be("DeckHeading1");
paragraphs.First(paragraph => paragraph.InnerText.Contains("Margins improved")) paragraphs.First(paragraph => paragraph.InnerText.Contains("Margins improved"))
.ParagraphProperties?.ParagraphStyleId?.Val?.Value .ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckBody"); .Should().Be("DeckBody");
paragraphs.First(paragraph => paragraph.InnerText.Contains("Focus on margin recovery"))
.ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckCallout");
var firstHeaderParagraph = doc.MainDocumentPart.Document.Body!
.Descendants<Table>()
.First()
.Descendants<TableRow>()
.First()
.Descendants<Paragraph>()
.First();
firstHeaderParagraph.ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckTableHeader");
} }
finally finally
{ {

View File

@@ -32,6 +32,7 @@ public class ExcelSkillDashboardSummaryTests
"path": "dashboard-review.xlsx", "path": "dashboard-review.xlsx",
"summary_sheet": { "summary_sheet": {
"name": "Summary", "name": "Summary",
"dashboard_sheet_name": "Dashboard",
"title": "Operating Review", "title": "Operating Review",
"decision_summary": [ "decision_summary": [
{ "label": "Decision Ask", "value": "Approve regional rollout", "owner": "COO" } { "label": "Decision Ask", "value": "Approve regional rollout", "owner": "COO" }
@@ -65,6 +66,9 @@ public class ExcelSkillDashboardSummaryTests
using var doc = SpreadsheetDocument.Open(Path.Combine(workDir, "dashboard-review.xlsx"), false); using var doc = SpreadsheetDocument.Open(Path.Combine(workDir, "dashboard-review.xlsx"), false);
var workbookPart = doc.WorkbookPart!; var workbookPart = doc.WorkbookPart!;
workbookPart.Workbook.Sheets!.Elements<Sheet>().Select(sheet => sheet.Name!.Value)
.Should().Contain(["Summary", "Dashboard", "Revenue"]);
var summarySheet = workbookPart.Workbook.Sheets!.Elements<Sheet>().First(sheet => sheet.Name == "Summary"); var summarySheet = workbookPart.Workbook.Sheets!.Elements<Sheet>().First(sheet => sheet.Name == "Summary");
var summaryPart = (WorksheetPart)workbookPart.GetPartById(summarySheet.Id!); var summaryPart = (WorksheetPart)workbookPart.GetPartById(summarySheet.Id!);
var texts = summaryPart.Worksheet.Descendants<Cell>() var texts = summaryPart.Worksheet.Descendants<Cell>()
@@ -80,6 +84,16 @@ public class ExcelSkillDashboardSummaryTests
texts.Should().Contain("-10"); texts.Should().Contain("-10");
texts.Should().Contain("Revenue"); texts.Should().Contain("Revenue");
texts.Should().Contain("SMB margin pressure"); texts.Should().Contain("SMB margin pressure");
var dashboardSheet = workbookPart.Workbook.Sheets!.Elements<Sheet>().First(sheet => sheet.Name == "Dashboard");
var dashboardPart = (WorksheetPart)workbookPart.GetPartById(dashboardSheet.Id!);
var dashboardTexts = dashboardPart.Worksheet.Descendants<Cell>()
.Select(cell => cell.CellValue?.Text)
.Where(text => !string.IsNullOrWhiteSpace(text))
.ToList();
dashboardTexts.Should().Contain("Operating Review Dashboard");
dashboardTexts.Should().Contain("Linked Detail Sheets");
dashboardTexts.Should().Contain("Open: Revenue");
} }
finally finally
{ {

View File

@@ -0,0 +1,124 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class PptxSkillGoldenDeckTests
{
[Fact]
public async Task ExecuteAsync_WithStrongBoardDeck_ShouldReturnStableQualitySummary()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-pptx-golden-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var tool = new PptxSkill();
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"path": "board-golden.pptx",
"theme": "professional",
"audience": "Executive committee",
"objective": "Approve the phase-1 operating model rollout",
"decision_ask": "Approve the phase-1 rollout and funding release",
"slides": [
{
"layout": "title",
"title": "Operating Model Refresh"
},
{
"layout": "executive_summary",
"title": "Executive Summary",
"headline": "Phase-1 rollout can improve service consistency while protecting margin.",
"summary_points": [
"Three pilot functions account for most process variation and rework.",
"Phase-1 can be delivered without slowing current-quarter commitments.",
"The operating model change is expected to reduce handoff delay by 18%."
],
"recommendation": "Approve the phase-1 rollout and lock the pilot governance model.",
"kpis": [
{ "label": "Cycle Time", "value": "-18%", "trend": "target", "note": "handoff simplification" },
{ "label": "Margin", "value": "+2.4pt", "trend": "run-rate", "note": "mix and rework" },
{ "label": "On-time Delivery", "value": "96%", "trend": "pilot", "note": "stable" }
]
},
{
"layout": "comparison",
"title": "Options",
"headline": "A phased rollout balances speed, control, and adoption risk.",
"options": [
{ "name": "Pilot only", "pros": "Lowest delivery risk", "cons": "Value capture is delayed", "verdict": "Too cautious" },
{ "name": "Phased rollout", "pros": "Balanced speed and control", "cons": "Requires PMO discipline", "verdict": "Recommended" },
{ "name": "Full rollout", "pros": "Fastest value capture", "cons": "High adoption risk", "verdict": "Too disruptive" }
]
},
{
"layout": "roadmap",
"title": "Roadmap",
"headline": "Roll out in three waves with explicit governance checkpoints.",
"phases": [
{ "title": "Design", "detail": "Finalize scope, KPIs, and governance", "timeline": "Weeks 1-3", "owner": "PMO" },
{ "title": "Pilot", "detail": "Launch pilot functions and refine playbooks", "timeline": "Weeks 4-8", "owner": "Operations" },
{ "title": "Scale", "detail": "Expand to the remaining regions", "timeline": "Weeks 9-14", "owner": "Business" }
]
},
{
"layout": "recommendation",
"title": "Recommendation",
"recommendation": "Approve phase-1 rollout, assign PMO governance, and release pilot funding.",
"summary_points": [
"The phased path protects execution quality while capturing measurable value in-quarter.",
"The pilot governance model can be reused for the scale phase.",
"The decision can be taken now because the key delivery risks are already bounded."
],
"next_steps": [
"Confirm phase-1 scope this week",
"Launch pilot governance cadence next week",
"Review pilot performance at week 8"
]
},
{
"layout": "table",
"title": "Appendix & Evidence",
"headers": ["Evidence", "Detail"],
"rows": [
["Process review", "Pilot teams show the highest rework and handoff load."],
["Financial impact", "Run-rate margin lift is driven by lower rework and better prioritization."]
]
}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
File.Exists(Path.Combine(workDir, "board-golden.pptx")).Should().BeTrue();
result.Output.Should().Contain("PPT quality");
result.Output.Should().NotContain("Slide alerts:");
result.Output.Should().Contain("Needs work: none");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}

View File

@@ -68,6 +68,7 @@ public sealed record WorkbookReviewInput(
int DataValidationCount, int DataValidationCount,
int ConditionalFormattingCount, int ConditionalFormattingCount,
bool HasSummarySheet, bool HasSummarySheet,
bool HasDashboardSheet,
bool HasHighlightSection, bool HasHighlightSection,
bool HasActionSection, bool HasActionSection,
bool HasScorecardSection, bool HasScorecardSection,
@@ -197,6 +198,7 @@ public static class ArtifactQualityReviewService
var issues = new List<ArtifactReviewIssue>(); var issues = new List<ArtifactReviewIssue>();
if (input.HasSummarySheet) strengths.Add("Includes summary sheet"); if (input.HasSummarySheet) strengths.Add("Includes summary sheet");
if (input.HasDashboardSheet) strengths.Add("Includes dashboard sheet");
if (input.DetailSheetCount > 1) strengths.Add($"Contains {input.DetailSheetCount} detail sheets"); if (input.DetailSheetCount > 1) strengths.Add($"Contains {input.DetailSheetCount} detail sheets");
if (input.FormulaCount > 0) strengths.Add($"Includes {input.FormulaCount} formula cell(s)"); if (input.FormulaCount > 0) strengths.Add($"Includes {input.FormulaCount} formula cell(s)");
if (input.HyperlinkCount > 0) strengths.Add("Includes navigation links"); if (input.HyperlinkCount > 0) strengths.Add("Includes navigation links");
@@ -220,6 +222,8 @@ public static class ArtifactQualityReviewService
issues.Add(new("Conditional formatting is limited for a workbook with enough data to prioritize or flag.", ArtifactReviewSeverity.Info)); issues.Add(new("Conditional formatting is limited for a workbook with enough data to prioritize or flag.", ArtifactReviewSeverity.Info));
if (input.HasSummarySheet && !input.HasScorecardSection && !input.HasDecisionSection && !input.HasHighlightSection) if (input.HasSummarySheet && !input.HasScorecardSection && !input.HasDecisionSection && !input.HasHighlightSection)
issues.Add(new("Summary sheet could better surface KPIs, decisions, or highlights.", ArtifactReviewSeverity.Info)); issues.Add(new("Summary sheet could better surface KPIs, decisions, or highlights.", ArtifactReviewSeverity.Info));
if (input.HasSummarySheet && input.DetailSheetCount >= 2 && !input.HasDashboardSheet)
issues.Add(new("Workbook could benefit from a dashboard sheet to summarize multi-sheet trends.", ArtifactReviewSeverity.Info));
return BuildReport("xlsx", strengths, issues); return BuildReport("xlsx", strengths, issues);
} }

View File

@@ -15,7 +15,10 @@ public class DocumentAssemblerTool : IAgentTool
string? TitleStyle, string? TitleStyle,
string? Heading1Style, string? Heading1Style,
string? Heading2Style, string? Heading2Style,
string? BodyStyle); string? BodyStyle,
string? SubtitleStyle,
string? CalloutStyle,
string? TableHeaderStyle);
private static string GetDefaultOutputFormat() private static string GetDefaultOutputFormat()
{ {
@@ -67,7 +70,7 @@ public class DocumentAssemblerTool : IAgentTool
["header"] = new() { Type = "string", Description = "Header text for DOCX output." }, ["header"] = new() { Type = "string", Description = "Header text for DOCX output." },
["footer"] = new() { Type = "string", Description = "Footer text for DOCX output. Use {page} for page number." }, ["footer"] = new() { Type = "string", Description = "Footer text for DOCX output. Use {page} for page number." },
["page_numbers"] = new() { Type = "boolean", Description = "Show page numbers in DOCX footer. Default: true when header or footer is set." }, ["page_numbers"] = new() { Type = "boolean", Description = "Show page numbers in DOCX footer. Default: true when header or footer is set." },
["style_map"] = new() { Type = "object", Description = "Optional paragraph style map for template-based DOCX assembly. Example: {\"title\":\"Title\",\"heading1\":\"Heading1\",\"heading2\":\"Heading2\",\"body\":\"Normal\"}." }, ["style_map"] = new() { Type = "object", Description = "Optional paragraph style map for template-based DOCX assembly. Example: {\"title\":\"Title\",\"heading1\":\"Heading1\",\"heading2\":\"Heading2\",\"body\":\"Normal\",\"cover_subtitle\":\"Subtitle\",\"callout\":\"Quote\",\"table_header\":\"TableHeader\"}." },
}, },
Required = ["path", "title", "sections"] Required = ["path", "title", "sections"]
}; };
@@ -421,7 +424,9 @@ public class DocumentAssemblerTool : IAgentTool
var cell = new DocumentFormat.OpenXml.Wordprocessing.TableCell(); var cell = new DocumentFormat.OpenXml.Wordprocessing.TableCell();
if (isHeader) if (isHeader)
{ {
cell.AppendChild(CreateParagraph(cellText, fontSize: "21", bold: true, color: "1F3A5F", fill: "E8EEF8")); cell.AppendChild(!string.IsNullOrWhiteSpace(styleMap.TableHeaderStyle)
? CreateParagraph(cellText, fontSize: "21", bold: true, color: "1F3A5F", fill: "E8EEF8", styleId: styleMap.TableHeaderStyle)
: CreateParagraph(cellText, fontSize: "21", bold: true, color: "1F3A5F", fill: "E8EEF8"));
} }
else else
{ {
@@ -488,7 +493,7 @@ public class DocumentAssemblerTool : IAgentTool
var includeCover = !string.IsNullOrWhiteSpace(coverSubtitle); var includeCover = !string.IsNullOrWhiteSpace(coverSubtitle);
if (includeCover) if (includeCover)
AppendAssemblerCoverPage(body, title, coverSubtitle!, styleMap.TitleStyle); AppendAssemblerCoverPage(body, title, coverSubtitle!, styleMap.TitleStyle, styleMap.SubtitleStyle);
else else
body.AppendChild(CreateTitleParagraph(title)); body.AppendChild(CreateTitleParagraph(title));
@@ -601,7 +606,13 @@ public class DocumentAssemblerTool : IAgentTool
else if (Regex.IsMatch(block, @"^<(blockquote|div)", RegexOptions.IgnoreCase)) else if (Regex.IsMatch(block, @"^<(blockquote|div)", RegexOptions.IgnoreCase))
{ {
var fill = Regex.IsMatch(block, "highlight-box", RegexOptions.IgnoreCase) ? "FFF7DA" : "EDF4FF"; var fill = Regex.IsMatch(block, "highlight-box", RegexOptions.IgnoreCase) ? "FFF7DA" : "EDF4FF";
body.AppendChild(CreateParagraph(ExtractStructuredText(block), fontSize: "21", bold: true, color: "1F3A5F", fill: fill)); body.AppendChild(CreateParagraph(
ExtractStructuredText(block),
fontSize: "21",
bold: true,
color: "1F3A5F",
fill: fill,
styleId: styleMap.CalloutStyle));
if (Regex.IsMatch(block, "highlight-box", RegexOptions.IgnoreCase)) if (Regex.IsMatch(block, "highlight-box", RegexOptions.IgnoreCase))
highlightCount++; highlightCount++;
else else
@@ -705,13 +716,16 @@ public class DocumentAssemblerTool : IAgentTool
JsonElement styleMapArg) JsonElement styleMapArg)
{ {
if (styleMapArg.ValueKind != JsonValueKind.Object) if (styleMapArg.ValueKind != JsonValueKind.Object)
return new AssemblerDocxStyleMap(null, null, null, null); return new AssemblerDocxStyleMap(null, null, null, null, null, null, null);
return new AssemblerDocxStyleMap( return new AssemblerDocxStyleMap(
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : null), FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("heading1", out var heading1El) ? heading1El.SafeGetString() : null), FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("heading1", out var heading1El) ? heading1El.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("heading2", out var heading2El) ? heading2El.SafeGetString() : null), FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("heading2", out var heading2El) ? heading2El.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("body", out var bodyEl) ? bodyEl.SafeGetString() : null)); FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("body", out var bodyEl) ? bodyEl.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("cover_subtitle", out var subtitleEl) ? subtitleEl.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("callout", out var calloutEl) ? calloutEl.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("table_header", out var tableHeaderEl) ? tableHeaderEl.SafeGetString() : null));
} }
private static string? FindAssemblerStyle( private static string? FindAssemblerStyle(
@@ -732,7 +746,7 @@ public class DocumentAssemblerTool : IAgentTool
: null; : null;
} }
private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle, string? titleStyleId) private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle, string? titleStyleId, string? subtitleStyleId)
{ {
var titleParagraphProperties = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties var titleParagraphProperties = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
{ {
@@ -762,24 +776,30 @@ public class DocumentAssemblerTool : IAgentTool
titleRun.Append(new DocumentFormat.OpenXml.Wordprocessing.Text(title)); titleRun.Append(new DocumentFormat.OpenXml.Wordprocessing.Text(title));
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(titleParagraphProperties, titleRun)); body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(titleParagraphProperties, titleRun));
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph( var subtitleParagraphProperties = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties {
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
{ {
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Center
{
Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Center
},
SpacingBetweenLines = new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines
{
After = "260"
}
}, },
new DocumentFormat.OpenXml.Wordprocessing.Run( SpacingBetweenLines = new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines
new DocumentFormat.OpenXml.Wordprocessing.RunProperties( {
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "26" }, After = "260"
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "5B6472" }, }
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }), };
new DocumentFormat.OpenXml.Wordprocessing.Text(subtitle)))); if (!string.IsNullOrWhiteSpace(subtitleStyleId))
subtitleParagraphProperties.ParagraphStyleId = new DocumentFormat.OpenXml.Wordprocessing.ParagraphStyleId { Val = subtitleStyleId };
var subtitleRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
if (string.IsNullOrWhiteSpace(subtitleStyleId))
{
subtitleRun.Append(new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "26" },
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "5B6472" },
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }));
}
subtitleRun.Append(new DocumentFormat.OpenXml.Wordprocessing.Text(subtitle));
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(subtitleParagraphProperties, subtitleRun));
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph( body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties

View File

@@ -217,7 +217,8 @@ public class ExcelSkill : IAgentTool
conditionalFormattingCount, conditionalFormattingCount,
false, false,
false, false,
summaryArg.ValueKind == JsonValueKind.Object, false,
false,
false, false,
false, false,
false)); false));
@@ -259,12 +260,17 @@ public class ExcelSkill : IAgentTool
stylesPart.Stylesheet.Save(); stylesPart.Stylesheet.Save();
var wbSheets = workbookPart.Workbook.AppendChild(new Sheets()); var wbSheets = workbookPart.Workbook.AppendChild(new Sheets());
var dashboardSheetName = GetDashboardSheetName(summarySheet);
var hasDashboardSheet = HasDashboardSheet(summarySheet);
var summaryLinkedSheets = hasDashboardSheet
? new List<string> { dashboardSheetName, sheetName }
: [sheetName];
var summaryName = summarySheet.SafeTryGetProperty("name", out var summaryNameEl) var summaryName = summarySheet.SafeTryGetProperty("name", out var summaryNameEl)
? summaryNameEl.SafeGetString() ?? "Summary" ? summaryNameEl.SafeGetString() ?? "Summary"
: "Summary"; : "Summary";
var summaryPart = workbookPart.AddNewPart<WorksheetPart>(); var summaryPart = workbookPart.AddNewPart<WorksheetPart>();
WriteExecutiveSummarySheet(summaryPart, summarySheet, [sheetName]); WriteExecutiveSummarySheet(summaryPart, summarySheet, summaryLinkedSheets);
wbSheets.Append(new Sheet wbSheets.Append(new Sheet
{ {
Id = workbookPart.GetIdOfPart(summaryPart), Id = workbookPart.GetIdOfPart(summaryPart),
@@ -272,6 +278,19 @@ public class ExcelSkill : IAgentTool
Name = summaryName, Name = summaryName,
}); });
uint nextSheetId = 2;
if (hasDashboardSheet)
{
var dashboardPart = workbookPart.AddNewPart<WorksheetPart>();
WriteDashboardSheet(dashboardPart, summarySheet, [sheetName]);
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(dashboardPart),
SheetId = nextSheetId++,
Name = dashboardSheetName,
});
}
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>(); var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(); worksheetPart.Worksheet = new Worksheet();
@@ -287,7 +306,7 @@ public class ExcelSkill : IAgentTool
wbSheets.Append(new Sheet wbSheets.Append(new Sheet
{ {
Id = workbookPart.GetIdOfPart(worksheetPart), Id = workbookPart.GetIdOfPart(worksheetPart),
SheetId = 2, SheetId = nextSheetId,
Name = sheetName, Name = sheetName,
}); });
@@ -299,14 +318,15 @@ public class ExcelSkill : IAgentTool
numFmts.Count > 0, alignments.Count > 0, themeName); numFmts.Count > 0, alignments.Count > 0, themeName);
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput( var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
summaryName, summaryName,
2, hasDashboardSheet ? 3 : 2,
1, 1,
rowCount, rowCount,
CountFormulaCells(rows), CountFormulaCells(rows),
1, summaryLinkedSheets.Count,
validationCount, validationCount,
conditionalFormattingCount, conditionalFormattingCount,
true, true,
hasDashboardSheet,
HasSummaryItems(summarySheet, "highlights"), HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions"), HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"), HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"),
@@ -356,6 +376,8 @@ public class ExcelSkill : IAgentTool
var totalValidationCount = 0; var totalValidationCount = 0;
var totalConditionalFormattingCount = 0; var totalConditionalFormattingCount = 0;
var detailSheetNames = new List<string>(); var detailSheetNames = new List<string>();
var dashboardSheetName = GetDashboardSheetName(summarySheet);
var hasDashboardSheet = HasDashboardSheet(summarySheet);
foreach (var sheetDef in sheetsArr.EnumerateArray()) foreach (var sheetDef in sheetsArr.EnumerateArray())
{ {
@@ -371,13 +393,28 @@ public class ExcelSkill : IAgentTool
? summaryNameEl.SafeGetString() ?? "Summary" ? summaryNameEl.SafeGetString() ?? "Summary"
: "Summary"; : "Summary";
var summaryPart = workbookPart.AddNewPart<WorksheetPart>(); var summaryPart = workbookPart.AddNewPart<WorksheetPart>();
WriteExecutiveSummarySheet(summaryPart, summarySheet, detailSheetNames); var summaryLinkedSheets = hasDashboardSheet
? new[] { dashboardSheetName }.Concat(detailSheetNames).ToList()
: detailSheetNames;
WriteExecutiveSummarySheet(summaryPart, summarySheet, summaryLinkedSheets);
wbSheets.Append(new Sheet wbSheets.Append(new Sheet
{ {
Id = workbookPart.GetIdOfPart(summaryPart), Id = workbookPart.GetIdOfPart(summaryPart),
SheetId = sheetId++, SheetId = sheetId++,
Name = summaryName, Name = summaryName,
}); });
if (hasDashboardSheet)
{
var dashboardPart = workbookPart.AddNewPart<WorksheetPart>();
WriteDashboardSheet(dashboardPart, summarySheet, detailSheetNames);
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(dashboardPart),
SheetId = sheetId++,
Name = dashboardSheetName,
});
}
} }
foreach (var sheetDef in sheetsArr.EnumerateArray()) foreach (var sheetDef in sheetsArr.EnumerateArray())
@@ -425,14 +462,15 @@ public class ExcelSkill : IAgentTool
workbookPart.Workbook.Save(); workbookPart.Workbook.Save();
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput( var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
fullPath, fullPath,
totalSheets + (summarySheet.ValueKind == JsonValueKind.Object ? 1 : 0), totalSheets + (summarySheet.ValueKind == JsonValueKind.Object ? 1 : 0) + (hasDashboardSheet ? 1 : 0),
totalSheets, totalSheets,
totalRows, totalRows,
totalFormulaCount, totalFormulaCount,
summarySheet.ValueKind == JsonValueKind.Object ? detailSheetNames.Count : 0, summarySheet.ValueKind == JsonValueKind.Object ? detailSheetNames.Count + (hasDashboardSheet ? 1 : 0) : 0,
totalValidationCount, totalValidationCount,
totalConditionalFormattingCount, totalConditionalFormattingCount,
summarySheet.ValueKind == JsonValueKind.Object, summarySheet.ValueKind == JsonValueKind.Object,
hasDashboardSheet,
HasSummaryItems(summarySheet, "highlights"), HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions"), HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"), HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"),
@@ -544,6 +582,72 @@ public class ExcelSkill : IAgentTool
} }
} }
private static void WriteDashboardSheet(WorksheetPart worksheetPart, JsonElement summarySheet, IReadOnlyList<string> detailSheetNames)
{
worksheetPart.Worksheet = new Worksheet();
var columns = new Columns(
new Column { Min = 1, Max = 1, Width = 22, CustomWidth = true },
new Column { Min = 2, Max = 2, Width = 18, CustomWidth = true },
new Column { Min = 3, Max = 3, Width = 18, CustomWidth = true },
new Column { Min = 4, Max = 4, Width = 22, CustomWidth = true },
new Column { Min = 5, Max = 5, Width = 18, CustomWidth = true },
new Column { Min = 6, Max = 6, Width = 30, CustomWidth = true });
worksheetPart.Worksheet.Append(columns);
var sheetData = new SheetData();
worksheetPart.Worksheet.Append(sheetData);
var merges = new MergeCells();
var hyperlinks = new Hyperlinks();
uint rowIndex = 1;
var baseTitle = summarySheet.SafeTryGetProperty("title", out var titleEl)
? titleEl.SafeGetString() ?? "Executive Summary"
: "Executive Summary";
var subtitle = summarySheet.SafeTryGetProperty("subtitle", out var subtitleEl)
? subtitleEl.SafeGetString() ?? string.Empty
: string.Empty;
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", $"{baseTitle} Dashboard", 1);
if (!string.IsNullOrWhiteSpace(subtitle))
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", subtitle, 3);
rowIndex++;
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
AppendTrendSection(summarySheet, sheetData, ref rowIndex);
AppendSheetSummarySection(summarySheet, sheetData, ref rowIndex);
AppendSummaryTextSection(summarySheet, "actions", "Priority Actions", sheetData, merges, ref rowIndex);
if (detailSheetNames.Count > 0)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Linked Detail Sheets", 1);
var startRow = rowIndex;
foreach (var detailSheetName in detailSheetNames)
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", $"Open: {detailSheetName}", 0);
for (var i = 0; i < detailSheetNames.Count; i++)
{
hyperlinks.Append(new Hyperlink
{
Reference = $"A{startRow + (uint)i}",
Location = $"'{detailSheetNames[i]}'!A1",
Display = detailSheetNames[i]
});
}
}
if (merges.HasChildren)
worksheetPart.Worksheet.InsertAfter(merges, sheetData);
if (hyperlinks.HasChildren)
{
var anchor = merges.HasChildren ? (OpenXmlElement)merges : sheetData;
worksheetPart.Worksheet.InsertAfter(hyperlinks, anchor);
}
}
private static void AppendDecisionSummarySection(JsonElement summarySheet, SheetData sheetData, MergeCells merges, ref uint rowIndex) private static void AppendDecisionSummarySection(JsonElement summarySheet, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{ {
if (!summarySheet.SafeTryGetProperty("decision_summary", out var decisionSummary)) if (!summarySheet.SafeTryGetProperty("decision_summary", out var decisionSummary))
@@ -685,6 +789,31 @@ public class ExcelSkill : IAgentTool
rowIndex++; rowIndex++;
} }
private static bool HasDashboardSheet(JsonElement summarySheet)
{
if (summarySheet.ValueKind != JsonValueKind.Object)
return false;
if (summarySheet.SafeTryGetProperty("dashboard_sheet_name", out var dashboardNameEl)
&& dashboardNameEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(dashboardNameEl.SafeGetString()))
return true;
return summarySheet.SafeTryGetProperty("trend_series", out var trendSeries)
&& trendSeries.ValueKind == JsonValueKind.Array
&& trendSeries.GetArrayLength() > 0;
}
private static string GetDashboardSheetName(JsonElement summarySheet)
{
if (summarySheet.SafeTryGetProperty("dashboard_sheet_name", out var dashboardNameEl)
&& dashboardNameEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(dashboardNameEl.SafeGetString()))
return dashboardNameEl.SafeGetString()!;
return "Dashboard";
}
private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex) private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{ {
if (!summarySheet.SafeTryGetProperty(key, out var items) || items.ValueKind != JsonValueKind.Array || items.GetArrayLength() == 0) if (!summarySheet.SafeTryGetProperty(key, out var items) || items.ValueKind != JsonValueKind.Array || items.GetArrayLength() == 0)