AX Agent IBM 응답 정규화 및 도구 노출 순서 추가 보강

이번 커밋은 후속 과제로 남아 있던 IBM Qwen 응답 포맷 차이와 보조 도구 과노출 문제를 추가 정리했다.

핵심 변경 사항:

- LlmService.ToolUse에서 content/reasoning_content/generated_text/output_text가 배열 또는 블록 형태로 와도 텍스트를 추출하도록 메시지 파서 보강

- content 배열 안의 tool_use/tool_call 블록도 직접 ContentBlock으로 복구해 IBM/Qwen 응답 변형에 더 유연하게 대응

- ToolRegistry 활성 도구 목록 노출 순서를 기본 파일/검색/생성/실행 도구 우선으로 재정렬하고 tool_search, MCP, spawn_agent, task 계열은 뒤로 배치

문서 반영:

- README.md, docs/DEVELOPMENT.md에 2026-04-09 23:02 (KST) 기준 이력 추가

검증 결과:

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\

- 경고 0개, 오류 0개
This commit is contained in:
2026-04-09 23:08:33 +09:00
parent 1fab215344
commit 6bd8d5bb2c
4 changed files with 331 additions and 17 deletions

View File

@@ -62,7 +62,174 @@ public class ToolRegistry : IDisposable
if (disabledNames == null) return All;
var disabled = new HashSet<string>(disabledNames, StringComparer.OrdinalIgnoreCase);
if (disabled.Count == 0) return All;
return _tools.Values.Where(t => !disabled.Contains(t.Name)).ToList().AsReadOnly();
return OrderToolsForExposure(_tools.Values.Where(t => !disabled.Contains(t.Name)))
.ToList()
.AsReadOnly();
}
/// <summary>비활성 도구를 제외하고 현재 탭에 해당하는 도구만 반환합니다.</summary>
public IReadOnlyCollection<IAgentTool> GetActiveToolsForTab(string activeTab, IEnumerable<string>? disabledNames = null)
{
var disabled = disabledNames != null
? new HashSet<string>(disabledNames, StringComparer.OrdinalIgnoreCase)
: null;
return OrderToolsForExposure(_tools.Values.Where(t =>
{
if (disabled != null && disabled.Contains(t.Name)) return false;
return IsToolAvailableForTab(t, activeTab);
})).ToList().AsReadOnly();
}
private static IEnumerable<IAgentTool> OrderToolsForExposure(IEnumerable<IAgentTool> tools)
{
return tools
.OrderBy(GetToolExposureBucket)
.ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase);
}
private static int GetToolExposureBucket(IAgentTool tool)
{
return tool.Name switch
{
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "document_read"
or "process" or "dev_env_detect" or "build_run" or "git_tool" or "lsp_code_intel"
or "document_plan" or "document_assemble" or "docx_create" or "html_create" or "markdown_create"
or "excel_create" or "csv_create" or "pptx_create" or "chart_create" => 0,
"document_review" or "format_convert" or "tool_search" or "code_search" => 1,
"mcp_list_resources" or "mcp_read_resource" or "spawn_agent" or "wait_agents" => 2,
_ when tool.Name.StartsWith("task_", StringComparison.OrdinalIgnoreCase) => 3,
_ => 1
};
}
/// <summary>도구가 해당 탭에서 사용 가능한지 확인합니다.</summary>
private static bool IsToolAvailableForTab(IAgentTool tool, string activeTab)
{
var category = ResolveTabCategory(tool);
if (string.IsNullOrEmpty(category)) return true; // null = 모든 탭
// 쉼표 구분 복수 탭: "Cowork,Code"
foreach (var part in category.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (string.Equals(part, activeTab, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
/// <summary>
/// 도구별 탭 카테고리 오버라이드.
/// IAgentTool.TabCategory가 null인 도구는 이 맵을 참조합니다.
/// 키: 도구 이름, 값: 허용 탭 (쉼표 구분). 맵에 없으면 = 모든 탭.
/// </summary>
private static readonly Dictionary<string, string> ToolTabOverrides = new(StringComparer.OrdinalIgnoreCase)
{
// ════════════════════════════════════════════════════════════
// Chat = 순수 대화 (도구 없음). 아래 맵에 없는 공통 도구도
// Chat에선 제외하려면 여기에 "Cowork,Code"로 등록.
// ════════════════════════════════════════════════════════════
// ── 파일/검색 기본 도구: Cowork + Code ──
["file_read"] = "Cowork,Code",
["file_write"] = "Cowork,Code",
["file_edit"] = "Cowork,Code",
["glob"] = "Cowork,Code",
["grep"] = "Cowork,Code",
["process"] = "Cowork,Code",
["folder_map"] = "Cowork,Code",
["document_read"] = "Cowork,Code",
["file_manage"] = "Cowork,Code",
["file_info"] = "Cowork,Code",
["multi_read"] = "Cowork,Code",
["zip"] = "Cowork,Code",
["open_external"] = "Cowork,Code",
// ── 데이터/유틸리티: Cowork + Code ──
["json"] = "Cowork,Code",
["regex"] = "Cowork,Code",
["base64"] = "Cowork,Code",
["hash"] = "Cowork,Code",
["datetime"] = "Cowork,Code",
["math"] = "Cowork,Code",
["encoding"] = "Cowork,Code",
["http"] = "Cowork,Code",
["clipboard"] = "Cowork,Code",
["env"] = "Cowork,Code",
["notify"] = "Cowork,Code",
["user_ask"] = "Cowork,Code",
["memory"] = "Cowork,Code",
["skill_manager"] = "Cowork,Code",
["tool_search"] = "Cowork,Code",
["mcp_list_resources"] = "Cowork,Code",
["mcp_read_resource"] = "Cowork,Code",
// ── 문서 생성/처리: Cowork 전용 ──
["xlsx_create"] = "Cowork",
["excel_create"] = "Cowork",
["docx_create"] = "Cowork",
["csv_create"] = "Cowork",
["md_create"] = "Cowork",
["markdown_create"] = "Cowork",
["html_create"] = "Cowork",
["chart_create"] = "Cowork",
["batch_create"] = "Cowork",
["pptx_create"] = "Cowork",
["document_plan"] = "Cowork",
["document_assemble"] = "Cowork",
["document_review"] = "Cowork",
["format_convert"] = "Cowork",
["data_pivot"] = "Cowork",
["template_render"] = "Cowork",
["text_summarize"] = "Cowork",
["sql"] = "Cowork",
["xml"] = "Cowork",
["image_analyze"] = "Cowork",
// ── 개발 도구: Code 전용 ──
["dev_env_detect"] = "Code",
["build_run"] = "Code",
["git"] = "Code",
["lsp"] = "Code",
["code_search"] = "Code",
["code_review"] = "Code",
["project_rule"] = "Code",
["snippet_run"] = "Code",
["diff"] = "Code",
["diff_preview"] = "Code",
["sub_agent"] = "Code",
["wait_agents"] = "Code",
["test_loop"] = "Code",
["file_watch"] = "Code",
// ── 태스크/워크트리/팀: Code 전용 ──
["task_tracker"] = "Code",
["todo_write"] = "Code",
["task_create"] = "Code",
["task_get"] = "Code",
["task_list"] = "Code",
["task_update"] = "Code",
["task_stop"] = "Code",
["task_output"] = "Code",
["enter_worktree"] = "Code",
["exit_worktree"] = "Code",
["team_create"] = "Code",
["team_delete"] = "Code",
["cron_create"] = "Code",
["cron_delete"] = "Code",
["cron_list"] = "Code",
["checkpoint"] = "Code",
["suggest_actions"] = "Code",
["playbook"] = "Code",
};
/// <summary>도구의 실질 탭 카테고리를 결정합니다 (IAgentTool.TabCategory → 오버라이드 맵 순).</summary>
private static string? ResolveTabCategory(IAgentTool tool)
{
// 도구 자체에 TabCategory가 명시되어 있으면 우선
if (!string.IsNullOrEmpty(tool.TabCategory))
return tool.TabCategory;
// 오버라이드 맵에서 조회
return ToolTabOverrides.TryGetValue(tool.Name, out var cat) ? cat : null;
}
/// <summary>IDisposable 도구를 모두 해제합니다.</summary>