using System.Text.Json; namespace AxCopilot.Services.Agent; /// /// JSON 파싱·변환·검증·포맷팅 도구. /// jq 스타일 경로 쿼리, 유효성 검사, 포맷 변환을 지원합니다. /// public class JsonTool : IAgentTool { public string Name => "json_tool"; public string Description => "JSON processing tool. Actions: " + "'validate' — check if text is valid JSON and report errors; " + "'format' — pretty-print or minify JSON; " + "'query' — extract value by dot-path (e.g. 'data.users[0].name'); " + "'keys' — list top-level keys; " + "'convert' — convert between JSON/CSV (flat arrays only)."; public ToolParameterSchema Parameters => new() { Properties = new() { ["action"] = new() { Type = "string", Description = "Action to perform", Enum = ["validate", "format", "query", "keys", "convert"], }, ["json"] = new() { Type = "string", Description = "JSON text to process", }, ["path"] = new() { Type = "string", Description = "Dot-path for query action (e.g. 'data.items[0].name')", }, ["minify"] = new() { Type = "string", Description = "For format action: 'true' to minify, 'false' to pretty-print (default)", }, ["target_format"] = new() { Type = "string", Description = "For convert action: target format", Enum = ["csv"], }, }, Required = ["action", "json"], }; public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var action = args.GetProperty("action").SafeGetString() ?? ""; var json = args.GetProperty("json").SafeGetString() ?? ""; try { return Task.FromResult(action switch { "validate" => Validate(json), "format" => Format(json, args.SafeTryGetProperty("minify", out var m) && m.SafeGetString() == "true"), "query" => Query(json, args.SafeTryGetProperty("path", out var p) ? p.SafeGetString() ?? "" : ""), "keys" => Keys(json), "convert" => Convert(json, args.SafeTryGetProperty("target_format", out var tf) ? tf.SafeGetString() ?? "csv" : "csv"), _ => ToolResult.Fail($"Unknown action: {action}"), }); } catch (Exception ex) { return Task.FromResult(ToolResult.Fail($"JSON 처리 오류: {ex.Message}")); } } private static ToolResult Validate(string json) { try { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; var kind = root.ValueKind switch { JsonValueKind.Object => $"Object ({root.EnumerateObject().Count()} keys)", JsonValueKind.Array => $"Array ({root.GetArrayLength()} items)", _ => root.ValueKind.ToString(), }; return ToolResult.Ok($"✓ Valid JSON — {kind}"); } catch (JsonException ex) { return ToolResult.Ok($"✗ Invalid JSON — {ex.Message}"); } } private static ToolResult Format(string json, bool minify) { using var doc = JsonDocument.Parse(json); var opts = new JsonSerializerOptions { WriteIndented = !minify }; var result = JsonSerializer.Serialize(doc.RootElement, opts); if (result.Length > 8000) result = result[..8000] + "\n... (truncated)"; return ToolResult.Ok(result); } private static ToolResult Query(string json, string path) { if (string.IsNullOrEmpty(path)) return ToolResult.Fail("path parameter is required for query action"); using var doc = JsonDocument.Parse(json); var current = doc.RootElement; foreach (var segment in ParsePath(path)) { if (segment.IsIndex) { if (current.ValueKind != JsonValueKind.Array || segment.Index >= current.GetArrayLength()) return ToolResult.Fail($"Array index [{segment.Index}] out of range"); current = current[segment.Index]; } else { if (current.ValueKind != JsonValueKind.Object || !current.SafeTryGetProperty(segment.Key, out var prop)) return ToolResult.Fail($"Key '{segment.Key}' not found"); current = prop; } } var value = current.ValueKind switch { JsonValueKind.String => current.SafeGetString() ?? "", JsonValueKind.Number => current.GetRawText(), JsonValueKind.True => "true", JsonValueKind.False => "false", JsonValueKind.Null => "null", _ => JsonSerializer.Serialize(current, new JsonSerializerOptions { WriteIndented = true }), }; if (value.Length > 5000) value = value[..5000] + "\n... (truncated)"; return ToolResult.Ok(value); } private static ToolResult Keys(string json) { using var doc = JsonDocument.Parse(json); if (doc.RootElement.ValueKind != JsonValueKind.Object) return ToolResult.Fail("Root element is not an object"); var keys = doc.RootElement.EnumerateObject().Select(p => { var type = p.Value.ValueKind switch { JsonValueKind.Object => "object", JsonValueKind.Array => $"array[{p.Value.GetArrayLength()}]", JsonValueKind.String => "string", JsonValueKind.Number => "number", JsonValueKind.True or JsonValueKind.False => "boolean", _ => "null", }; return $" {p.Name}: {type}"; }); return ToolResult.Ok($"Keys ({doc.RootElement.EnumerateObject().Count()}):\n{string.Join("\n", keys)}"); } private static ToolResult Convert(string json, string targetFormat) { if (targetFormat != "csv") return ToolResult.Fail($"Unsupported target format: {targetFormat}"); using var doc = JsonDocument.Parse(json); if (doc.RootElement.ValueKind != JsonValueKind.Array) return ToolResult.Fail("JSON must be an array for CSV conversion"); var arr = doc.RootElement; if (arr.GetArrayLength() == 0) return ToolResult.Ok("(empty array)"); // 모든 키 수집 var allKeys = new List(); foreach (var item in arr.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Object) return ToolResult.Fail("All array items must be objects for CSV conversion"); foreach (var prop in item.EnumerateObject()) if (!allKeys.Contains(prop.Name)) allKeys.Add(prop.Name); } var sb = new System.Text.StringBuilder(); sb.AppendLine(string.Join(",", allKeys.Select(k => $"\"{k}\""))); foreach (var item in arr.EnumerateArray()) { var values = allKeys.Select(k => { if (!item.SafeTryGetProperty(k, out var v)) return "\"\""; return v.ValueKind == JsonValueKind.String ? $"\"{v.SafeGetString()?.Replace("\"", "\"\"") ?? ""}\"" : v.GetRawText(); }); sb.AppendLine(string.Join(",", values)); } var result = sb.ToString(); if (result.Length > 8000) result = result[..8000] + "\n... (truncated)"; return ToolResult.Ok(result); } private record PathSegment(string Key, int Index, bool IsIndex); private static List ParsePath(string path) { var segments = new List(); foreach (var part in path.Split('.')) { var bracketIdx = part.IndexOf('['); if (bracketIdx >= 0) { var key = part[..bracketIdx]; if (!string.IsNullOrEmpty(key)) segments.Add(new PathSegment(key, 0, false)); var idxStr = part[(bracketIdx + 1)..].TrimEnd(']'); if (int.TryParse(idxStr, out var idx)) segments.Add(new PathSegment("", idx, true)); } else { segments.Add(new PathSegment(part, 0, false)); } } return segments; } }