Files

206 lines
6.7 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace AxCopilot.Services.Agent;
public class TemplateRenderTool : IAgentTool
{
public string Name => "template_render";
public string Description => "Render a template file with variable substitution and loops. Supports Mustache-style syntax: {{variable}}, {{#list}}...{{/list}} loops, {{^variable}}...{{/variable}} inverted sections (if empty/false). Useful for generating repetitive documents like emails, reports, invoices from templates.";
public ToolParameterSchema Parameters
{
get
{
ToolParameterSchema obj = new ToolParameterSchema
{
Properties = new Dictionary<string, ToolProperty>
{
["template_path"] = new ToolProperty
{
Type = "string",
Description = "Path to template file (.html, .md, .txt). Relative to work folder."
},
["template_text"] = new ToolProperty
{
Type = "string",
Description = "Inline template text (used if template_path is not provided)."
},
["variables"] = new ToolProperty
{
Type = "object",
Description = "Key-value pairs for substitution. Values can be strings, numbers, or arrays of objects for loops. Example: {\"name\": \"홍길동\", \"items\": [{\"product\": \"A\", \"qty\": 10}]}"
},
["output_path"] = new ToolProperty
{
Type = "string",
Description = "Output file path. If not provided, returns rendered text."
}
}
};
int num = 1;
List<string> list = new List<string>(num);
CollectionsMarshal.SetCount(list, num);
CollectionsMarshal.AsSpan(list)[0] = "variables";
obj.Required = list;
return obj;
}
}
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
{
string template;
if (args.TryGetProperty("template_path", out var tpEl) && !string.IsNullOrEmpty(tpEl.GetString()))
{
string templatePath = FileReadTool.ResolvePath(tpEl.GetString(), context.WorkFolder);
if (!context.IsPathAllowed(templatePath))
{
return ToolResult.Fail("경로 접근 차단: " + templatePath);
}
if (!File.Exists(templatePath))
{
return ToolResult.Fail("템플릿 파일 없음: " + templatePath);
}
template = await File.ReadAllTextAsync(templatePath, ct);
}
else
{
if (!args.TryGetProperty("template_text", out var ttEl) || string.IsNullOrEmpty(ttEl.GetString()))
{
return ToolResult.Fail("template_path 또는 template_text가 필요합니다.");
}
template = ttEl.GetString();
}
if (!args.TryGetProperty("variables", out var varsEl))
{
return ToolResult.Fail("variables가 필요합니다.");
}
try
{
string rendered = Render(template, varsEl);
if (args.TryGetProperty("output_path", out var opEl) && !string.IsNullOrEmpty(opEl.GetString()))
{
string outputPath = FileReadTool.ResolvePath(opEl.GetString(), context.WorkFolder);
if (context.ActiveTab == "Cowork")
{
outputPath = AgentContext.EnsureTimestampedPath(outputPath);
}
if (!context.IsPathAllowed(outputPath))
{
return ToolResult.Fail("경로 접근 차단: " + outputPath);
}
if (!(await context.CheckWritePermissionAsync(Name, outputPath)))
{
return ToolResult.Fail("쓰기 권한 거부: " + outputPath);
}
string dir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
await File.WriteAllTextAsync(outputPath, rendered, Encoding.UTF8, ct);
return ToolResult.Ok($"✅ 템플릿 렌더링 완료: {Path.GetFileName(outputPath)} ({rendered.Length:N0}자)", outputPath);
}
if (rendered.Length > 4000)
{
return ToolResult.Ok($"✅ 렌더링 완료 ({rendered.Length:N0}자):\n{rendered.Substring(0, 3900)}...\n[이하 생략]");
}
return ToolResult.Ok($"✅ 렌더링 완료 ({rendered.Length:N0}자):\n{rendered}");
}
catch (Exception ex)
{
return ToolResult.Fail("템플릿 렌더링 실패: " + ex.Message);
}
}
internal static string Render(string template, JsonElement variables)
{
string input = template;
input = Regex.Replace(input, "\\{\\{#(\\w+)\\}\\}(.*?)\\{\\{/\\1\\}\\}", delegate(Match match)
{
string value = match.Groups[1].Value;
string value2 = match.Groups[2].Value;
if (!variables.TryGetProperty(value, out var value3))
{
return "";
}
if (value3.ValueKind == JsonValueKind.Array)
{
StringBuilder stringBuilder = new StringBuilder();
int num = 0;
foreach (JsonElement item in value3.EnumerateArray())
{
string text = value2;
if (item.ValueKind == JsonValueKind.Object)
{
foreach (JsonProperty item2 in item.EnumerateObject())
{
text = text.Replace("{{" + item2.Name + "}}", item2.Value.ToString()).Replace("{{." + item2.Name + "}}", item2.Value.ToString());
}
}
else
{
text = text.Replace("{{.}}", item.ToString());
}
text = text.Replace("{{@index}}", (num + 1).ToString());
stringBuilder.Append(text);
num++;
}
return stringBuilder.ToString();
}
if (value3.ValueKind == JsonValueKind.True)
{
return RenderSimpleVars(value2, variables);
}
return (value3.ValueKind != JsonValueKind.False && value3.ValueKind != JsonValueKind.Null && value3.ValueKind != JsonValueKind.Undefined) ? RenderSimpleVars(value2, variables) : "";
}, RegexOptions.Singleline);
input = Regex.Replace(input, "\\{\\{\\^(\\w+)\\}\\}(.*?)\\{\\{/\\1\\}\\}", delegate(Match match)
{
string value = match.Groups[1].Value;
string value2 = match.Groups[2].Value;
if (!variables.TryGetProperty(value, out var value3))
{
return value2;
}
return (value3.ValueKind == JsonValueKind.False || value3.ValueKind == JsonValueKind.Null || (value3.ValueKind == JsonValueKind.Array && value3.GetArrayLength() == 0) || (value3.ValueKind == JsonValueKind.String && string.IsNullOrEmpty(value3.GetString()))) ? value2 : "";
}, RegexOptions.Singleline);
return RenderSimpleVars(input, variables);
}
private static string RenderSimpleVars(string text, JsonElement variables)
{
return Regex.Replace(text, "\\{\\{(\\w+)\\}\\}", delegate(Match match)
{
string value = match.Groups[1].Value;
if (!variables.TryGetProperty(value, out var value2))
{
return match.Value;
}
JsonValueKind valueKind = value2.ValueKind;
if (1 == 0)
{
}
string result = valueKind switch
{
JsonValueKind.String => value2.GetString() ?? "",
JsonValueKind.Number => value2.ToString(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
_ => value2.ToString(),
};
if (1 == 0)
{
}
return result;
});
}
}