Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/XmlTool.cs

190 lines
6.8 KiB
C#

using System.IO;
using System.Text;
using System.Text.Json;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
namespace AxCopilot.Services.Agent;
/// <summary>XML 파싱, XPath 쿼리, 변환 도구.</summary>
public class XmlTool : IAgentTool
{
public string Name => "xml_tool";
public string Description =>
"Parse and query XML documents. Actions: " +
"'parse' — parse XML file/string and return structure summary; " +
"'xpath' — evaluate XPath expression and return matching nodes; " +
"'to_json' — convert XML to JSON; " +
"'format' — pretty-print XML with indentation.";
public ToolParameterSchema Parameters => new()
{
Properties = new()
{
["action"] = new()
{
Type = "string",
Description = "Action: parse, xpath, to_json, format",
Enum = ["parse", "xpath", "to_json", "format"],
},
["path"] = new()
{
Type = "string",
Description = "XML file path (optional if 'xml' is provided)",
},
["xml"] = new()
{
Type = "string",
Description = "XML string (optional if 'path' is provided)",
},
["expression"] = new()
{
Type = "string",
Description = "XPath expression (for 'xpath' action)",
},
},
Required = ["action"],
};
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
{
var action = args.GetProperty("action").GetString() ?? "";
var xmlStr = args.TryGetProperty("xml", out var x) ? x.GetString() ?? "" : "";
var rawPath = args.TryGetProperty("path", out var pv) ? pv.GetString() ?? "" : "";
var expression = args.TryGetProperty("expression", out var ex) ? ex.GetString() ?? "" : "";
try
{
// XML 소스 결정
if (string.IsNullOrEmpty(xmlStr) && !string.IsNullOrEmpty(rawPath))
{
var path = Path.IsPathRooted(rawPath) ? rawPath : Path.Combine(context.WorkFolder, rawPath);
if (!context.IsPathAllowed(path))
return Task.FromResult(ToolResult.Fail($"경로 접근 차단: {path}"));
if (!File.Exists(path))
return Task.FromResult(ToolResult.Fail($"파일 없음: {path}"));
xmlStr = TextFileCodec.ReadAllText(path).Text;
}
if (string.IsNullOrEmpty(xmlStr))
return Task.FromResult(ToolResult.Fail("'xml' 또는 'path' 중 하나를 지정해야 합니다."));
var doc = XDocument.Parse(xmlStr);
return action switch
{
"parse" => Task.FromResult(ParseSummary(doc)),
"xpath" => Task.FromResult(EvalXPath(doc, expression)),
"to_json" => Task.FromResult(XmlToJson(doc)),
"format" => Task.FromResult(FormatXml(doc)),
_ => Task.FromResult(ToolResult.Fail($"Unknown action: {action}")),
};
}
catch (XmlException xe)
{
return Task.FromResult(ToolResult.Fail($"XML 파싱 오류: {xe.Message}"));
}
catch (Exception e)
{
return Task.FromResult(ToolResult.Fail($"XML 처리 오류: {e.Message}"));
}
}
private static ToolResult ParseSummary(XDocument doc)
{
var sb = new StringBuilder();
sb.AppendLine($"Root: {doc.Root?.Name.LocalName ?? "(none)"}");
if (doc.Root != null)
{
var ns = doc.Root.Name.Namespace;
if (!string.IsNullOrEmpty(ns.NamespaceName))
sb.AppendLine($"Namespace: {ns.NamespaceName}");
var elements = doc.Descendants().Count();
var attrs = doc.Descendants().SelectMany(e => e.Attributes()).Count();
sb.AppendLine($"Elements: {elements}");
sb.AppendLine($"Attributes: {attrs}");
// 최상위 자식 요소 나열 (최대 20개)
var children = doc.Root.Elements().Take(20).ToList();
sb.AppendLine($"Top-level children ({doc.Root.Elements().Count()}):");
foreach (var child in children)
sb.AppendLine($" <{child.Name.LocalName}> ({child.Elements().Count()} children)");
}
return ToolResult.Ok(sb.ToString());
}
private static ToolResult EvalXPath(XDocument doc, string xpath)
{
if (string.IsNullOrEmpty(xpath))
return ToolResult.Fail("XPath 'expression'이 필요합니다.");
var results = doc.XPathSelectElements(xpath).Take(50).ToList();
if (results.Count == 0)
return ToolResult.Ok("매칭 노드 없음.");
var sb = new StringBuilder();
sb.AppendLine($"매칭: {results.Count}개 노드");
foreach (var el in results)
{
var text = el.ToString();
if (text.Length > 500) text = text[..500] + "...";
sb.AppendLine(text);
}
return ToolResult.Ok(sb.ToString());
}
private static ToolResult XmlToJson(XDocument doc)
{
var json = System.Text.Json.JsonSerializer.Serialize(
XmlToDict(doc.Root!),
new JsonSerializerOptions { WriteIndented = true });
if (json.Length > 50_000) json = json[..50_000] + "\n... (truncated)";
return ToolResult.Ok(json);
}
private static Dictionary<string, object?> XmlToDict(XElement el)
{
var dict = new Dictionary<string, object?>();
foreach (var attr in el.Attributes())
dict[$"@{attr.Name.LocalName}"] = attr.Value;
var groups = el.Elements().GroupBy(e => e.Name.LocalName).ToList();
foreach (var g in groups)
{
var items = g.ToList();
if (items.Count == 1)
{
var child = items[0];
dict[g.Key] = child.HasElements ? XmlToDict(child) : (object?)child.Value;
}
else
{
dict[g.Key] = items.Select(c => c.HasElements ? (object)XmlToDict(c) : c.Value).ToList();
}
}
if (!el.HasElements && groups.Count == 0 && !string.IsNullOrEmpty(el.Value))
dict["#text"] = el.Value;
return dict;
}
private static ToolResult FormatXml(XDocument doc)
{
var sb = new StringBuilder();
using var writer = XmlWriter.Create(sb, new XmlWriterSettings
{
Indent = true,
IndentChars = " ",
OmitXmlDeclaration = false,
});
doc.WriteTo(writer);
writer.Flush();
var result = sb.ToString();
if (result.Length > 50_000) result = result[..50_000] + "\n... (truncated)";
return ToolResult.Ok(result);
}
}