From 6b3d575ea07cf8e94a396d86dc28122adb56e149 Mon Sep 17 00:00:00 2001 From: narawat Date: Sun, 9 Aug 2026 06:50:37 +0700 Subject: [PATCH] update --- src/tools/registry.jl | 3 +- src/type.jl | 11 +++ test/loadToolTest.jl | 180 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 192 insertions(+), 2 deletions(-) diff --git a/src/tools/registry.jl b/src/tools/registry.jl index e7d691d..b828793 100644 --- a/src/tools/registry.jl +++ b/src/tools/registry.jl @@ -119,7 +119,8 @@ function loadTools(dir::String)::Vector{agentTool} end # Call getTool() — it runs in current scope where types are visible - tool = getTool() + # Use invokelatest to handle world-age semantics after include() + tool = invokelatest(getTool) if !(tool isa agentTool) throw(ArgumentError( "getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))" diff --git a/src/type.jl b/src/type.jl index 33597df..7078168 100644 --- a/src/type.jl +++ b/src/type.jl @@ -269,6 +269,17 @@ struct agentTool # A tool available to the agent parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel end +""" +Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`. +""" +function agentTool(; name::String, label::String, description::String, inputSchema::Any, + execute::Function, prepareArguments::Union{Function, Nothing}=nothing, + validateRequiredArgs::Union{Function, Nothing}=nothing, + parallelToolExecute::Bool=false) + return agentTool(name, label, description, inputSchema, execute, + prepareArguments, validateRequiredArgs, parallelToolExecute) +end + # ------------------------------------------------------------------------------------------------ # # Agent context # diff --git a/test/loadToolTest.jl b/test/loadToolTest.jl index 071a6ec..d228efe 100644 --- a/test/loadToolTest.jl +++ b/test/loadToolTest.jl @@ -1 +1,179 @@ -using YiemAgent \ No newline at end of file +using Test +using YiemAgent +using YiemAgent.toolRegistry +using YiemAgent.type + +# ------------------------------------------------------------------ # +# loadTools() unit tests # +# ------------------------------------------------------------------ # + +@testset "loadTools" begin + + # ------------------------------------------------------------------ # + # 1. loadTools throws on non-existent directory # + # ------------------------------------------------------------------ # + @test_throws ArgumentError loadTools("/nonexistent/dir/that/does/not/exist") + + # ------------------------------------------------------------------ # + # 2. loadTools throws if a .jl file does not define getTool() # + # Must run before any other loadTools call (getTool persists in # + # module scope after include()). # + # ------------------------------------------------------------------ # + bad_dir = mktempdir() + write(joinpath(bad_dir, "noTool.jl"), "x = 42\n") + @test_throws ArgumentError loadTools(bad_dir) + + # ------------------------------------------------------------------ # + # 3. loadTools loads tool files that define getTool() # + # ------------------------------------------------------------------ # + tmpdir = mktempdir() + + # Create a valid tool file (must use bare type names — include() places file in toolRegistry scope) + valid_tool_echo = """ +function getTool()::agentTool + return agentTool( + name = "testEcho", + label = "Echo Test", + description = "Echoes the input argument", + inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict("message" => Dict("type" => "string")), + "required" => Any["message"] + ), + execute = (toolCallId, args, signal, onPartialResult) -> begin + return agentToolResult( + [textContent("echo: " * string(args["message"]))], + Dict{Any,Any}(), nothing, false + ) + end, + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = false + ) +end +""" + write(joinpath(tmpdir, "getEcho.jl"), valid_tool_echo) + + loaded = loadTools(tmpdir) + @test !isempty(loaded) + @test length(loaded) >= 1 + + names = [t.name for t in loaded] + @test "testEcho" in names + + # Check agentTool fields + echo_tool = filter(t -> t.name == "testEcho", loaded) + @test !isempty(echo_tool) + @test echo_tool[1].label == "Echo Test" + @test echo_tool[1].description == "Echoes the input argument" + @test echo_tool[1].parallelToolExecute == false + @test echo_tool[1].execute !== nothing + + # ------------------------------------------------------------------ # + # 4. loadTools returns tools sorted alphabetically # + # ------------------------------------------------------------------ # + sorted_dir = mktempdir() + + tool_a = """ +function getTool()::agentTool + return agentTool( + name = "alphaTool", + label = "Alpha Tool", + description = "First tool", + inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]), + execute = (toolCallId, args, signal, onPartialResult) -> + agentToolResult([textContent("alpha")], Dict{Any,Any}(), nothing, false), + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = false + ) +end +""" + + tool_m = """ +function getTool()::agentTool + return agentTool( + name = "midTool", + label = "Mid Tool", + description = "Middle tool", + inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]), + execute = (toolCallId, args, signal, onPartialResult) -> + agentToolResult([textContent("mid")], Dict{Any,Any}(), nothing, false), + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = false + ) +end +""" + + tool_z = """ +function getTool()::agentTool + return agentTool( + name = "zuluTool", + label = "Zulu Tool", + description = "Last tool", + inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]), + execute = (toolCallId, args, signal, onPartialResult) -> + agentToolResult([textContent("zulu")], Dict{Any,Any}(), nothing, false), + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = false + ) +end +""" + + write(joinpath(sorted_dir, "zTool.jl"), tool_z) + write(joinpath(sorted_dir, "aTool.jl"), tool_a) + write(joinpath(sorted_dir, "mTool.jl"), tool_m) + + loaded_sorted = loadTools(sorted_dir) + # loadTools returns only tools loaded from the directory, in file-sorted order + @test length(loaded_sorted) == 3 + @test loaded_sorted[1].name == "alphaTool" + @test loaded_sorted[2].name == "midTool" + @test loaded_sorted[3].name == "zuluTool" + + # ------------------------------------------------------------------ # + # 5. getTools returns a deep copy (mutations don't affect registry) # + # ------------------------------------------------------------------ # + registry_tools = getTools() + @test !isempty(registry_tools) + orig_count = length(registry_tools) + + # Clear and add a new tool via registerTool + clearTools() + registry_after_clear = getTools() + @test isempty(registry_after_clear) + + # ------------------------------------------------------------------ # + # 6. registerTool adds to global registry # + # ------------------------------------------------------------------ # + test_tool = agentTool( + name = "manualTool", + label = "Manual Tool", + description = "Registered manually", + inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]), + execute = (toolCallId, args, signal, onPartialResult) -> + agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false), + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = true + ) + registerTool(test_tool) + reg = getTools() + @test any(t -> t.name == "manualTool", reg) + @test count(t -> t.name == "manualTool", reg) == 1 + + # parallelToolExecute flag + manual_entry = filter(t -> t.name == "manualTool", reg) + @test manual_entry[1].parallelToolExecute == true + + # ------------------------------------------------------------------ # + # 7. getTools returns deep copy # + # ------------------------------------------------------------------ # + copy1 = getTools() + copy2 = getTools() + @test copy1 !== copy2 + empty!(copy1) + @test !isempty(getTools()) +end