[Phase46] 대형 파일 분할 리팩터링 2차 — 19개 신규 파셜 파일 생성
## 분할 대상 및 결과 ### AgentLoopService.cs (1,334줄 → 846줄) - AgentLoopService.HtmlReport.cs (151줄): AutoSaveAsHtml, ConvertTextToHtml, EscapeHtml - AgentLoopService.Verification.cs (349줄): 도구 분류 판별 + RunPostToolVerificationAsync + EmitEvent + CheckDecisionRequired + FormatToolCallSummary ### ChatWindow 분할 (8개 신규 파셜 파일) - ChatWindow.PlanViewer.cs (474줄): 계획 뷰어 — AddPlanningCard, AddDecisionButtons, CollapseDecisionButtons, ShowPlanButton 등 8개 메서드 - ChatWindow.EventBanner.cs (411줄): AddAgentEventBanner, BuildFileQuickActions - ChatWindow.TaskDecomposition.cs (1,170줄 → 307줄): RenderSuggestActionChips, BuildFeedbackContext, UpdateProgressBar, BuildDiffView 잔류 - ChatWindow.BottomBar.cs (345줄): BuildBottomBar, BuildCodeBottomBar, ShowLogLevelMenu, ShowLanguageMenu 등 - ChatWindow.MoodMenu.cs (456줄): ShowFormatMenu, ShowMoodMenu, ShowCustomMoodDialog 등 - ChatWindow.CustomPresets.cs (978줄 → 203줄): ShowCustomPresetDialog, SelectTopic 잔류 - ChatWindow.ConversationMenu.cs (255줄): ShowConversationMenu (카테고리/삭제/즐겨찾기 팝업) - ChatWindow.ConversationTitleEdit.cs (108줄): EnterTitleEditMode ### SettingsViewModel 분할 - SettingsViewModel.LlmProperties.cs (417줄): LLM·에이전트 관련 바인딩 프로퍼티 - SettingsViewModel.Properties.cs (837줄 → 427줄): 기능 토글·테마·스니펫 등 앱 수준 프로퍼티 ### TemplateService 분할 - TemplateService.Css.cs (559줄): 11종 CSS 테마 문자열 상수 - TemplateService.cs (734줄 → 179줄): 메서드 로직만 잔류 ### PlanViewerWindow 분할 - PlanViewerWindow.StepRenderer.cs (616줄): RenderSteps + SwapSteps + EditStep + 버튼 빌더 9개 - PlanViewerWindow.cs (931줄 → 324줄): Win32/생성자/공개 API 잔류 ### App.xaml.cs 분할 (776줄 → 452줄) - App.Settings.cs (252줄): SetupTrayIcon, OpenSettings, ToggleDockBar, RefreshDockBar, OpenAiChat - App.Helpers.cs (92줄): LoadAppIcon, IsAutoStartEnabled, SetAutoStart, OnExit ### LlmService.ToolUse.cs 분할 (719줄 → 115줄) - LlmService.ClaudeTools.cs (180줄): SendClaudeWithToolsAsync, BuildClaudeToolBody - LlmService.GeminiTools.cs (175줄): SendGeminiWithToolsAsync, BuildGeminiToolBody - LlmService.OpenAiTools.cs (215줄): SendOpenAiWithToolsAsync, BuildOpenAiToolBody ### SettingsWindow.UI.cs 분할 (802줄 → 310줄) - SettingsWindow.Storage.cs (167줄): RefreshStorageInfo, BtnStorageCleanup_Click 등 - SettingsWindow.HotkeyUI.cs (127줄): RefreshHotkeyBadges, EnsureHotkeyInCombo, GetKeyName 등 - SettingsWindow.DevMode.cs (90줄): DevModeCheckBox_Checked, UpdateDevModeContentVisibility ## 빌드 결과: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -824,349 +824,6 @@ public partial class AgentLoopService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>LLM 텍스트 응답을 HTML 보고서 파일로 자동 저장합니다.</summary>
|
||||
private string? AutoSaveAsHtml(string textContent, string userQuery, AgentContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 파일명 생성 — 동사/명령어를 제거하여 깔끔한 파일명 만들기
|
||||
var title = userQuery.Length > Defaults.QueryTitleMaxLength ? userQuery[..Defaults.QueryTitleMaxLength] : userQuery;
|
||||
// 파일명에 불필요한 동사/명령어 제거
|
||||
var removeWords = new[] { "작성해줘", "작성해 줘", "만들어줘", "만들어 줘", "써줘", "써 줘",
|
||||
"생성해줘", "생성해 줘", "작성해", "만들어", "생성해", "해줘", "해 줘", "부탁해" };
|
||||
var safeTitle = title;
|
||||
foreach (var w in removeWords)
|
||||
safeTitle = safeTitle.Replace(w, "", StringComparison.OrdinalIgnoreCase);
|
||||
foreach (var c in System.IO.Path.GetInvalidFileNameChars())
|
||||
safeTitle = safeTitle.Replace(c, '_');
|
||||
safeTitle = safeTitle.Trim().TrimEnd('.').Trim();
|
||||
|
||||
var fileName = $"{safeTitle}.html";
|
||||
var fullPath = FileReadTool.ResolvePath(fileName, context.WorkFolder);
|
||||
if (context.ActiveTab == "Cowork")
|
||||
fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
||||
|
||||
var dir = System.IO.Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrEmpty(dir)) System.IO.Directory.CreateDirectory(dir);
|
||||
|
||||
// 텍스트 → HTML 변환
|
||||
var css = TemplateService.GetCss("professional");
|
||||
var htmlBody = ConvertTextToHtml(textContent);
|
||||
|
||||
var html = $@"<!DOCTYPE html>
|
||||
<html lang=""ko"">
|
||||
<head>
|
||||
<meta charset=""utf-8"">
|
||||
<title>{EscapeHtml(title)}</title>
|
||||
<style>
|
||||
{css}
|
||||
.doc {{ max-width: 900px; margin: 0 auto; padding: 40px 30px 60px; }}
|
||||
.doc h1 {{ font-size: 28px; margin-bottom: 8px; border-bottom: 3px solid var(--accent, #4B5EFC); padding-bottom: 10px; }}
|
||||
.doc h2 {{ font-size: 22px; margin-top: 36px; margin-bottom: 12px; border-bottom: 2px solid var(--accent, #4B5EFC); padding-bottom: 6px; }}
|
||||
.doc h3 {{ font-size: 18px; margin-top: 24px; margin-bottom: 8px; }}
|
||||
.doc .meta {{ color: #888; font-size: 13px; margin-bottom: 24px; }}
|
||||
.doc p {{ line-height: 1.8; margin-bottom: 12px; }}
|
||||
.doc ul, .doc ol {{ line-height: 1.8; margin-bottom: 16px; }}
|
||||
.doc table {{ border-collapse: collapse; width: 100%; margin: 16px 0; }}
|
||||
.doc th {{ background: var(--accent, #4B5EFC); color: #fff; padding: 10px 14px; text-align: left; }}
|
||||
.doc td {{ padding: 8px 14px; border-bottom: 1px solid #e5e7eb; }}
|
||||
.doc tr:nth-child(even) {{ background: #f8f9fa; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""doc"">
|
||||
<h1>{EscapeHtml(title)}</h1>
|
||||
<div class=""meta"">작성일: {DateTime.Now:yyyy-MM-dd} | AX Copilot 자동 생성</div>
|
||||
{htmlBody}
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
System.IO.File.WriteAllText(fullPath, html, System.Text.Encoding.UTF8);
|
||||
LogService.Info($"[AgentLoop] 문서 자동 저장 완료: {fullPath}");
|
||||
return fullPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"[AgentLoop] 문서 자동 저장 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>LLM 텍스트(마크다운 형식)를 HTML로 변환합니다.</summary>
|
||||
private static string ConvertTextToHtml(string text)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var lines = text.Split('\n');
|
||||
var inList = false;
|
||||
var listType = "ul";
|
||||
|
||||
foreach (var rawLine in lines)
|
||||
{
|
||||
var line = rawLine.TrimEnd();
|
||||
|
||||
// 빈 줄
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
if (inList) { sb.AppendLine($"</{listType}>"); inList = false; }
|
||||
continue;
|
||||
}
|
||||
|
||||
// 마크다운 제목
|
||||
if (line.StartsWith("### "))
|
||||
{
|
||||
if (inList) { sb.AppendLine($"</{listType}>"); inList = false; }
|
||||
sb.AppendLine($"<h3>{EscapeHtml(line[4..])}</h3>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("## "))
|
||||
{
|
||||
if (inList) { sb.AppendLine($"</{listType}>"); inList = false; }
|
||||
sb.AppendLine($"<h2>{EscapeHtml(line[3..])}</h2>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("# "))
|
||||
{
|
||||
if (inList) { sb.AppendLine($"</{listType}>"); inList = false; }
|
||||
sb.AppendLine($"<h2>{EscapeHtml(line[2..])}</h2>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 번호 리스트 (1. 2. 등) - 대제목급이면 h2로
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(line, @"^\d+\.\s+\S"))
|
||||
{
|
||||
var content = System.Text.RegularExpressions.Regex.Replace(line, @"^\d+\.\s+", "");
|
||||
// 짧고 제목 같으면 h2, 길면 리스트
|
||||
if (content.Length < 80 && !content.Contains('.') && !line.StartsWith(" "))
|
||||
{
|
||||
if (inList) { sb.AppendLine($"</{listType}>"); inList = false; }
|
||||
sb.AppendLine($"<h2>{EscapeHtml(line)}</h2>");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!inList) { sb.AppendLine("<ol>"); inList = true; listType = "ol"; }
|
||||
sb.AppendLine($"<li>{EscapeHtml(content)}</li>");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 불릿 리스트
|
||||
if (line.TrimStart().StartsWith("- ") || line.TrimStart().StartsWith("* ") || line.TrimStart().StartsWith("• "))
|
||||
{
|
||||
var content = line.TrimStart()[2..].Trim();
|
||||
if (!inList) { sb.AppendLine("<ul>"); inList = true; listType = "ul"; }
|
||||
sb.AppendLine($"<li>{EscapeHtml(content)}</li>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 일반 텍스트
|
||||
if (inList) { sb.AppendLine($"</{listType}>"); inList = false; }
|
||||
sb.AppendLine($"<p>{EscapeHtml(line)}</p>");
|
||||
}
|
||||
|
||||
if (inList) sb.AppendLine($"</{listType}>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeHtml(string text)
|
||||
=> text.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
|
||||
/// <summary>사용자 요청이 문서/보고서 생성인지 판단합니다.</summary>
|
||||
private static bool IsDocumentCreationRequest(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return false;
|
||||
// 문서 생성 관련 키워드 패턴
|
||||
var keywords = new[]
|
||||
{
|
||||
"보고서", "리포트", "report", "문서", "작성해", "써줘", "써 줘", "만들어",
|
||||
"분석서", "제안서", "기획서", "회의록", "매뉴얼", "가이드",
|
||||
"excel", "엑셀", "docx", "word", "html", "pptx", "ppt",
|
||||
"프레젠테이션", "발표자료", "슬라이드"
|
||||
};
|
||||
var q = query.ToLowerInvariant();
|
||||
return keywords.Any(k => q.Contains(k, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>문서 생성 도구인지 확인합니다 (Cowork 검증 대상).</summary>
|
||||
private static bool IsDocumentCreationTool(string toolName)
|
||||
{
|
||||
return toolName is "file_write" or "docx_create" or "html_create"
|
||||
or "excel_create" or "csv_create" or "script_create" or "pptx_create";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 도구가 성공하면 작업이 완료된 것으로 간주해 루프를 즉시 종료합니다.
|
||||
/// Ollama 등 멀티턴 tool_result 미지원 모델에서 불필요한 추가 LLM 호출과 "도구 호출 거부" 오류를 방지합니다.
|
||||
/// document_assemble/html_create/docx_create 같은 최종 파일 생성 도구가 해당합니다.
|
||||
/// </summary>
|
||||
private static bool IsTerminalDocumentTool(string toolName)
|
||||
{
|
||||
return toolName is "html_create" or "docx_create" or "excel_create"
|
||||
or "pptx_create" or "document_assemble" or "csv_create";
|
||||
}
|
||||
|
||||
/// <summary>코드 생성/수정 도구인지 확인합니다 (Code 검증 대상).</summary>
|
||||
private static bool IsCodeVerificationTarget(string toolName)
|
||||
{
|
||||
return toolName is "file_write" or "file_edit" or "script_create"
|
||||
or "process"; // 빌드/테스트 실행 결과 검증
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문서 생성 도구 실행 후 검증 전용 LLM 호출을 삽입합니다.
|
||||
/// LLM에게 생성된 파일을 읽고 품질을 평가하도록 강제합니다.
|
||||
/// OpenHands 등 오픈소스에서는 이런 강제 검증이 없으며, AX Copilot 차별화 포인트입니다.
|
||||
/// </summary>
|
||||
/// <summary>읽기 전용 검증 도구 목록 (file_read만 허용)</summary>
|
||||
private static readonly HashSet<string> VerificationAllowedTools = ["file_read", "directory_list"];
|
||||
|
||||
private async Task RunPostToolVerificationAsync(
|
||||
List<ChatMessage> messages, string toolName, ToolResult result,
|
||||
AgentContext context, CancellationToken ct)
|
||||
{
|
||||
EmitEvent(AgentEventType.Thinking, "", "🔍 생성 결과물 검증 중...");
|
||||
|
||||
// 생성된 파일 경로 추출
|
||||
var filePath = result.FilePath ?? "";
|
||||
var fileRef = string.IsNullOrEmpty(filePath) ? "방금 생성한 결과물" : $"파일 '{filePath}'";
|
||||
|
||||
// 탭별 검증 프롬프트 생성 — 읽기 + 보고만 (수정 금지)
|
||||
var isCodeTab = context.ActiveTab == "Code";
|
||||
var checkList = isCodeTab
|
||||
? " - 구문 오류가 없는가?\n" +
|
||||
" - 참조하는 클래스/메서드/변수가 존재하는가?\n" +
|
||||
" - 코딩 컨벤션이 일관적인가?\n" +
|
||||
" - 에지 케이스 처리가 누락되지 않았는가?"
|
||||
: " - 사용자 요청에 맞는 내용이 모두 포함되었는가?\n" +
|
||||
" - 구조와 형식이 올바른가?\n" +
|
||||
" - 누락된 섹션이나 불완전한 내용이 없는가?\n" +
|
||||
" - 한국어 맞춤법/표현이 자연스러운가?";
|
||||
|
||||
var verificationPrompt = new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = $"[System:Verification] {fileRef}을 검증하세요.\n" +
|
||||
"1. file_read 도구로 생성된 파일의 내용을 읽으세요.\n" +
|
||||
"2. 다음 항목을 확인하세요:\n" +
|
||||
checkList + "\n" +
|
||||
"3. 결과를 간단히 보고하세요. 문제가 있으면 구체적으로 무엇이 잘못되었는지 설명하세요.\n" +
|
||||
"⚠️ 중요: 이 단계에서는 파일을 직접 수정하지 마세요. 보고만 하세요."
|
||||
};
|
||||
|
||||
// 검증 메시지를 임시로 추가 (검증 완료 후 전부 제거)
|
||||
var insertIndex = messages.Count;
|
||||
messages.Add(verificationPrompt);
|
||||
var addedMessages = new List<ChatMessage> { verificationPrompt };
|
||||
|
||||
try
|
||||
{
|
||||
// 읽기 전용 도구만 제공 (file_write, file_edit 등 쓰기 도구 차단)
|
||||
var allTools = _tools.GetActiveTools(_settings.Settings.Llm.DisabledTools);
|
||||
var readOnlyTools = allTools
|
||||
.Where(t => VerificationAllowedTools.Contains(t.Name))
|
||||
.ToList();
|
||||
|
||||
var verifyBlocks = await _llm.SendWithToolsAsync(messages, readOnlyTools, ct);
|
||||
|
||||
// 검증 응답 처리
|
||||
var verifyText = new List<string>();
|
||||
var verifyToolCalls = new List<LlmService.ContentBlock>();
|
||||
|
||||
foreach (var block in verifyBlocks)
|
||||
{
|
||||
if (block.Type == "text" && !string.IsNullOrWhiteSpace(block.Text))
|
||||
verifyText.Add(block.Text);
|
||||
else if (block.Type == "tool_use")
|
||||
verifyToolCalls.Add(block);
|
||||
}
|
||||
|
||||
var verifyResponse = string.Join("\n", verifyText);
|
||||
|
||||
// file_read 도구 호출 처리 (읽기만 허용)
|
||||
if (verifyToolCalls.Count > 0)
|
||||
{
|
||||
var contentBlocks = new List<object>();
|
||||
if (!string.IsNullOrEmpty(verifyResponse))
|
||||
contentBlocks.Add(new { type = "text", text = verifyResponse });
|
||||
foreach (var tc in verifyToolCalls)
|
||||
contentBlocks.Add(new { type = "tool_use", id = tc.ToolId, name = tc.ToolName, input = tc.ToolInput });
|
||||
var assistantContent = System.Text.Json.JsonSerializer.Serialize(new { _tool_use_blocks = contentBlocks });
|
||||
var assistantMsg = new ChatMessage { Role = "assistant", Content = assistantContent };
|
||||
messages.Add(assistantMsg);
|
||||
addedMessages.Add(assistantMsg);
|
||||
|
||||
foreach (var tc in verifyToolCalls)
|
||||
{
|
||||
var tool = _tools.Get(tc.ToolName);
|
||||
if (tool == null)
|
||||
{
|
||||
var errMsg = LlmService.CreateToolResultMessage(tc.ToolId, tc.ToolName, "검증 단계에서는 읽기 도구만 사용 가능합니다.");
|
||||
messages.Add(errMsg);
|
||||
addedMessages.Add(errMsg);
|
||||
continue;
|
||||
}
|
||||
|
||||
EmitEvent(AgentEventType.ToolCall, tc.ToolName, $"[검증] {FormatToolCallSummary(tc)}");
|
||||
try
|
||||
{
|
||||
var input = tc.ToolInput ?? System.Text.Json.JsonDocument.Parse("{}").RootElement;
|
||||
var verifyResult = await tool.ExecuteAsync(input, context, ct);
|
||||
var toolMsg = LlmService.CreateToolResultMessage(
|
||||
tc.ToolId, tc.ToolName, TruncateOutput(verifyResult.Output, Defaults.ToolResultTruncateLength));
|
||||
messages.Add(toolMsg);
|
||||
addedMessages.Add(toolMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errMsg = LlmService.CreateToolResultMessage(
|
||||
tc.ToolId, tc.ToolName, $"검증 도구 실행 오류: {ex.Message}");
|
||||
messages.Add(errMsg);
|
||||
addedMessages.Add(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// file_read 결과를 받은 후 최종 검증 판단을 받기 위해 한 번 더 호출
|
||||
var finalBlocks = await _llm.SendWithToolsAsync(messages, readOnlyTools, ct);
|
||||
verifyResponse = string.Join("\n",
|
||||
finalBlocks.Where(b => b.Type == "text" && !string.IsNullOrWhiteSpace(b.Text)).Select(b => b.Text));
|
||||
}
|
||||
|
||||
// 검증 결과를 이벤트로 표시
|
||||
if (!string.IsNullOrEmpty(verifyResponse))
|
||||
{
|
||||
var summary = verifyResponse.Length > Defaults.VerificationSummaryMaxLength ? verifyResponse[..Defaults.VerificationSummaryMaxLength] + "…" : verifyResponse;
|
||||
EmitEvent(AgentEventType.Thinking, "", $"✅ 검증 결과: {summary}");
|
||||
|
||||
// 문제가 발견된 경우: 검증 보고서를 컨텍스트에 남겨서 다음 루프에서 자연스럽게 수정
|
||||
var hasIssues = verifyResponse.Contains("문제") || verifyResponse.Contains("수정") ||
|
||||
verifyResponse.Contains("누락") || verifyResponse.Contains("오류") ||
|
||||
verifyResponse.Contains("잘못") || verifyResponse.Contains("부족");
|
||||
if (hasIssues)
|
||||
{
|
||||
// 검증 관련 임시 메시지를 모두 제거
|
||||
foreach (var msg in addedMessages)
|
||||
messages.Remove(msg);
|
||||
|
||||
// 검증 보고서만 간결하게 남기기 (다음 루프에서 LLM이 자연스럽게 수정)
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = $"[System] 방금 생성한 {fileRef}에 대한 자동 검증 결과, 다음 문제가 발견되었습니다:\n{verifyResponse}\n\n위 문제를 수정해 주세요."
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmitEvent(AgentEventType.Error, "", $"검증 LLM 호출 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
// 검증 통과 또는 실패: 임시 메시지 전부 제거 (컨텍스트 오염 방지)
|
||||
foreach (var msg in addedMessages)
|
||||
messages.Remove(msg);
|
||||
}
|
||||
|
||||
private AgentContext BuildContext(string? tabOverride = null)
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
@@ -1186,149 +843,4 @@ public partial class AgentLoopService
|
||||
DevModeStepApproval = llm.DevModeStepApproval,
|
||||
};
|
||||
}
|
||||
|
||||
private void EmitEvent(AgentEventType type, string toolName, string summary,
|
||||
string? filePath = null, int stepCurrent = 0, int stepTotal = 0, List<string>? steps = null,
|
||||
long elapsedMs = 0, int inputTokens = 0, int outputTokens = 0,
|
||||
string? toolInput = null, int iteration = 0)
|
||||
{
|
||||
// AgentLogLevel에 따라 이벤트 필터링
|
||||
var logLevel = _settings.Settings.Llm.AgentLogLevel;
|
||||
|
||||
// simple: ToolCall, ToolResult, Error, Complete, StepStart, StepDone, Decision만
|
||||
if (logLevel == "simple" && type is AgentEventType.Thinking or AgentEventType.Planning)
|
||||
return;
|
||||
|
||||
// simple: Summary 200자 제한
|
||||
if (logLevel == "simple" && summary.Length > 200)
|
||||
summary = summary[..200] + "…";
|
||||
|
||||
// debug 아닌 경우 ToolInput 제거
|
||||
if (logLevel != "debug")
|
||||
toolInput = null;
|
||||
|
||||
var evt = new AgentEvent
|
||||
{
|
||||
Type = type,
|
||||
ToolName = toolName,
|
||||
Summary = summary,
|
||||
FilePath = filePath,
|
||||
Success = type != AgentEventType.Error,
|
||||
StepCurrent = stepCurrent,
|
||||
StepTotal = stepTotal,
|
||||
Steps = steps,
|
||||
ElapsedMs = elapsedMs,
|
||||
InputTokens = inputTokens,
|
||||
OutputTokens = outputTokens,
|
||||
ToolInput = toolInput,
|
||||
Iteration = iteration,
|
||||
};
|
||||
|
||||
if (Dispatcher != null)
|
||||
Dispatcher(() => { Events.Add(evt); EventOccurred?.Invoke(evt); });
|
||||
else
|
||||
{
|
||||
Events.Add(evt);
|
||||
EventOccurred?.Invoke(evt);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>영향 범위 기반 의사결정 체크. 확인이 필요하면 메시지를 반환, 불필요하면 null.</summary>
|
||||
private string? CheckDecisionRequired(LlmService.ContentBlock call, AgentContext context)
|
||||
{
|
||||
var level = _settings.Settings.Llm.AgentDecisionLevel ?? "normal";
|
||||
var toolName = call.ToolName ?? "";
|
||||
var input = call.ToolInput;
|
||||
|
||||
// Git 커밋 — 수준에 관계없이 무조건 확인
|
||||
if (toolName == "git_tool")
|
||||
{
|
||||
var action = input?.TryGetProperty("action", out var a) == true ? a.GetString() : "";
|
||||
if (action == "commit")
|
||||
{
|
||||
var msg = input?.TryGetProperty("args", out var m) == true ? m.GetString() : "";
|
||||
return $"Git 커밋을 실행하시겠습니까?\n\n커밋 메시지: {msg}";
|
||||
}
|
||||
}
|
||||
|
||||
// minimal: 파일 삭제, 외부 명령만
|
||||
if (level == "minimal")
|
||||
{
|
||||
// process 도구 (외부 명령 실행)
|
||||
if (toolName == "process")
|
||||
{
|
||||
var cmd = input?.TryGetProperty("command", out var c) == true ? c.GetString() : "";
|
||||
return $"외부 명령을 실행하시겠습니까?\n\n명령: {cmd}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// normal: + 새 파일 생성, 여러 파일 수정, 문서 생성, 외부 명령
|
||||
if (level == "normal" || level == "detailed")
|
||||
{
|
||||
// 외부 명령 실행
|
||||
if (toolName == "process")
|
||||
{
|
||||
var cmd = input?.TryGetProperty("command", out var c) == true ? c.GetString() : "";
|
||||
return $"외부 명령을 실행하시겠습니까?\n\n명령: {cmd}";
|
||||
}
|
||||
|
||||
// 새 파일 생성
|
||||
if (toolName == "file_write")
|
||||
{
|
||||
var path = input?.TryGetProperty("file_path", out var p) == true ? p.GetString() : "";
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var fullPath = System.IO.Path.IsPathRooted(path) ? path
|
||||
: System.IO.Path.Combine(context.WorkFolder, path ?? "");
|
||||
if (!System.IO.File.Exists(fullPath))
|
||||
return $"새 파일을 생성하시겠습니까?\n\n경로: {path}";
|
||||
}
|
||||
}
|
||||
|
||||
// 문서 생성 (Excel, Word, HTML 등)
|
||||
if (toolName is "excel_create" or "docx_create" or "html_create" or "csv_create" or "script_create")
|
||||
{
|
||||
var path = input?.TryGetProperty("file_path", out var p) == true ? p.GetString() : "";
|
||||
return $"문서를 생성하시겠습니까?\n\n도구: {toolName}\n경로: {path}";
|
||||
}
|
||||
|
||||
// 빌드/테스트 실행
|
||||
if (toolName is "build_run" or "test_loop")
|
||||
{
|
||||
var action = input?.TryGetProperty("action", out var a) == true ? a.GetString() : "";
|
||||
return $"빌드/테스트를 실행하시겠습니까?\n\n도구: {toolName}\n액션: {action}";
|
||||
}
|
||||
}
|
||||
|
||||
// detailed: 모든 파일 수정
|
||||
if (level == "detailed")
|
||||
{
|
||||
if (toolName is "file_write" or "file_edit")
|
||||
{
|
||||
var path = input?.TryGetProperty("file_path", out var p) == true ? p.GetString() : "";
|
||||
return $"파일을 수정하시겠습니까?\n\n경로: {path}";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FormatToolCallSummary(LlmService.ContentBlock call)
|
||||
{
|
||||
if (call.ToolInput == null) return call.ToolName;
|
||||
try
|
||||
{
|
||||
// 주요 파라미터만 표시
|
||||
var input = call.ToolInput.Value;
|
||||
if (input.TryGetProperty("path", out var path))
|
||||
return $"{call.ToolName}: {path.GetString()}";
|
||||
if (input.TryGetProperty("command", out var cmd))
|
||||
return $"{call.ToolName}: {cmd.GetString()}";
|
||||
if (input.TryGetProperty("pattern", out var pat))
|
||||
return $"{call.ToolName}: {pat.GetString()}";
|
||||
return call.ToolName;
|
||||
}
|
||||
catch (Exception) { return call.ToolName; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user