197 lines
6.4 KiB
Julia
197 lines
6.4 KiB
Julia
module toolRegistry
|
|
|
|
export loadTools, registerTool, getTools, clearTools
|
|
|
|
using Dates
|
|
using JSON, DataStructures
|
|
using ..type
|
|
|
|
# Global registry — populated at runtime by loadTools() or registerTool()
|
|
const _registry = Vector{agentTool}()
|
|
|
|
# Module references — kept alive to prevent GC of tool code that closures depend on
|
|
const _tool_modules = Vector{Module}()
|
|
|
|
# Auto-register the built-in listTools tool
|
|
function __init__()
|
|
registerTool(_listTool())
|
|
end
|
|
|
|
"""
|
|
List tool definition — lets the agent query available tools for collision detection
|
|
when creating new tools via writeTool.
|
|
"""
|
|
function _listTool()::agentTool
|
|
return agentTool(
|
|
name = "listTools",
|
|
label = "List Tools",
|
|
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
|
|
inputSchema = Dict{String,Any}(
|
|
"type" => "object",
|
|
"properties" => Dict{String,Any}(),
|
|
"required" => Any[]
|
|
),
|
|
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
|
tools = getTools()
|
|
if isempty(tools)
|
|
result_text = "No tools registered."
|
|
else
|
|
lines = String["- $(t.name): $(t.label) — $(t.description)" for t in tools]
|
|
result_text = "Available tools:\n" * join(lines, "\n")
|
|
end
|
|
return agentToolResult(
|
|
[textContent(result_text)],
|
|
Dict{Any,Any}("count" => length(tools)),
|
|
nothing, false
|
|
)
|
|
end,
|
|
prepareArguments = nothing,
|
|
validateRequiredArgs = nothing,
|
|
parallelToolExecute = false
|
|
)
|
|
end
|
|
|
|
"""
|
|
Load all tool modules from a directory.
|
|
|
|
Scans `dir` for `.jl` files. Each file must define a function named
|
|
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
|
registration order is deterministic.
|
|
|
|
Each `.jl` file is loaded into its own **submodule** so that all functions
|
|
defined in the file (`validateRequiredArgs`, `prepareArguments`, `executeTool`,
|
|
and any helper functions) are namespaced and never collide with other tools.
|
|
|
|
# Tool file format
|
|
Each `.jl` file defines one function `getTool()` that returns an `agentTool`.
|
|
Inside the file you can freely define as many helper functions as you need —
|
|
they will all be scoped under the tool's submodule.
|
|
|
|
```julia
|
|
# src/tools/getWeather.jl
|
|
|
|
# These are namespaced — no collision with getTime.validateRequiredArgs, etc.
|
|
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
|
...
|
|
end
|
|
|
|
function getTool()::agentTool
|
|
return agentTool(
|
|
name = "getWeather",
|
|
...
|
|
)
|
|
end
|
|
```
|
|
|
|
# Arguments
|
|
- `dir::String`: Directory path to scan for `.jl` tool files
|
|
|
|
# Returns
|
|
- `Vector{agentTool}`: All loaded tools
|
|
|
|
# Errors
|
|
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
|
"""
|
|
function loadTools(dir::String)::OrderedDict{String, agentTool}
|
|
if !isdir(dir)
|
|
throw(ArgumentError("Tool directory does not exist: $dir"))
|
|
end
|
|
|
|
tools = OrderedDict{String, agentTool}()
|
|
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
|
|
sort!(jl_files)
|
|
|
|
for filename in jl_files
|
|
filepath = joinpath(dir, filename)
|
|
|
|
# Derive a unique module name from the filename only (not full path).
|
|
# e.g. "getWeather.jl" -> "_tool_getWeather"
|
|
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
|
|
|
|
# Build the complete module as a string and eval the parsed code.
|
|
# Julia does not allow `module ... end` inside eval(quote ...),
|
|
# and constructing the module AST by hand is fragile.
|
|
# Instead, we generate the full module source as a string,
|
|
# parse it, and eval the resulting expression.
|
|
# Also import Dates, UUIDs, DataStructures, JSON — common dependencies
|
|
# that tool files use (and that the ..type module transitively uses).
|
|
file_content = read(filepath, String)
|
|
module_code = """
|
|
module $(mod_name)
|
|
using ..type
|
|
using Dates, UUIDs, DataStructures, JSON
|
|
$(file_content)
|
|
end
|
|
"""
|
|
mod = eval(Meta.parse(module_code))
|
|
|
|
# Call getTool() via Core.eval in the submodule's scope.
|
|
# This evaluates getTool() entirely within the new module's world,
|
|
# completely avoiding world-age issues — no invokelatest needed.
|
|
# Note: all uses of `tool` must be inside the `try` block because
|
|
# Julia 1.12's SSA form doesn't track `tool` as definitely assigned
|
|
# after a `try-catch` where it's only assigned inside `try`.
|
|
try
|
|
tool = Core.eval(mod, :(getTool()))
|
|
if !(tool isa agentTool)
|
|
throw(ArgumentError(
|
|
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
|
))
|
|
end
|
|
# Keep module reference alive — closures in the agentTool (execute,
|
|
# validateRequiredArgs, prepareArguments) may reference module-scoped
|
|
# functions. Without this, GC could collect the module.
|
|
push!(_tool_modules, mod)
|
|
push!(_registry, tool)
|
|
tools[tool.name] = tool
|
|
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
|
catch e
|
|
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
|
|
throw(ArgumentError(
|
|
"Tool file $(filepath) does not define a `getTool()` function in module $(mod_name). " *
|
|
"Each tool file must define: function getTool()::agentTool ... end"
|
|
))
|
|
end
|
|
rethrow(e)
|
|
end
|
|
end
|
|
|
|
return tools
|
|
end
|
|
|
|
"""
|
|
Register a single agentTool into the global registry.
|
|
|
|
# Arguments
|
|
- `tool::agentTool`: The tool to register
|
|
|
|
# Returns
|
|
- `Vector{agentTool}`: Updated registry
|
|
"""
|
|
function registerTool(tool::agentTool)::Vector{agentTool}
|
|
push!(_registry, tool)
|
|
println("[toolRegistry] Registered tool: $(tool.name)")
|
|
return _registry
|
|
end
|
|
|
|
"""
|
|
Get all registered tools.
|
|
|
|
# Returns
|
|
- `Vector{agentTool}`: Copy of the registry
|
|
"""
|
|
function getTools()::Vector{agentTool}
|
|
return deepcopy(_registry)
|
|
end
|
|
|
|
"""
|
|
Clear all registered tools from the global registry.
|
|
"""
|
|
function clearTools()::Nothing
|
|
empty!(_registry)
|
|
println("[toolRegistry] Registry cleared")
|
|
return nothing
|
|
end
|
|
|
|
end # module
|