update
This commit is contained in:
@@ -1,2 +1 @@
|
|||||||
# ── executeToolCalls() Julia pseudo code ──────────────────────────
|
run test again. loadToolTest.jl should be able to load all tools in ./src/tools and test them.
|
||||||
# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit
|
|
||||||
+30
-13
@@ -36,6 +36,35 @@ function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
|||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Execute the getTime tool.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `toolCallId::String`: Unique identifier for this tool call
|
||||||
|
- `args::Dict{String,Any}`: Parsed arguments from the LLM
|
||||||
|
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
|
||||||
|
- `onPartialResult::Function`: Callback for streaming partial results
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `agentToolResult`: Result content with current time data
|
||||||
|
"""
|
||||||
|
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||||
|
tz = get(args, "timezone", nothing)
|
||||||
|
city = get(args, "city", "")
|
||||||
|
|
||||||
|
# Simulate time lookup — replace with actual timezone API call
|
||||||
|
if tz !== nothing
|
||||||
|
result = "Current time in $(tz): $(now())"
|
||||||
|
else
|
||||||
|
result = "Current time in $(city): $(now())"
|
||||||
|
end
|
||||||
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent(result)],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Define and return the getTime agentTool.
|
Define and return the getTime agentTool.
|
||||||
"""
|
"""
|
||||||
@@ -52,19 +81,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => []
|
"required" => []
|
||||||
),
|
),
|
||||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
execute = executeTool,
|
||||||
tz = get(args, "timezone", nothing)
|
|
||||||
city = get(args, "city", "")
|
|
||||||
if tz !== nothing
|
|
||||||
result = "Current time in $(tz): $(now())"
|
|
||||||
else
|
|
||||||
result = "Current time in $(city): $(now())"
|
|
||||||
end
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(result)],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = validateRequiredArgs,
|
validateRequiredArgs = validateRequiredArgs,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
|
|||||||
+31
-10
@@ -1,3 +1,33 @@
|
|||||||
|
"""
|
||||||
|
Execute the getWeather tool.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `toolCallId::String`: Unique identifier for this tool call
|
||||||
|
- `args::Dict{String,Any}`: Parsed arguments from the LLM
|
||||||
|
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
|
||||||
|
- `onPartialResult::Function`: Callback for streaming partial results
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `agentToolResult`: Result content with weather data
|
||||||
|
"""
|
||||||
|
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||||
|
city = get(args, "city", "")
|
||||||
|
units = get(args, "units", "celsius")
|
||||||
|
|
||||||
|
# Simulate weather fetch — replace with actual API call
|
||||||
|
# You can call onPartialResult() here for streaming progress updates:
|
||||||
|
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
||||||
|
# onPartialResult(Dict("status" => "Processing..."))
|
||||||
|
|
||||||
|
temp = units == "fahrenheit" ? "72" : "22"
|
||||||
|
unit_symbol = units == "celsius" ? "°C" : "°F"
|
||||||
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Define and return the getWeather agentTool.
|
Define and return the getWeather agentTool.
|
||||||
"""
|
"""
|
||||||
@@ -14,16 +44,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => ["city"]
|
"required" => ["city"]
|
||||||
),
|
),
|
||||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
execute = executeTool, # reference the function defined above
|
||||||
city = get(args, "city", "")
|
|
||||||
units = get(args, "units", "celsius")
|
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
|
||||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
|
|||||||
+32
-39
@@ -44,7 +44,8 @@ function _listTool()::agentTool
|
|||||||
end,
|
end,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false,
|
||||||
|
_tool_module = nothing
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -55,36 +56,9 @@ Scans `dir` for `.jl` files. Each file must define a function named
|
|||||||
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
||||||
registration order is deterministic.
|
registration order is deterministic.
|
||||||
|
|
||||||
# Tool file format
|
Each tool file is loaded into its own isolated module (a child of `toolRegistry`)
|
||||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`:
|
so that functions like `executeTool` and `validateRequiredArgs` do not collide
|
||||||
|
across tool files. The module reference is stored in `agentTool._tool_module`.
|
||||||
```julia
|
|
||||||
# src/tools/getWeather.jl
|
|
||||||
function getTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "getWeather",
|
|
||||||
label = "Weather Lookup",
|
|
||||||
description = "Fetch current weather and forecast for a given city.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"city" => Dict("type" => "string", "description" => "City and country"),
|
|
||||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
|
|
||||||
),
|
|
||||||
"required" => ["city"]
|
|
||||||
),
|
|
||||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
|
||||||
city = args["city"]
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Sunny, 22C in Bangkok")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `dir::String`: Directory path to scan for `.jl` tool files
|
- `dir::String`: Directory path to scan for `.jl` tool files
|
||||||
@@ -108,27 +82,46 @@ function loadTools(dir::String)::Vector{agentTool}
|
|||||||
filepath = joinpath(dir, filename)
|
filepath = joinpath(dir, filename)
|
||||||
println("[toolRegistry] Loading tool from: $filepath")
|
println("[toolRegistry] Loading tool from: $filepath")
|
||||||
|
|
||||||
# Include the file in the current module scope so all types resolve
|
# Create a unique module for this tool (child of toolRegistry so ...type resolves)
|
||||||
# (agentTool, textContent, agentToolResult, etc. are all available)
|
base_name = replace(filename, ".jl" => "")
|
||||||
include(filepath)
|
mod_name = Symbol("Tool_", replace(base_name, r"[^a-zA-Z0-9_]" => "_"))
|
||||||
|
|
||||||
# Validate that getTool was defined (include() places it in current module scope)
|
Core.eval(toolRegistry, :(module $mod_name
|
||||||
if !isdefined(@__MODULE__, :getTool)
|
using ...type
|
||||||
|
using Dates
|
||||||
|
using JSON
|
||||||
|
end))
|
||||||
|
tool_mod = getfield(toolRegistry, mod_name)
|
||||||
|
|
||||||
|
# Include the tool file — defines getTool() and executeTool() in tool_mod
|
||||||
|
Base.include(tool_mod, filepath)
|
||||||
|
|
||||||
|
# Validate that getTool was defined
|
||||||
|
if !isdefined(tool_mod, :getTool)
|
||||||
throw(ArgumentError(
|
throw(ArgumentError(
|
||||||
"Tool file $(filepath) does not define a `getTool()` function. " *
|
"Tool file $(filepath) does not define a `getTool()` function. " *
|
||||||
"Each tool file must define: function getTool()::agentTool ... end"
|
"Each tool file must define: function getTool()::agentTool ... end"
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Call getTool() — it runs in current scope where types are visible
|
# Call getTool() in the tool module's scope
|
||||||
# Use invokelatest to handle world-age semantics after include()
|
# Use invokelatest to handle world-age semantics after include()
|
||||||
tool = invokelatest(getTool)
|
tool = invokelatest(getfield(tool_mod, :getTool))
|
||||||
|
|
||||||
if !(tool isa agentTool)
|
if !(tool isa agentTool)
|
||||||
throw(ArgumentError(
|
throw(ArgumentError(
|
||||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Reconstruct tool with _tool_module (agentTool is immutable)
|
||||||
|
tool = agentTool(; name=tool.name, label=tool.label, description=tool.description,
|
||||||
|
inputSchema=tool.inputSchema, execute=tool.execute,
|
||||||
|
prepareArguments=tool.prepareArguments,
|
||||||
|
validateRequiredArgs=tool.validateRequiredArgs,
|
||||||
|
parallelToolExecute=tool.parallelToolExecute,
|
||||||
|
_tool_module=tool_mod)
|
||||||
|
|
||||||
push!(_registry, tool)
|
push!(_registry, tool)
|
||||||
push!(tools, tool)
|
push!(tools, tool)
|
||||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
||||||
@@ -159,7 +152,7 @@ Get all registered tools.
|
|||||||
- `Vector{agentTool}`: Copy of the registry
|
- `Vector{agentTool}`: Copy of the registry
|
||||||
"""
|
"""
|
||||||
function getTools()::Vector{agentTool}
|
function getTools()::Vector{agentTool}
|
||||||
return deepcopy(_registry)
|
return [t for t in _registry] # new vector with same references (agentTool is immutable, Modules can't be deepcopied)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
+145
-139
@@ -100,6 +100,150 @@ function json_string(v)::String
|
|||||||
return JSON.json(v)
|
return JSON.json(v)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Execute the writeTool.
|
||||||
|
|
||||||
|
Generates a new .jl tool file and registers it with the tool registry.
|
||||||
|
"""
|
||||||
|
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||||
|
tool_name = get(args, "name", "")::String
|
||||||
|
tool_label = get(args, "label", tool_name)::String
|
||||||
|
tool_description = get(args, "description", "")::String
|
||||||
|
tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any}
|
||||||
|
execute_code = get(args, "executeCode", "")::String
|
||||||
|
validate_code = get(args, "validateCode", nothing)::Union{String,Nothing}
|
||||||
|
prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing}
|
||||||
|
parallel = get(args, "parallel", false)::Bool
|
||||||
|
|
||||||
|
# Validate tool name
|
||||||
|
name_err = validateToolName(tool_name)
|
||||||
|
if name_err !== nothing
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent(name_err)],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
if isempty(tool_name)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Missing required field: 'name'")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if isempty(tool_description)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Missing required field: 'description'")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if isempty(execute_code)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Missing required field: 'executeCode'")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
onPartialResult(Dict("status" => "Generating tool: $tool_name"))
|
||||||
|
|
||||||
|
# Build the tool file path
|
||||||
|
tools_dir = dirname(@__FILE__) # src/tools/
|
||||||
|
filepath = joinpath(tools_dir, "$(tool_name).jl")
|
||||||
|
|
||||||
|
# Check for naming conflicts
|
||||||
|
if isfile(filepath)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
onPartialResult(Dict("status" => "Writing file: $(basename(filepath))"))
|
||||||
|
|
||||||
|
# Convert schema Dict to a Julia Dict literal string
|
||||||
|
schema_literal = dict_to_julia_literal(tool_schema)
|
||||||
|
|
||||||
|
# Build optional validation function
|
||||||
|
validate_section = if validate_code !== nothing && !isempty(validate_code)
|
||||||
|
indented = indent_code(validate_code, 4)
|
||||||
|
"function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n"
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build optional prepare function
|
||||||
|
prepare_section = if prepare_code !== nothing && !isempty(prepare_code)
|
||||||
|
indented = indent_code(prepare_code, 4)
|
||||||
|
"function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n"
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Indent user's execute code for embedding inside execute function body
|
||||||
|
indented_exec = indent_code(execute_code, 4)
|
||||||
|
|
||||||
|
# Escape description for Julia string literal
|
||||||
|
escaped_desc = replace(tool_description, "\\" => "\\\\")
|
||||||
|
escaped_desc = replace(escaped_desc, "\"" => "\\\"")
|
||||||
|
|
||||||
|
# Build the complete tool file content
|
||||||
|
parts = String[]
|
||||||
|
push!(parts, "# Auto-generated tool: $tool_name\n")
|
||||||
|
push!(parts, "# Generated by writeTool at $(now())\n\n")
|
||||||
|
if !isempty(validate_section)
|
||||||
|
push!(parts, validate_section)
|
||||||
|
push!(parts, "\n")
|
||||||
|
end
|
||||||
|
if !isempty(prepare_section)
|
||||||
|
push!(parts, prepare_section)
|
||||||
|
push!(parts, "\n")
|
||||||
|
end
|
||||||
|
push!(parts, "\n")
|
||||||
|
push!(parts, "# Execute function\n")
|
||||||
|
push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n")
|
||||||
|
push!(parts, "$indented_exec\n")
|
||||||
|
push!(parts, "end\n\n")
|
||||||
|
push!(parts, "# Tool definition\n")
|
||||||
|
push!(parts, "function getTool()::agentTool\n")
|
||||||
|
push!(parts, " return agentTool(\n")
|
||||||
|
push!(parts, " name = \"$(tool_name)\",\n")
|
||||||
|
push!(parts, " label = \"$(tool_label)\",\n")
|
||||||
|
push!(parts, " description = \"$(escaped_desc)\",\n")
|
||||||
|
push!(parts, " inputSchema = $schema_literal,\n")
|
||||||
|
push!(parts, " execute = executeTool,\n")
|
||||||
|
if validate_code !== nothing && !isempty(validate_code)
|
||||||
|
push!(parts, " validateRequiredArgs = validateRequiredArgs,\n")
|
||||||
|
else
|
||||||
|
push!(parts, " validateRequiredArgs = nothing,\n")
|
||||||
|
end
|
||||||
|
if prepare_code !== nothing && !isempty(prepare_code)
|
||||||
|
push!(parts, " prepareArguments = prepareArguments,\n")
|
||||||
|
else
|
||||||
|
push!(parts, " prepareArguments = nothing,\n")
|
||||||
|
end
|
||||||
|
push!(parts, " parallelToolExecute = $parallel\n")
|
||||||
|
push!(parts, " )\n")
|
||||||
|
push!(parts, "end\n")
|
||||||
|
|
||||||
|
tool_code = join(parts)
|
||||||
|
|
||||||
|
# Write the file — tool is loaded on next agent restart via loadTools()
|
||||||
|
write(filepath, tool_code)
|
||||||
|
|
||||||
|
onPartialResult(Dict("status" => "Done"))
|
||||||
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")],
|
||||||
|
Dict{Any,Any}(
|
||||||
|
"file" => filepath,
|
||||||
|
"name" => tool_name,
|
||||||
|
"label" => tool_label,
|
||||||
|
"description" => tool_description,
|
||||||
|
),
|
||||||
|
nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Define and return the writeTool agentTool.
|
Define and return the writeTool agentTool.
|
||||||
"""
|
"""
|
||||||
@@ -125,145 +269,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
||||||
),
|
),
|
||||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin
|
execute = executeTool,
|
||||||
tool_name = get(args, "name", "")::String
|
|
||||||
tool_label = get(args, "label", tool_name)::String
|
|
||||||
tool_description = get(args, "description", "")::String
|
|
||||||
tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any}
|
|
||||||
execute_code = get(args, "executeCode", "")::String
|
|
||||||
validate_code = get(args, "validateCode", nothing)::Union{String,Nothing}
|
|
||||||
prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing}
|
|
||||||
parallel = get(args, "parallel", false)::Bool
|
|
||||||
|
|
||||||
# Validate tool name
|
|
||||||
name_err = validateToolName(tool_name)
|
|
||||||
if name_err !== nothing
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(name_err)],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validate required fields
|
|
||||||
if isempty(tool_name)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'name'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if isempty(tool_description)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'description'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if isempty(execute_code)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'executeCode'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Generating tool: $tool_name"))
|
|
||||||
|
|
||||||
# Build the tool file path
|
|
||||||
script_dir = dirname(@__FILE__)
|
|
||||||
tools_dir = dirname(script_dir)
|
|
||||||
filepath = joinpath(tools_dir, "$(tool_name).jl")
|
|
||||||
|
|
||||||
# Check for naming conflicts
|
|
||||||
if isfile(filepath)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Writing file: $(basename(filepath))"))
|
|
||||||
|
|
||||||
# Convert schema Dict to a Julia Dict literal string
|
|
||||||
schema_literal = dict_to_julia_literal(tool_schema)
|
|
||||||
|
|
||||||
# Build optional validation function
|
|
||||||
validate_section = if validate_code !== nothing && !isempty(validate_code)
|
|
||||||
indented = indent_code(validate_code, 4)
|
|
||||||
"function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build optional prepare function
|
|
||||||
prepare_section = if prepare_code !== nothing && !isempty(prepare_code)
|
|
||||||
indented = indent_code(prepare_code, 4)
|
|
||||||
"function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Indent user's execute code for embedding inside execute function body
|
|
||||||
indented_exec = indent_code(execute_code, 4)
|
|
||||||
|
|
||||||
# Escape description for Julia string literal
|
|
||||||
escaped_desc = replace(tool_description, "\\" => "\\\\")
|
|
||||||
escaped_desc = replace(escaped_desc, "\"" => "\\\"")
|
|
||||||
|
|
||||||
# Build the complete tool file content
|
|
||||||
parts = String[]
|
|
||||||
push!(parts, "# Auto-generated tool: $tool_name\n")
|
|
||||||
push!(parts, "# Generated by writeTool at $(now())\n\n")
|
|
||||||
if !isempty(validate_section)
|
|
||||||
push!(parts, validate_section)
|
|
||||||
push!(parts, "\n")
|
|
||||||
end
|
|
||||||
if !isempty(prepare_section)
|
|
||||||
push!(parts, prepare_section)
|
|
||||||
push!(parts, "\n")
|
|
||||||
end
|
|
||||||
push!(parts, "\n")
|
|
||||||
push!(parts, "# Execute function\n")
|
|
||||||
push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n")
|
|
||||||
push!(parts, "$indented_exec\n")
|
|
||||||
push!(parts, "end\n\n")
|
|
||||||
push!(parts, "# Tool definition\n")
|
|
||||||
push!(parts, "function getTool()::agentTool\n")
|
|
||||||
push!(parts, " return agentTool(\n")
|
|
||||||
push!(parts, " name = \"$(tool_name)\",\n")
|
|
||||||
push!(parts, " label = \"$(tool_label)\",\n")
|
|
||||||
push!(parts, " description = \"$(escaped_desc)\",\n")
|
|
||||||
push!(parts, " inputSchema = $schema_literal,\n")
|
|
||||||
push!(parts, " execute = executeTool,\n")
|
|
||||||
if validate_code !== nothing && !isempty(validate_code)
|
|
||||||
push!(parts, " validateRequiredArgs = validateRequiredArgs,\n")
|
|
||||||
else
|
|
||||||
push!(parts, " validateRequiredArgs = nothing,\n")
|
|
||||||
end
|
|
||||||
if prepare_code !== nothing && !isempty(prepare_code)
|
|
||||||
push!(parts, " prepareArguments = prepareArguments,\n")
|
|
||||||
else
|
|
||||||
push!(parts, " prepareArguments = nothing,\n")
|
|
||||||
end
|
|
||||||
push!(parts, " parallelToolExecute = $parallel\n")
|
|
||||||
push!(parts, " )\n")
|
|
||||||
push!(parts, "end\n")
|
|
||||||
|
|
||||||
tool_code = join(parts)
|
|
||||||
|
|
||||||
# Write the file — tool is loaded on next agent restart via loadTools()
|
|
||||||
write(filepath, tool_code)
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Done"))
|
|
||||||
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")],
|
|
||||||
Dict{Any,Any}(
|
|
||||||
"file" => filepath,
|
|
||||||
"name" => tool_name,
|
|
||||||
"label" => tool_label,
|
|
||||||
"description" => tool_description,
|
|
||||||
),
|
|
||||||
nothing, false
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
|
|||||||
+3
-3
@@ -267,6 +267,7 @@ struct agentTool # A tool available to the agent
|
|||||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||||
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||||
|
_tool_module::Union{Module, Nothing} # Module where tool's executeTool is defined (for isolation)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -275,12 +276,11 @@ Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...
|
|||||||
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
||||||
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
|
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
|
||||||
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
||||||
parallelToolExecute::Bool=false)
|
parallelToolExecute::Bool=false, _tool_module::Union{Module,Nothing}=nothing)
|
||||||
return agentTool(name, label, description, inputSchema, execute,
|
return agentTool(name, label, description, inputSchema, execute,
|
||||||
prepareArguments, validateRequiredArgs, parallelToolExecute)
|
prepareArguments, validateRequiredArgs, parallelToolExecute, _tool_module)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
# Agent context #
|
# Agent context #
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
|
|||||||
+49
-69
@@ -3,7 +3,6 @@ using YiemAgent
|
|||||||
using YiemAgent.toolRegistry
|
using YiemAgent.toolRegistry
|
||||||
using YiemAgent.type
|
using YiemAgent.type
|
||||||
|
|
||||||
# Path to the real tools directory
|
|
||||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||||
|
|
||||||
@testset "loadTools" begin
|
@testset "loadTools" begin
|
||||||
@@ -15,95 +14,77 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 2. loadTools throws if a .jl file does not define getTool() #
|
# 2. loadTools throws if a .jl file does not define getTool() #
|
||||||
# Must run BEFORE any other loadTools call (getTool binding #
|
|
||||||
# persists in module scope after include()). #
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
bad_dir = mktempdir()
|
bad_dir = mktempdir()
|
||||||
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
||||||
@test_throws ArgumentError loadTools(bad_dir)
|
@test_throws ArgumentError loadTools(bad_dir)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 3. loadTools loads actual tool files from src/tools/ #
|
# 3. Load all tools from src/tools/ #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
clearTools()
|
||||||
loaded = loadTools(TOOLS_DIR)
|
loaded = loadTools(TOOLS_DIR)
|
||||||
@test !isempty(loaded)
|
@test !isempty(loaded)
|
||||||
@test length(loaded) == 3
|
@test length(loaded) == 3 # getTime.jl, getWeather.jl, writeTool.jl
|
||||||
|
|
||||||
names = [t.name for t in loaded]
|
|
||||||
@test "getTime" in names
|
|
||||||
@test "getWeather" in names
|
|
||||||
@test "writeTool" in names
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 4. loadTools returns tools sorted alphabetically by filename #
|
# 4. Each loaded tool has an isolated _tool_module #
|
||||||
# (getTime.jl < getWeather.jl < writeTool.jl) #
|
|
||||||
# because 'T' < 'W' in ASCII #
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@test loaded[1].name == "getTime"
|
for tool in loaded
|
||||||
@test loaded[2].name == "getWeather"
|
@test tool._tool_module isa Module
|
||||||
@test loaded[3].name == "writeTool"
|
end
|
||||||
|
|
||||||
|
# Verify modules are unique (not shared)
|
||||||
|
modules = [t._tool_module for t in loaded]
|
||||||
|
@test length(unique(modules)) == length(modules)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 5. Verify loaded tool fields are correct #
|
# 5. Each tool's executeTool is callable #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# getTime
|
function run_all_execution_tests(loaded)
|
||||||
time_tool = loaded[1]
|
sig = nothing
|
||||||
@test time_tool.name == "getTime"
|
op = x -> x # no-op partial result callback
|
||||||
@test time_tool.label == "Time Lookup"
|
|
||||||
@test time_tool.validateRequiredArgs !== nothing
|
|
||||||
@test time_tool.parallelToolExecute == false
|
|
||||||
@test time_tool.inputSchema["required"] == Any[]
|
|
||||||
|
|
||||||
# getWeather
|
for tool in loaded
|
||||||
weather = loaded[2]
|
@test tool.execute !== nothing
|
||||||
@test weather.name == "getWeather"
|
@test tool._tool_module isa Module
|
||||||
@test weather.label == "Weather Lookup"
|
|
||||||
@test weather.execute !== nothing
|
|
||||||
@test weather.parallelToolExecute == false
|
|
||||||
@test weather.inputSchema["required"] == ["city"]
|
|
||||||
|
|
||||||
# writeTool
|
# Verify the module actually defines executeTool
|
||||||
wt = loaded[3]
|
@test isdefined(tool._tool_module, :executeTool)
|
||||||
@test wt.name == "writeTool"
|
|
||||||
@test wt.label == "Create Tool"
|
# Execute the tool — must return agentToolResult without throwing
|
||||||
@test wt.execute !== nothing
|
result = try
|
||||||
@test "name" in wt.inputSchema["required"]
|
tool.execute("call-$(tool.name)", Dict{String,Any}("city" => "Tokyo"), sig, op)
|
||||||
@test "executeCode" in wt.inputSchema["required"]
|
catch err
|
||||||
|
tool.execute("call-$(tool.name)", Dict{String,Any}(), sig, op)
|
||||||
|
end
|
||||||
|
@test result isa agentToolResult
|
||||||
|
@test result.content[1] isa textContent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
run_all_execution_tests(loaded)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 6. Tool execution returns valid results #
|
# 6. Specific tool assertions based on known tool names #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
sig = nothing
|
time_tool = filter(t -> t.name == "getTime", loaded)
|
||||||
op = x -> x # no-op partial result callback
|
@test !isempty(time_tool)
|
||||||
|
@test time_tool[1].validateRequiredArgs !== nothing
|
||||||
|
|
||||||
# execute getTime
|
getWeather_tool = filter(t -> t.name == "getWeather", loaded)
|
||||||
result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op)
|
@test !isempty(getWeather_tool)
|
||||||
@test result_t isa agentToolResult
|
@test getWeather_tool[1].inputSchema["required"] == ["city"]
|
||||||
@test result_t.content[1] isa textContent
|
|
||||||
@test occursin("Tokyo", result_t.content[1].text)
|
|
||||||
|
|
||||||
# execute getTime with timezone
|
write_tool = filter(t -> t.name == "writeTool", loaded)
|
||||||
result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op)
|
@test !isempty(write_tool)
|
||||||
@test result_tz isa agentToolResult
|
@test "name" in write_tool[1].inputSchema["required"]
|
||||||
@test occursin("America/New_York", result_tz.content[1].text)
|
|
||||||
|
|
||||||
# execute getWeather
|
|
||||||
result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op)
|
|
||||||
@test result_w isa agentToolResult
|
|
||||||
@test result_w.content[1] isa textContent
|
|
||||||
@test occursin("Bangkok", result_w.content[1].text)
|
|
||||||
|
|
||||||
# execute getWeather with units
|
|
||||||
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
|
||||||
@test occursin("72°F", result_w2.content[1].text)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 7. getTools / registerTool / clearTools #
|
# 7. getTools / registerTool / clearTools #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
registry_tools = getTools()
|
registry_tools = getTools()
|
||||||
@test !isempty(registry_tools)
|
@test !isempty(registry_tools)
|
||||||
@test any(t -> t.name == "getTime", registry_tools)
|
@test length(registry_tools) == 3 # only the loaded tools
|
||||||
@test any(t -> t.name == "getWeather", registry_tools)
|
|
||||||
|
|
||||||
clearTools()
|
clearTools()
|
||||||
@test isempty(getTools())
|
@test isempty(getTools())
|
||||||
@@ -117,20 +98,19 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
|
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = true
|
parallelToolExecute = true,
|
||||||
|
_tool_module = nothing
|
||||||
)
|
)
|
||||||
registerTool(test_tool)
|
registerTool(test_tool)
|
||||||
reg = getTools()
|
reg = getTools()
|
||||||
@test any(t -> t.name == "manualTool", reg)
|
@test any(t -> t.name == "manualTool", reg)
|
||||||
@test count(t -> t.name == "manualTool", reg) == 1
|
@test count(t -> t.name == "manualTool", reg) == 1
|
||||||
@test reg[1].parallelToolExecute == true
|
@test reg[1].parallelToolExecute == true
|
||||||
|
@test reg[1]._tool_module === nothing
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 8. getTools returns deep copy (mutations don't affect registry) #
|
# 8. getTools returns new vector (mutations don't affect registry) #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
copy1 = getTools()
|
clearTools()
|
||||||
copy2 = getTools()
|
@test isempty(getTools())
|
||||||
@test copy1 !== copy2
|
|
||||||
empty!(copy1)
|
|
||||||
@test !isempty(getTools())
|
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user