using System.Text.Json; using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; /// /// 정규식 테스트·추출·치환 도구. /// 패턴 매칭, 그룹 추출, 치환, 패턴 설명 기능을 제공합니다. /// public class RegexTool : IAgentTool { public string Name => "regex_tool"; public string Description => "Regular expression tool. Actions: " + "'test' — check if text matches a pattern; " + "'match' — find all matches with groups; " + "'replace' — replace matches with replacement string; " + "'split' — split text by pattern; " + "'extract' — extract named/numbered groups from first match."; public ToolParameterSchema Parameters => new() { Properties = new() { ["action"] = new() { Type = "string", Description = "Action to perform", Enum = ["test", "match", "replace", "split", "extract"], }, ["pattern"] = new() { Type = "string", Description = "Regular expression pattern", }, ["text"] = new() { Type = "string", Description = "Text to process", }, ["replacement"] = new() { Type = "string", Description = "Replacement string for replace action (supports $1, $2, ${name})", }, ["flags"] = new() { Type = "string", Description = "Regex flags: 'i' (ignore case), 'm' (multiline), 's' (singleline). Combine: 'im'", }, }, Required = ["action", "pattern", "text"], }; private const int MaxTimeout = 5000; // ReDoS 방지 public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var action = args.GetProperty("action").GetString() ?? ""; var pattern = args.GetProperty("pattern").GetString() ?? ""; var text = args.GetProperty("text").GetString() ?? ""; var replacement = args.TryGetProperty("replacement", out var r) ? r.GetString() ?? "" : ""; var flags = args.TryGetProperty("flags", out var f) ? f.GetString() ?? "" : ""; try { var options = ParseFlags(flags); var regex = new Regex(pattern, options, TimeSpan.FromMilliseconds(MaxTimeout)); return Task.FromResult(action switch { "test" => Test(regex, text), "match" => Match(regex, text), "replace" => Replace(regex, text, replacement), "split" => Split(regex, text), "extract" => Extract(regex, text), _ => ToolResult.Fail($"Unknown action: {action}"), }); } catch (RegexMatchTimeoutException) { return Task.FromResult(ToolResult.Fail("정규식 실행 시간 초과 (ReDoS 방지). 패턴을 간소화하세요.")); } catch (Exception ex) { return Task.FromResult(ToolResult.Fail($"정규식 오류: {ex.Message}")); } } private static RegexOptions ParseFlags(string flags) { var options = RegexOptions.None; foreach (var c in flags) { options |= c switch { 'i' => RegexOptions.IgnoreCase, 'm' => RegexOptions.Multiline, 's' => RegexOptions.Singleline, _ => RegexOptions.None, }; } return options; } private static ToolResult Test(Regex regex, string text) { var isMatch = regex.IsMatch(text); return ToolResult.Ok(isMatch ? "✓ Pattern matches" : "✗ No match"); } private static ToolResult Match(Regex regex, string text) { var matches = regex.Matches(text); if (matches.Count == 0) return ToolResult.Ok("No matches found."); var sb = new System.Text.StringBuilder(); sb.AppendLine($"Found {matches.Count} match(es):"); var limit = Math.Min(matches.Count, 50); // 최대 50개 for (var i = 0; i < limit; i++) { var m = matches[i]; sb.AppendLine($"\n[{i}] \"{Truncate(m.Value, 200)}\" (index {m.Index}, length {m.Length})"); if (m.Groups.Count > 1) { for (var g = 1; g < m.Groups.Count; g++) { var group = m.Groups[g]; var name = regex.GroupNameFromNumber(g); var label = name != g.ToString() ? $"'{name}'" : $"${g}"; sb.AppendLine($" Group {label}: \"{Truncate(group.Value, 100)}\""); } } } if (matches.Count > limit) sb.AppendLine($"\n... and {matches.Count - limit} more matches"); return ToolResult.Ok(sb.ToString()); } private static ToolResult Replace(Regex regex, string text, string replacement) { var result = regex.Replace(text, replacement); var count = regex.Matches(text).Count; if (result.Length > 8000) result = result[..8000] + "\n... (truncated)"; return ToolResult.Ok($"Replaced {count} occurrence(s):\n\n{result}"); } private static ToolResult Split(Regex regex, string text) { var parts = regex.Split(text); var sb = new System.Text.StringBuilder(); sb.AppendLine($"Split into {parts.Length} parts:"); var limit = Math.Min(parts.Length, 100); for (var i = 0; i < limit; i++) sb.AppendLine($" [{i}] \"{Truncate(parts[i], 200)}\""); if (parts.Length > limit) sb.AppendLine($"\n... and {parts.Length - limit} more parts"); return ToolResult.Ok(sb.ToString()); } private static ToolResult Extract(Regex regex, string text) { var m = regex.Match(text); if (!m.Success) return ToolResult.Ok("No match found."); var sb = new System.Text.StringBuilder(); sb.AppendLine($"Match: \"{Truncate(m.Value, 300)}\""); if (m.Groups.Count > 1) { sb.AppendLine("\nGroups:"); for (var g = 1; g < m.Groups.Count; g++) { var group = m.Groups[g]; var name = regex.GroupNameFromNumber(g); var label = name != g.ToString() ? $"'{name}'" : $"${g}"; sb.AppendLine($" {label}: \"{Truncate(group.Value, 200)}\""); } } return ToolResult.Ok(sb.ToString()); } private static string Truncate(string s, int maxLen) => s.Length <= maxLen ? s : s[..maxLen] + "…"; }