V0.8.0 async think loop #40
@@ -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")
|
||||
using .utils
|
||||
|
||||
include("tools/registry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
# include("llmfunction.jl")
|
||||
# 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.
|
||||
|
||||
Maps MCP server tool definitions to an executable Julia tool.
|
||||
|
||||
# Arguments
|
||||
- `name::String`: Tool identifier
|
||||
- `label::String`: Human-readable tool name
|
||||
- `description::String`: What the tool does
|
||||
- `parameters::TParameters`: Tool parameters schema (JSON schema)
|
||||
- `execute::Function`: Tool execution function
|
||||
- `name::String`: Tool identifier (from MCP `name`)
|
||||
- `label::String`: Human-readable tool name (from MCP `title`)
|
||||
- `description::String`: What the tool does (from MCP `description`)
|
||||
- `inputSchema::Any`: Tool parameters schema (from MCP `inputSchema`, JSON Schema format)
|
||||
- `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
|
||||
- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel
|
||||
|
||||
# Returns
|
||||
- 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
|
||||
label::String # Human-readable tool name
|
||||
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
|
||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||
|
||||
Reference in New Issue
Block a user