using System.IO; using System.Text.Json; using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; /// /// 생성된 문서를 자동 검증하는 도구. /// HTML/Markdown/텍스트 파일의 구조적 완성도, 날짜 정합성, 빈 섹션 등을 점검합니다. /// public class DocumentReviewTool : IAgentTool { public string Name => "document_review"; public string Description => "Review a generated document for quality issues. " + "Checks: empty sections, placeholder text, date consistency, missing headings, broken HTML tags, " + "content completeness. Returns a structured review report with issues found and suggestions."; public ToolParameterSchema Parameters => new() { Properties = new() { ["path"] = new() { Type = "string", Description = "Path to the document to review" }, ["expected_sections"] = new() { Type = "array", Description = "Optional list of expected section titles to verify presence", Items = new() { Type = "string" }, }, }, Required = ["path"] }; public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { if (!args.TryGetProperty("path", out var pathEl)) return Task.FromResult(ToolResult.Fail("path가 필요합니다.")); var path = pathEl.GetString() ?? ""; var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (!context.IsPathAllowed(fullPath)) return Task.FromResult(ToolResult.Fail($"경로 접근 차단: {fullPath}")); if (!File.Exists(fullPath)) return Task.FromResult(ToolResult.Fail($"파일 없음: {fullPath}")); var content = TextFileCodec.ReadAllText(fullPath).Text; var ext = Path.GetExtension(fullPath).ToLowerInvariant(); var issues = new List(); var stats = new List(); // 기본 통계 var lineCount = content.Split('\n').Length; var charCount = content.Length; stats.Add($"파일: {Path.GetFileName(fullPath)} ({charCount:N0}자, {lineCount}줄)"); // 1. 빈 콘텐츠 검사 if (string.IsNullOrWhiteSpace(content)) { issues.Add("[CRITICAL] 파일 내용이 비어있습니다"); return Task.FromResult(ToolResult.Ok(FormatReport(stats, issues, []), fullPath)); } // 2. 플레이스홀더 텍스트 검사 var placeholders = new[] { "TODO", "TBD", "FIXME", "Lorem ipsum", "[여기에", "[INSERT", "placeholder", "예시 텍스트" }; foreach (var ph in placeholders) { if (content.Contains(ph, StringComparison.OrdinalIgnoreCase)) issues.Add($"[WARNING] 플레이스홀더 텍스트 발견: \"{ph}\""); } // 3. 날짜 정합성 (미래 날짜, 너무 오래된 날짜) var datePattern = new Regex(@"\d{4}[-년.]\s*\d{1,2}[-월.]\s*\d{1,2}[일]?"); foreach (Match m in datePattern.Matches(content)) { var cleaned = Regex.Replace(m.Value, @"[년월일\s]", "-").TrimEnd('-'); if (DateTime.TryParse(cleaned, out var dt)) { if (dt > DateTime.Now.AddDays(365)) issues.Add($"[WARNING] 미래 날짜 감지: {m.Value}"); else if (dt < DateTime.Now.AddYears(-50)) issues.Add($"[INFO] 매우 오래된 날짜: {m.Value}"); } } // 4. HTML 전용 검사 if (ext is ".html" or ".htm") { // 빈 섹션 (h2/h3 뒤에 내용 없이 바로 다음 h2/h3) var emptySection = Regex.Matches(content, @"]*>.*?\s* 0) issues.Add($"[WARNING] 빈 섹션 {emptySection.Count}개 감지 (헤딩 뒤 내용 없음)"); // 닫히지 않은 태그 var openTags = Regex.Matches(content, @"<(table|div|section|article)\b[^/]*>").Count; var closeTags = Regex.Matches(content, @"").Count; if (openTags != closeTags) issues.Add($"[WARNING] HTML 태그 불균형: 열림 {openTags}개, 닫힘 {closeTags}개"); // 이미지 alt 텍스트 누락 var imgNoAlt = Regex.Matches(content, @"]*\balt\s*=)[^>]*>"); if (imgNoAlt.Count > 0) issues.Add($"[INFO] alt 속성 없는 이미지 {imgNoAlt.Count}개"); // 제목 태그 수 var h1Count = Regex.Matches(content, @" s.Length > 20) .GroupBy(s => s.Trim()) .Where(g => g.Count() >= 3); foreach (var dup in sentences.Take(3)) issues.Add($"[WARNING] 반복 텍스트 ({dup.Count()}회): \"{dup.Key[..Math.Min(50, dup.Key.Length)]}...\""); var suggestions = new List(); if (issues.Count == 0) suggestions.Add("문서 검증 통과 — 구조적 이슈가 발견되지 않았습니다."); else { suggestions.Add($"총 {issues.Count}개 이슈 발견. 수정 후 다시 검증하세요."); if (issues.Any(i => i.Contains("플레이스홀더"))) suggestions.Add("플레이스홀더를 실제 내용으로 교체하세요."); if (issues.Any(i => i.Contains("빈 섹션"))) suggestions.Add("빈 섹션에 내용을 추가하거나 불필요한 헤딩을 제거하세요."); } return Task.FromResult(ToolResult.Ok(FormatReport(stats, issues, suggestions), fullPath)); } private static string FormatReport(List stats, List issues, List suggestions) { var sb = new System.Text.StringBuilder(); sb.AppendLine("=== 문서 검증 보고서 ===\n"); foreach (var s in stats) sb.AppendLine($"📊 {s}"); sb.AppendLine(); if (issues.Count == 0) sb.AppendLine("✅ 이슈 없음 — 문서가 정상입니다."); else { sb.AppendLine($"⚠ 발견된 이슈 ({issues.Count}건):"); foreach (var i in issues) sb.AppendLine($" {i}"); } sb.AppendLine(); foreach (var s in suggestions) sb.AppendLine($"💡 {s}"); return sb.ToString(); } }