190 lines
6.8 KiB
C#
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);
|
|
}
|
|
}
|