317 lines
12 KiB
C#
317 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public class ProjectRuleTool : IAgentTool
|
|
{
|
|
public string Name => "project_rules";
|
|
|
|
public string Description => "프로젝트 개발 지침(AX.md) 및 규칙(.ax/rules/)을 관리합니다.\n- read: 현재 AX.md 내용을 읽습니다\n- append: 새 규칙/지침을 AX.md에 추가합니다 (사용자 승인 필요)\n- write: AX.md를 새 내용으로 덮어씁니다 (사용자 승인 필요)\n- list_rules: .ax/rules/ 디렉토리의 프로젝트 규칙 파일 목록을 조회합니다\n- read_rule: .ax/rules/ 디렉토리의 특정 규칙 파일을 읽습니다\n사용자가 '개발 지침에 추가해', '규칙을 저장해', 'AX.md에 기록해' 등을 요청하면 이 도구를 사용하세요.";
|
|
|
|
public ToolParameterSchema Parameters
|
|
{
|
|
get
|
|
{
|
|
ToolParameterSchema toolParameterSchema = new ToolParameterSchema();
|
|
Dictionary<string, ToolProperty> dictionary = new Dictionary<string, ToolProperty>();
|
|
ToolProperty obj = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "read (AX.md 읽기), append (추가), write (전체 덮어쓰기), list_rules (.ax/rules/ 목록), read_rule (규칙 파일 읽기)"
|
|
};
|
|
int num = 5;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "read";
|
|
span[1] = "append";
|
|
span[2] = "write";
|
|
span[3] = "list_rules";
|
|
span[4] = "read_rule";
|
|
obj.Enum = list;
|
|
dictionary["action"] = obj;
|
|
dictionary["rule_name"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "read_rule 시 읽을 규칙 파일 이름 (확장자 제외). 예: 'coding-conventions'"
|
|
};
|
|
dictionary["content"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "append/write 시 저장할 내용. 마크다운 형식을 권장합니다."
|
|
};
|
|
dictionary["section"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "append 시 섹션 제목. 예: '코딩 컨벤션', '빌드 규칙'. 비어있으면 파일 끝에 추가."
|
|
};
|
|
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))
|
|
{
|
|
JsonElement a;
|
|
string action = (args.TryGetProperty("action", out a) ? (a.GetString() ?? "") : "");
|
|
JsonElement c;
|
|
string content = (args.TryGetProperty("content", out c) ? (c.GetString() ?? "") : "");
|
|
JsonElement s;
|
|
string section = (args.TryGetProperty("section", out s) ? (s.GetString() ?? "") : "");
|
|
JsonElement rn;
|
|
string ruleName = (args.TryGetProperty("rule_name", out rn) ? (rn.GetString() ?? "") : "");
|
|
if (string.IsNullOrEmpty(context.WorkFolder))
|
|
{
|
|
return ToolResult.Fail("작업 폴더가 설정되어 있지 않습니다.");
|
|
}
|
|
string axMdPath = FindAxMd(context.WorkFolder) ?? Path.Combine(context.WorkFolder, "AX.md");
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
ToolResult result = action switch
|
|
{
|
|
"read" => ReadAxMd(axMdPath),
|
|
"append" => await AppendAxMdAsync(axMdPath, content, section, context),
|
|
"write" => await WriteAxMdAsync(axMdPath, content, context),
|
|
"list_rules" => ListRules(context.WorkFolder),
|
|
"read_rule" => ReadRule(context.WorkFolder, ruleName),
|
|
_ => ToolResult.Fail("지원하지 않는 action: " + action + ". read, append, write, list_rules, read_rule 중 선택하세요."),
|
|
};
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static ToolResult ReadAxMd(string path)
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
return ToolResult.Ok("AX.md 파일이 없습니다.\n경로: " + path + "\n\n새로 생성하려면 append 또는 write 액션을 사용하세요.");
|
|
}
|
|
try
|
|
{
|
|
string text = File.ReadAllText(path, Encoding.UTF8);
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return ToolResult.Ok("AX.md 파일이 비어 있습니다.");
|
|
}
|
|
return ToolResult.Ok($"[AX.md 내용 ({text.Length}자)]\n\n{text}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("AX.md 읽기 실패: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static async Task<ToolResult> AppendAxMdAsync(string path, string content, string section, AgentContext context)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
{
|
|
return ToolResult.Fail("추가할 content가 필요합니다.");
|
|
}
|
|
string desc = "AX.md에 개발 지침을 추가합니다:\n" + ((content.Length > 200) ? (content.Substring(0, 200) + "...") : content);
|
|
if (!(await context.CheckWritePermissionAsync("project_rules", desc)))
|
|
{
|
|
return ToolResult.Ok("사용자가 AX.md 수정을 거부했습니다.");
|
|
}
|
|
try
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
if (File.Exists(path))
|
|
{
|
|
sb.Append(File.ReadAllText(path, Encoding.UTF8));
|
|
}
|
|
if (sb.Length > 0 && !sb.ToString().EndsWith('\n'))
|
|
{
|
|
sb.AppendLine();
|
|
}
|
|
sb.AppendLine();
|
|
if (!string.IsNullOrEmpty(section))
|
|
{
|
|
StringBuilder stringBuilder = sb;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder);
|
|
handler.AppendLiteral("## ");
|
|
handler.AppendFormatted(section);
|
|
stringBuilder.AppendLine(ref handler);
|
|
}
|
|
sb.AppendLine(content.Trim());
|
|
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
|
return ToolResult.Ok($"AX.md에 개발 지침이 추가되었습니다.\n경로: {path}\n추가된 내용 ({content.Length}자):\n{content}", path);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("AX.md 쓰기 실패: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static async Task<ToolResult> WriteAxMdAsync(string path, string content, AgentContext context)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
{
|
|
return ToolResult.Fail("저장할 content가 필요합니다.");
|
|
}
|
|
string desc = $"AX.md를 전체 덮어씁니다 ({content.Length}자):\n{((content.Length > 200) ? (content.Substring(0, 200) + "...") : content)}";
|
|
if (!(await context.CheckWritePermissionAsync("project_rules", desc)))
|
|
{
|
|
return ToolResult.Ok("사용자가 AX.md 수정을 거부했습니다.");
|
|
}
|
|
try
|
|
{
|
|
File.WriteAllText(path, content, Encoding.UTF8);
|
|
return ToolResult.Ok($"AX.md가 저장되었습니다.\n경로: {path}\n내용 ({content.Length}자)", path);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("AX.md 쓰기 실패: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static ToolResult ListRules(string workFolder)
|
|
{
|
|
List<ProjectRulesService.ProjectRule> list = ProjectRulesService.LoadRules(workFolder);
|
|
if (list.Count == 0)
|
|
{
|
|
string text = ProjectRulesService.FindRulesDirectory(workFolder);
|
|
string text2 = text ?? Path.Combine(workFolder, ".ax", "rules");
|
|
return ToolResult.Ok("프로젝트 규칙이 없습니다.\n규칙 파일을 추가하려면 " + text2 + " 디렉토리에 .md 파일을 생성하세요.\n\n예시 규칙 파일 형식:\n---\nname: 코딩 컨벤션\ndescription: C# 코딩 규칙\napplies-to: \"*.cs\"\nwhen: always\n---\n\n규칙 내용...");
|
|
}
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(12, 1, stringBuilder2);
|
|
handler.AppendLiteral("[프로젝트 규칙 ");
|
|
handler.AppendFormatted(list.Count);
|
|
handler.AppendLiteral("개]\n");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
foreach (ProjectRulesService.ProjectRule item in list)
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2);
|
|
handler.AppendLiteral(" • ");
|
|
handler.AppendFormatted(item.Name);
|
|
stringBuilder4.AppendLine(ref handler);
|
|
if (!string.IsNullOrEmpty(item.Description))
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder5 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2);
|
|
handler.AppendLiteral(" 설명: ");
|
|
handler.AppendFormatted(item.Description);
|
|
stringBuilder5.AppendLine(ref handler);
|
|
}
|
|
if (!string.IsNullOrEmpty(item.AppliesTo))
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder6 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2);
|
|
handler.AppendLiteral(" 적용 대상: ");
|
|
handler.AppendFormatted(item.AppliesTo);
|
|
stringBuilder6.AppendLine(ref handler);
|
|
}
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder7 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2);
|
|
handler.AppendLiteral(" 적용 시점: ");
|
|
handler.AppendFormatted(item.When);
|
|
stringBuilder7.AppendLine(ref handler);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder8 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2);
|
|
handler.AppendLiteral(" 파일: ");
|
|
handler.AppendFormatted(item.FilePath);
|
|
stringBuilder8.AppendLine(ref handler);
|
|
stringBuilder.AppendLine();
|
|
}
|
|
return ToolResult.Ok(stringBuilder.ToString());
|
|
}
|
|
|
|
private static ToolResult ReadRule(string workFolder, string ruleName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ruleName))
|
|
{
|
|
return ToolResult.Fail("rule_name 파라미터가 필요합니다.");
|
|
}
|
|
List<ProjectRulesService.ProjectRule> source = ProjectRulesService.LoadRules(workFolder);
|
|
ProjectRulesService.ProjectRule projectRule = source.FirstOrDefault((ProjectRulesService.ProjectRule r) => r.Name.Equals(ruleName, StringComparison.OrdinalIgnoreCase) || Path.GetFileNameWithoutExtension(r.FilePath).Equals(ruleName, StringComparison.OrdinalIgnoreCase));
|
|
if (projectRule == null)
|
|
{
|
|
return ToolResult.Fail("규칙 '" + ruleName + "'을(를) 찾을 수 없습니다. list_rules로 목록을 확인하세요.");
|
|
}
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(6, 1, stringBuilder2);
|
|
handler.AppendLiteral("[규칙: ");
|
|
handler.AppendFormatted(projectRule.Name);
|
|
handler.AppendLiteral("]");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
if (!string.IsNullOrEmpty(projectRule.Description))
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2);
|
|
handler.AppendLiteral("설명: ");
|
|
handler.AppendFormatted(projectRule.Description);
|
|
stringBuilder4.AppendLine(ref handler);
|
|
}
|
|
if (!string.IsNullOrEmpty(projectRule.AppliesTo))
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder5 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder2);
|
|
handler.AppendLiteral("적용 대상: ");
|
|
handler.AppendFormatted(projectRule.AppliesTo);
|
|
stringBuilder5.AppendLine(ref handler);
|
|
}
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder6 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder2);
|
|
handler.AppendLiteral("적용 시점: ");
|
|
handler.AppendFormatted(projectRule.When);
|
|
stringBuilder6.AppendLine(ref handler);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder7 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2);
|
|
handler.AppendLiteral("파일: ");
|
|
handler.AppendFormatted(projectRule.FilePath);
|
|
stringBuilder7.AppendLine(ref handler);
|
|
stringBuilder.AppendLine();
|
|
stringBuilder.AppendLine(projectRule.Body);
|
|
return ToolResult.Ok(stringBuilder.ToString(), projectRule.FilePath);
|
|
}
|
|
|
|
private static string? FindAxMd(string workFolder)
|
|
{
|
|
string text = workFolder;
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
break;
|
|
}
|
|
string text2 = Path.Combine(text, "AX.md");
|
|
if (File.Exists(text2))
|
|
{
|
|
return text2;
|
|
}
|
|
text = Directory.GetParent(text)?.FullName;
|
|
}
|
|
return null;
|
|
}
|
|
}
|