문서 고도화 다음 단계를 반영해 엑셀 summary와 DOCX 조립 품질을 끌어올렸습니다

핵심 수정사항:
- ExcelSkill summary_sheet에 decision_summary, scorecards, sheet_summaries를 추가해 dashboard형 summary 시트를 생성하도록 확장했습니다.
- ArtifactQualityReviewService의 workbook 리뷰 입력을 확장해 KPI/decision/detail summary 존재 여부를 품질 점수와 보완 포인트에 반영했습니다.
- DocumentAssemblerTool에 style_map 파라미터를 추가해 template 기반 DOCX 조립에서 title/heading/body 문단 스타일을 실제 Word 스타일로 매핑하도록 개선했습니다.
- DocumentAssemblerStyleMapTests, ExcelSkillDashboardSummaryTests를 추가하고 기존 ArtifactQualityReviewServiceTests를 갱신했습니다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\ : 통과 11
This commit is contained in:
2026-04-14 23:06:53 +09:00
parent 3232db1b12
commit 1edeffa206
8 changed files with 471 additions and 56 deletions

View File

@@ -1822,3 +1822,11 @@ MIT License
- 테스트로 `ExcelSkillDataValidationTests`를 추가했고, `DocumentAssemblerDocxFeaturesTests`, `HtmlSkillConsultingSectionsTests`를 확장해 DOCX 템플릿/페이지 번호와 HTML decision/evidence 블록을 회귀 검증했습니다. - 테스트로 `ExcelSkillDataValidationTests`를 추가했고, `DocumentAssemblerDocxFeaturesTests`, `HtmlSkillConsultingSectionsTests`를 확장해 DOCX 템플릿/페이지 번호와 HTML decision/evidence 블록을 회귀 검증했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\` 통과 9 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\` 통과 9
업데이트: 2026-04-14 23:05 (KST)
- 문서 포맷 고도화 다음 단계를 반영했습니다. [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `summary_sheet``decision_summary`, `scorecards`, `sheet_summaries`를 받을 수 있게 확장되어, 단순 KPI 표를 넘어 의사결정 요약과 상세 시트별 상태를 함께 보여주는 dashboard형 summary 시트를 생성합니다.
- 같은 파일의 워크북 품질 리뷰 입력도 확장해 summary sheet가 KPI/scorecard, decision summary, detail sheet summary를 실제로 담고 있는지 점수화하도록 [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)를 보강했습니다.
- [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)는 `style_map` 파라미터를 지원합니다. 템플릿 기반 DOCX 조립에서 `title`, `heading1`, `heading2`, `body` 문단 스타일을 실제 Word 문단에 연결해 cover title, 섹션 헤딩, 본문 문단이 사내 템플릿 스타일을 더 잘 따르도록 개선했습니다.
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 추가했고, [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)도 새 workbook review 입력 형식에 맞춰 갱신했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\` 통과 11

View File

@@ -891,3 +891,11 @@ UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
- 테스트로 [ExcelSkillDataValidationTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDataValidationTests.cs)를 추가했고, [DocumentAssemblerDocxFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerDocxFeaturesTests.cs), [HtmlSkillConsultingSectionsTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs)를 확장해 DOCX 템플릿/페이지 번호와 HTML decision/evidence 블록을 회귀 검증했습니다. - 테스트로 [ExcelSkillDataValidationTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDataValidationTests.cs)를 추가했고, [DocumentAssemblerDocxFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerDocxFeaturesTests.cs), [HtmlSkillConsultingSectionsTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs)를 확장해 DOCX 템플릿/페이지 번호와 HTML decision/evidence 블록을 회귀 검증했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\` 통과 9 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\` 통과 9
업데이트: 2026-04-14 23:05 (KST)
- 문서 고도화 다음 단계를 반영했습니다. [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `summary_sheet`에 `decision_summary`, `scorecards`, `sheet_summaries`를 추가로 받을 수 있게 확장됐고, executive summary sheet에서 의사결정 요청, 핵심 scorecard, 상세 시트별 상태를 순서대로 렌더링합니다.
- 워크북 품질 리뷰 입력도 같은 구조를 인식하도록 [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)의 `WorkbookReviewInput`과 `ReviewWorkbook()`를 확장했습니다. 이제 summary sheet가 KPI/decision/detail summary를 충분히 담고 있는지 강점과 보완 포인트로 함께 표시합니다.
- [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)는 `style_map` 파라미터를 받아 template-based DOCX assembly에서 `title`, `heading1`, `heading2`, `body` 문단 스타일을 실제 Word 문단에 매핑합니다. cover title, 섹션 헤딩, 본문 문단이 사내 템플릿 스타일을 더 자연스럽게 따라가도록 정리했습니다.
- 새 회귀 테스트 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 추가했고, [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)도 새 record 시그니처에 맞춰 갱신했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\` 통과 11

View File

@@ -68,6 +68,9 @@ public class ArtifactQualityReviewServiceTests
0, 0,
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

@@ -0,0 +1,91 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class DocumentAssemblerStyleMapTests
{
[Fact]
public async Task ExecuteAsync_WithStyleMap_ShouldApplyTemplateParagraphStyles()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-doc-assemble-stylemap-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
var templatePath = Path.Combine(workDir, "style-template.docx");
try
{
using (var template = WordprocessingDocument.Create(templatePath, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
{
var mainPart = template.AddMainDocumentPart();
mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("Template Placeholder")))));
var stylePart = mainPart.AddNewPart<StyleDefinitionsPart>();
stylePart.Styles = new Styles(
new Style { Type = StyleValues.Paragraph, StyleId = "DeckTitle", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckHeading1", CustomStyle = true },
new Style { Type = StyleValues.Paragraph, StyleId = "DeckBody", CustomStyle = true });
stylePart.Styles.Save();
mainPart.Document.Save();
}
var tool = new DocumentAssemblerTool();
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"path": "assembled-stylemap.docx",
"title": "Board Update",
"format": "docx",
"template_path": "style-template.docx",
"style_map": {
"title": "DeckTitle",
"heading1": "DeckHeading1",
"body": "DeckBody"
},
"sections": [
{ "heading": "1. Executive Summary", "level": 1, "content": "<p>Margins improved across key accounts.</p>" }
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
using var doc = WordprocessingDocument.Open(Path.Combine(workDir, "assembled-stylemap.docx"), false);
var paragraphs = doc.MainDocumentPart!.Document.Body!.Elements<Paragraph>().ToList();
paragraphs.First(paragraph => paragraph.InnerText == "Board Update")
.ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckTitle");
paragraphs.First(paragraph => paragraph.InnerText == "1. Executive Summary")
.ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckHeading1");
paragraphs.First(paragraph => paragraph.InnerText.Contains("Margins improved"))
.ParagraphProperties?.ParagraphStyleId?.Val?.Value
.Should().Be("DeckBody");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}

View File

@@ -0,0 +1,91 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class ExcelSkillDashboardSummaryTests
{
[Fact]
public async Task ExecuteAsync_WithDashboardSummary_ShouldRenderDecisionAndSheetSummaryBlocks()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-xlsx-dashboard-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var tool = new ExcelSkill();
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"path": "dashboard-review.xlsx",
"summary_sheet": {
"name": "Summary",
"title": "Operating Review",
"decision_summary": [
{ "label": "Decision Ask", "value": "Approve regional rollout", "owner": "COO" }
],
"scorecards": [
{ "label": "Run-rate", "value": "96%", "status": "On Track", "note": "Ahead of plan" }
],
"sheet_summaries": [
{ "sheet": "Revenue", "status": "Watch", "summary": "SMB margin pressure", "owner": "Sales Ops" }
],
"highlights": ["Expansion pipeline improved"],
"actions": ["Lock next-quarter target"]
},
"sheets": [
{
"name": "Revenue",
"headers": ["Metric", "Value"],
"rows": [["Revenue", 120], ["Margin", 18]]
}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
result.Output.Should().Contain("Quality score");
using var doc = SpreadsheetDocument.Open(Path.Combine(workDir, "dashboard-review.xlsx"), false);
var workbookPart = doc.WorkbookPart!;
var summarySheet = workbookPart.Workbook.Sheets!.Elements<Sheet>().First(sheet => sheet.Name == "Summary");
var summaryPart = (WorksheetPart)workbookPart.GetPartById(summarySheet.Id!);
var texts = summaryPart.Worksheet.Descendants<Cell>()
.Select(cell => cell.CellValue?.Text)
.Where(text => !string.IsNullOrWhiteSpace(text))
.ToList();
texts.Should().Contain("Decision Summary");
texts.Should().Contain("Approve regional rollout");
texts.Should().Contain("Scorecards");
texts.Should().Contain("Run-rate");
texts.Should().Contain("Revenue");
texts.Should().Contain("SMB margin pressure");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}

View File

@@ -66,7 +66,10 @@ public sealed record WorkbookReviewInput(
int ConditionalFormattingCount, int ConditionalFormattingCount,
bool HasSummarySheet, bool HasSummarySheet,
bool HasHighlightSection, bool HasHighlightSection,
bool HasActionSection); bool HasActionSection,
bool HasScorecardSection,
bool HasDecisionSection,
bool HasSheetSummarySection);
public static class ArtifactQualityReviewService public static class ArtifactQualityReviewService
{ {
@@ -192,6 +195,9 @@ public static class ArtifactQualityReviewService
if (input.ConditionalFormattingCount > 0) strengths.Add("Includes conditional formatting"); if (input.ConditionalFormattingCount > 0) strengths.Add("Includes conditional formatting");
if (input.HasHighlightSection) strengths.Add("Includes highlight section"); if (input.HasHighlightSection) strengths.Add("Includes highlight section");
if (input.HasActionSection) strengths.Add("Includes action section"); if (input.HasActionSection) strengths.Add("Includes action section");
if (input.HasScorecardSection) strengths.Add("Includes KPI or scorecard section");
if (input.HasDecisionSection) strengths.Add("Includes decision summary");
if (input.HasSheetSummarySection) strengths.Add("Includes detail sheet summaries");
if (input.SheetCount > 1 && !input.HasSummarySheet) if (input.SheetCount > 1 && !input.HasSummarySheet)
issues.Add(new("Workbook has multiple sheets but no summary sheet.", ArtifactReviewSeverity.Warning)); issues.Add(new("Workbook has multiple sheets but no summary sheet.", ArtifactReviewSeverity.Warning));
@@ -203,6 +209,8 @@ public static class ArtifactQualityReviewService
issues.Add(new("Input controls are limited for a workbook with editable data.", ArtifactReviewSeverity.Info)); issues.Add(new("Input controls are limited for a workbook with editable data.", ArtifactReviewSeverity.Info));
if (input.ConditionalFormattingCount == 0 && input.DataRowCount >= 8) if (input.ConditionalFormattingCount == 0 && input.DataRowCount >= 8)
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)
issues.Add(new("Summary sheet could better surface KPIs, decisions, or highlights.", ArtifactReviewSeverity.Info));
return BuildReport("xlsx", strengths, issues); return BuildReport("xlsx", strengths, issues);
} }

View File

@@ -11,6 +11,12 @@ namespace AxCopilot.Services.Agent;
/// </summary> /// </summary>
public class DocumentAssemblerTool : IAgentTool public class DocumentAssemblerTool : IAgentTool
{ {
private sealed record AssemblerDocxStyleMap(
string? TitleStyle,
string? Heading1Style,
string? Heading2Style,
string? BodyStyle);
private static string GetDefaultOutputFormat() private static string GetDefaultOutputFormat()
{ {
var app = System.Windows.Application.Current as App; var app = System.Windows.Application.Current as App;
@@ -61,6 +67,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\"}." },
}, },
Required = ["path", "title", "sections"] Required = ["path", "title", "sections"]
}; };
@@ -76,6 +83,7 @@ public class DocumentAssemblerTool : IAgentTool
var templatePath = args.SafeTryGetProperty("template_path", out var templateEl) ? templateEl.SafeGetString() : null; var templatePath = args.SafeTryGetProperty("template_path", out var templateEl) ? templateEl.SafeGetString() : null;
var headerText = args.SafeTryGetProperty("header", out var hdr) ? hdr.SafeGetString() : null; var headerText = args.SafeTryGetProperty("header", out var hdr) ? hdr.SafeGetString() : null;
var footerText = args.SafeTryGetProperty("footer", out var ftr) ? ftr.SafeGetString() : null; var footerText = args.SafeTryGetProperty("footer", out var ftr) ? ftr.SafeGetString() : null;
var styleMapArg = args.SafeTryGetProperty("style_map", out var styleMapEl) ? styleMapEl : default;
var showPageNumbers = args.SafeTryGetProperty("page_numbers", out var pageNumbersEl) var showPageNumbers = args.SafeTryGetProperty("page_numbers", out var pageNumbersEl)
? pageNumbersEl.ValueKind == JsonValueKind.True ? pageNumbersEl.ValueKind == JsonValueKind.True
: !string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText); : !string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText);
@@ -140,7 +148,7 @@ public class DocumentAssemblerTool : IAgentTool
switch (format) switch (format)
{ {
case "docx": case "docx":
resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, templateFullPath, headerText, footerText, showPageNumbers); resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, templateFullPath, headerText, footerText, showPageNumbers, styleMapArg);
break; break;
case "markdown": case "markdown":
resultMsg = AssembleMarkdown(fullPath, title, sections); resultMsg = AssembleMarkdown(fullPath, title, sections);
@@ -259,7 +267,7 @@ public class DocumentAssemblerTool : IAgentTool
} }
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections, private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections,
bool useToc, string? coverSubtitle, string? templatePath, string? headerText, string? footerText, bool showPageNumbers) bool useToc, string? coverSubtitle, string? templatePath, string? headerText, string? footerText, bool showPageNumbers, JsonElement styleMapArg)
{ {
var templateApplied = !string.IsNullOrWhiteSpace(templatePath); var templateApplied = !string.IsNullOrWhiteSpace(templatePath);
if (templateApplied) if (templateApplied)
@@ -272,6 +280,7 @@ public class DocumentAssemblerTool : IAgentTool
var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart(); var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart();
EnsureAssemblerStyles(mainPart, templateApplied); EnsureAssemblerStyles(mainPart, templateApplied);
var styleMap = ResolveAssemblerStyleMap(mainPart, styleMapArg);
var body = InitializeAssemblerBody(mainPart, templateApplied); var body = InitializeAssemblerBody(mainPart, templateApplied);
@@ -308,10 +317,13 @@ public class DocumentAssemblerTool : IAgentTool
string fontSize = "22", string fontSize = "22",
bool bold = false, bool bold = false,
string? color = null, string? color = null,
string? fill = null) string? fill = null,
string? styleId = null)
{ {
var para = new DocumentFormat.OpenXml.Wordprocessing.Paragraph(); var para = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
var props = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties(); var props = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties();
if (!string.IsNullOrWhiteSpace(styleId))
props.ParagraphStyleId = new DocumentFormat.OpenXml.Wordprocessing.ParagraphStyleId { Val = styleId };
props.SpacingBetweenLines = new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines props.SpacingBetweenLines = new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines
{ {
After = "160" After = "160"
@@ -336,6 +348,13 @@ public class DocumentAssemblerTool : IAgentTool
para.AppendChild(props); para.AppendChild(props);
var run = new DocumentFormat.OpenXml.Wordprocessing.Run(); var run = new DocumentFormat.OpenXml.Wordprocessing.Run();
var applyInlineFormatting = string.IsNullOrWhiteSpace(styleId)
|| !string.IsNullOrWhiteSpace(fill)
|| bold
|| !string.IsNullOrWhiteSpace(color)
|| fontSize != "22";
if (applyInlineFormatting)
{
var runProps = new DocumentFormat.OpenXml.Wordprocessing.RunProperties var runProps = new DocumentFormat.OpenXml.Wordprocessing.RunProperties
{ {
RunFonts = KoreanFonts(), RunFonts = KoreanFonts(),
@@ -348,6 +367,7 @@ public class DocumentAssemblerTool : IAgentTool
runProps.Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = color }; runProps.Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = color };
run.AppendChild(runProps); run.AppendChild(runProps);
}
run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(text) run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(text)
{ {
Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve
@@ -356,6 +376,24 @@ public class DocumentAssemblerTool : IAgentTool
return para; return para;
} }
DocumentFormat.OpenXml.Wordprocessing.Paragraph CreateTitleParagraph(string text)
=> !string.IsNullOrWhiteSpace(styleMap.TitleStyle)
? CreateParagraph(text, styleId: styleMap.TitleStyle)
: CreateParagraph(text, fontSize: "48", bold: true, color: "1F3A5F");
DocumentFormat.OpenXml.Wordprocessing.Paragraph CreateSectionHeadingParagraph(string text, int level)
{
var headingStyleId = level <= 1 ? styleMap.Heading1Style : styleMap.Heading2Style;
return !string.IsNullOrWhiteSpace(headingStyleId)
? CreateParagraph(text, styleId: headingStyleId)
: CreateParagraph(text, fontSize: level <= 1 ? "32" : "28", bold: true, color: "2B579A");
}
DocumentFormat.OpenXml.Wordprocessing.Paragraph CreateBodyParagraph(string text)
=> !string.IsNullOrWhiteSpace(styleMap.BodyStyle)
? CreateParagraph(text, styleId: styleMap.BodyStyle)
: CreateParagraph(text);
DocumentFormat.OpenXml.Wordprocessing.Table CreateTableFromHtml(string tableHtml) DocumentFormat.OpenXml.Wordprocessing.Table CreateTableFromHtml(string tableHtml)
{ {
var table = new DocumentFormat.OpenXml.Wordprocessing.Table(); var table = new DocumentFormat.OpenXml.Wordprocessing.Table();
@@ -387,7 +425,7 @@ public class DocumentAssemblerTool : IAgentTool
} }
else else
{ {
cell.AppendChild(CreateParagraph(cellText)); cell.AppendChild(CreateBodyParagraph(cellText));
} }
cell.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.TableCellProperties( cell.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.TableCellProperties(
new DocumentFormat.OpenXml.Wordprocessing.TableCellWidth new DocumentFormat.OpenXml.Wordprocessing.TableCellWidth
@@ -415,23 +453,23 @@ public class DocumentAssemblerTool : IAgentTool
if (Regex.IsMatch(line, @"^#{2,6}\s+")) if (Regex.IsMatch(line, @"^#{2,6}\s+"))
{ {
body.AppendChild(CreateParagraph(Regex.Replace(line, @"^#{2,6}\s+", ""), fontSize: "26", bold: true, color: "2B579A")); body.AppendChild(CreateSectionHeadingParagraph(Regex.Replace(line, @"^#{2,6}\s+", ""), 2));
continue; continue;
} }
if (Regex.IsMatch(line, @"^[-*]\s+")) if (Regex.IsMatch(line, @"^[-*]\s+"))
{ {
body.AppendChild(CreateParagraph($"• {Regex.Replace(line, @"^[-*]\s+", "")}")); body.AppendChild(CreateBodyParagraph($"• {Regex.Replace(line, @"^[-*]\s+", "")}"));
continue; continue;
} }
if (Regex.IsMatch(line, @"^\d+\.\s+")) if (Regex.IsMatch(line, @"^\d+\.\s+"))
{ {
body.AppendChild(CreateParagraph(line)); body.AppendChild(CreateBodyParagraph(line));
continue; continue;
} }
body.AppendChild(CreateParagraph(line)); body.AppendChild(CreateBodyParagraph(line));
} }
} }
@@ -443,16 +481,16 @@ public class DocumentAssemblerTool : IAgentTool
{ {
var text = ExtractStructuredText(match.Groups[1].Value); var text = ExtractStructuredText(match.Groups[1].Value);
var prefix = ordered ? $"{number}. " : "• "; var prefix = ordered ? $"{number}. " : "• ";
body.AppendChild(CreateParagraph(prefix + text)); body.AppendChild(CreateBodyParagraph(prefix + text));
number++; number++;
} }
} }
var includeCover = !string.IsNullOrWhiteSpace(coverSubtitle); var includeCover = !string.IsNullOrWhiteSpace(coverSubtitle);
if (includeCover) if (includeCover)
AppendAssemblerCoverPage(body, title, coverSubtitle!); AppendAssemblerCoverPage(body, title, coverSubtitle!, styleMap.TitleStyle);
else else
body.AppendChild(CreateParagraph(title, fontSize: "48", bold: true, color: "1F3A5F")); body.AppendChild(CreateTitleParagraph(title));
if (!includeCover) if (!includeCover)
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph()); body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
@@ -468,18 +506,7 @@ public class DocumentAssemblerTool : IAgentTool
foreach (var (heading, content, level) in sections) foreach (var (heading, content, level) in sections)
{ {
var headPara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph(); body.AppendChild(CreateSectionHeadingParagraph(heading, level));
var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
{
RunFonts = KoreanFonts(),
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" },
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" },
});
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(heading));
headPara.AppendChild(headRun);
body.AppendChild(headPara);
headings.Add(heading); headings.Add(heading);
AppendStructuredContent(content); AppendStructuredContent(content);
@@ -565,7 +592,11 @@ public class DocumentAssemblerTool : IAgentTool
} }
else if (Regex.IsMatch(block, @"^<h", RegexOptions.IgnoreCase)) else if (Regex.IsMatch(block, @"^<h", RegexOptions.IgnoreCase))
{ {
body.AppendChild(CreateParagraph(ExtractStructuredText(block), fontSize: "26", bold: true, color: "2B579A")); var headingMatch = Regex.Match(block, @"^<h([2-6])", RegexOptions.IgnoreCase);
var headingLevel = headingMatch.Success && int.TryParse(headingMatch.Groups[1].Value, out var parsedLevel)
? Math.Max(1, parsedLevel - 1)
: 2;
body.AppendChild(CreateSectionHeadingParagraph(ExtractStructuredText(block), headingLevel));
} }
else if (Regex.IsMatch(block, @"^<(blockquote|div)", RegexOptions.IgnoreCase)) else if (Regex.IsMatch(block, @"^<(blockquote|div)", RegexOptions.IgnoreCase))
{ {
@@ -669,10 +700,41 @@ public class DocumentAssemblerTool : IAgentTool
} }
} }
private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle) private static AssemblerDocxStyleMap ResolveAssemblerStyleMap(
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
JsonElement styleMapArg)
{ {
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph( if (styleMapArg.ValueKind != JsonValueKind.Object)
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties return new AssemblerDocxStyleMap(null, null, null, null);
return new AssemblerDocxStyleMap(
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("heading2", out var heading2El) ? heading2El.SafeGetString() : null),
FindAssemblerStyle(mainPart, styleMapArg.SafeTryGetProperty("body", out var bodyEl) ? bodyEl.SafeGetString() : null));
}
private static string? FindAssemblerStyle(
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
string? styleId)
{
if (string.IsNullOrWhiteSpace(styleId))
return null;
var styles = mainPart.StyleDefinitionsPart?.Styles;
if (styles == null)
return null;
return styles
.Elements<DocumentFormat.OpenXml.Wordprocessing.Style>()
.Any(style => string.Equals(style.StyleId?.Value, styleId, StringComparison.OrdinalIgnoreCase))
? styleId
: null;
}
private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle, string? titleStyleId)
{
var titleParagraphProperties = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
{ {
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
{ {
@@ -683,14 +745,22 @@ public class DocumentAssemblerTool : IAgentTool
Before = "1600", Before = "1600",
After = "120" After = "120"
} }
}, };
new DocumentFormat.OpenXml.Wordprocessing.Run( if (!string.IsNullOrWhiteSpace(titleStyleId))
new DocumentFormat.OpenXml.Wordprocessing.RunProperties( titleParagraphProperties.ParagraphStyleId = new DocumentFormat.OpenXml.Wordprocessing.ParagraphStyleId { Val = titleStyleId };
var titleRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
if (string.IsNullOrWhiteSpace(titleStyleId))
{
titleRun.Append(new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
new DocumentFormat.OpenXml.Wordprocessing.Bold(), new DocumentFormat.OpenXml.Wordprocessing.Bold(),
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "44" }, new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "44" },
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "1F3A5F" }, new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "1F3A5F" },
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }), new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }));
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( body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties

View File

@@ -217,7 +217,10 @@ public class ExcelSkill : IAgentTool
conditionalFormattingCount, conditionalFormattingCount,
false, false,
false, false,
summaryArg.ValueKind == JsonValueKind.Object)); summaryArg.ValueKind == JsonValueKind.Object,
false,
false,
false));
features += $"\n{review.ToToolSummary()}"; features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok( return ToolResult.Ok(
@@ -305,7 +308,10 @@ public class ExcelSkill : IAgentTool
conditionalFormattingCount, conditionalFormattingCount,
true, true,
HasSummaryItems(summarySheet, "highlights"), HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions"))); HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis"),
HasStructuredSummaryContent(summarySheet, "decision_summary"),
HasSummaryItems(summarySheet, "sheet_summaries")));
features += $"\n{review.ToToolSummary()}"; features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok( return ToolResult.Ok(
@@ -428,7 +434,10 @@ public class ExcelSkill : IAgentTool
totalConditionalFormattingCount, totalConditionalFormattingCount,
summarySheet.ValueKind == JsonValueKind.Object, summarySheet.ValueKind == JsonValueKind.Object,
HasSummaryItems(summarySheet, "highlights"), HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions"))); HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis"),
HasStructuredSummaryContent(summarySheet, "decision_summary"),
HasSummaryItems(summarySheet, "sheet_summaries")));
return ToolResult.Ok( return ToolResult.Ok(
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}\n{review.ToToolSummary()}", $"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}\n{review.ToToolSummary()}",
fullPath); fullPath);
@@ -471,6 +480,9 @@ public class ExcelSkill : IAgentTool
rowIndex++; rowIndex++;
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
if (summarySheet.SafeTryGetProperty("kpis", out var kpis) && kpis.ValueKind == JsonValueKind.Array && kpis.GetArrayLength() > 0) if (summarySheet.SafeTryGetProperty("kpis", out var kpis) && kpis.ValueKind == JsonValueKind.Array && kpis.GetArrayLength() > 0)
{ {
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Key KPIs", 1); AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Key KPIs", 1);
@@ -497,6 +509,7 @@ public class ExcelSkill : IAgentTool
rowIndex++; rowIndex++;
} }
AppendSheetSummarySection(summarySheet, sheetData, ref rowIndex);
AppendSummaryTextSection(summarySheet, "highlights", "Key Highlights", sheetData, merges, ref rowIndex); AppendSummaryTextSection(summarySheet, "highlights", "Key Highlights", sheetData, merges, ref rowIndex);
AppendSummaryTextSection(summarySheet, "actions", "Next Actions", sheetData, merges, ref rowIndex); AppendSummaryTextSection(summarySheet, "actions", "Next Actions", sheetData, merges, ref rowIndex);
@@ -530,6 +543,115 @@ public class ExcelSkill : IAgentTool
} }
} }
private static void AppendDecisionSummarySection(JsonElement summarySheet, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty("decision_summary", out var decisionSummary))
return;
var appended = false;
if (decisionSummary.ValueKind == JsonValueKind.Array && decisionSummary.GetArrayLength() > 0)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Decision Summary", 1);
foreach (var item in decisionSummary.EnumerateArray())
{
var label = item.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? "" : "";
var value = item.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? "" : "";
var owner = item.SafeTryGetProperty("owner", out var ownerEl) ? ownerEl.SafeGetString() ?? "" : "";
var row = new Row { RowIndex = rowIndex++ };
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, label, 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, value, 3));
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, owner, 0));
sheetData.Append(row);
appended = true;
}
}
else if (decisionSummary.ValueKind == JsonValueKind.Object)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Decision Summary", 1);
foreach (var property in decisionSummary.EnumerateObject())
{
var row = new Row { RowIndex = rowIndex++ };
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, property.Name, 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, property.Value.SafeGetString() ?? property.Value.ToString(), 3));
sheetData.Append(row);
appended = true;
}
}
if (appended)
rowIndex++;
}
private static void AppendScorecardSection(JsonElement summarySheet, SheetData sheetData, ref uint rowIndex)
{
JsonElement scorecards;
var hasScorecards = summarySheet.SafeTryGetProperty("scorecards", out scorecards)
&& scorecards.ValueKind == JsonValueKind.Array
&& scorecards.GetArrayLength() > 0;
if (!hasScorecards)
{
hasScorecards = summarySheet.SafeTryGetProperty("cards", out scorecards)
&& scorecards.ValueKind == JsonValueKind.Array
&& scorecards.GetArrayLength() > 0;
}
if (!hasScorecards)
return;
var headerRow = new Row { RowIndex = rowIndex++ };
headerRow.Append(CreateSummaryCell("A", headerRow.RowIndex!.Value, "Scorecards", 1));
headerRow.Append(CreateSummaryCell("B", headerRow.RowIndex!.Value, "Value", 1));
headerRow.Append(CreateSummaryCell("C", headerRow.RowIndex!.Value, "Status", 1));
headerRow.Append(CreateSummaryCell("D", headerRow.RowIndex!.Value, "Note", 1));
sheetData.Append(headerRow);
var cardIndex = 0;
foreach (var card in scorecards.EnumerateArray())
{
var row = new Row { RowIndex = rowIndex++ };
var stripeStyle = cardIndex % 2 == 0 ? (uint)0 : (uint)2;
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, card.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? "" : "", 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, card.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? "" : "", 3));
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, card.SafeTryGetProperty("status", out var statusEl) ? statusEl.SafeGetString() ?? "" : "", stripeStyle));
row.Append(CreateSummaryCell("D", row.RowIndex!.Value, card.SafeTryGetProperty("note", out var noteEl) ? noteEl.SafeGetString() ?? "" : "", stripeStyle));
sheetData.Append(row);
cardIndex++;
}
rowIndex++;
}
private static void AppendSheetSummarySection(JsonElement summarySheet, SheetData sheetData, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty("sheet_summaries", out var sheetSummaries)
|| sheetSummaries.ValueKind != JsonValueKind.Array
|| sheetSummaries.GetArrayLength() == 0)
return;
var headerRow = new Row { RowIndex = rowIndex++ };
headerRow.Append(CreateSummaryCell("A", headerRow.RowIndex!.Value, "Sheet", 1));
headerRow.Append(CreateSummaryCell("B", headerRow.RowIndex!.Value, "Status", 1));
headerRow.Append(CreateSummaryCell("C", headerRow.RowIndex!.Value, "Summary", 1));
headerRow.Append(CreateSummaryCell("D", headerRow.RowIndex!.Value, "Owner", 1));
sheetData.Append(headerRow);
var summaryIndex = 0;
foreach (var sheetSummary in sheetSummaries.EnumerateArray())
{
var row = new Row { RowIndex = rowIndex++ };
var stripeStyle = summaryIndex % 2 == 0 ? (uint)0 : (uint)2;
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("sheet", out var sheetEl) ? sheetEl.SafeGetString() ?? "" : "", 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("status", out var statusEl) ? statusEl.SafeGetString() ?? "" : "", 3));
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("summary", out var summaryEl) ? summaryEl.SafeGetString() ?? "" : "", stripeStyle));
row.Append(CreateSummaryCell("D", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("owner", out var ownerEl) ? ownerEl.SafeGetString() ?? "" : "", stripeStyle));
sheetData.Append(row);
summaryIndex++;
}
rowIndex++;
}
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)
@@ -1235,6 +1357,20 @@ public class ExcelSkill : IAgentTool
&& items.GetArrayLength() > 0; && items.GetArrayLength() > 0;
} }
private static bool HasStructuredSummaryContent(JsonElement summarySheet, string propertyName)
{
if (summarySheet.ValueKind != JsonValueKind.Object
|| !summarySheet.SafeTryGetProperty(propertyName, out var value))
return false;
return value.ValueKind switch
{
JsonValueKind.Array => value.GetArrayLength() > 0,
JsonValueKind.Object => value.EnumerateObject().Any(),
_ => false,
};
}
private static string BuildFeatureList(bool isStyled, bool freezeHeader, private static string BuildFeatureList(bool isStyled, bool freezeHeader,
bool hasMerges, bool hasSummary, bool hasNumFmts, bool hasAlignments, string? theme) bool hasMerges, bool hasSummary, bool hasNumFmts, bool hasAlignments, string? theme)
{ {