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; /// XML 파싱, XPath 쿼리, 변환 도구. 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 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 XmlToDict(XElement el) { var dict = new Dictionary(); 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); } }