문서 고도화 다음 단계를 반영해 엑셀 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

@@ -68,6 +68,9 @@ public class ArtifactQualityReviewServiceTests
0,
false,
false,
false,
false,
false,
false));
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
{
}
}
}
}