?? ??? 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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user