using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; 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 { get { ToolParameterSchema obj = new ToolParameterSchema { Properties = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "Path to the document to review" }, ["expected_sections"] = new ToolProperty { Type = "array", Description = "Optional list of expected section titles to verify presence", Items = new ToolProperty { Type = "string" } } } }; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); CollectionsMarshal.AsSpan(list)[0] = "path"; obj.Required = list; return obj; } } public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { if (!args.TryGetProperty("path", out var value)) { return Task.FromResult(ToolResult.Fail("path가 필요합니다.")); } string path = value.GetString() ?? ""; string text = FileReadTool.ResolvePath(path, context.WorkFolder); if (!context.IsPathAllowed(text)) { return Task.FromResult(ToolResult.Fail("경로 접근 차단: " + text)); } if (!File.Exists(text)) { return Task.FromResult(ToolResult.Fail("파일 없음: " + text)); } string text2 = File.ReadAllText(text); string text3 = Path.GetExtension(text).ToLowerInvariant(); List list = new List(); List list2 = new List(); int value2 = text2.Split('\n').Length; int length = text2.Length; list2.Add($"파일: {Path.GetFileName(text)} ({length:N0}자, {value2}줄)"); if (string.IsNullOrWhiteSpace(text2)) { list.Add("[CRITICAL] 파일 내용이 비어있습니다"); return Task.FromResult(ToolResult.Ok(FormatReport(list2, list, new List()), text)); } string[] array = new string[8] { "TODO", "TBD", "FIXME", "Lorem ipsum", "[여기에", "[INSERT", "placeholder", "예시 텍스트" }; string[] array2 = array; foreach (string text4 in array2) { if (text2.Contains(text4, StringComparison.OrdinalIgnoreCase)) { list.Add("[WARNING] 플레이스홀더 텍스트 발견: \"" + text4 + "\""); } } Regex regex = new Regex("\\d{4}[-년.]\\s*\\d{1,2}[-월.]\\s*\\d{1,2}[일]?"); foreach (Match item in regex.Matches(text2)) { string s = Regex.Replace(item.Value, "[년월일\\s]", "-").TrimEnd('-'); if (DateTime.TryParse(s, out var result)) { if (result > DateTime.Now.AddDays(365.0)) { list.Add("[WARNING] 미래 날짜 감지: " + item.Value); } else if (result < DateTime.Now.AddYears(-50)) { list.Add("[INFO] 매우 오래된 날짜: " + item.Value); } } } if ((text3 == ".html" || text3 == ".htm") ? true : false) { MatchCollection matchCollection = Regex.Matches(text2, "]*>.*?\\s* 0) { list.Add($"[WARNING] 빈 섹션 {matchCollection.Count}개 감지 (헤딩 뒤 내용 없음)"); } int count = Regex.Matches(text2, "<(table|div|section|article)\\b[^/]*>").Count; int count2 = Regex.Matches(text2, "").Count; if (count != count2) { list.Add($"[WARNING] HTML 태그 불균형: 열림 {count}개, 닫힘 {count2}개"); } MatchCollection matchCollection2 = Regex.Matches(text2, "]*\\balt\\s*=)[^>]*>"); if (matchCollection2.Count > 0) { list.Add($"[INFO] alt 속성 없는 이미지 {matchCollection2.Count}개"); } int count3 = Regex.Matches(text2, "> source = from text6 in Regex.Split(text2, "[.!?。]\\s+") where text6.Length > 20 group text6 by text6.Trim() into g where g.Count() >= 3 select g; foreach (IGrouping item3 in source.Take(3)) { list.Add($"[WARNING] 반복 텍스트 ({item3.Count()}회): \"{item3.Key.Substring(0, Math.Min(50, item3.Key.Length))}...\""); } List list3 = new List(); if (list.Count == 0) { list3.Add("문서 검증 통과 — 구조적 이슈가 발견되지 않았습니다."); } else { list3.Add($"총 {list.Count}개 이슈 발견. 수정 후 다시 검증하세요."); if (list.Any((string text6) => text6.Contains("플레이스홀더"))) { list3.Add("플레이스홀더를 실제 내용으로 교체하세요."); } if (list.Any((string text6) => text6.Contains("빈 섹션"))) { list3.Add("빈 섹션에 내용을 추가하거나 불필요한 헤딩을 제거하세요."); } } return Task.FromResult(ToolResult.Ok(FormatReport(list2, list, list3), text)); } private static string FormatReport(List stats, List issues, List suggestions) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=== 문서 검증 보고서 ===\n"); foreach (string stat in stats) { StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); handler.AppendLiteral("\ud83d\udcca "); handler.AppendFormatted(stat); stringBuilder3.AppendLine(ref handler); } stringBuilder.AppendLine(); if (issues.Count == 0) { stringBuilder.AppendLine("✅ 이슈 없음 — 문서가 정상입니다."); } else { StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder2); handler.AppendLiteral("⚠ 발견된 이슈 ("); handler.AppendFormatted(issues.Count); handler.AppendLiteral("건):"); stringBuilder4.AppendLine(ref handler); foreach (string issue in issues) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder5 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); handler.AppendLiteral(" "); handler.AppendFormatted(issue); stringBuilder5.AppendLine(ref handler); } } stringBuilder.AppendLine(); foreach (string suggestion in suggestions) { StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder6 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); handler.AppendLiteral("\ud83d\udca1 "); handler.AppendFormatted(suggestion); stringBuilder6.AppendLine(ref handler); } return stringBuilder.ToString(); } }