Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a27630ccf | |||
| ed91260468 | |||
| c13aeb3a74 | |||
| 287704778f | |||
| a9fa23f01b | |||
| c5cb18f0f1 | |||
| c78f4b023d | |||
| 1b69f69c7d | |||
| 3891099eaa |
+1
-1
@@ -13,7 +13,7 @@ module YiemAgent
|
|||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
using .utils
|
using .utils
|
||||||
|
|
||||||
include("tools/registry.jl")
|
include("toolRegistry.jl")
|
||||||
using .toolRegistry
|
using .toolRegistry
|
||||||
|
|
||||||
# include("llmfunction.jl")
|
# include("llmfunction.jl")
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
module toolRegistry
|
||||||
|
|
||||||
|
export toolStore, loadTools, registerTool, getTools, clearTools, listTool
|
||||||
|
|
||||||
|
using Dates
|
||||||
|
using JSON, DataStructures
|
||||||
|
using ..type
|
||||||
|
|
||||||
|
"""
|
||||||
|
Per-agent isolated tool storage.
|
||||||
|
|
||||||
|
Each agent gets its own `toolStore` so tool registration is independent —
|
||||||
|
`registerTool(store, tool)` only affects that agent's tool set.
|
||||||
|
|
||||||
|
# Fields
|
||||||
|
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
||||||
|
- `name::String` — identifier for debugging/logs
|
||||||
|
"""
|
||||||
|
struct toolStore
|
||||||
|
tools::OrderedDict{String, agentTool}
|
||||||
|
name::String
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
toolStore(; name="default") -> toolStore
|
||||||
|
|
||||||
|
Create a new empty tool store.
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `name::String`: Display name for logging (default: `"default"`)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
julia> store = toolStore(name="agent1")
|
||||||
|
toolStore(OrderedDict{String, agentTool}(), "agent1")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function toolStore(; name::String="default")::toolStore
|
||||||
|
toolStore(OrderedDict{String, agentTool}(), name)
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
listTool(store::toolStore) -> agentTool
|
||||||
|
|
||||||
|
Return an `agentTool` definition for listing registered tools.
|
||||||
|
|
||||||
|
Each call produces a **new** tool object that captures (closes over)
|
||||||
|
`store`. `loadTools` auto-registers one so the LLM can discover tools
|
||||||
|
at runtime.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `store`: The tool store whose tools will be listed when the tool runs
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
julia> store = toolStore(name="agent1");
|
||||||
|
|
||||||
|
julia> loadTools(store, "src/tools") # auto-registers listTools
|
||||||
|
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||||
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
|
|
||||||
|
julia> tools = getTools(store)
|
||||||
|
OrderedDict{String, agentTool} with 4 entries:
|
||||||
|
"getWeather" => agentTool(...)
|
||||||
|
"getTime" => agentTool(...)
|
||||||
|
"writeTool" => agentTool(...)
|
||||||
|
"listTools" => agentTool(...)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function listTool(store::toolStore)::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(store)
|
||||||
|
if isempty(tools)
|
||||||
|
result_text = "No tools registered."
|
||||||
|
else
|
||||||
|
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, 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 `.jl` tool files from `dir` into `store`, then auto-register
|
||||||
|
`listTool` so the LLM can discover available tools at runtime.
|
||||||
|
|
||||||
|
Each `.jl` file must define `function getTool()::agentTool ... end`.
|
||||||
|
Files are sorted alphabetically for deterministic registration order.
|
||||||
|
Each file is loaded into its own Julia submodule to avoid name collisions.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `store`: Tool store to populate
|
||||||
|
- `dir`: Directory containing `.jl` tool files
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- The same `store.tools` dict (modified in place)
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
- Throws `ArgumentError` if `dir` does not exist or a file lacks `getTool()`
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
julia> store = toolStore(name="agent1");
|
||||||
|
|
||||||
|
julia> loadTools(store, "src/tools")
|
||||||
|
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||||
|
[toolRegistry:agent1] Loaded tool: getTime (Time Lookup)
|
||||||
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
|
OrderedDict{String, agentTool} with 3 entries:
|
||||||
|
"getWeather" => agentTool(...)
|
||||||
|
"getTime" => agentTool(...)
|
||||||
|
"listTools" => agentTool(...)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
||||||
|
if !isdir(dir)
|
||||||
|
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||||
|
end
|
||||||
|
|
||||||
|
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.
|
||||||
|
# Each tool file declares its own dependencies via `using` statements
|
||||||
|
# at the top of the file — the registry only injects `using ..type`
|
||||||
|
# to make core types (agentTool, textContent, etc.) available.
|
||||||
|
file_content = read(filepath, String)
|
||||||
|
module_code = """
|
||||||
|
module $(mod_name)
|
||||||
|
using ..type
|
||||||
|
$(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
|
||||||
|
store.tools[tool.name] = tool
|
||||||
|
println("[$(store.name)] 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
|
||||||
|
|
||||||
|
registerTool(store, listTool(store))
|
||||||
|
return store.tools
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||||
|
|
||||||
|
Add `tool` to `store`, overwriting any existing tool with the same name.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `store`: Tool store to modify
|
||||||
|
- `tool`: The `agentTool` to register
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- The same `store.tools` dict (modified in place)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
julia> store = toolStore(name="agent1");
|
||||||
|
|
||||||
|
julia> registerTool(store, listTool(store))
|
||||||
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
|
OrderedDict{String, agentTool} with 1 entry:
|
||||||
|
"listTools" => agentTool(...)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
|
||||||
|
store.tools[tool.name] = tool
|
||||||
|
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||||
|
return store.tools
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return the tools registered in `store`.
|
||||||
|
|
||||||
|
The returned dict is the **same object** stored inside `store` — mutations
|
||||||
|
to it (e.g. via `registerTool`) are visible through subsequent calls.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `store`: Tool store to query
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
julia> tools = getTools(store)
|
||||||
|
OrderedDict{String, agentTool} with 2 entries:
|
||||||
|
"getWeather" => agentTool(...)
|
||||||
|
"getTime" => agentTool(...)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
||||||
|
return store.tools
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Remove all tools from `store`.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `store`: Tool store to clear
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `nothing`
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
julia> clearTools(store)
|
||||||
|
[toolRegistry:agent1] Registry cleared
|
||||||
|
nothing
|
||||||
|
|
||||||
|
julia> getTools(store)
|
||||||
|
OrderedDict{String, agentTool} with 0 entries
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function clearTools(store::toolStore)::Nothing
|
||||||
|
empty!(store.tools)
|
||||||
|
println("[$(store.name)] Registry cleared")
|
||||||
|
return nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
end # module
|
||||||
+1504
-404
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
|||||||
|
using Dates
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Validate required arguments for the getTime tool.
|
Validate required arguments for the getTime tool.
|
||||||
|
|
||||||
@@ -41,7 +43,8 @@ Execute the getTime tool.
|
|||||||
|
|
||||||
Returns mock time data for the given timezone or city.
|
Returns mock time data for the given timezone or city.
|
||||||
"""
|
"""
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
|
onPartialResult::Function)::agentToolResult
|
||||||
tz = get(args, "timezone", nothing)
|
tz = get(args, "timezone", nothing)
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
if tz !== nothing
|
if tz !== nothing
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Execute the getWeather tool.
|
|||||||
|
|
||||||
Returns mock weather data for the given city and temperature units.
|
Returns mock weather data for the given city and temperature units.
|
||||||
"""
|
"""
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
|
onPartialResult::Function)::agentToolResult
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
units = get(args, "units", "celsius")
|
units = get(args, "units", "celsius")
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
temp = units == "fahrenheit" ? "72" : "22"
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using JSON
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Tool that writes new Julia tool module files to disk.
|
Tool that writes new Julia tool module files to disk.
|
||||||
|
|
||||||
@@ -5,7 +7,7 @@ The agent can use this tool when it encounters a task that no existing tool
|
|||||||
can handle. Provide the tool's name, label, description, inputSchema, and
|
can handle. Provide the tool's name, label, description, inputSchema, and
|
||||||
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
|
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
|
||||||
|
|
||||||
After calling this tool, restart the agent so `loadTools("src/tools")` picks
|
After calling this tool, restart the agent so `loadTools(agent._tool_store, "src/tools")` picks
|
||||||
up the new file. The new tool is immediately available.
|
up the new file. The new tool is immediately available.
|
||||||
|
|
||||||
# Example
|
# Example
|
||||||
@@ -248,13 +250,13 @@ function getTool()::agentTool
|
|||||||
|
|
||||||
tool_code = join(parts)
|
tool_code = join(parts)
|
||||||
|
|
||||||
# Write the file — tool is loaded on next agent restart via loadTools()
|
# Write the file — tool is loaded on next agent restart via loadTools(store, "src/tools")
|
||||||
write(filepath, tool_code)
|
write(filepath, tool_code)
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Done"))
|
onPartialResult(Dict("status" => "Done"))
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")],
|
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools(agent._tool_store, \"src/tools\") picks it up, then call listTools to verify.")],
|
||||||
Dict{Any,Any}(
|
Dict{Any,Any}(
|
||||||
"file" => filepath,
|
"file" => filepath,
|
||||||
"name" => tool_name,
|
"name" => tool_name,
|
||||||
|
|||||||
+11
-6
@@ -567,9 +567,10 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
|||||||
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
|
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
|
||||||
sessionId::Union{String, Nothing} # Optional session identifier
|
sessionId::Union{String, Nothing} # Optional session identifier
|
||||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||||
parallelToolExecute::Bool # Default: false
|
parallelToolExecute::Bool # Default: false
|
||||||
agentEventSink::Function # agent emits its status via this function
|
agentEventSink::Function # agent emits its status via this function
|
||||||
end
|
_tool_store::Any # Reference to the toolStore for runtime registration
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create a new yiemAgent instance with a background loop task.
|
Create a new yiemAgent instance with a background loop task.
|
||||||
@@ -593,15 +594,17 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
|||||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
||||||
- `agentEventSink::Function`: Callback to receive agent events
|
- `agentEventSink::Function`: Callback to receive agent events
|
||||||
|
- `tool_store::Union{Any, Nothing}`: toolStore for runtime tool registration (default: `nothing`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A new `yiemAgent` instance with an active background task
|
- A new `yiemAgent` instance with an active background task
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```julia
|
```julia
|
||||||
julia> tools = loadTools("src/tools")
|
julia> store = toolStore(name="agent1")
|
||||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=...)
|
julia> tools = loadTools(store, "src/tools")
|
||||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
|
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
|
||||||
|
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
|
||||||
"""
|
"""
|
||||||
function yiemAgent(
|
function yiemAgent(
|
||||||
; systemPrompt::String="You are helpful assistant.",
|
; systemPrompt::String="You are helpful assistant.",
|
||||||
@@ -619,6 +622,7 @@ function yiemAgent(
|
|||||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||||
parallelToolExecute::Bool=false,
|
parallelToolExecute::Bool=false,
|
||||||
agentEventSink::Function,
|
agentEventSink::Function,
|
||||||
|
tool_store::Union{Any, Nothing}=nothing,
|
||||||
)
|
)
|
||||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||||
inputChannel = Channel(16)
|
inputChannel = Channel(16)
|
||||||
@@ -643,6 +647,7 @@ function yiemAgent(
|
|||||||
maxRetryDelayMs,
|
maxRetryDelayMs,
|
||||||
parallelToolExecute,
|
parallelToolExecute,
|
||||||
agentEventSink,
|
agentEventSink,
|
||||||
|
tool_store,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Spawn the background loop and attach it
|
# Spawn the background loop and attach it
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ using YiemAgent.type
|
|||||||
# Path to the real tools directory
|
# Path to the real tools directory
|
||||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||||
|
|
||||||
@testset "loadTools" begin
|
@testset "loadTools with toolStore" begin
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 1. loadTools throws on non-existent directory #
|
# 1. loadTools throws on non-existent directory #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@test_throws ArgumentError loadTools("/nonexistent/dir/that/does/not/exist")
|
store = toolStore(name="test1")
|
||||||
|
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 2. loadTools throws if a .jl file does not define getTool() #
|
# 2. loadTools throws if a .jl file does not define getTool() #
|
||||||
@@ -20,28 +21,30 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
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(store, bad_dir)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 3. loadTools loads actual tool files from src/tools/ #
|
# 3. loadTools loads actual tool files from src/tools/ #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
loaded = loadTools(TOOLS_DIR)
|
store2 = toolStore(name="test2")
|
||||||
|
loaded = loadTools(store2, TOOLS_DIR)
|
||||||
@test !isempty(loaded)
|
@test !isempty(loaded)
|
||||||
@test length(loaded) == 3
|
@test length(loaded) == 4 # 3 files + auto-registered listTools
|
||||||
|
|
||||||
names = [k for k in keys(loaded)]
|
names = [k for k in keys(loaded)]
|
||||||
@test "getTime" in names
|
@test "getTime" in names
|
||||||
@test "getWeather" in names
|
@test "getWeather" in names
|
||||||
@test "writeTool" in names
|
@test "writeTool" in names
|
||||||
|
@test "listTools" in names
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 4. loadTools returns tools sorted alphabetically by filename #
|
# 4. loadTools returns tools sorted alphabetically by filename #
|
||||||
# (getTime.jl < getWeather.jl < writeTool.jl) #
|
# (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
|
||||||
# because 'T' < 'W' in ASCII #
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@test collect(keys(loaded))[1] == "getTime"
|
@test collect(keys(loaded))[1] == "getTime"
|
||||||
@test collect(keys(loaded))[2] == "getWeather"
|
@test collect(keys(loaded))[2] == "getWeather"
|
||||||
@test collect(keys(loaded))[3] == "writeTool"
|
@test collect(keys(loaded))[3] == "writeTool"
|
||||||
|
@test collect(keys(loaded))[4] == "listTools"
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 5. Verify loaded tool fields are correct #
|
# 5. Verify loaded tool fields are correct #
|
||||||
@@ -98,15 +101,29 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
@test occursin("72°F", result_w2.content[1].text)
|
@test occursin("72°F", result_w2.content[1].text)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 7. getTools / registerTool / clearTools #
|
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
registry_tools = getTools()
|
store3 = toolStore(name="test3")
|
||||||
@test !isempty(registry_tools)
|
registry_tools = getTools(store3)
|
||||||
@test any(t -> t.name == "getTime", registry_tools)
|
@test isempty(registry_tools)
|
||||||
@test any(t -> t.name == "getWeather", registry_tools)
|
|
||||||
|
|
||||||
clearTools()
|
# listTool is not auto-registered anymore — each store starts empty
|
||||||
@test isempty(getTools())
|
# Register tools manually
|
||||||
|
registerTool(store3, loaded["getTime"])
|
||||||
|
registerTool(store3, loaded["getWeather"])
|
||||||
|
registerTool(store3, loaded["writeTool"])
|
||||||
|
|
||||||
|
reg = getTools(store3)
|
||||||
|
@test !isempty(reg)
|
||||||
|
@test "getTime" in keys(reg)
|
||||||
|
@test "getWeather" in keys(reg)
|
||||||
|
@test "writeTool" in keys(reg)
|
||||||
|
@test collect(keys(reg))[1] == "getTime"
|
||||||
|
@test collect(keys(reg))[2] == "getWeather"
|
||||||
|
@test collect(keys(reg))[3] == "writeTool"
|
||||||
|
|
||||||
|
clearTools(store3)
|
||||||
|
@test isempty(getTools(store3))
|
||||||
|
|
||||||
test_tool = agentTool(
|
test_tool = agentTool(
|
||||||
name = "manualTool",
|
name = "manualTool",
|
||||||
@@ -119,18 +136,77 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = true
|
parallelToolExecute = true
|
||||||
)
|
)
|
||||||
registerTool(test_tool)
|
registerTool(store3, test_tool)
|
||||||
reg = getTools()
|
reg = getTools(store3)
|
||||||
@test any(t -> t.name == "manualTool", reg)
|
@test haskey(reg, "manualTool")
|
||||||
@test count(t -> t.name == "manualTool", reg) == 1
|
@test length(reg) == 1
|
||||||
@test reg[1].parallelToolExecute == true
|
@test reg["manualTool"].parallelToolExecute == true
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 8. getTools returns deep copy (mutations don't affect registry) #
|
# 8. getTools returns direct reference (mutations affect registry) #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
copy1 = getTools()
|
copy1 = getTools(store3)
|
||||||
copy2 = getTools()
|
copy2 = getTools(store3)
|
||||||
@test copy1 !== copy2
|
@test copy1 === copy2 # same reference, not a deep copy
|
||||||
empty!(copy1)
|
empty!(copy1)
|
||||||
@test !isempty(getTools())
|
@test isempty(getTools(store3)) # mutation propagates
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 9. Per-store isolation — two stores don't share tools #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
storeA = toolStore(name="isolationA")
|
||||||
|
storeB = toolStore(name="isolationB")
|
||||||
|
|
||||||
|
registerTool(storeA, loaded["getTime"])
|
||||||
|
registerTool(storeB, loaded["getWeather"])
|
||||||
|
|
||||||
|
regA = getTools(storeA)
|
||||||
|
regB = getTools(storeB)
|
||||||
|
|
||||||
|
@test "getTime" in keys(regA)
|
||||||
|
@test "getWeather" ∉ keys(regA)
|
||||||
|
@test "getWeather" in keys(regB)
|
||||||
|
@test "getTime" ∉ keys(regB)
|
||||||
|
|
||||||
|
clearTools(storeA)
|
||||||
|
@test isempty(getTools(storeA))
|
||||||
|
@test !isempty(getTools(storeB)) # storeB unaffected
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "listTool" begin
|
||||||
|
store = toolStore(name="test_list")
|
||||||
|
loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools
|
||||||
|
|
||||||
|
# loadTools auto-registers listTool
|
||||||
|
@test "listTools" in keys(loaded)
|
||||||
|
|
||||||
|
# listTool returns an agentTool, not a string or array
|
||||||
|
list_t = listTool(store)
|
||||||
|
@test list_t isa agentTool
|
||||||
|
@test list_t.name == "listTools"
|
||||||
|
@test list_t.label == "List Tools"
|
||||||
|
@test isempty(list_t.inputSchema["required"])
|
||||||
|
|
||||||
|
# Verify all tools appear (3 loaded + listTools = 4)
|
||||||
|
result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x)
|
||||||
|
@test result isa agentToolResult
|
||||||
|
@test result.content[1] isa textContent
|
||||||
|
@test occursin("listTools", result.content[1].text)
|
||||||
|
@test occursin("getWeather", result.content[1].text)
|
||||||
|
@test occursin("getTime", result.content[1].text)
|
||||||
|
@test occursin("writeTool", result.content[1].text)
|
||||||
|
@test result.details["count"] == 4
|
||||||
|
|
||||||
|
# Each listTool call creates an independent closure
|
||||||
|
storeB = toolStore(name="test_listB")
|
||||||
|
registerTool(storeB, loaded["getWeather"])
|
||||||
|
list_tB = listTool(storeB)
|
||||||
|
|
||||||
|
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
||||||
|
resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x)
|
||||||
|
|
||||||
|
@test occursin("getWeather", resultA.content[1].text)
|
||||||
|
@test occursin("getWeather", resultB.content[1].text)
|
||||||
|
@test occursin("getTime", resultA.content[1].text)
|
||||||
|
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user