Files

218 lines
7.7 KiB
C#

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<string, ToolProperty>
{
["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<string> list = new List<string>(num);
CollectionsMarshal.SetCount(list, num);
CollectionsMarshal.AsSpan(list)[0] = "path";
obj.Required = list;
return obj;
}
}
public Task<ToolResult> 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<string> list = new List<string>();
List<string> list2 = new List<string>();
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<string>()), 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, "<h[23][^>]*>.*?</h[23]>\\s*<h[23]");
if (matchCollection.Count > 0)
{
list.Add($"[WARNING] 빈 섹션 {matchCollection.Count}개 감지 (헤딩 뒤 내용 없음)");
}
int count = Regex.Matches(text2, "<(table|div|section|article)\\b[^/]*>").Count;
int count2 = Regex.Matches(text2, "</(table|div|section|article)>").Count;
if (count != count2)
{
list.Add($"[WARNING] HTML 태그 불균형: 열림 {count}개, 닫힘 {count2}개");
}
MatchCollection matchCollection2 = Regex.Matches(text2, "<img\\b(?![^>]*\\balt\\s*=)[^>]*>");
if (matchCollection2.Count > 0)
{
list.Add($"[INFO] alt 속성 없는 이미지 {matchCollection2.Count}개");
}
int count3 = Regex.Matches(text2, "<h1\\b").Count;
int count4 = Regex.Matches(text2, "<h2\\b").Count;
list2.Add($"구조: h1={count3}, h2={count4}개 섹션");
}
if (args.TryGetProperty("expected_sections", out var value3) && value3.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement item2 in value3.EnumerateArray())
{
string text5 = item2.GetString() ?? "";
if (!string.IsNullOrEmpty(text5) && !text2.Contains(text5, StringComparison.OrdinalIgnoreCase))
{
list.Add("[MISSING] 기대 섹션 누락: \"" + text5 + "\"");
}
}
}
IEnumerable<IGrouping<string, string>> 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<string, string> item3 in source.Take(3))
{
list.Add($"[WARNING] 반복 텍스트 ({item3.Count()}회): \"{item3.Key.Substring(0, Math.Min(50, item3.Key.Length))}...\"");
}
List<string> list3 = new List<string>();
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<string> stats, List<string> issues, List<string> 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();
}
}