420 lines
13 KiB
C#
420 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public class PlaybookTool : IAgentTool
|
|
{
|
|
private class PlaybookData
|
|
{
|
|
public int Id { get; set; }
|
|
|
|
public string Name { get; set; } = "";
|
|
|
|
public string Description { get; set; } = "";
|
|
|
|
public List<string> Steps { get; set; } = new List<string>();
|
|
|
|
public List<string> ToolsUsed { get; set; } = new List<string>();
|
|
|
|
public string CreatedAt { get; set; } = "";
|
|
}
|
|
|
|
public string Name => "playbook";
|
|
|
|
public string Description => "Save, list, describe, or delete execution playbooks. A playbook captures a successful task workflow for reuse.\n- action=\"save\": Save a new playbook (name, description, steps required)\n- action=\"list\": List all saved playbooks\n- action=\"describe\": Show full playbook details (id required)\n- action=\"delete\": Delete a playbook (id required)";
|
|
|
|
public ToolParameterSchema Parameters
|
|
{
|
|
get
|
|
{
|
|
ToolParameterSchema toolParameterSchema = new ToolParameterSchema();
|
|
Dictionary<string, ToolProperty> dictionary = new Dictionary<string, ToolProperty>();
|
|
ToolProperty obj = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "save | list | describe | delete"
|
|
};
|
|
int num = 4;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "save";
|
|
span[1] = "list";
|
|
span[2] = "describe";
|
|
span[3] = "delete";
|
|
obj.Enum = list;
|
|
dictionary["action"] = obj;
|
|
dictionary["name"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Playbook name (for save)"
|
|
};
|
|
dictionary["description"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "What this playbook does (for save)"
|
|
};
|
|
dictionary["steps"] = new ToolProperty
|
|
{
|
|
Type = "array",
|
|
Description = "List of step descriptions (for save)",
|
|
Items = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Step description"
|
|
}
|
|
};
|
|
dictionary["tools_used"] = new ToolProperty
|
|
{
|
|
Type = "array",
|
|
Description = "List of tool names used in this workflow (for save)",
|
|
Items = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Tool name"
|
|
}
|
|
};
|
|
dictionary["id"] = new ToolProperty
|
|
{
|
|
Type = "integer",
|
|
Description = "Playbook ID (for describe/delete)"
|
|
};
|
|
toolParameterSchema.Properties = dictionary;
|
|
num = 1;
|
|
List<string> list2 = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list2, num);
|
|
CollectionsMarshal.AsSpan(list2)[0] = "action";
|
|
toolParameterSchema.Required = list2;
|
|
return toolParameterSchema;
|
|
}
|
|
}
|
|
|
|
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken))
|
|
{
|
|
if (!args.TryGetProperty("action", out var actionEl))
|
|
{
|
|
return ToolResult.Fail("action이 필요합니다.");
|
|
}
|
|
string action = actionEl.GetString() ?? "";
|
|
if (string.IsNullOrEmpty(context.WorkFolder))
|
|
{
|
|
return ToolResult.Fail("작업 폴더가 설정되지 않았습니다.");
|
|
}
|
|
string playbookDir = Path.Combine(context.WorkFolder, ".ax", "playbooks");
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
ToolResult result = action switch
|
|
{
|
|
"save" => await SavePlaybook(args, playbookDir, ct),
|
|
"list" => ListPlaybooks(playbookDir),
|
|
"describe" => await DescribePlaybook(args, playbookDir, ct),
|
|
"delete" => DeletePlaybook(args, playbookDir),
|
|
_ => ToolResult.Fail("알 수 없는 액션: " + action + ". save | list | describe | delete 중 선택하세요."),
|
|
};
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static async Task<ToolResult> SavePlaybook(JsonElement args, string playbookDir, CancellationToken ct)
|
|
{
|
|
JsonElement n;
|
|
string name = (args.TryGetProperty("name", out n) ? (n.GetString() ?? "") : "");
|
|
JsonElement d;
|
|
string description = (args.TryGetProperty("description", out d) ? (d.GetString() ?? "") : "");
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
return ToolResult.Fail("플레이북 name이 필요합니다.");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(description))
|
|
{
|
|
return ToolResult.Fail("플레이북 description이 필요합니다.");
|
|
}
|
|
List<string> steps = new List<string>();
|
|
if (args.TryGetProperty("steps", out var stepsEl) && stepsEl.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (JsonElement item in stepsEl.EnumerateArray())
|
|
{
|
|
string s = item.GetString();
|
|
if (!string.IsNullOrWhiteSpace(s))
|
|
{
|
|
steps.Add(s);
|
|
}
|
|
}
|
|
}
|
|
if (steps.Count == 0)
|
|
{
|
|
return ToolResult.Fail("최소 1개 이상의 step이 필요합니다.");
|
|
}
|
|
List<string> toolsUsed = new List<string>();
|
|
if (args.TryGetProperty("tools_used", out var toolsEl) && toolsEl.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (JsonElement item2 in toolsEl.EnumerateArray())
|
|
{
|
|
string t = item2.GetString();
|
|
if (!string.IsNullOrWhiteSpace(t))
|
|
{
|
|
toolsUsed.Add(t);
|
|
}
|
|
}
|
|
}
|
|
try
|
|
{
|
|
Directory.CreateDirectory(playbookDir);
|
|
int nextId = GetNextId(playbookDir);
|
|
string safeName = string.Join("_", name.Split(Path.GetInvalidFileNameChars()));
|
|
string fileName = $"{nextId}_{safeName}.json";
|
|
PlaybookData playbook = new PlaybookData
|
|
{
|
|
Id = nextId,
|
|
Name = name,
|
|
Description = description,
|
|
Steps = steps,
|
|
ToolsUsed = toolsUsed,
|
|
CreatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
|
};
|
|
string json = JsonSerializer.Serialize(playbook, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
});
|
|
string filePath = Path.Combine(playbookDir, fileName);
|
|
await File.WriteAllTextAsync(filePath, json, ct);
|
|
return ToolResult.Ok($"플레이북 저장 완료: [{nextId}] {name}\n단계 수: {steps.Count}, 사용 도구: {toolsUsed.Count}개");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("플레이북 저장 오류: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static ToolResult ListPlaybooks(string playbookDir)
|
|
{
|
|
if (!Directory.Exists(playbookDir))
|
|
{
|
|
return ToolResult.Ok("저장된 플레이북이 없습니다.");
|
|
}
|
|
List<string> list = (from f in Directory.GetFiles(playbookDir, "*.json")
|
|
orderby f
|
|
select f).ToList();
|
|
if (list.Count == 0)
|
|
{
|
|
return ToolResult.Ok("저장된 플레이북이 없습니다.");
|
|
}
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder2);
|
|
handler.AppendLiteral("플레이북 ");
|
|
handler.AppendFormatted(list.Count);
|
|
handler.AppendLiteral("개:");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
foreach (string item in list)
|
|
{
|
|
try
|
|
{
|
|
string json = File.ReadAllText(item);
|
|
PlaybookData playbookData = JsonSerializer.Deserialize<PlaybookData>(json);
|
|
if (playbookData != null)
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(15, 5, stringBuilder2);
|
|
handler.AppendLiteral(" [");
|
|
handler.AppendFormatted(playbookData.Id);
|
|
handler.AppendLiteral("] ");
|
|
handler.AppendFormatted(playbookData.Name);
|
|
handler.AppendLiteral(" — ");
|
|
handler.AppendFormatted(playbookData.Description);
|
|
handler.AppendLiteral(" (");
|
|
handler.AppendFormatted(playbookData.Steps.Count);
|
|
handler.AppendLiteral("단계, ");
|
|
handler.AppendFormatted(playbookData.CreatedAt);
|
|
handler.AppendLiteral(")");
|
|
stringBuilder4.AppendLine(ref handler);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder5 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder2);
|
|
handler.AppendLiteral(" [?] ");
|
|
handler.AppendFormatted(Path.GetFileName(item));
|
|
handler.AppendLiteral(" — 파싱 오류");
|
|
stringBuilder5.AppendLine(ref handler);
|
|
}
|
|
}
|
|
return ToolResult.Ok(stringBuilder.ToString());
|
|
}
|
|
|
|
private static async Task<ToolResult> DescribePlaybook(JsonElement args, string playbookDir, CancellationToken ct)
|
|
{
|
|
if (!args.TryGetProperty("id", out var idEl))
|
|
{
|
|
return ToolResult.Fail("플레이북 id가 필요합니다.");
|
|
}
|
|
int parsed;
|
|
int id = ((idEl.ValueKind == JsonValueKind.Number) ? idEl.GetInt32() : (int.TryParse(idEl.GetString(), out parsed) ? parsed : (-1)));
|
|
PlaybookData playbook = await FindPlaybookById(playbookDir, id, ct);
|
|
if (playbook == null)
|
|
{
|
|
return ToolResult.Fail($"ID {id}의 플레이북을 찾을 수 없습니다.");
|
|
}
|
|
StringBuilder sb = new StringBuilder();
|
|
StringBuilder stringBuilder = sb;
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 2, stringBuilder);
|
|
handler.AppendLiteral("플레이북: ");
|
|
handler.AppendFormatted(playbook.Name);
|
|
handler.AppendLiteral(" (ID: ");
|
|
handler.AppendFormatted(playbook.Id);
|
|
handler.AppendLiteral(")");
|
|
stringBuilder2.AppendLine(ref handler);
|
|
stringBuilder = sb;
|
|
StringBuilder stringBuilder3 = stringBuilder;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder);
|
|
handler.AppendLiteral("설명: ");
|
|
handler.AppendFormatted(playbook.Description);
|
|
stringBuilder3.AppendLine(ref handler);
|
|
stringBuilder = sb;
|
|
StringBuilder stringBuilder4 = stringBuilder;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder);
|
|
handler.AppendLiteral("생성일: ");
|
|
handler.AppendFormatted(playbook.CreatedAt);
|
|
stringBuilder4.AppendLine(ref handler);
|
|
sb.AppendLine();
|
|
sb.AppendLine("단계:");
|
|
for (int i = 0; i < playbook.Steps.Count; i++)
|
|
{
|
|
stringBuilder = sb;
|
|
StringBuilder stringBuilder5 = stringBuilder;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder);
|
|
handler.AppendLiteral(" ");
|
|
handler.AppendFormatted(i + 1);
|
|
handler.AppendLiteral(". ");
|
|
handler.AppendFormatted(playbook.Steps[i]);
|
|
stringBuilder5.AppendLine(ref handler);
|
|
}
|
|
if (playbook.ToolsUsed.Count > 0)
|
|
{
|
|
sb.AppendLine();
|
|
stringBuilder = sb;
|
|
StringBuilder stringBuilder6 = stringBuilder;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder);
|
|
handler.AppendLiteral("사용 도구: ");
|
|
handler.AppendFormatted(string.Join(", ", playbook.ToolsUsed));
|
|
stringBuilder6.AppendLine(ref handler);
|
|
}
|
|
return ToolResult.Ok(sb.ToString());
|
|
}
|
|
|
|
private static ToolResult DeletePlaybook(JsonElement args, string playbookDir)
|
|
{
|
|
if (!args.TryGetProperty("id", out var value))
|
|
{
|
|
return ToolResult.Fail("삭제할 플레이북 id가 필요합니다.");
|
|
}
|
|
int result;
|
|
int num = ((value.ValueKind == JsonValueKind.Number) ? value.GetInt32() : (int.TryParse(value.GetString(), out result) ? result : (-1)));
|
|
if (!Directory.Exists(playbookDir))
|
|
{
|
|
return ToolResult.Fail("저장된 플레이북이 없습니다.");
|
|
}
|
|
string[] files = Directory.GetFiles(playbookDir, $"{num}_*.json");
|
|
if (files.Length == 0)
|
|
{
|
|
string[] files2 = Directory.GetFiles(playbookDir, "*.json");
|
|
foreach (string path in files2)
|
|
{
|
|
try
|
|
{
|
|
string json = File.ReadAllText(path);
|
|
PlaybookData playbookData = JsonSerializer.Deserialize<PlaybookData>(json);
|
|
if (playbookData != null && playbookData.Id == num)
|
|
{
|
|
string name = playbookData.Name;
|
|
File.Delete(path);
|
|
return ToolResult.Ok($"플레이북 삭제됨: [{num}] {name}");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
return ToolResult.Fail($"ID {num}의 플레이북을 찾을 수 없습니다.");
|
|
}
|
|
try
|
|
{
|
|
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(files[0]);
|
|
File.Delete(files[0]);
|
|
return ToolResult.Ok("플레이북 삭제됨: " + fileNameWithoutExtension);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("플레이북 삭제 오류: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static int GetNextId(string playbookDir)
|
|
{
|
|
if (!Directory.Exists(playbookDir))
|
|
{
|
|
return 1;
|
|
}
|
|
int num = 0;
|
|
string[] files = Directory.GetFiles(playbookDir, "*.json");
|
|
foreach (string path in files)
|
|
{
|
|
try
|
|
{
|
|
string json = File.ReadAllText(path);
|
|
PlaybookData playbookData = JsonSerializer.Deserialize<PlaybookData>(json);
|
|
if (playbookData != null && playbookData.Id > num)
|
|
{
|
|
num = playbookData.Id;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
return num + 1;
|
|
}
|
|
|
|
private static async Task<PlaybookData?> FindPlaybookById(string playbookDir, int id, CancellationToken ct)
|
|
{
|
|
if (!Directory.Exists(playbookDir))
|
|
{
|
|
return null;
|
|
}
|
|
string[] files = Directory.GetFiles(playbookDir, "*.json");
|
|
foreach (string file in files)
|
|
{
|
|
try
|
|
{
|
|
PlaybookData pb = JsonSerializer.Deserialize<PlaybookData>(await File.ReadAllTextAsync(file, ct));
|
|
if (pb != null && pb.Id == id)
|
|
{
|
|
return pb;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|