From 25f84686964178230d69005bae497223b915b5d5 Mon Sep 17 00:00:00 2001 From: narawat Date: Sun, 16 Aug 2026 17:24:29 +0700 Subject: [PATCH] text message process works --- src/agentCore.jl | 29 +++++++++++++++++++++++------ src/type.jl | 23 +++++++++++++---------- test/_extractToolCalls.jl | 6 +++++- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 5284d54..09c4297 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -227,8 +227,11 @@ function _agentLoop(agent::yiemAgent) else # _processMessage() done and no followUp message. agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))") result = agent._state.messages[end] + filtered_content = [c for c in result.content if !(c isa reasoningContent)] put!(agent.outputChannel, result) - agent.agentEventSink(result.content[1].text) + if !isempty(filtered_content) && filtered_content[1] isa textContent + agent.agentEventSink(filtered_content[1].text) + end processingTask = nothing # reset newUserMsg = nothing # reset result = nothing # reset @@ -381,7 +384,7 @@ function _processMessage( # Extract tool calls from LLM response content blocks hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response) agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))") - agentEventSink(string(assistant_msg)) + agentEventSink("assistant_msg " * string(assistant_msg)) agentEventSink("_processMessage 11-1") # Add assistant message (tool calls or text) to history for next LLM turn @@ -406,6 +409,7 @@ function _processMessage( # save toolResults to messages for toolResult in toolResultBatch.messages + agentEventSink("toolResult " * string(toolResult)) push!(agentMsgHistory, toolResult) end agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))") @@ -554,7 +558,9 @@ The message is constructed from: `(hasToolCalls, toolCallList, message)` where message is `assistantMessageToolCall` when tool calls exist, `assistantMessage` otherwise -# Example response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")])) +# Example + 1) llm_useTool = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")])) + 2) llm_notUseTool = JSON.Object{String, Any}("finish_reason" => "stop", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "The weather in London, UK is currently Sunny with a temperature of 22°C.", "reasoning_content" => "The user asked for the weather in London.\nI called the `getWeather` tool for London, UK.\nThe response indicates it's Sunny and 22°C.\nI will convey this information to the user.\n")) """ function _extractToolCalls(response) hasToolCalls = false @@ -642,14 +648,15 @@ function _extractToolCalls(response) end if reasoning_text isa String reasoning_text = reasoning_text - elseif reasoning_text isa textContent + elseif reasoning_text isa reasoningContent reasoning_text = reasoning_text.text else reasoning_text = "" end - reasoning_content = !isempty(reasoning_text) ? [textContent(reasoning_text)] : textContent[] + reasoning_content = !isempty(reasoning_text) ? [reasoningContent(reasoning_text)] : reasoningContent[] # Collect content blocks from response.content array (Format 2: OpenAI-style) + # or as a plain string (Format 1: LMStudio.jl non-tool-calls response) content_from_response = get(response, "content", nothing) content_blocks = Vector{messageContent}() if content_from_response isa Vector @@ -662,12 +669,22 @@ function _extractToolCalls(response) elseif get(block, "type", "") == "tool_calls" # tool_calls blocks — don't add text content for these elseif get(block, "type", "") == "reasoning" - push!(content_blocks, textContent(get(block, "text", ""))) + push!(content_blocks, reasoningContent(get(block, "text", ""))) else push!(content_blocks, textContent(get(block, "text", ""))) end end end + elseif content_from_response isa AbstractString && !isempty(content_from_response) + push!(content_blocks, textContent(content_from_response)) + end + + # Format 1 fallback: response["message"]["content"] as plain string + if isempty(content_blocks) && msg !== nothing && msg isa AbstractDict + msg_content = get(msg, "content", nothing) + if msg_content isa AbstractString && !isempty(msg_content) + push!(content_blocks, textContent(msg_content)) + end end # Combine reasoning_content field + content array blocks, deduplicating reasoning diff --git a/src/type.jl b/src/type.jl index d7eee74..bc224af 100644 --- a/src/type.jl +++ b/src/type.jl @@ -4,8 +4,8 @@ messageContent, agentMessage, agent, # Model types modelCost, llmModel, llmUsage, - # Message content types - textContent, imageContent, + # Message content types + textContent, imageContent, reasoningContent, # Message types userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage, # Tool types @@ -31,6 +31,13 @@ using GeneralUtils const Timestamp = DateTime +struct agentToolCall # A tool invocation from the LLM + type::String # Always "function" + id::String # Unique tool call identifier + name::String # Tool name + arguments::Dict{String, Any} # Parsed tool arguments +end + # ------------------------------------------------------------------------------------------------ # # LLM model info # # ------------------------------------------------------------------------------------------------ # @@ -75,6 +82,10 @@ struct imageContent <: messageContent # Image message content mimeType::String # MIME type (e.g., "image/png") end +struct reasoningContent <: messageContent # LLM reasoning/thinking content + text::String # The reasoning text +end + # ------------------------------------------------------------------------------------------------ # # Message types # @@ -404,14 +415,6 @@ function agentState( end -struct agentToolCall # A tool invocation from the LLM - type::String # Always "function" - id::String # Unique tool call identifier - name::String # Tool name - arguments::Dict{String, Any} # Parsed tool arguments -end - - """ Context for preparing the next conversation turn. diff --git a/test/_extractToolCalls.jl b/test/_extractToolCalls.jl index 94abd46..6540f26 100644 --- a/test/_extractToolCalls.jl +++ b/test/_extractToolCalls.jl @@ -44,7 +44,7 @@ import YiemAgent.agentCore: _extractToolCalls @test assistant_msg.role == "assistant" @test assistant_msg.stopReason == "tool_calls" @test length(assistant_msg.content) == 1 - @test assistant_msg.content[1] isa textContent + @test assistant_msg.content[1] isa reasoningContent @test assistant_msg.content[1].text == "Let me check the weather." end @@ -241,7 +241,9 @@ import YiemAgent.agentCore: _extractToolCalls @test has_toolcalls == false @test length(tc_list) == 0 @test length(assistant_msg.content) == 2 + @test assistant_msg.content[1] isa reasoningContent @test assistant_msg.content[1].text == "Thinking..." + @test assistant_msg.content[2] isa textContent @test assistant_msg.content[2].text == "Here's the answer." end @@ -307,7 +309,9 @@ import YiemAgent.agentCore: _extractToolCalls ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test length(assistant_msg.content) == 2 + @test assistant_msg.content[1] isa reasoningContent @test assistant_msg.content[1].text == "internal thought" + @test assistant_msg.content[2] isa textContent @test assistant_msg.content[2].text == "output" end