218 lines
7.7 KiB
C#
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();
|
|
}
|
|
}
|