using Test using YiemAgent using YiemAgent.agentCore using YiemAgent.type using JSON # Import the function from the private module scope import YiemAgent.agentCore: _extractToolCalls @testset "_extractToolCalls" begin # ------------------------------------------------------------------ # # Format 1: response["message"]["tool_calls"] (LMStudio.jl style) # # ------------------------------------------------------------------ # @testset "single tool call via message format" begin response = Dict{String,Any}( "finish_reason" => "tool_calls", "index" => 0, "message" => Dict{String,Any}( "role" => "assistant", "content" => "", "reasoning_content" => "Let me check the weather.", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}", ), "id" => "tc_001", ) ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getWeather" @test tc_list[1].id == "tc_001" @test tc_list[1].type == "function" @test tc_list[1].arguments["city"] == "Bangkok, Thailand" @test assistant_msg isa assistantMessage @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].text == "Let me check the weather." end @testset "multiple tool calls via message format" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "content" => "", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => "{\"city\":\"Tokyo, Japan\"}", ), "id" => "tc_001", ), Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getTime", "arguments" => "{\"timezone\":\"Asia/Tokyo\"}", ), "id" => "tc_002", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 2 @test tc_list[1].name == "getWeather" @test tc_list[1].arguments["city"] == "Tokyo, Japan" @test tc_list[2].name == "getTime" @test tc_list[2].arguments["timezone"] == "Asia/Tokyo" @test assistant_msg.role == "assistant" end @testset "tool call with empty arguments string" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "listTools", "arguments" => "{}", ), "id" => "tc_empty", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "listTools" @test tc_list[1].arguments == Dict{String,Any}() @test assistant_msg.stopReason == "end_turn" end @testset "tool call with missing id falls back to uuid" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getTime", "arguments" => "{\"city\":\"NYC\"}", ), ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test !isempty(tc_list[1].id) @test tc_list[1].name == "getTime" end @testset "tool call with non-string arguments (pre-parsed dict)" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => Dict{String,Any}("city" => "London", "units" => "fahrenheit"), ), "id" => "tc_parsed", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].arguments["city"] == "London" @test tc_list[1].arguments["units"] == "fahrenheit" end @testset "tool call with api/provider/model/usage metadata" begin response = Dict{String,Any}( "api" => "openai", "provider" => "anthropic", "model" => "claude-3-opus", "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getTime", "arguments" => "{}", ), "id" => "tc_meta", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test assistant_msg.api == "openai" @test assistant_msg.provider == "anthropic" @test assistant_msg.model == "claude-3-opus" end # --------------------------------------------------------------- # # Format 2: response.content blocks (OpenAI API style) # # --------------------------------------------------------------- # @testset "content blocks with tool_calls" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}("type" => "text", "text" => "Let me check."), Dict{String,Any}( "type" => "tool_calls", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => "{\"city\":\"Paris\"}", ), "id" => "tc_block_1", ), ], ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getWeather" @test tc_list[1].arguments["city"] == "Paris" # text block before tool_calls should be included in content @test length(assistant_msg.content) == 1 @test assistant_msg.content[1].text == "Let me check." end @testset "content blocks with tool_call (single-call format)" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}( "type" => "tool_call", "id" => "tc_single", "name" => "getTime", "arguments" => Dict{String,Any}("timezone" => "Europe/London"), ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getTime" @test tc_list[1].id == "tc_single" @test tc_list[1].arguments["timezone"] == "Europe/London" end @testset "content blocks with reasoning and text" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}("type" => "reasoning", "text" => "Thinking..."), Dict{String,Any}("type" => "text", "text" => "Here's the answer."), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 @test length(assistant_msg.content) == 2 @test assistant_msg.content[1].text == "Thinking..." @test assistant_msg.content[2].text == "Here's the answer." end @testset "content blocks with text and tool_call (tool_call not in content)" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}("type" => "text", "text" => "Sure, I'll check."), Dict{String,Any}( "type" => "tool_call", "id" => "tc_mix", "name" => "getWeather", "arguments" => Dict{String,Any}("city" => "London"), ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getWeather" # text block included, tool_call block excluded from content @test length(assistant_msg.content) == 1 @test assistant_msg.content[1].text == "Sure, I'll check." end # ------------------------------------------------------------------ # # assistantMessage construction # # ------------------------------------------------------------------ # @testset "assistantMessage with error_message and errorMessage fallback" begin response = Dict{String,Any}( "error_message" => "rate limit", "content" => Any[Dict{String,Any}("type" => "text", "text" => "fail")], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test assistant_msg.errorMessage == "rate limit" end @testset "assistantMessage with usage tracking" begin response = Dict{String,Any}( "content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")], "usage" => llmUsage(100, 50), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test assistant_msg.usage.inputTokens == 100 @test assistant_msg.usage.outputTokens == 50 end @testset "assistantMessage with invalid usage defaults to zero" begin response = Dict{String,Any}( "content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")], "usage" => "invalid", ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test assistant_msg.usage.inputTokens == 0 @test assistant_msg.usage.outputTokens == 0 end @testset "reasoning_content as textContent" begin response = Dict{String,Any}( "reasoning_content" => textContent("internal thought"), "content" => Any[Dict{String,Any}("type" => "text", "text" => "output")], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test length(assistant_msg.content) == 2 @test assistant_msg.content[1].text == "internal thought" @test assistant_msg.content[2].text == "output" end # ------------------------------------------------------------------ # # No tool call cases # # ------------------------------------------------------------------ # @testset "no tool calls found" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}("type" => "text", "text" => "Hello world."), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 @test assistant_msg.stopReason == "end_turn" end @testset "empty message" begin response = Dict{String,Any}() has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 @test assistant_msg.role == "assistant" @test assistant_msg.stopReason == "end_turn" @test length(assistant_msg.content) == 0 end @testset "message with empty tool_calls array" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 @test assistant_msg.role == "assistant" end @testset "message field is not a Dict" begin response = Dict{String,Any}( "message" => "not a dict", "content" => Any[Dict{String,Any}("type" => "text", "text" => "fallback")], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 @test length(assistant_msg.content) == 1 end @testset "Format 1 takes priority over Format 2" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => "{\"city\":\"Format1\"}", ), "id" => "tc_fmt1", ), ], ), "content" => Any[ Dict{String,Any}( "type" => "tool_call", "id" => "tc_fmt2", "name" => "getTime", "arguments" => Dict{String,Any}("city" => "Format2"), ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getWeather" end # ------------------------------------------------------------------ # # Edge cases # # ------------------------------------------------------------------ # @testset "tool call with null arguments" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getTime", "arguments" => nothing, ), "id" => "tc_null", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getTime" end @testset "tool call with missing function key" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "id" => "tc_nofunc", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "" end @testset "tool call with missing name in function block" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "assistant", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}("arguments" => "{}"), "id" => "tc_noname", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "" end @testset "message format with JSON.Object (JSON.parse result)" begin json_str = JSON.json(Dict( "message" => Dict( "role" => "assistant", "tool_calls" => [ Dict( "type" => "function", "function" => Dict("name" => "getWeather", "arguments" => "{\"city\":\"Test\"}"), "id" => "tc_jsonobj", ), ], ), )) parsed = JSON.parse(json_str) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(parsed) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getWeather" @test tc_list[1].arguments["city"] == "Test" end @testset "tool_calls block with mixed content types (text + tool_calls)" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}("type" => "text", "text" => "I'll check both."), Dict{String,Any}( "type" => "tool_calls", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => "{\"city\":\"London\"}", ), "id" => "tc_mix1", ), Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getTime", "arguments" => "{\"timezone\":\"UTC\"}", ), "id" => "tc_mix2", ), ], ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 2 @test tc_list[1].name == "getWeather" @test tc_list[2].name == "getTime" @test length(assistant_msg.content) == 1 @test assistant_msg.content[1].text == "I'll check both." end @testset "tool_call block without arguments field" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}( "type" => "tool_call", "id" => "tc_noargs", "name" => "getTime", ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].arguments == Dict{String,Any}() end @testset "tool_calls block with empty tool_calls array" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}( "type" => "tool_calls", "tool_calls" => Any[], ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 end @testset "tool_calls block with non-AbstractDict elements" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}( "type" => "tool_calls", "tool_calls" => Any["not a dict", 42, nothing], ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 end @testset "content field is not a Vector" begin response = Dict{String,Any}( "content" => "not a vector", ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(tc_list) == 0 @test length(assistant_msg.content) == 0 end @testset "Dict-based response with all metadata fields" begin response = Dict{String,Any}( "api" => "openai", "provider" => "anthropic", "model" => "claude-3-sonnet", "content" => Any[ Dict{String,Any}( "type" => "tool_call", "id" => "tc_meta", "name" => "getTime", "arguments" => Dict{String,Any}("city" => "Seoul"), ), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == true @test length(tc_list) == 1 @test tc_list[1].name == "getTime" @test tc_list[1].arguments["city"] == "Seoul" @test assistant_msg.api == "openai" @test assistant_msg.provider == "anthropic" @test assistant_msg.model == "claude-3-sonnet" end @testset "default role is assistant" begin response = Dict{String,Any}( "content" => Any[Dict{String,Any}("type" => "text", "text" => "no role specified")], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test assistant_msg.role == "assistant" end @testset "tool call with custom role in message format" begin response = Dict{String,Any}( "message" => Dict{String,Any}( "role" => "custom_role", "tool_calls" => Any[ Dict{String,Any}( "type" => "function", "function" => Dict{String,Any}( "name" => "getWeather", "arguments" => "{}", ), "id" => "tc_role", ), ], ), ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test assistant_msg.role == "custom_role" end @testset "image content block handling" begin response = Dict{String,Any}( "content" => Any[ Dict{String,Any}( "type" => "image_url", "image_url" => Dict("url" => "data:image/png;base64,abc123"), ), Dict{String,Any}("type" => "text", "text" => "What is this?"), ], ) has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response) @test has_toolcalls == false @test length(assistant_msg.content) == 2 @test assistant_msg.content[1] isa textContent @test assistant_msg.content[1].text == "" @test assistant_msg.content[2].text == "What is this?" end end