Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c13aeb3a74 | |||
| 287704778f | |||
| a9fa23f01b | |||
| c5cb18f0f1 | |||
| c78f4b023d | |||
| 1b69f69c7d | |||
| 3891099eaa | |||
| 268d340e2f | |||
| 189bc2efcf | |||
| 92b3e4081f | |||
| 750eff483b |
+1
-1
@@ -13,7 +13,7 @@ module YiemAgent
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("tools/registry.jl")
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
# include("llmfunction.jl")
|
||||
|
||||
+3
-7
@@ -516,7 +516,7 @@ function prepareToolCall(
|
||||
signal::Union{Nothing, abortSignal},
|
||||
)::Union{preparedToolCall,immediateOutcome}
|
||||
|
||||
tool = find(t -> t.name == toolCall.name, context.tools)
|
||||
tool = get(context.tools, toolCall.name, nothing)
|
||||
if tool === nothing
|
||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||
end
|
||||
@@ -996,16 +996,12 @@ function executeToolCalls(
|
||||
|
||||
hasSequential = false
|
||||
for tc in toolCalls
|
||||
for t in context.tools
|
||||
if t.name == tc.name && !t.parallelToolExecute
|
||||
t = get(context.tools, tc.name, nothing)
|
||||
if t !== nothing && !t.parallelToolExecute
|
||||
hasSequential = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if hasSequential
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if config.toolExecution == "sequential" || hasSequential
|
||||
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit)
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
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
|
||||
|
||||
"""
|
||||
Create a new isolated tool store.
|
||||
|
||||
# Keyword Arguments
|
||||
- `name::String`: Identifier for this store (default: "default")
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
store = ToolStore(name="agent1")
|
||||
tools = loadTools(store, "src/tools")
|
||||
registerTool(store, my_tool)
|
||||
agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store)
|
||||
```
|
||||
"""
|
||||
function ToolStore(; name::String="default")::ToolStore
|
||||
ToolStore(OrderedDict{String, agentTool}(), name)
|
||||
end
|
||||
|
||||
"""
|
||||
List tool definition — lets the agent query available tools for collision detection
|
||||
when creating new tools via writeTool.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to list from
|
||||
|
||||
Each `ToolStore` gets its own `listTool` instance bound to that store,
|
||||
so each agent sees only its own tools.
|
||||
"""
|
||||
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 all tool modules from a directory into a specific ToolStore.
|
||||
|
||||
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.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to register tools into
|
||||
- `dir::String`: Directory path to scan for `.jl` tool files
|
||||
|
||||
# Returns
|
||||
- `OrderedDict{String, agentTool}`: All loaded tools keyed by name
|
||||
|
||||
# Errors
|
||||
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> store = ToolStore(name="agent1")
|
||||
julia> tools = loadTools(store, "src/tools")
|
||||
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
|
||||
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
Register a single agentTool into a specific ToolStore.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to register into
|
||||
- `tool::agentTool`: The tool to register
|
||||
|
||||
# Returns
|
||||
- `OrderedDict{String, agentTool}`: Updated tool dict for this store
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> store = ToolStore(name="agent1")
|
||||
julia> registerTool(store, my_tool)
|
||||
[toolRegistry:agent1] Registered tool: my_tool
|
||||
```
|
||||
"""
|
||||
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
|
||||
|
||||
"""
|
||||
Get the registered tools from a specific ToolStore.
|
||||
|
||||
Returns the internal `OrderedDict` directly — O(1) lookup by name,
|
||||
ordered iteration preserving registration order.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to query
|
||||
|
||||
# Returns
|
||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> getTools(store)
|
||||
OrderedDict{String, agentTool} with 3 entries:
|
||||
"listTools" => agentTool(...)
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function getTools(store::ToolStore)::OrderedDict{String, agentTool}
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
Clear all registered tools from a specific ToolStore.
|
||||
|
||||
# Arguments
|
||||
- `store::ToolStore`: The tool store to clear
|
||||
|
||||
# Returns
|
||||
- `nothing`
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> clearTools(store)
|
||||
[toolRegistry:agent1] Registry cleared
|
||||
```
|
||||
"""
|
||||
function clearTools(store::ToolStore)::Nothing
|
||||
empty!(store.tools)
|
||||
println("[$(store.name)] Registry cleared")
|
||||
return nothing
|
||||
end
|
||||
|
||||
end # module
|
||||
+1535
-438
File diff suppressed because it is too large
Load Diff
+22
-13
@@ -1,3 +1,5 @@
|
||||
using Dates
|
||||
|
||||
"""
|
||||
Validate required arguments for the getTime tool.
|
||||
|
||||
@@ -36,6 +38,25 @@ function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
Execute the getTime tool.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
"""
|
||||
Define and return the getTime agentTool.
|
||||
"""
|
||||
@@ -52,19 +73,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => []
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
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,
|
||||
execute = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
|
||||
+17
-10
@@ -1,3 +1,19 @@
|
||||
"""
|
||||
Execute the getWeather tool.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
"""
|
||||
Define and return the getWeather agentTool.
|
||||
"""
|
||||
@@ -14,16 +30,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
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,
|
||||
execute = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
module toolRegistry
|
||||
|
||||
export loadTools, registerTool, getTools, clearTools
|
||||
|
||||
using Dates
|
||||
using JSON
|
||||
using ..type
|
||||
|
||||
# Global registry — populated at runtime by loadTools() or registerTool()
|
||||
const _registry = Vector{agentTool}()
|
||||
|
||||
# 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.
|
||||
|
||||
# Tool file format
|
||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`:
|
||||
|
||||
```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
|
||||
- `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)::Vector{agentTool}
|
||||
if !isdir(dir)
|
||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||
end
|
||||
|
||||
tools = 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)
|
||||
println("[toolRegistry] Loading tool from: $filepath")
|
||||
|
||||
# Include the file in the current module scope so all types resolve
|
||||
# (agentTool, textContent, agentToolResult, etc. are all available)
|
||||
include(filepath)
|
||||
|
||||
# Validate that getTool was defined (include() places it in current module scope)
|
||||
if !isdefined(@__MODULE__, :getTool)
|
||||
throw(ArgumentError(
|
||||
"Tool file $(filepath) does not define a `getTool()` function. " *
|
||||
"Each tool file must define: function getTool()::agentTool ... end"
|
||||
))
|
||||
end
|
||||
|
||||
# Call getTool() — it runs in current scope where types are visible
|
||||
# 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))"
|
||||
))
|
||||
end
|
||||
|
||||
push!(_registry, tool)
|
||||
push!(tools, tool)
|
||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
# Example
|
||||
@@ -248,13 +250,13 @@ function getTool()::agentTool
|
||||
|
||||
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)
|
||||
|
||||
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.")],
|
||||
[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}(
|
||||
"file" => filepath,
|
||||
"name" => tool_name,
|
||||
|
||||
+21
-16
@@ -291,7 +291,7 @@ Snapshot of the agent's conversation context.
|
||||
# Arguments
|
||||
- `systemPrompt::String`: System prompt for the agent
|
||||
- `messages::Vector{agentMessage}`: Conversation messages
|
||||
- `tools::Union{Vector{agentTool}, Nothing}`: Available tools
|
||||
- `tools::Union{Dict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
|
||||
|
||||
# Returns
|
||||
- A new `agentContext` instance
|
||||
@@ -299,7 +299,7 @@ Snapshot of the agent's conversation context.
|
||||
struct agentContext # Snapshot of the agent's conversation context
|
||||
systemPrompt::String # System prompt for the agent
|
||||
messages::Vector{agentMessage} # Conversation messages
|
||||
tools::Union{Vector{agentTool}, Nothing} # Available tools
|
||||
tools::Union{Dict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||
end
|
||||
|
||||
|
||||
@@ -308,9 +308,9 @@ end
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
|
||||
mutable struct agentState # Mutable runtime state of an agent
|
||||
systemPrompt::String # System prompt text
|
||||
systemPrompt::String # System prompt for the agent
|
||||
model::llmModel # LLM model to use
|
||||
tools::Vector{agentTool} # Available tools
|
||||
tools::OrderedDict{String, agentTool} # Available tools keyed by name, insertion-ordered
|
||||
|
||||
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
|
||||
messages::Vector{agentMessage}
|
||||
@@ -329,7 +329,7 @@ new state from external references.
|
||||
# Arguments
|
||||
- `systemPrompt::String`: System prompt text
|
||||
- `model::llmModel`: LLM model to use (defaults to an unknown model)
|
||||
- `tools::Vector{agentTool}`: Available tools (deep copied)
|
||||
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (deep copied)
|
||||
- `messages::Vector{agentMessage}`: Conversation messages (deep copied)
|
||||
|
||||
# Returns
|
||||
@@ -338,13 +338,13 @@ new state from external references.
|
||||
# Examples
|
||||
```julia
|
||||
julia> state = agentState(systemPrompt="You are a helpful assistant")
|
||||
agentState("You are a helpful assistant", ..., agentTool[], agentMessage[], String[], nothing)
|
||||
```
|
||||
agentState("You are a helpful assistant", OrderedDict{String, agentTool}(), agentMessage[], String[], nothing)
|
||||
"""
|
||||
function agentState(
|
||||
systemPrompt::String="",
|
||||
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||
tools::Vector{agentTool}=agentTool[],
|
||||
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[],
|
||||
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||
messages::Vector{agentMessage}=agentMessage[],
|
||||
)
|
||||
agentState(
|
||||
@@ -395,13 +395,13 @@ end
|
||||
Configuration for the agent tool execution loop.
|
||||
|
||||
# Arguments
|
||||
- `tools::Vector{agentTool}`: Available tools
|
||||
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name
|
||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||
"""
|
||||
struct agentLoopConfig
|
||||
tools::Vector{agentTool}
|
||||
tools::OrderedDict{String, agentTool}
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
toolExecution::String
|
||||
@@ -569,6 +569,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||
parallelToolExecute::Bool # Default: false
|
||||
agentEventSink::Function # agent emits its status via this function
|
||||
_tool_store::Any # Reference to the ToolStore for runtime registration
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -580,7 +581,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
||||
# Keyword Arguments
|
||||
- `systemPrompt::String`: System prompt for the agent
|
||||
- `model`: LLM model to use
|
||||
- `tools::Vector{agentTool}`: Available tools (default: empty)
|
||||
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
|
||||
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
|
||||
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
|
||||
- `llmCall::Function`: Function to invoke the LLM (required)
|
||||
@@ -593,20 +594,22 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
||||
- `agentEventSink::Function`: Callback to receive agent events
|
||||
- `tool_store::Union{Any, Nothing}`: ToolStore for runtime tool registration (default: `nothing`)
|
||||
|
||||
# Returns
|
||||
- A new `yiemAgent` instance with an active background task
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model)
|
||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
|
||||
```
|
||||
julia> store = ToolStore(name="agent1")
|
||||
julia> tools = loadTools(store, "src/tools")
|
||||
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(
|
||||
; systemPrompt::String="You are helpful assistant.",
|
||||
model=nothing,
|
||||
tools::Vector{agentTool}=agentTool[],
|
||||
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||
messages::Vector{agentMessage}=agentMessage[],
|
||||
prepareContext::Union{Function, Nothing}=nothing,
|
||||
formatMsgForLLM::Function=defaultformatMsgForLLM,
|
||||
@@ -619,6 +622,7 @@ function yiemAgent(
|
||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false,
|
||||
agentEventSink::Function,
|
||||
tool_store::Union{Any, Nothing}=nothing,
|
||||
)
|
||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||
inputChannel = Channel(16)
|
||||
@@ -643,6 +647,7 @@ function yiemAgent(
|
||||
maxRetryDelayMs,
|
||||
parallelToolExecute,
|
||||
agentEventSink,
|
||||
tool_store,
|
||||
)
|
||||
|
||||
# Spawn the background loop and attach it
|
||||
|
||||
@@ -6,12 +6,13 @@ using YiemAgent.type
|
||||
# Path to the real tools directory
|
||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
|
||||
@testset "loadTools" begin
|
||||
@testset "loadTools with ToolStore" begin
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 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() #
|
||||
@@ -20,16 +21,17 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
# ------------------------------------------------------------------ #
|
||||
bad_dir = mktempdir()
|
||||
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/ #
|
||||
# ------------------------------------------------------------------ #
|
||||
loaded = loadTools(TOOLS_DIR)
|
||||
store2 = ToolStore(name="test2")
|
||||
loaded = loadTools(store2, TOOLS_DIR)
|
||||
@test !isempty(loaded)
|
||||
@test length(loaded) == 3
|
||||
|
||||
names = [t.name for t in loaded]
|
||||
names = [k for k in keys(loaded)]
|
||||
@test "getTime" in names
|
||||
@test "getWeather" in names
|
||||
@test "writeTool" in names
|
||||
@@ -39,15 +41,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
# (getTime.jl < getWeather.jl < writeTool.jl) #
|
||||
# because 'T' < 'W' in ASCII #
|
||||
# ------------------------------------------------------------------ #
|
||||
@test loaded[1].name == "getTime"
|
||||
@test loaded[2].name == "getWeather"
|
||||
@test loaded[3].name == "writeTool"
|
||||
@test collect(keys(loaded))[1] == "getTime"
|
||||
@test collect(keys(loaded))[2] == "getWeather"
|
||||
@test collect(keys(loaded))[3] == "writeTool"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Verify loaded tool fields are correct #
|
||||
# ------------------------------------------------------------------ #
|
||||
# getTime
|
||||
time_tool = loaded[1]
|
||||
time_tool = loaded["getTime"]
|
||||
@test time_tool.name == "getTime"
|
||||
@test time_tool.label == "Time Lookup"
|
||||
@test time_tool.validateRequiredArgs !== nothing
|
||||
@@ -55,7 +57,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test time_tool.inputSchema["required"] == Any[]
|
||||
|
||||
# getWeather
|
||||
weather = loaded[2]
|
||||
weather = loaded["getWeather"]
|
||||
@test weather.name == "getWeather"
|
||||
@test weather.label == "Weather Lookup"
|
||||
@test weather.execute !== nothing
|
||||
@@ -63,7 +65,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test weather.inputSchema["required"] == ["city"]
|
||||
|
||||
# writeTool
|
||||
wt = loaded[3]
|
||||
wt = loaded["writeTool"]
|
||||
@test wt.name == "writeTool"
|
||||
@test wt.label == "Create Tool"
|
||||
@test wt.execute !== nothing
|
||||
@@ -98,15 +100,29 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test occursin("72°F", result_w2.content[1].text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. getTools / registerTool / clearTools #
|
||||
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
||||
# ------------------------------------------------------------------ #
|
||||
registry_tools = getTools()
|
||||
@test !isempty(registry_tools)
|
||||
@test any(t -> t.name == "getTime", registry_tools)
|
||||
@test any(t -> t.name == "getWeather", registry_tools)
|
||||
store3 = ToolStore(name="test3")
|
||||
registry_tools = getTools(store3)
|
||||
@test isempty(registry_tools)
|
||||
|
||||
clearTools()
|
||||
@test isempty(getTools())
|
||||
# listTool is not auto-registered anymore — each store starts empty
|
||||
# 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(
|
||||
name = "manualTool",
|
||||
@@ -119,18 +135,39 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = true
|
||||
)
|
||||
registerTool(test_tool)
|
||||
reg = getTools()
|
||||
@test any(t -> t.name == "manualTool", reg)
|
||||
@test count(t -> t.name == "manualTool", reg) == 1
|
||||
@test reg[1].parallelToolExecute == true
|
||||
registerTool(store3, test_tool)
|
||||
reg = getTools(store3)
|
||||
@test haskey(reg, "manualTool")
|
||||
@test length(reg) == 1
|
||||
@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()
|
||||
copy2 = getTools()
|
||||
@test copy1 !== copy2
|
||||
copy1 = getTools(store3)
|
||||
copy2 = getTools(store3)
|
||||
@test copy1 === copy2 # same reference, not a deep copy
|
||||
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
|
||||
Reference in New Issue
Block a user