문서 대시보드·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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
124
src/AxCopilot.Tests/Services/PptxSkillGoldenDeckTests.cs
Normal file
124
src/AxCopilot.Tests/Services/PptxSkillGoldenDeckTests.cs
Normal 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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user