문서 생성 품질 게이트와 산출물 고도화 2차 반영

공통 ArtifactQualityReviewService를 추가해 HTML, DOCX, XLSX 결과물에 로컬 품질 점수와 보완 포인트를 부여했습니다.

DocxSkill에 template_path, cover_subtitle, cover_meta, toc 흐름을 붙여 템플릿 기반 문서와 커버/목차 생성을 강화했고, Excel summary sheet에는 detail sheet 링크와 workbook review를 연결했습니다. HtmlSkill도 결과 요약에 품질 리뷰를 포함하도록 보강했습니다.

executive-brief, kpi-workbook, board-report-html 번들 스킬을 추가했고, ArtifactQualityReviewServiceTests, DocxSkillTemplateFeaturesTests, ExcelSkillExecutiveSummaryLinkTests를 포함한 관련 테스트를 보강했습니다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_doc_phase2\ -p:IntermediateOutputPath=obj\verify_doc_phase2\ (경고 0 / 오류 0)

검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ArtifactQualityReviewServiceTests|DocxSkillTemplateFeaturesTests|ExcelSkillExecutiveSummaryLinkTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|HtmlSkillConsultingSectionsTests|ExcelSkillSummarySheetTests -p:OutputPath=bin\verify_doc_phase2_tests\ -p:IntermediateOutputPath=obj\verify_doc_phase2_tests\ (통과 9)
This commit is contained in:
2026-04-14 21:26:58 +09:00
parent d9cb02f3c4
commit 6c7fba9dff
12 changed files with 836 additions and 10 deletions

View File

@@ -202,6 +202,19 @@ public class ExcelSkill : IAgentTool
summaryArg.ValueKind == JsonValueKind.Object,
numFmts.Count > 0, alignments.Count > 0, themeName);
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
sheetName,
1,
1,
rowCount,
CountFormulaCells(rows),
0,
0,
false,
false,
summaryArg.ValueKind == JsonValueKind.Object));
features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok(
$"Excel 파일 생성 완료: {fullPath}\n시트: {sheetName}, 열: {colCount}, 행: {rowCount}{features}",
fullPath);
@@ -274,6 +287,18 @@ public class ExcelSkill : IAgentTool
mergesArg.ValueKind == JsonValueKind.Array,
summaryArg.ValueKind == JsonValueKind.Object,
numFmts.Count > 0, alignments.Count > 0, themeName);
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
summaryName,
2,
1,
rowCount,
CountFormulaCells(rows),
1,
0,
true,
HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions")));
features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok(
$"Excel ?뚯씪 ?앹꽦 ?꾨즺: {fullPath}\n?쒗듃: {summaryName}, {sheetName}, ?? {colCount}, ?? {rowCount}{features} [?붿빟 ?쒗듃]",
@@ -313,6 +338,7 @@ public class ExcelSkill : IAgentTool
uint sheetId = 1;
var totalSheets = 0;
var totalRows = 0;
var totalFormulaCount = 0;
var detailSheetNames = new List<string>();
foreach (var sheetDef in sheetsArr.EnumerateArray())
@@ -373,10 +399,10 @@ public class ExcelSkill : IAgentTool
sheetId++;
totalSheets++;
totalRows += rowCount;
totalFormulaCount += CountFormulaCells(rows);
}
workbookPart.Workbook.Save();
return ToolResult.Ok(
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}",
fullPath);
@@ -403,6 +429,7 @@ public class ExcelSkill : IAgentTool
worksheetPart.Worksheet.Append(sheetData);
var merges = new MergeCells();
var hyperlinks = new Hyperlinks();
uint rowIndex = 1;
var title = summarySheet.SafeTryGetProperty("title", out var titleEl)
@@ -454,8 +481,27 @@ public class ExcelSkill : IAgentTool
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", $"• {detailSheetName}", 0);
}
if (detailSheetNames.Count > 0)
{
var hyperlinkStartRow = rowIndex - (uint)detailSheetNames.Count;
for (var i = 0; i < detailSheetNames.Count; i++)
{
hyperlinks.Append(new Hyperlink
{
Reference = $"A{hyperlinkStartRow + (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 AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
@@ -979,6 +1025,36 @@ public class ExcelSkill : IAgentTool
return result;
}
private static int CountFormulaCells(JsonElement rows)
{
if (rows.ValueKind != JsonValueKind.Array)
return 0;
var count = 0;
foreach (var row in rows.EnumerateArray())
{
if (row.ValueKind != JsonValueKind.Array)
continue;
foreach (var cell in row.EnumerateArray())
{
var text = cell.ToString();
if (!string.IsNullOrWhiteSpace(text) && text.StartsWith('='))
count++;
}
}
return count;
}
private static bool HasSummaryItems(JsonElement summarySheet, string propertyName)
{
return summarySheet.ValueKind == JsonValueKind.Object
&& summarySheet.SafeTryGetProperty(propertyName, out var items)
&& items.ValueKind == JsonValueKind.Array
&& items.GetArrayLength() > 0;
}
private static string BuildFeatureList(bool isStyled, bool freezeHeader,
bool hasMerges, bool hasSummary, bool hasNumFmts, bool hasAlignments, string? theme)
{