206 lines
6.7 KiB
C#
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;
|
|
});
|
|
}
|
|
}
|