update
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Dynamic Tool Loading
|
||||||
|
|
||||||
|
Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory without hardcoding filenames in the main module.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. `src/tools/registry.jl` defines a `load_tools(dir::String)` function that scans a directory for `.jl` files
|
||||||
|
2. Each tool file must define a single function: `get_tool() :: agentTool`
|
||||||
|
3. `load_tools()` sorts files alphabetically, includes each one, calls `get_tool()`, and registers the result
|
||||||
|
4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent`
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── tools/
|
||||||
|
│ ├── registry.jl # Tool loader (do not edit)
|
||||||
|
│ ├── get_weather.jl # Your tool
|
||||||
|
│ └── query_db.jl # Another tool
|
||||||
|
├── type.jl
|
||||||
|
├── utils.jl
|
||||||
|
├── agentCore.jl
|
||||||
|
├── api.jl
|
||||||
|
└── YiemAgent.jl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a Tool
|
||||||
|
|
||||||
|
Each `.jl` file in `src/tools/` must define `get_tool()` returning an `agentTool`:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# src/tools/get_weather.jl
|
||||||
|
function get_tool() :: agentTool
|
||||||
|
return agentTool(
|
||||||
|
name = "get_weather",
|
||||||
|
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("Weather in $(city): Sunny, 22C")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
prepareArguments = nothing,
|
||||||
|
parallelToolExecute = false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
No `module` wrapper needed — the registry includes each file in the current module scope so all types (`agentTool`, `textContent`, `agentToolResult`, etc.) resolve correctly.
|
||||||
|
|
||||||
|
## Loading Tools
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using .YiemAgent
|
||||||
|
using .YiemAgent: toolRegistry
|
||||||
|
|
||||||
|
# Load all tool files from src/tools/
|
||||||
|
tools = load_tools(joinpath(@__DIR__, "src", "tools"))
|
||||||
|
|
||||||
|
# Create agent with loaded tools
|
||||||
|
agent = yiemAgent(
|
||||||
|
systemPrompt = "You are a helpful assistant.",
|
||||||
|
model = my_model,
|
||||||
|
tools = tools,
|
||||||
|
llmCall = my_llm_call,
|
||||||
|
agentEventSink = my_event_sink
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `load_tools(dir::String)` | Scan directory and load all `.jl` tool files |
|
||||||
|
| `register_tool(tool::agentTool)` | Register a single tool into the global registry |
|
||||||
|
| `get_tools()` | Get deep copy of all registered tools |
|
||||||
|
| `list_tools()` | List all registered tools as `(name, label)` pairs |
|
||||||
|
| `clear_tools()` | Clear the global registry |
|
||||||
|
|
||||||
|
## File Loading Order
|
||||||
|
|
||||||
|
Files are sorted alphabetically before loading, so `01_database.jl` loads before `02_weather.jl`. This ensures deterministic registration order.
|
||||||
@@ -13,6 +13,9 @@ module YiemAgent
|
|||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
using .utils
|
using .utils
|
||||||
|
|
||||||
|
include("tools/registry.jl")
|
||||||
|
using .toolRegistry
|
||||||
|
|
||||||
# include("llmfunction.jl")
|
# include("llmfunction.jl")
|
||||||
# using .llmfunction
|
# using .llmfunction
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Execute the get_weather 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 execute_tool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) :: agentToolResult
|
||||||
|
city = get(args, "city", "")
|
||||||
|
units = get(args, "units", "celsius")
|
||||||
|
|
||||||
|
# Validate required arguments
|
||||||
|
if isempty(city)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Error: 'city' argument is required.")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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 get_weather agentTool.
|
||||||
|
"""
|
||||||
|
function get_tool() :: agentTool
|
||||||
|
return agentTool(
|
||||||
|
name = "get_weather",
|
||||||
|
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, e.g., 'San Francisco, CA' or 'Tokyo, Japan'"),
|
||||||
|
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius", "description" => "Temperature scale")
|
||||||
|
),
|
||||||
|
"required" => ["city"]
|
||||||
|
),
|
||||||
|
execute = execute_tool, # reference the function defined above
|
||||||
|
prepareArguments = nothing,
|
||||||
|
parallelToolExecute = false
|
||||||
|
)
|
||||||
|
end
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
module toolRegistry
|
||||||
|
|
||||||
|
export load_tools, register_tool, get_tools, list_tools, clear_tools
|
||||||
|
|
||||||
|
using ..type
|
||||||
|
|
||||||
|
# Global registry — populated at runtime by load_tools() or register_tool()
|
||||||
|
const _registry = Vector{agentTool}()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Load all tool modules from a directory.
|
||||||
|
|
||||||
|
Scans `dir` for `.jl` files. Each file must define a function named
|
||||||
|
`get_tool() :: agentTool`. Files are sorted alphabetically so tool
|
||||||
|
registration order is deterministic.
|
||||||
|
|
||||||
|
# Tool file format
|
||||||
|
Each `.jl` file defines one function `get_tool()` that returns an `agentTool`:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# src/tools/get_weather.jl
|
||||||
|
function get_tool() :: agentTool
|
||||||
|
return agentTool(
|
||||||
|
name = "get_weather",
|
||||||
|
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 $(city)")],
|
||||||
|
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 `get_tool` function
|
||||||
|
"""
|
||||||
|
function load_tools(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"), 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 get_tool was defined (include() places it in current module scope)
|
||||||
|
if !isdefined(:get_tool)
|
||||||
|
throw(ArgumentError(
|
||||||
|
"Tool file $(filepath) does not define a `get_tool()` function. " *
|
||||||
|
"Each tool file must define: function get_tool() :: agentTool ... end"
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Call get_tool() — it runs in current scope where types are visible
|
||||||
|
tool = get_tool()
|
||||||
|
if !(tool isa agentTool)
|
||||||
|
throw(ArgumentError(
|
||||||
|
"get_tool() 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 register_tool(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 get_tools()::Vector{agentTool}
|
||||||
|
return deepcopy(_registry)
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
List all registered tool names and labels.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `Vector{Tuple{String,String}}`: Pairs of (name, label)
|
||||||
|
"""
|
||||||
|
function list_tools()::Vector{Tuple{String,String}}
|
||||||
|
return [(t.name, t.label) for t in _registry]
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Clear all registered tools from the global registry.
|
||||||
|
"""
|
||||||
|
function clear_tools()::Nothing
|
||||||
|
empty!(_registry)
|
||||||
|
println("[toolRegistry] Registry cleared")
|
||||||
|
return nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
end # module
|
||||||
+51
-7
@@ -197,23 +197,67 @@ end
|
|||||||
"""
|
"""
|
||||||
A tool available to the agent.
|
A tool available to the agent.
|
||||||
|
|
||||||
|
Maps MCP server tool definitions to an executable Julia tool.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `name::String`: Tool identifier
|
- `name::String`: Tool identifier (from MCP `name`)
|
||||||
- `label::String`: Human-readable tool name
|
- `label::String`: Human-readable tool name (from MCP `title`)
|
||||||
- `description::String`: What the tool does
|
- `description::String`: What the tool does (from MCP `description`)
|
||||||
- `parameters::TParameters`: Tool parameters schema (JSON schema)
|
- `inputSchema::Any`: Tool parameters schema (from MCP `inputSchema`, JSON Schema format)
|
||||||
- `execute::Function`: Tool execution function
|
- `execute::Function`: Tool execution function, signature:
|
||||||
|
`execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)`
|
||||||
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
|
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
|
||||||
- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel
|
- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A new `agentTool` instance
|
- A new `agentTool` instance
|
||||||
|
|
||||||
|
# MCP Tool Example
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"name": "get_weather",
|
||||||
|
"title": "Weather Lookup",
|
||||||
|
"description": "Fetch current weather and forecast for a given city.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"city": { "type": "string", "description": "City and state/country" },
|
||||||
|
"units": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" }
|
||||||
|
},
|
||||||
|
"required": ["city"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
tool = agentTool(
|
||||||
|
name="get_weather",
|
||||||
|
label="Weather Lookup",
|
||||||
|
description="Fetch current weather and forecast for a given city.",
|
||||||
|
inputSchema=Dict(
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => Dict(
|
||||||
|
"city" => Dict("type" => "string", "description" => "City name"),
|
||||||
|
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"])
|
||||||
|
),
|
||||||
|
"required" => ["city"]
|
||||||
|
),
|
||||||
|
execute=(toolCallId, args, signal, onPartialResult) -> begin
|
||||||
|
city = args["city"]
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Sunny, 22C in $(city)")],
|
||||||
|
Dict{Any,Any}(), nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
struct agentTool{TParameters, TDetails} # A tool available to the agent
|
struct agentTool # A tool available to the agent
|
||||||
name::String # Tool identifier
|
name::String # Tool identifier
|
||||||
label::String # Human-readable tool name
|
label::String # Human-readable tool name
|
||||||
description::String # What the tool does
|
description::String # What the tool does
|
||||||
parameters::TParameters # Tool parameters schema (JSON schema)
|
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||||
execute::Function # Tool execution function
|
execute::Function # Tool execution function
|
||||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||||
|
|||||||
Reference in New Issue
Block a user