문서 생성 품질 게이트와 산출물 고도화 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:
@@ -7,6 +7,15 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||
`docs/claw-code-parity-plan.md`
|
||||
|
||||
- 업데이트: 2026-04-14 21:25 (KST)
|
||||
- 문서 생성 고도화 2차를 반영했습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)를 추가해 HTML/DOCX/XLSX 결과물에 공통 품질 점수와 보완 포인트를 부여하고, [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs), [DocxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocxSkill.cs), [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)에 연결했습니다.
|
||||
- Word 쪽은 템플릿·커버·목차를 강화했습니다. `docx_create`는 `template_path`, `cover_subtitle`, `cover_meta`, `toc`를 받아 템플릿 기반 문서와 커버 페이지, TOC 필드를 함께 만들 수 있게 되었고, 결과 요약에도 문서 품질 리뷰가 같이 남습니다.
|
||||
- Excel 쪽은 요약 시트 품질을 높였습니다. summary sheet가 detail sheet 링크를 자동으로 넣고, KPI/하이라이트/액션 구조를 가진 워크북에 대해 품질 점수를 함께 반환합니다.
|
||||
- HTML 쪽은 결과 요약에 업무 문서 기준의 로컬 품질 리뷰를 붙였습니다. 커버, 목차, 인쇄 최적화, 구조화 블록 사용 여부를 함께 평가해 후속 보정 근거를 더 쉽게 확인할 수 있습니다.
|
||||
- 번들 스킬도 확장했습니다. [executive-brief.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/executive-brief.skill.md), [kpi-workbook.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/kpi-workbook.skill.md), [board-report-html.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/board-report-html.skill.md)을 추가해 임원 보고 문서, KPI 워크북, HTML 이사회 보고서 생성을 바로 유도할 수 있게 했습니다.
|
||||
- 검증: `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
|
||||
|
||||
- 업데이트: 2026-04-14 19:13 (KST)
|
||||
- `claude-code` 기준 Phase 4 범위를 이어서 반영했습니다. MCP 서버 메타데이터를 `mcp` 스코프의 synthetic skill로 노출하는 [McpSkillCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/McpSkillCatalog.cs)를 추가했고, 스킬 source 정책은 `managed/user/additional/project/plugin/mcp/legacy` 단위로 켜고 끌 수 있게 확장했습니다.
|
||||
- 슬래시 명령 합성도 정리했습니다. [SlashCommandCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SlashCommandCatalog.cs)와 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)가 이제 builtin command와 skill을 우선순위 기반으로 dedupe해 같은 `/명령`이 겹칠 때 더 안정적으로 하나만 노출합니다.
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
업데이트: 2026-04-14 19:50 (KST)
|
||||
업데이트: 2026-04-14 21:25 (KST)
|
||||
- 문서 생성 고도화 2차를 반영했습니다. `src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs`를 추가해 HTML/DOCX/XLSX 산출물에 대해 로컬 품질 점수와 보완 포인트를 계산하고, `HtmlSkill`, `DocxSkill`, `ExcelSkill`이 같은 리뷰 모델을 공유하도록 맞췄습니다.
|
||||
- `src/AxCopilot/Services/Agent/DocxSkill.cs`는 `template_path`, `cover_subtitle`, `cover_meta`, `toc`를 지원하도록 확장했습니다. DOCX 템플릿 복제 후 본문을 재구성하고, 커버 페이지와 TOC 필드를 삽입한 뒤 structured review 결과를 함께 반환합니다.
|
||||
- `src/AxCopilot/Services/Agent/ExcelSkill.cs`는 executive summary sheet에 detail sheet 링크를 자동 생성하고, KPI/highlights/actions 구조를 workbook quality review와 연결하도록 보강했습니다.
|
||||
- `src/AxCopilot/Services/Agent/HtmlSkill.cs`는 로컬 품질 리뷰를 결과 요약에 포함하도록 바꿨고, 문서형 번들 스킬로 `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
|
||||
|
||||
업데이트: 2026-04-14 19:50 (KST)
|
||||
- Agent loop/queue/context 품질을 보강했습니다. `src/AxCopilot/Services/Agent/AgentCommandQueue.cs`로 실행 중 추가 입력을 우선순위와 interrupt 여부까지 포함해 관리하고, `AgentLoopService`는 이를 안전하게 반영합니다.
|
||||
- `AgentToolResultBudget`, `AgentQueryContextBuilder`, `ChatModels`는 tool result preview를 메시지에 캐시해 긴 세션과 재질문에서도 같은 축약 결과를 재사용하도록 정리했습니다.
|
||||
- 코드 탭의 내장 언어 지원을 `src/AxCopilot/Services/CodeLanguageCatalog.cs`로 통합했고, 설정의 코드 탭에 지원 언어(LSP)와 코드 탭 기본 지원 언어를 명시적으로 표시합니다.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class ArtifactQualityReviewServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReviewHtml_ShouldDetectRichBusinessStructure()
|
||||
{
|
||||
var html =
|
||||
"""
|
||||
<h2>Executive Summary</h2><p>Summary text.</p><p>Another supporting paragraph.</p>
|
||||
<h2>Current State</h2><p>Current state detail.</p><p>Evidence paragraph.</p>
|
||||
<div class="callout-info">Important message</div>
|
||||
<table><tr><th>Metric</th><th>Value</th></tr><tr><td>NPS</td><td>61</td></tr></table>
|
||||
<div class="comparison-grid"></div>
|
||||
<div class="roadmap-block"></div>
|
||||
<h2>Recommendation</h2><p>Recommendation text.</p><p>Action support text.</p>
|
||||
<h2>Appendix</h2><p>Reference detail.</p><p>More detail.</p>
|
||||
""";
|
||||
|
||||
var review = ArtifactQualityReviewService.ReviewHtml("Board Report", html, hasCover: true, hasTableOfContents: true, printReady: true);
|
||||
|
||||
review.Score.Should().BeGreaterThan(75);
|
||||
review.Strengths.Should().Contain(s => s.Contains("커버") || s.Contains("목차"));
|
||||
review.Issues.Should().NotContain(i => i.Severity == ArtifactReviewSeverity.Critical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReviewStructuredDocument_ShouldFlagMissingExecutiveSections()
|
||||
{
|
||||
var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
|
||||
"Ops Memo",
|
||||
2,
|
||||
600,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false));
|
||||
|
||||
review.Issues.Should().Contain(i => i.Message.Contains("Executive Summary"));
|
||||
review.Issues.Should().Contain(i => i.Message.Contains("권고안"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReviewWorkbook_ShouldFlagMissingSummaryForMultiSheet()
|
||||
{
|
||||
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
|
||||
"PMO Tracker",
|
||||
3,
|
||||
3,
|
||||
40,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false));
|
||||
|
||||
review.Issues.Should().Contain(i => i.Message.Contains("요약 시트"));
|
||||
review.Score.Should().BeLessThan(80);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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 DocxSkillTemplateFeaturesTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ShouldCreateDocx_WithTemplateCoverAndToc()
|
||||
{
|
||||
var workDir = Path.Combine(Path.GetTempPath(), "ax-docx-template-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(workDir);
|
||||
|
||||
try
|
||||
{
|
||||
var templatePath = Path.Combine(workDir, "template.docx");
|
||||
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 Root")))));
|
||||
mainPart.Document.Save();
|
||||
}
|
||||
|
||||
var tool = new DocxSkill();
|
||||
var context = new AgentContext
|
||||
{
|
||||
WorkFolder = workDir,
|
||||
Permission = "Auto",
|
||||
OperationMode = "external",
|
||||
};
|
||||
|
||||
var args = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"path": "executive-brief.docx",
|
||||
"title": "Executive Brief",
|
||||
"template_path": "template.docx",
|
||||
"cover_subtitle": "Q2 Operating Review",
|
||||
"cover_meta": ["Prepared for Steering Committee", "Confidential"],
|
||||
"toc": true,
|
||||
"sections": [
|
||||
{ "heading": "Executive Summary", "body": "Summary line one.\nSummary line two.", "level": 1 },
|
||||
{ "heading": "Recommendation", "body": "Recommendation details.", "level": 1 }
|
||||
]
|
||||
}
|
||||
""").RootElement;
|
||||
|
||||
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
var outputPath = Path.Combine(workDir, "executive-brief.docx");
|
||||
File.Exists(outputPath).Should().BeTrue();
|
||||
|
||||
using var doc = WordprocessingDocument.Open(outputPath, false);
|
||||
doc.MainDocumentPart!.Document.Body!.InnerText.Should().Contain("Q2 Operating Review");
|
||||
doc.MainDocumentPart.Document.Body.InnerText.Should().Contain("Prepared for Steering Committee");
|
||||
doc.MainDocumentPart.Document.Body.Descendants<FieldCode>().Should().Contain(f => f.Text.Contains("TOC"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(workDir))
|
||||
Directory.Delete(workDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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 ExcelSkillExecutiveSummaryLinkTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ShouldCreateSummarySheetWithDetailLinks()
|
||||
{
|
||||
var workDir = Path.Combine(Path.GetTempPath(), "ax-xlsx-summary-" + 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": "ops-review.xlsx",
|
||||
"summary_sheet": {
|
||||
"name": "Summary",
|
||||
"title": "Ops Review",
|
||||
"highlights": ["Margin improved", "Backlog stable"],
|
||||
"actions": ["Expand automation", "Tighten forecast"]
|
||||
},
|
||||
"sheets": [
|
||||
{
|
||||
"name": "Revenue",
|
||||
"headers": ["Metric", "Value"],
|
||||
"rows": [["Revenue", 120], ["Margin", 18], ["Delta", "=B2-B3"]]
|
||||
},
|
||||
{
|
||||
"name": "Pipeline",
|
||||
"headers": ["Stage", "Count"],
|
||||
"rows": [["Qualified", 14], ["Proposal", 8]]
|
||||
}
|
||||
]
|
||||
}
|
||||
""").RootElement;
|
||||
|
||||
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
var outputPath = Path.Combine(workDir, "ops-review.xlsx");
|
||||
File.Exists(outputPath).Should().BeTrue();
|
||||
|
||||
using var doc = SpreadsheetDocument.Open(outputPath, false);
|
||||
var workbookPart = doc.WorkbookPart;
|
||||
workbookPart.Should().NotBeNull();
|
||||
var summarySheet = workbookPart!.Workbook.Sheets!.Elements<Sheet>().First(s => s.Name == "Summary");
|
||||
var summaryPart = (WorksheetPart)workbookPart.GetPartById(summarySheet.Id!);
|
||||
summaryPart.Worksheet.Elements<Hyperlinks>().Should().ContainSingle();
|
||||
summaryPart.Worksheet.Descendants<Hyperlink>().Count().Should().BeGreaterOrEqualTo(2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(workDir))
|
||||
Directory.Delete(workDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
216
src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
Normal file
216
src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public enum ArtifactReviewSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
public sealed record ArtifactReviewIssue(string Message, ArtifactReviewSeverity Severity);
|
||||
|
||||
public sealed record ArtifactQualityReport(
|
||||
string ArtifactType,
|
||||
int Score,
|
||||
IReadOnlyList<string> Strengths,
|
||||
IReadOnlyList<ArtifactReviewIssue> Issues)
|
||||
{
|
||||
public string ToToolSummary()
|
||||
{
|
||||
var strengths = Strengths.Take(3).ToList();
|
||||
var issues = Issues.OrderByDescending(i => i.Severity).Take(3).Select(i => i.Message).ToList();
|
||||
|
||||
var parts = new List<string> { $"품질 점수 {Score}/100" };
|
||||
if (strengths.Count > 0)
|
||||
parts.Add("강점: " + string.Join(", ", strengths));
|
||||
if (issues.Count > 0)
|
||||
parts.Add("보완: " + string.Join(", ", issues));
|
||||
else
|
||||
parts.Add("보완 필요 사항 없음");
|
||||
|
||||
return string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record StructuredDocumentReviewInput(
|
||||
string Title,
|
||||
int SectionCount,
|
||||
int BodyCharacterCount,
|
||||
int TableCount,
|
||||
int ListCount,
|
||||
int CalloutCount,
|
||||
int HighlightCount,
|
||||
int IconCount,
|
||||
bool HasCoverPage,
|
||||
bool HasTableOfContents,
|
||||
bool HasTemplate,
|
||||
bool HasHeaderFooter,
|
||||
bool HasExecutiveSummarySection,
|
||||
bool HasRecommendationSection,
|
||||
bool HasAppendixSection);
|
||||
|
||||
public sealed record WorkbookReviewInput(
|
||||
string Title,
|
||||
int SheetCount,
|
||||
int DetailSheetCount,
|
||||
int DataRowCount,
|
||||
int FormulaCount,
|
||||
int HyperlinkCount,
|
||||
int DataValidationCount,
|
||||
bool HasSummarySheet,
|
||||
bool HasHighlightSection,
|
||||
bool HasActionSection);
|
||||
|
||||
public static class ArtifactQualityReviewService
|
||||
{
|
||||
private static readonly string[] ExecutiveKeywords = ["executive summary", "요약", "핵심 요약", "summary"];
|
||||
private static readonly string[] RecommendationKeywords = ["recommendation", "권고", "제안", "next steps", "실행 과제"];
|
||||
private static readonly string[] AppendixKeywords = ["appendix", "부록", "참고", "reference"];
|
||||
|
||||
public static ArtifactQualityReport ReviewHtml(string title, string html, bool hasCover, bool hasTableOfContents, bool printReady)
|
||||
{
|
||||
title ??= string.Empty;
|
||||
html ??= string.Empty;
|
||||
|
||||
var strengths = new List<string>();
|
||||
var issues = new List<ArtifactReviewIssue>();
|
||||
|
||||
var sectionCount = Regex.Matches(html, @"<h2\b", RegexOptions.IgnoreCase).Count;
|
||||
var subSectionCount = Regex.Matches(html, @"<h3\b", RegexOptions.IgnoreCase).Count;
|
||||
var paragraphCount = Regex.Matches(html, @"<p\b", RegexOptions.IgnoreCase).Count;
|
||||
var tableCount = Regex.Matches(html, @"<table\b", RegexOptions.IgnoreCase).Count;
|
||||
var calloutCount = Regex.Matches(html, @"callout-(info|warning|tip|danger)", RegexOptions.IgnoreCase).Count;
|
||||
var comparisonCount = Regex.Matches(html, @"comparison-grid|comparison-card", RegexOptions.IgnoreCase).Count;
|
||||
var roadmapCount = Regex.Matches(html, @"roadmap(-block|-phase)?", RegexOptions.IgnoreCase).Count;
|
||||
var matrixCount = Regex.Matches(html, @"matrix-grid|risk-matrix", RegexOptions.IgnoreCase).Count;
|
||||
var kpiCount = Regex.Matches(html, @"kpi-card|kpi-grid|metric-card", RegexOptions.IgnoreCase).Count;
|
||||
var placeholderCount = CountPlaceholders(html);
|
||||
|
||||
if (hasCover) strengths.Add("커버 페이지 포함");
|
||||
if (hasTableOfContents) strengths.Add("목차 자동 생성");
|
||||
if (printReady) strengths.Add("인쇄 최적화 CSS 포함");
|
||||
if (sectionCount >= 5) strengths.Add($"핵심 섹션 {sectionCount}개 구성");
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || kpiCount > 0)
|
||||
strengths.Add("표/비교/로드맵/KPI 등 구조화 블록 활용");
|
||||
if (calloutCount > 0) strengths.Add("핵심 메시지 강조 블록 포함");
|
||||
|
||||
if (html.Length < 1800)
|
||||
issues.Add(new("문서 본문이 짧아 메시지와 근거 밀도가 낮을 수 있습니다", ArtifactReviewSeverity.Warning));
|
||||
if (sectionCount < 4)
|
||||
issues.Add(new("업무 문서 기준 핵심 섹션 수가 부족합니다", ArtifactReviewSeverity.Warning));
|
||||
if (paragraphCount < sectionCount * 2)
|
||||
issues.Add(new("일부 섹션의 서술이 충분하지 않습니다", ArtifactReviewSeverity.Warning));
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + kpiCount == 0)
|
||||
issues.Add(new("구조화 시각 요소가 없어 보고서 완성도가 낮을 수 있습니다", ArtifactReviewSeverity.Warning));
|
||||
if (placeholderCount > 0)
|
||||
issues.Add(new($"플레이스홀더/미완성 표현 {placeholderCount}건이 남아 있습니다", ArtifactReviewSeverity.Critical));
|
||||
if (!ContainsAny(html, ExecutiveKeywords))
|
||||
issues.Add(new("요약 또는 핵심 메시지 섹션이 명확하지 않습니다", ArtifactReviewSeverity.Warning));
|
||||
|
||||
return BuildReport("html", strengths, issues);
|
||||
}
|
||||
|
||||
public static ArtifactQualityReport ReviewStructuredDocument(StructuredDocumentReviewInput input)
|
||||
{
|
||||
var strengths = new List<string>();
|
||||
var issues = new List<ArtifactReviewIssue>();
|
||||
|
||||
if (input.HasTemplate) strengths.Add("템플릿 기반 스타일 상속");
|
||||
if (input.HasCoverPage) strengths.Add("커버 페이지 포함");
|
||||
if (input.HasTableOfContents) strengths.Add("목차 포함");
|
||||
if (input.HasHeaderFooter) strengths.Add("머리글/바닥글 적용");
|
||||
if (input.SectionCount >= 5) strengths.Add($"핵심 섹션 {input.SectionCount}개 구성");
|
||||
if (input.TableCount > 0) strengths.Add($"테이블 {input.TableCount}개 포함");
|
||||
if (input.CalloutCount + input.HighlightCount > 0) strengths.Add("강조 블록 활용");
|
||||
if (input.ListCount > 0) strengths.Add("실행 항목/목록 구조 포함");
|
||||
|
||||
if (input.BodyCharacterCount < 1400)
|
||||
issues.Add(new("문서 분량이 짧아 경영 보고용 밀도가 부족할 수 있습니다", ArtifactReviewSeverity.Warning));
|
||||
if (input.SectionCount < 4)
|
||||
issues.Add(new("업무 문서 기준 핵심 섹션 수가 부족합니다", ArtifactReviewSeverity.Warning));
|
||||
if (!input.HasExecutiveSummarySection)
|
||||
issues.Add(new("Executive Summary 또는 요약 섹션이 없습니다", ArtifactReviewSeverity.Warning));
|
||||
if (!input.HasRecommendationSection)
|
||||
issues.Add(new("권고안 또는 다음 단계 섹션이 없습니다", ArtifactReviewSeverity.Warning));
|
||||
if (input.SectionCount >= 6 && !input.HasAppendixSection)
|
||||
issues.Add(new("긴 문서인데 부록/참고 섹션이 없습니다", ArtifactReviewSeverity.Info));
|
||||
if (input.TableCount + input.ListCount + input.CalloutCount + input.HighlightCount == 0)
|
||||
issues.Add(new("표, 목록, 강조 블록이 없어 문서가 단조로울 수 있습니다", ArtifactReviewSeverity.Warning));
|
||||
|
||||
return BuildReport("docx", strengths, issues);
|
||||
}
|
||||
|
||||
public static ArtifactQualityReport ReviewWorkbook(WorkbookReviewInput input)
|
||||
{
|
||||
var strengths = new List<string>();
|
||||
var issues = new List<ArtifactReviewIssue>();
|
||||
|
||||
if (input.HasSummarySheet) strengths.Add("요약 시트 포함");
|
||||
if (input.DetailSheetCount > 1) strengths.Add($"상세 시트 {input.DetailSheetCount}개 구성");
|
||||
if (input.FormulaCount > 0) strengths.Add($"수식 {input.FormulaCount}개 포함");
|
||||
if (input.HyperlinkCount > 0) strengths.Add("시트 간 빠른 이동 링크 포함");
|
||||
if (input.DataValidationCount > 0) strengths.Add("데이터 검증 규칙 포함");
|
||||
if (input.HasHighlightSection) strengths.Add("핵심 인사이트 영역 포함");
|
||||
if (input.HasActionSection) strengths.Add("후속 액션 영역 포함");
|
||||
|
||||
if (input.SheetCount > 1 && !input.HasSummarySheet)
|
||||
issues.Add(new("멀티시트 워크북인데 요약 시트가 없습니다", ArtifactReviewSeverity.Warning));
|
||||
if (input.DataRowCount >= 10 && input.FormulaCount == 0)
|
||||
issues.Add(new("데이터 행 수 대비 수식/집계가 부족합니다", ArtifactReviewSeverity.Warning));
|
||||
if (input.HasSummarySheet && input.HyperlinkCount == 0)
|
||||
issues.Add(new("요약 시트에서 상세 시트로 이동하는 링크가 없습니다", ArtifactReviewSeverity.Warning));
|
||||
if (input.DataValidationCount == 0 && input.DataRowCount >= 5)
|
||||
issues.Add(new("입력 통제를 위한 데이터 검증 규칙이 없습니다", ArtifactReviewSeverity.Info));
|
||||
|
||||
return BuildReport("xlsx", strengths, issues);
|
||||
}
|
||||
|
||||
public static bool ContainsBusinessKeyword(string text, params string[] keywords)
|
||||
{
|
||||
return ContainsAny(text, keywords);
|
||||
}
|
||||
|
||||
private static ArtifactQualityReport BuildReport(
|
||||
string artifactType,
|
||||
List<string> strengths,
|
||||
List<ArtifactReviewIssue> issues)
|
||||
{
|
||||
var score = 72;
|
||||
score += Math.Min(18, strengths.Count * 4);
|
||||
score -= issues.Sum(issue => issue.Severity switch
|
||||
{
|
||||
ArtifactReviewSeverity.Critical => 12,
|
||||
ArtifactReviewSeverity.Warning => 6,
|
||||
_ => 2,
|
||||
});
|
||||
score = Math.Clamp(score, 35, 98);
|
||||
|
||||
return new ArtifactQualityReport(artifactType, score, strengths, issues);
|
||||
}
|
||||
|
||||
private static int CountPlaceholders(string text)
|
||||
{
|
||||
var count = 0;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return count;
|
||||
|
||||
var patterns = new[]
|
||||
{
|
||||
@"\[(todo|placeholder|내용.*작성|fill me)\]",
|
||||
@"lorem ipsum",
|
||||
@"tbd",
|
||||
};
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
count += Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string text, IEnumerable<string> keywords)
|
||||
{
|
||||
return keywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,15 @@ public class DocxSkill : IAgentTool
|
||||
["path"] = new() { Type = "string", Description = "Output file path (.docx). Relative to work folder." },
|
||||
["title"] = new() { Type = "string", Description = "Document title (optional)." },
|
||||
["theme"] = new() { Type = "string", Description = "Visual theme: professional (default), modern, dark, minimal, creative." },
|
||||
["template_path"] = new() { Type = "string", Description = "Optional .docx template path. Reuses template styles and section setup." },
|
||||
["cover_subtitle"] = new() { Type = "string", Description = "Optional subtitle shown on the cover page." },
|
||||
["cover_meta"] = new()
|
||||
{
|
||||
Type = "array",
|
||||
Description = "Optional cover page meta lines such as author, organization, review window, or distribution note.",
|
||||
Items = new() { Type = "string" }
|
||||
},
|
||||
["toc"] = new() { Type = "boolean", Description = "Include a table of contents field. Default: true when there are 4+ sections." },
|
||||
["sections"] = new()
|
||||
{
|
||||
Type = "array",
|
||||
@@ -114,6 +123,13 @@ public class DocxSkill : IAgentTool
|
||||
var footerText = args.SafeTryGetProperty("footer", out var ftr) ? ftr.SafeGetString() : null;
|
||||
var showPageNumbers = args.SafeTryGetProperty("page_numbers", out var pn) ? pn.GetBoolean() :
|
||||
(headerText != null || footerText != null);
|
||||
var coverSubtitle = args.SafeTryGetProperty("cover_subtitle", out var subtitleEl) ? subtitleEl.SafeGetString() : null;
|
||||
var templatePath = args.SafeTryGetProperty("template_path", out var templateEl) ? templateEl.SafeGetString() : null;
|
||||
var coverMeta = ReadStringArray(args, "cover_meta");
|
||||
var sections = args.GetProperty("sections");
|
||||
var useToc = args.SafeTryGetProperty("toc", out var tocEl)
|
||||
? tocEl.ValueKind == JsonValueKind.True
|
||||
: sections.GetArrayLength() >= 4;
|
||||
|
||||
var themeName = args.SafeTryGetProperty("theme", out var th) ? th.SafeGetString() ?? "professional" : "professional";
|
||||
var theme = Themes.TryGetValue(themeName, out var tc) ? tc : Themes["professional"];
|
||||
@@ -131,26 +147,48 @@ public class DocxSkill : IAgentTool
|
||||
|
||||
try
|
||||
{
|
||||
var sections = args.GetProperty("sections");
|
||||
|
||||
var dir = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||
string? templateFullPath = null;
|
||||
var templateApplied = false;
|
||||
|
||||
using var doc = WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
if (!string.IsNullOrWhiteSpace(templatePath))
|
||||
{
|
||||
templateFullPath = FileReadTool.ResolvePath(templatePath, context.WorkFolder);
|
||||
if (!templateFullPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
|
||||
templateFullPath += ".docx";
|
||||
|
||||
if (string.Equals(Path.GetFullPath(templateFullPath), Path.GetFullPath(fullPath), StringComparison.OrdinalIgnoreCase))
|
||||
return ToolResult.Fail("template_path와 path는 같은 파일일 수 없습니다.");
|
||||
if (!context.IsPathAllowed(templateFullPath))
|
||||
return ToolResult.Fail($"템플릿 경로 접근 차단: {templateFullPath}");
|
||||
if (!File.Exists(templateFullPath))
|
||||
return ToolResult.Fail($"DOCX 템플릿을 찾을 수 없습니다: {templateFullPath}");
|
||||
|
||||
File.Copy(templateFullPath, fullPath, true);
|
||||
templateApplied = true;
|
||||
}
|
||||
|
||||
using var doc = templateApplied
|
||||
? WordprocessingDocument.Open(fullPath, true)
|
||||
: WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart();
|
||||
|
||||
// 기본 스타일 파트 추가 (styles.xml)
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
EnsureStyles(mainPart, templateApplied);
|
||||
|
||||
mainPart.Document = new Document();
|
||||
var body = mainPart.Document.AppendChild(new Body());
|
||||
var body = InitializeDocumentBody(mainPart, templateApplied);
|
||||
|
||||
// 머리글/바닥글 설정
|
||||
if (headerText != null || footerText != null || showPageNumbers)
|
||||
AddHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title) || !string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0)
|
||||
AppendCoverPage(body, title, coverSubtitle, coverMeta, theme);
|
||||
|
||||
if (useToc)
|
||||
AppendTableOfContents(body);
|
||||
|
||||
// 제목
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
@@ -167,6 +205,11 @@ public class DocxSkill : IAgentTool
|
||||
|
||||
int sectionCount = 0;
|
||||
int tableCount = 0;
|
||||
int listCount = 0;
|
||||
int calloutCount = 0;
|
||||
int highlightCount = 0;
|
||||
int iconCount = 0;
|
||||
var headings = new List<string>();
|
||||
foreach (var section in sections.EnumerateArray())
|
||||
{
|
||||
var blockType = section.SafeTryGetProperty("type", out var bt) ? bt.SafeGetString()?.ToLower() : null;
|
||||
@@ -187,24 +230,28 @@ public class DocxSkill : IAgentTool
|
||||
if (blockType == "list")
|
||||
{
|
||||
AppendList(body, section);
|
||||
listCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockType == "callout")
|
||||
{
|
||||
AppendCallout(body, section);
|
||||
calloutCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockType == "highlight_box")
|
||||
{
|
||||
body.Append(CreateHighlightBox(section));
|
||||
highlightCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockType == "icon")
|
||||
{
|
||||
body.Append(CreateIconParagraph(section));
|
||||
iconCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -214,7 +261,10 @@ public class DocxSkill : IAgentTool
|
||||
var level = section.SafeTryGetProperty("level", out var lv) ? lv.GetInt32() : 1;
|
||||
|
||||
if (!string.IsNullOrEmpty(heading))
|
||||
{
|
||||
body.Append(CreateHeadingParagraph(heading, level, theme));
|
||||
headings.Add(heading);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bodyText))
|
||||
{
|
||||
@@ -231,6 +281,9 @@ public class DocxSkill : IAgentTool
|
||||
mainPart.Document.Save();
|
||||
|
||||
var parts = new List<string>();
|
||||
if (templateApplied) parts.Add("템플릿");
|
||||
if (useToc) parts.Add("목차");
|
||||
if (!string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0) parts.Add("커버");
|
||||
if (!string.IsNullOrEmpty(title)) parts.Add($"제목: {title}");
|
||||
if (sectionCount > 0) parts.Add($"섹션: {sectionCount}개");
|
||||
if (tableCount > 0) parts.Add($"테이블: {tableCount}개");
|
||||
@@ -238,6 +291,46 @@ public class DocxSkill : IAgentTool
|
||||
if (showPageNumbers) parts.Add("페이지번호");
|
||||
parts.Add($"테마: {themeName}");
|
||||
|
||||
#if false
|
||||
var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
|
||||
title,
|
||||
sectionCount,
|
||||
sections.ToString().Length,
|
||||
tableCount,
|
||||
listCount,
|
||||
calloutCount,
|
||||
highlightCount,
|
||||
iconCount,
|
||||
!string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0,
|
||||
useToc,
|
||||
templateApplied,
|
||||
headerText != null || footerText != null || showPageNumbers,
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary", "요약")),
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "권고", "제안", "next step", "실행")),
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "부록", "참고"))
|
||||
));
|
||||
|
||||
parts.Add(review.ToToolSummary());
|
||||
#endif
|
||||
var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
|
||||
title,
|
||||
sectionCount,
|
||||
sections.ToString().Length,
|
||||
tableCount,
|
||||
listCount,
|
||||
calloutCount,
|
||||
highlightCount,
|
||||
iconCount,
|
||||
!string.IsNullOrWhiteSpace(coverSubtitle) || coverMeta.Count > 0,
|
||||
useToc,
|
||||
templateApplied,
|
||||
headerText != null || footerText != null || showPageNumbers,
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary")),
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "proposal", "next step", "action")),
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "reference", "supplement"))
|
||||
));
|
||||
|
||||
parts.Add(review.ToToolSummary());
|
||||
return ToolResult.Ok(
|
||||
$"Word 문서 생성 완료: {fullPath}\n{string.Join(", ", parts)}",
|
||||
fullPath);
|
||||
@@ -252,6 +345,118 @@ public class DocxSkill : IAgentTool
|
||||
// 제목/소제목/본문 단락 생성
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private static List<string> ReadStringArray(JsonElement args, string propertyName)
|
||||
{
|
||||
var items = new List<string>();
|
||||
if (!args.SafeTryGetProperty(propertyName, out var arr) || arr.ValueKind != JsonValueKind.Array)
|
||||
return items;
|
||||
|
||||
foreach (var item in arr.EnumerateArray())
|
||||
{
|
||||
var text = item.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
items.Add(text);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static Body InitializeDocumentBody(MainDocumentPart mainPart, bool preserveSectionSetup)
|
||||
{
|
||||
mainPart.Document ??= new Document();
|
||||
|
||||
SectionProperties? preservedSection = null;
|
||||
if (preserveSectionSetup)
|
||||
preservedSection = mainPart.Document.Body?.Elements<SectionProperties>().LastOrDefault()?.CloneNode(true) as SectionProperties;
|
||||
|
||||
mainPart.Document.Body = new Body();
|
||||
var body = mainPart.Document.Body;
|
||||
|
||||
if (preservedSection != null)
|
||||
body.Append(preservedSection);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private static void EnsureStyles(MainDocumentPart mainPart, bool preserveExisting)
|
||||
{
|
||||
var stylesPart = mainPart.StyleDefinitionsPart;
|
||||
if (stylesPart == null)
|
||||
{
|
||||
stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!preserveExisting || stylesPart.Styles == null)
|
||||
{
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendCoverPage(Body body, string title, string? subtitle, IReadOnlyList<string> metaLines, ThemeColors theme)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
body.Append(CreateTitleParagraph(title, theme));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subtitle))
|
||||
{
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties
|
||||
{
|
||||
Justification = new Justification { Val = JustificationValues.Center },
|
||||
SpacingBetweenLines = new SpacingBetweenLines { After = "220" },
|
||||
},
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "26" },
|
||||
new Color { Val = theme.H2 },
|
||||
new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }),
|
||||
new Text(subtitle))));
|
||||
}
|
||||
|
||||
foreach (var line in metaLines)
|
||||
{
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties
|
||||
{
|
||||
Justification = new Justification { Val = JustificationValues.Center },
|
||||
SpacingBetweenLines = new SpacingBetweenLines { After = "80" },
|
||||
},
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "20" },
|
||||
new Color { Val = "666666" },
|
||||
new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }),
|
||||
new Text(line))));
|
||||
}
|
||||
|
||||
body.Append(CreatePageBreak());
|
||||
}
|
||||
|
||||
private static void AppendTableOfContents(Body body)
|
||||
{
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new SpacingBetweenLines { Before = "120", After = "120" }),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Bold(),
|
||||
new FontSize { Val = "28" },
|
||||
new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }),
|
||||
new Text("목차"))));
|
||||
|
||||
var paragraph = new Paragraph();
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
paragraph.Append(new Run(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
paragraph.Append(new Run(new Text("목차를 업데이트하려면 Word에서 필드를 새로 고치세요.")));
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
body.Append(paragraph);
|
||||
body.Append(CreatePageBreak());
|
||||
}
|
||||
|
||||
private static Paragraph CreateTitleParagraph(string text, ThemeColors theme)
|
||||
{
|
||||
var para = new Paragraph();
|
||||
@@ -779,6 +984,7 @@ public class DocxSkill : IAgentTool
|
||||
|
||||
// SectionProperties에 머리글 연결
|
||||
var secProps = body.GetFirstChild<SectionProperties>() ?? body.AppendChild(new SectionProperties());
|
||||
secProps.Elements<HeaderReference>().ToList().ForEach(r => r.Remove());
|
||||
secProps.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
@@ -825,6 +1031,7 @@ public class DocxSkill : IAgentTool
|
||||
footerPart.Footer = footer;
|
||||
|
||||
var secProps = body.GetFirstChild<SectionProperties>() ?? body.AppendChild(new SectionProperties());
|
||||
secProps.Elements<FooterReference>().ToList().ForEach(r => r.Remove());
|
||||
secProps.Append(new FooterReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -288,6 +288,8 @@ public class HtmlSkill : IAgentTool
|
||||
if (!string.IsNullOrEmpty(accentColor)) features.Add($"색상:{accentColor}");
|
||||
if (usePrint) features.Add("인쇄최적화");
|
||||
var featureStr = features.Count > 0 ? $" [{string.Join(", ", features)}]" : "";
|
||||
var review = ArtifactQualityReviewService.ReviewHtml(title, sb.ToString(), hasCover, useToc, usePrint);
|
||||
featureStr += $"\n{review.ToToolSummary()}";
|
||||
|
||||
return ToolResult.Ok(
|
||||
$"HTML 문서 생성 완료: {fullPath} (디자인: {mood}{featureStr})",
|
||||
|
||||
26
src/AxCopilot/skills/board-report-html.skill.md
Normal file
26
src/AxCopilot/skills/board-report-html.skill.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: board-report-html
|
||||
label: HTML 보고서
|
||||
description: 이사회/운영위원회용 HTML 보고서를 구조화된 섹션과 인쇄형 레이아웃으로 생성합니다.
|
||||
icon: \uE8A7
|
||||
when_to_use: 웹 공유와 인쇄를 모두 고려한 HTML 경영 보고서가 필요할 때
|
||||
argument-hint: <보고 목적과 청중>
|
||||
allowed-tools:
|
||||
- folder_map
|
||||
- file_read
|
||||
- document_plan
|
||||
- document_assemble
|
||||
- html_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
HTML 업무 보고서를 작성하세요.
|
||||
|
||||
작성 원칙:
|
||||
- `html_create`를 우선 사용하고 `toc`, `print`, `cover`, `sections`를 적극 활용하세요.
|
||||
- 문서는 `Executive Summary -> Current State -> Key Issues -> Options -> Recommendation -> Roadmap -> Appendix` 순서를 기본으로 잡으세요.
|
||||
- comparison, roadmap, matrix, KPI, callout 블록을 내용에 맞게 섞어 쓰세요.
|
||||
- 섹션마다 최소 두 개 이상의 실질 문단 또는 구조화 블록을 포함하세요.
|
||||
- 단순 꾸밈보다 의사결정에 필요한 근거, 비교, 후속 조치를 분명히 드러내세요.
|
||||
|
||||
결과물은 작업 폴더에 저장하고, 인쇄 가능 여부와 핵심 결론을 함께 요약하세요.
|
||||
26
src/AxCopilot/skills/executive-brief.skill.md
Normal file
26
src/AxCopilot/skills/executive-brief.skill.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: executive-brief
|
||||
label: 임원 보고 문서
|
||||
description: 경영진 보고용 Word 문서를 AX 네이티브 도구 중심으로 구성합니다.
|
||||
icon: \uE9D9
|
||||
when_to_use: 경영진, 임원진, steering committee, 의사결정 회의용 Word 보고 문서를 빠르게 만들 때
|
||||
argument-hint: <보고 주제와 의사결정 포인트>
|
||||
allowed-tools:
|
||||
- folder_map
|
||||
- file_read
|
||||
- document_plan
|
||||
- document_assemble
|
||||
- docx_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
임원 보고 문서를 작성하세요.
|
||||
|
||||
핵심 원칙:
|
||||
- Python 우회 대신 `document_plan`, `document_assemble`, `docx_create`를 우선 사용하세요.
|
||||
- 문서는 `Executive Summary -> Situation -> Key Findings -> Recommendation -> Next Steps -> Appendix` 구조를 기본으로 잡으세요.
|
||||
- 각 섹션 제목은 메시지형으로 쓰고, 표와 강조 블록을 적극 활용하세요.
|
||||
- 긴 문서는 `document_plan`으로 구조를 먼저 만들고, 최종 산출은 DOCX로 조립하세요.
|
||||
- 사내 템플릿이 있다면 `template_path`, `cover_subtitle`, `toc`를 포함해 완성도를 높이세요.
|
||||
|
||||
결과물은 작업 폴더에 저장하고, 최종 응답에는 파일 경로와 핵심 섹션을 요약하세요.
|
||||
25
src/AxCopilot/skills/kpi-workbook.skill.md
Normal file
25
src/AxCopilot/skills/kpi-workbook.skill.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: kpi-workbook
|
||||
label: KPI 워크북
|
||||
description: 요약 시트와 상세 시트를 갖춘 KPI 분석용 Excel 워크북을 생성합니다.
|
||||
icon: \uE9D2
|
||||
when_to_use: KPI 대시보드, 운영 리뷰, 예산 대비 실적, PMO 추적용 Excel 워크북이 필요할 때
|
||||
argument-hint: <분석 주제와 포함할 핵심 지표>
|
||||
allowed-tools:
|
||||
- folder_map
|
||||
- file_read
|
||||
- data_pivot
|
||||
- excel_create
|
||||
tabs: cowork
|
||||
---
|
||||
|
||||
요약 시트가 있는 KPI 워크북을 만드세요.
|
||||
|
||||
작성 원칙:
|
||||
- 단일 표보다 `Summary + Detail` 구조의 워크북을 우선 구성하세요.
|
||||
- 요약 시트에는 KPI, highlights, actions를 넣고 상세 시트로 이어지는 구조를 만드세요.
|
||||
- 수치 데이터가 있으면 가능한 범위에서 수식과 집계 행을 포함하세요.
|
||||
- 시트 이름은 이해하기 쉽게 짓고, 운영 회의에서 바로 쓰일 수 있도록 핵심 지표와 후속 액션을 함께 정리하세요.
|
||||
- 원본 데이터가 여러 개면 먼저 `data_pivot`으로 정리한 뒤 `excel_create`를 호출하세요.
|
||||
|
||||
결과물은 작업 폴더에 저장하고, 시트 구성과 핵심 KPI를 함께 요약하세요.
|
||||
Reference in New Issue
Block a user