using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class CsvSkill : IAgentTool { public string Name => "csv_create"; public string Description => "Create a CSV (.csv) file with structured data. Provide headers and rows as JSON arrays."; public ToolParameterSchema Parameters { get { ToolParameterSchema obj = new ToolParameterSchema { Properties = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "Output file path (.csv). Relative to work folder." }, ["headers"] = new ToolProperty { Type = "array", Description = "Column headers as JSON array of strings.", Items = new ToolProperty { Type = "string" } }, ["rows"] = new ToolProperty { Type = "array", Description = "Data rows as JSON array of arrays.", Items = new ToolProperty { Type = "array", Items = new ToolProperty { Type = "string" } } }, ["encoding"] = new ToolProperty { Type = "string", Description = "File encoding: 'utf-8' (default) or 'euc-kr'." } } }; int num = 3; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "path"; span[1] = "headers"; span[2] = "rows"; obj.Required = list; return obj; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { string path = args.GetProperty("path").GetString() ?? ""; JsonElement enc; string encodingName = (args.TryGetProperty("encoding", out enc) ? (enc.GetString() ?? "utf-8") : "utf-8"); string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") { fullPath = AgentContext.EnsureTimestampedPath(fullPath); } if (!fullPath.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) { fullPath += ".csv"; } if (!context.IsPathAllowed(fullPath)) { return ToolResult.Fail("경로 접근 차단: " + fullPath); } if (!(await context.CheckWritePermissionAsync(Name, fullPath))) { return ToolResult.Fail("쓰기 권한 거부: " + fullPath); } try { JsonElement headers = args.GetProperty("headers"); JsonElement rows = args.GetProperty("rows"); string dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) { Directory.CreateDirectory(dir); } Encoding fileEncoding; try { fileEncoding = Encoding.GetEncoding(encodingName); } catch { fileEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); } StringBuilder sb = new StringBuilder(); List headerValues = new List(); foreach (JsonElement item in headers.EnumerateArray()) { headerValues.Add(EscapeCsvField(item.GetString() ?? "")); } sb.AppendLine(string.Join(",", headerValues)); int rowCount = 0; foreach (JsonElement row in rows.EnumerateArray()) { List fields = new List(); foreach (JsonElement item2 in row.EnumerateArray()) { fields.Add(EscapeCsvField(item2.ToString())); } sb.AppendLine(string.Join(",", fields)); rowCount++; } await File.WriteAllTextAsync(fullPath, sb.ToString(), fileEncoding, ct); return ToolResult.Ok($"CSV 파일 생성 완료: {fullPath}\n열: {headerValues.Count}, 행: {rowCount}, 인코딩: {encodingName}", fullPath); } catch (Exception ex) { return ToolResult.Fail("CSV 생성 실패: " + ex.Message); } } private static string EscapeCsvField(string field) { if (field.Contains(',') || field.Contains('"') || field.Contains('\n') || field.Contains('\r')) { return "\"" + field.Replace("\"", "\"\"") + "\""; } return field; } }