?? ??? 3?? DOCX ????XLSX ?? ???HTML ???? ?? ??

- DocumentAssemblerTool? template_path/page_numbers? ??? DOCX ??? ??, ??????????? ?? ??? ??
- ExcelSkill? data_validations? ???? ??/??/?? ?? ??? ?? ??? ?? ?? ?? ??
- HtmlSkill? ArtifactQualityReviewService? decision_summary/evidence_cards ??? ?? ?? ?? ??? ??
- DocumentAssemblerDocxFeaturesTests, HtmlSkillConsultingSectionsTests, ExcelSkillDataValidationTests? ?? ??? ??
- README.md? docs/DEVELOPMENT.md? 2026-04-14 22:28 (KST) ???? ??

??:
- 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)
This commit is contained in:
2026-04-14 22:31:15 +09:00
parent 1ad5eea32e
commit 5607f6391e
9 changed files with 380 additions and 39 deletions

View File

@@ -57,8 +57,10 @@ public class DocumentAssemblerTool : IAgentTool
},
["toc"] = new() { Type = "boolean", Description = "Auto-generate table of contents. Default: true" },
["cover_subtitle"] = new() { Type = "string", Description = "Subtitle for cover page. If provided, a cover page is added." },
["template_path"] = new() { Type = "string", Description = "Optional .docx template path for DOCX output. Reuses template styles and section setup." },
["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." },
["page_numbers"] = new() { Type = "boolean", Description = "Show page numbers in DOCX footer. Default: true when header or footer is set." },
},
Required = ["path", "title", "sections"]
};
@@ -71,8 +73,12 @@ public class DocumentAssemblerTool : IAgentTool
var requestedMood = args.SafeTryGetProperty("mood", out var m) ? m.SafeGetString() ?? GetDefaultMood() : GetDefaultMood();
var useToc = !args.SafeTryGetProperty("toc", out var tocVal) || tocVal.GetBoolean(); // default true
var coverSubtitle = args.SafeTryGetProperty("cover_subtitle", out var cs) ? cs.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 footerText = args.SafeTryGetProperty("footer", out var ftr) ? ftr.SafeGetString() : null;
var showPageNumbers = args.SafeTryGetProperty("page_numbers", out var pageNumbersEl)
? pageNumbersEl.ValueKind == JsonValueKind.True
: !string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText);
if (!args.SafeTryGetProperty("sections", out var sectionsEl) || sectionsEl.ValueKind != JsonValueKind.Array)
return ToolResult.Fail("sections 배열이 필요합니다.");
@@ -114,13 +120,27 @@ public class DocumentAssemblerTool : IAgentTool
var dir = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
string? templateFullPath = null;
if (!string.IsNullOrWhiteSpace(templatePath))
{
templateFullPath = FileReadTool.ResolvePath(templatePath, context.WorkFolder);
if (!templateFullPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
templateFullPath += ".docx";
if (!context.IsPathAllowed(templateFullPath))
return ToolResult.Fail($"템플릿 경로 접근 차단: {templateFullPath}");
if (!File.Exists(templateFullPath))
return ToolResult.Fail($"DOCX 템플릿을 찾을 수 없습니다: {templateFullPath}");
if (string.Equals(Path.GetFullPath(templateFullPath), Path.GetFullPath(fullPath), StringComparison.OrdinalIgnoreCase))
return ToolResult.Fail("template_path와 path는 같은 파일일 수 없습니다.");
}
try
{
string resultMsg;
switch (format)
{
case "docx":
resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, headerText, footerText);
resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, templateFullPath, headerText, footerText, showPageNumbers);
break;
case "markdown":
resultMsg = AssembleMarkdown(fullPath, title, sections);
@@ -239,19 +259,21 @@ public class DocumentAssemblerTool : IAgentTool
}
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections,
bool useToc, string? coverSubtitle, string? headerText, string? footerText)
bool useToc, string? coverSubtitle, string? templatePath, string? headerText, string? footerText, bool showPageNumbers)
{
using var doc = DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
var templateApplied = !string.IsNullOrWhiteSpace(templatePath);
if (templateApplied)
File.Copy(templatePath!, path, true);
var mainPart = doc.AddMainDocumentPart();
using var doc = templateApplied
? DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Open(path, true)
: DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
stylesPart.Styles = CreateDefaultDocxStyles();
stylesPart.Styles.Save();
var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart();
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
EnsureAssemblerStyles(mainPart, templateApplied);
var body = InitializeAssemblerBody(mainPart, templateApplied);
DocumentFormat.OpenXml.Wordprocessing.RunFonts KoreanFonts() => new()
{
@@ -470,8 +492,8 @@ public class DocumentAssemblerTool : IAgentTool
Header = 720, Footer = 720, Gutter = 0 }
));
if (!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText))
AddAssemblerHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers: true);
if (!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText) || showPageNumbers)
AddAssemblerHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers);
mainPart.Document.Save();
@@ -486,7 +508,7 @@ public class DocumentAssemblerTool : IAgentTool
0,
includeCover,
useToc && sections.Count > 3,
false,
templateApplied,
!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText),
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary", "요약")),
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "proposal", "next step", "action", "권고", "실행")),
@@ -608,6 +630,45 @@ public class DocumentAssemblerTool : IAgentTool
return styles;
}
private static DocumentFormat.OpenXml.Wordprocessing.Body InitializeAssemblerBody(
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
bool preserveSectionSetup)
{
mainPart.Document ??= new DocumentFormat.OpenXml.Wordprocessing.Document();
DocumentFormat.OpenXml.Wordprocessing.SectionProperties? preservedSection = null;
if (preserveSectionSetup)
preservedSection = mainPart.Document.Body?.Elements<DocumentFormat.OpenXml.Wordprocessing.SectionProperties>().LastOrDefault()?.CloneNode(true) as DocumentFormat.OpenXml.Wordprocessing.SectionProperties;
mainPart.Document.Body = new DocumentFormat.OpenXml.Wordprocessing.Body();
var body = mainPart.Document.Body;
if (preservedSection != null)
body.Append(preservedSection);
return body;
}
private static void EnsureAssemblerStyles(
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
bool preserveExisting)
{
var stylesPart = mainPart.StyleDefinitionsPart;
if (stylesPart == null)
{
stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
stylesPart.Styles = CreateDefaultDocxStyles();
stylesPart.Styles.Save();
return;
}
if (!preserveExisting || stylesPart.Styles == null)
{
stylesPart.Styles = CreateDefaultDocxStyles();
stylesPart.Styles.Save();
}
}
private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle)
{
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(