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:
@@ -1254,11 +1254,22 @@ public partial class LlmService
|
||||
resultsEl.GetArrayLength() > 0)
|
||||
{
|
||||
var first = resultsEl[0];
|
||||
var generatedText = first.SafeTryGetProperty("generated_text", out var generatedTextEl)
|
||||
? generatedTextEl.SafeGetString()
|
||||
: first.SafeTryGetProperty("output_text", out var outputTextEl)
|
||||
? outputTextEl.SafeGetString()
|
||||
: null;
|
||||
string? generatedText = null;
|
||||
if (first.SafeTryGetProperty("generated_text", out var generatedTextEl))
|
||||
generatedText = TryExtractTextContent(generatedTextEl, out var extractedGeneratedText) ? extractedGeneratedText : generatedTextEl.SafeGetString();
|
||||
else if (first.SafeTryGetProperty("output_text", out var outputTextEl))
|
||||
generatedText = TryExtractTextContent(outputTextEl, out var extractedOutputText) ? extractedOutputText : outputTextEl.SafeGetString();
|
||||
else if (first.SafeTryGetProperty("message", out var ibmMessageEl) &&
|
||||
TryExtractMessageToolBlocks(ibmMessageEl, out var ibmMessageText, out var ibmToolBlocks))
|
||||
{
|
||||
generatedText = ibmMessageText;
|
||||
foreach (var toolBlock in ibmToolBlocks)
|
||||
{
|
||||
if (prefetchToolCallAsync != null)
|
||||
toolBlock.PrefetchedExecutionTask = prefetchToolCallAsync(toolBlock);
|
||||
yield return new ToolStreamEvent(ToolStreamEventKind.ToolCallReady, ToolCall: toolBlock);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(generatedText))
|
||||
{
|
||||
if (generatedText.StartsWith(lastIbmGeneratedText, StringComparison.Ordinal))
|
||||
@@ -1388,27 +1399,33 @@ public partial class LlmService
|
||||
message = nestedMessage;
|
||||
|
||||
var consumed = false;
|
||||
if (message.SafeTryGetProperty("content", out var contentEl) &&
|
||||
contentEl.ValueKind == JsonValueKind.String)
|
||||
if (message.SafeTryGetProperty("content", out var contentEl))
|
||||
{
|
||||
var parsedText = contentEl.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(parsedText))
|
||||
if (TryExtractTextContent(contentEl, out var parsedText))
|
||||
{
|
||||
text = parsedText;
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
if (contentEl.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var block in contentEl.EnumerateArray())
|
||||
{
|
||||
if (!TryParseContentArrayToolBlock(block, out var toolBlock))
|
||||
continue;
|
||||
|
||||
toolBlocks.Add(toolBlock);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Qwen3.5 thinking 모드 폴백: content가 비어있으면 reasoning_content 사용
|
||||
if (!consumed &&
|
||||
message.SafeTryGetProperty("reasoning_content", out var reasoningContentEl) &&
|
||||
reasoningContentEl.ValueKind == JsonValueKind.String)
|
||||
TryExtractTextContent(reasoningContentEl, out var reasoningText))
|
||||
{
|
||||
var reasoningText = reasoningContentEl.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(reasoningText))
|
||||
{
|
||||
text = reasoningText;
|
||||
consumed = true;
|
||||
}
|
||||
text = reasoningText;
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
if (message.SafeTryGetProperty("tool_calls", out var toolCallsEl) &&
|
||||
@@ -1452,6 +1469,125 @@ public partial class LlmService
|
||||
return consumed;
|
||||
}
|
||||
|
||||
private static bool TryExtractTextContent(JsonElement element, out string text)
|
||||
{
|
||||
text = "";
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var parsed = element.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(parsed))
|
||||
{
|
||||
text = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.ValueKind != JsonValueKind.Array)
|
||||
return false;
|
||||
|
||||
var segments = new List<string>();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = item.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(str))
|
||||
segments.Add(str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
continue;
|
||||
|
||||
if (item.SafeTryGetProperty("text", out var textEl) && textEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = textEl.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(str))
|
||||
segments.Add(str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.SafeTryGetProperty("content", out var nestedContentEl) && TryExtractTextContent(nestedContentEl, out var nestedText))
|
||||
{
|
||||
segments.Add(nestedText);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.SafeTryGetProperty("reasoning_content", out var reasoningEl) && TryExtractTextContent(reasoningEl, out var reasoningText))
|
||||
{
|
||||
segments.Add(reasoningText);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.SafeTryGetProperty("output_text", out var outputTextEl) && outputTextEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = outputTextEl.SafeGetString();
|
||||
if (!string.IsNullOrWhiteSpace(str))
|
||||
segments.Add(str);
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.Count == 0)
|
||||
return false;
|
||||
|
||||
text = string.Join("\n", segments.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
return !string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
private static bool TryParseContentArrayToolBlock(JsonElement block, out ContentBlock toolBlock)
|
||||
{
|
||||
toolBlock = default!;
|
||||
|
||||
if (block.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
var type = block.SafeTryGetProperty("type", out var typeEl) ? typeEl.SafeGetString() ?? "" : "";
|
||||
if (!string.Equals(type, "tool_use", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(type, "tool_call", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
JsonElement? parsedArgs = null;
|
||||
if (block.SafeTryGetProperty("input", out var inputEl))
|
||||
parsedArgs = inputEl.Clone();
|
||||
else if (block.SafeTryGetProperty("arguments", out var argsEl))
|
||||
{
|
||||
if (argsEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var argsDoc = JsonDocument.Parse(argsEl.SafeGetString() ?? "{}");
|
||||
parsedArgs = argsDoc.RootElement.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
parsedArgs = null;
|
||||
}
|
||||
}
|
||||
else if (argsEl.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
|
||||
{
|
||||
parsedArgs = argsEl.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
var toolName = block.SafeTryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
return false;
|
||||
|
||||
toolBlock = new ContentBlock
|
||||
{
|
||||
Type = "tool_use",
|
||||
ToolName = toolName,
|
||||
ToolId = block.SafeTryGetProperty("id", out var idEl)
|
||||
? idEl.SafeGetString() ?? Guid.NewGuid().ToString("N")[..12]
|
||||
: Guid.NewGuid().ToString("N")[..12],
|
||||
ToolInput = parsedArgs,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LooksLikeCompleteJson(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
|
||||
Reference in New Issue
Block a user