Initial commit to new repository
This commit is contained in:
188
src/AxCopilot/Services/Agent/RegexTool.cs
Normal file
188
src/AxCopilot/Services/Agent/RegexTool.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// 정규식 테스트·추출·치환 도구.
|
||||
/// 패턴 매칭, 그룹 추출, 치환, 패턴 설명 기능을 제공합니다.
|
||||
/// </summary>
|
||||
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<ToolResult> 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] + "…";
|
||||
}
|
||||
Reference in New Issue
Block a user