문서 생성 품질 게이트와 산출물 고도화 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:
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user