Initial commit to new repository
This commit is contained in:
286
src/AxCopilot/Services/Agent/IAgentTool.cs
Normal file
286
src/AxCopilot/Services/Agent/IAgentTool.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트 도구의 공통 인터페이스.
|
||||
/// LLM function calling을 통해 호출되며, JSON 파라미터를 받아 결과를 반환합니다.
|
||||
/// </summary>
|
||||
public interface IAgentTool
|
||||
{
|
||||
/// <summary>LLM에 노출되는 도구 이름 (snake_case). 예: "file_read"</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>LLM에 전달되는 도구 설명.</summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>LLM function calling용 파라미터 JSON Schema.</summary>
|
||||
ToolParameterSchema Parameters { get; }
|
||||
|
||||
/// <summary>도구를 실행하고 결과를 반환합니다.</summary>
|
||||
/// <param name="args">LLM이 생성한 JSON 파라미터</param>
|
||||
/// <param name="context">실행 컨텍스트 (작업 폴더, 권한 등)</param>
|
||||
/// <param name="ct">취소 토큰</param>
|
||||
Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>도구 실행 결과.</summary>
|
||||
public class ToolResult
|
||||
{
|
||||
/// <summary>성공 여부.</summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>결과 텍스트 (LLM에 피드백).</summary>
|
||||
public string Output { get; init; } = "";
|
||||
|
||||
/// <summary>생성/수정된 파일 경로 (UI 표시용).</summary>
|
||||
public string? FilePath { get; init; }
|
||||
|
||||
/// <summary>오류 메시지 (실패 시).</summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
public static ToolResult Ok(string output, string? filePath = null) =>
|
||||
new() { Success = true, Output = output, FilePath = filePath };
|
||||
|
||||
public static ToolResult Fail(string error) =>
|
||||
new() { Success = false, Output = error, Error = error };
|
||||
}
|
||||
|
||||
/// <summary>도구 파라미터 JSON Schema (LLM function calling용).</summary>
|
||||
public class ToolParameterSchema
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = "object";
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public Dictionary<string, ToolProperty> Properties { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("required")]
|
||||
public List<string> Required { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>파라미터 속성 정의.</summary>
|
||||
public class ToolProperty
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = "string";
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; init; } = "";
|
||||
|
||||
[JsonPropertyName("enum")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public List<string>? Enum { get; init; }
|
||||
|
||||
/// <summary>array 타입일 때 항목 스키마. Gemini API 필수.</summary>
|
||||
[JsonPropertyName("items")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ToolProperty? Items { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>에이전트 실행 컨텍스트.</summary>
|
||||
public class AgentContext
|
||||
{
|
||||
private static readonly HashSet<string> SensitiveTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_write", "file_edit", "file_manage",
|
||||
"html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create",
|
||||
"chart_create", "script_create", "document_assemble", "format_convert", "template_render", "checkpoint",
|
||||
"process", "build_run", "git_tool", "http_tool", "open_external", "snippet_runner",
|
||||
"spawn_agent", "test_loop",
|
||||
};
|
||||
|
||||
private readonly object _permissionLock = new();
|
||||
private readonly HashSet<string> _approvedPermissionCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>작업 폴더 경로.</summary>
|
||||
public string WorkFolder { get; init; } = "";
|
||||
|
||||
/// <summary>파일 접근 권한. Ask | Auto | Deny</summary>
|
||||
public string Permission { get; init; } = "Ask";
|
||||
|
||||
/// <summary>도구별 권한 오버라이드. 키: 도구명, 값: "ask" | "auto" | "deny".</summary>
|
||||
public Dictionary<string, string> ToolPermissions { get; init; } = new();
|
||||
|
||||
/// <summary>차단 경로 패턴 목록.</summary>
|
||||
public List<string> BlockedPaths { get; init; } = new();
|
||||
|
||||
/// <summary>차단 확장자 목록.</summary>
|
||||
public List<string> BlockedExtensions { get; init; } = new();
|
||||
|
||||
/// <summary>현재 활성 탭. "Chat" | "Cowork" | "Code".</summary>
|
||||
public string ActiveTab { get; init; } = "Chat";
|
||||
|
||||
/// <summary>운영 모드. internal(사내) | external(사외).</summary>
|
||||
public string OperationMode { get; init; } = AxCopilot.Services.OperationModePolicy.InternalMode;
|
||||
|
||||
/// <summary>개발자 모드: 상세 이력 표시.</summary>
|
||||
public bool DevMode { get; init; }
|
||||
|
||||
/// <summary>개발자 모드: 도구 실행 전 매번 사용자 승인 대기.</summary>
|
||||
public bool DevModeStepApproval { get; init; }
|
||||
|
||||
/// <summary>권한 확인 콜백 (Ask 모드). 반환값: true=승인, false=거부.</summary>
|
||||
public Func<string, string, Task<bool>>? AskPermission { get; init; }
|
||||
|
||||
/// <summary>사용자 의사결정 콜백. (질문, 선택지) → 사용자 응답 문자열.</summary>
|
||||
public Func<string, List<string>, Task<string?>>? UserDecision { get; init; }
|
||||
|
||||
/// <summary>에이전트 질문 콜백 (UserAskTool 전용). (질문, 선택지, 기본값) → 사용자 응답.</summary>
|
||||
public Func<string, List<string>, string, Task<string?>>? UserAskCallback { get; init; }
|
||||
|
||||
/// <summary>경로가 허용되는지 확인합니다.</summary>
|
||||
public bool IsPathAllowed(string path)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
|
||||
// 차단 확장자 검사
|
||||
var ext = Path.GetExtension(fullPath).ToLowerInvariant();
|
||||
if (BlockedExtensions.Any(e => string.Equals(e, ext, StringComparison.OrdinalIgnoreCase)))
|
||||
return false;
|
||||
|
||||
// 차단 경로 패턴 검사
|
||||
foreach (var pattern in BlockedPaths)
|
||||
{
|
||||
// 간단한 와일드카드 매칭: *\Windows\* → fullPath에 \Windows\ 포함 시 차단
|
||||
var clean = pattern.Replace("*", "");
|
||||
if (!string.IsNullOrEmpty(clean) && fullPath.Contains(clean, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
// 작업 폴더 제한: 작업 폴더가 설정되어 있으면 하위 경로만 허용
|
||||
if (!string.IsNullOrEmpty(WorkFolder))
|
||||
{
|
||||
var workFull = Path.GetFullPath(WorkFolder);
|
||||
if (!fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파일 경로에 타임스탬프를 추가합니다.
|
||||
/// 예: report.html → report_20260328_1430.html
|
||||
/// 동일 이름 파일이 이미 존재하면 자동으로 타임스탬프를 붙입니다.
|
||||
/// </summary>
|
||||
public static string EnsureTimestampedPath(string fullPath)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(fullPath) ?? "";
|
||||
var name = Path.GetFileNameWithoutExtension(fullPath);
|
||||
var ext = Path.GetExtension(fullPath);
|
||||
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmm");
|
||||
|
||||
// 이미 타임스탬프가 포함된 파일명이면 그대로 사용
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(name, @"_\d{8}_\d{4}$"))
|
||||
return fullPath;
|
||||
|
||||
var timestamped = Path.Combine(dir, $"{name}_{stamp}{ext}");
|
||||
return timestamped;
|
||||
}
|
||||
|
||||
/// <summary>파일 쓰기/수정 권한을 확인합니다. 도구별 권한 오버라이드를 우선 적용합니다.</summary>
|
||||
public async Task<bool> CheckWritePermissionAsync(string toolName, string filePath)
|
||||
{
|
||||
return await CheckToolPermissionAsync(toolName, filePath);
|
||||
}
|
||||
|
||||
public string GetEffectiveToolPermission(string toolName)
|
||||
{
|
||||
if (ToolPermissions.TryGetValue(toolName, out var toolPerm) &&
|
||||
!string.IsNullOrWhiteSpace(toolPerm))
|
||||
return toolPerm;
|
||||
if (ToolPermissions.TryGetValue("*", out var wildcardPerm) &&
|
||||
!string.IsNullOrWhiteSpace(wildcardPerm))
|
||||
return wildcardPerm;
|
||||
if (ToolPermissions.TryGetValue("default", out var defaultPerm) &&
|
||||
!string.IsNullOrWhiteSpace(defaultPerm))
|
||||
return defaultPerm;
|
||||
|
||||
return SensitiveTools.Contains(toolName) ? Permission : "Auto";
|
||||
}
|
||||
|
||||
public async Task<bool> CheckToolPermissionAsync(string toolName, string target)
|
||||
{
|
||||
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode)
|
||||
&& AxCopilot.Services.OperationModePolicy.IsBlockedAgentToolInInternalMode(toolName, target))
|
||||
return false;
|
||||
|
||||
var effectivePerm = GetEffectiveToolPermission(toolName);
|
||||
if (string.Equals(effectivePerm, "Deny", StringComparison.OrdinalIgnoreCase)) return false;
|
||||
if (string.Equals(effectivePerm, "Auto", StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (AskPermission == null) return false;
|
||||
|
||||
var normalizedTarget = string.IsNullOrWhiteSpace(target) ? toolName : target.Trim();
|
||||
var cacheKey = $"{toolName}|{normalizedTarget}";
|
||||
lock (_permissionLock)
|
||||
{
|
||||
if (_approvedPermissionCache.Contains(cacheKey))
|
||||
return true;
|
||||
}
|
||||
|
||||
var allowed = await AskPermission(toolName, normalizedTarget);
|
||||
if (allowed)
|
||||
{
|
||||
lock (_permissionLock)
|
||||
_approvedPermissionCache.Add(cacheKey);
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>에이전트 이벤트 (UI 표시용).</summary>
|
||||
public class AgentEvent
|
||||
{
|
||||
public DateTime Timestamp { get; init; } = DateTime.Now;
|
||||
public string RunId { get; init; } = "";
|
||||
public AgentEventType Type { get; init; }
|
||||
public string ToolName { get; init; } = "";
|
||||
public string Summary { get; init; } = "";
|
||||
public string? FilePath { get; init; }
|
||||
public bool Success { get; init; } = true;
|
||||
|
||||
/// <summary>Task Decomposition: 현재 단계 / 전체 단계 (진행률 표시용).</summary>
|
||||
public int StepCurrent { get; init; }
|
||||
public int StepTotal { get; init; }
|
||||
/// <summary>Task Decomposition: 단계 목록.</summary>
|
||||
public List<string>? Steps { get; init; }
|
||||
|
||||
// ── 워크플로우 분석기용 확장 필드 ──
|
||||
|
||||
/// <summary>도구 실행 소요 시간 (ms). 0이면 미측정.</summary>
|
||||
public long ElapsedMs { get; init; }
|
||||
|
||||
/// <summary>이번 LLM 호출의 입력 토큰 수.</summary>
|
||||
public int InputTokens { get; init; }
|
||||
|
||||
/// <summary>이번 LLM 호출의 출력 토큰 수.</summary>
|
||||
public int OutputTokens { get; init; }
|
||||
|
||||
/// <summary>도구 파라미터 JSON (debug 모드에서만 기록).</summary>
|
||||
public string? ToolInput { get; init; }
|
||||
|
||||
/// <summary>현재 에이전트 루프 반복 번호.</summary>
|
||||
public int Iteration { get; init; }
|
||||
}
|
||||
|
||||
public enum AgentEventType
|
||||
{
|
||||
Thinking, // LLM 사고 중
|
||||
Planning, // 작업 계획 수립
|
||||
StepStart, // 단계 시작
|
||||
StepDone, // 단계 완료
|
||||
HookResult, // 훅 실행 결과
|
||||
PermissionRequest, // 권한 승인 대기
|
||||
PermissionGranted, // 권한 승인됨
|
||||
PermissionDenied, // 권한 거부/차단
|
||||
ToolCall, // 도구 호출
|
||||
ToolResult, // 도구 결과
|
||||
SkillCall, // 스킬 호출
|
||||
Error, // 오류
|
||||
Complete, // 완료
|
||||
Decision, // 사용자 의사결정 대기
|
||||
Paused, // 에이전트 일시정지
|
||||
Resumed, // 에이전트 재개
|
||||
}
|
||||
Reference in New Issue
Block a user