static tool loading
This commit is contained in:
+13
-2
@@ -1,7 +1,6 @@
|
||||
module YiemAgent
|
||||
|
||||
# export agent
|
||||
|
||||
export register_all_tools
|
||||
|
||||
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
||||
files and each file can only depend on the file included before it.
|
||||
@@ -13,9 +12,21 @@ module YiemAgent
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("tools/getWeather.jl")
|
||||
include("tools/getTime.jl")
|
||||
include("tools/writeTool.jl")
|
||||
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
function register_all_tools(store::toolRegistry.toolStore)
|
||||
registerTool(store, getWeatherTool())
|
||||
registerTool(store, getTimeTool())
|
||||
registerTool(store, writeToolTool())
|
||||
registerTool(store, listTool(store))
|
||||
return store.tools
|
||||
end
|
||||
|
||||
# include("llmfunction.jl")
|
||||
# using .llmfunction
|
||||
|
||||
|
||||
+132
-89
@@ -1,16 +1,23 @@
|
||||
module agentCore
|
||||
|
||||
export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls
|
||||
export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls,
|
||||
executePreparedToolCall, prepareToolCall, executeToolCallsSequential,
|
||||
executeToolCallsParallel, executeToolCalls
|
||||
|
||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||
DataFrames, Base.Threads, NATS
|
||||
using GeneralUtils
|
||||
using ..type, ..utils, ..toolRegistry
|
||||
|
||||
function register_all_tools(store::toolRegistry.toolStore)
|
||||
# Call parent module's version which has access to tool functions
|
||||
parentmodule(@__MODULE__).register_all_tools(store)
|
||||
end
|
||||
|
||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||
|
||||
"""
|
||||
docstring
|
||||
docstring
|
||||
"""
|
||||
mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
_state::agentState # Current state (prompt, model, messages, tools, etc.)
|
||||
@@ -84,7 +91,6 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
||||
- A new `yiemAgent` instance with an active background task
|
||||
"""
|
||||
function yiemAgent(
|
||||
toolsFolderPath::String,
|
||||
llmCall,
|
||||
;
|
||||
systemPrompt::String="You are helpful assistant.",
|
||||
@@ -106,9 +112,9 @@ function yiemAgent(
|
||||
followUp = Channel(32)
|
||||
outputChannel = Channel(16)
|
||||
|
||||
# load tools from toolsFolderPath
|
||||
# load tools (statically registered at module init)
|
||||
toolStore1 = toolStore(name="myagent")
|
||||
loadTools(toolStore1, toolsFolderPath)
|
||||
register_all_tools(toolStore1)
|
||||
|
||||
# Create struct with a placeholder task, then spawn and replace it
|
||||
agent = yiemAgent(
|
||||
@@ -291,6 +297,8 @@ function _agentLoop(agent::yiemAgent)
|
||||
processingTask = nothing # reset
|
||||
end
|
||||
agent.agentEventSink("_agentLoop 6")
|
||||
agent.agentEventSink(string(typeof(processingTask)))
|
||||
agent.agentEventSink("_agentLoop 7")
|
||||
end
|
||||
catch e
|
||||
# On any error, send error response and exit the loop
|
||||
@@ -329,7 +337,7 @@ julia> # Currently returns a placeholder echo response
|
||||
function _processMessage(
|
||||
inputChannel::Channel,
|
||||
agentEventSink,
|
||||
messages::Vector{agentMessage},
|
||||
agentMsgHistory::Vector{agentMessage},
|
||||
systemPrompt::String,
|
||||
tools::OrderedDict{String, agentTool},
|
||||
prepareContext::Function,
|
||||
@@ -370,12 +378,12 @@ function _processMessage(
|
||||
end
|
||||
agentEventSink("_processMessage 5")
|
||||
user_msg = OpenAiToUserMessage(raw_msg)
|
||||
push!(messages, user_msg)
|
||||
push!(agentMsgHistory, user_msg)
|
||||
agentEventSink("_processMessage 6")
|
||||
end
|
||||
agentEventSink("_processMessage 7")
|
||||
# call prepareContext()
|
||||
state = agentState(systemPrompt, nothing, tools, messages)
|
||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||
agentEventSink("_processMessage 8")
|
||||
preparedContext = prepareContext(state, agentEventSink)
|
||||
agentEventSink("_processMessage 8")
|
||||
@@ -396,7 +404,7 @@ function _processMessage(
|
||||
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList")
|
||||
|
||||
if hasToolCalls && length(toolCallList) > 0
|
||||
#WORKING Build context and config for executeToolCalls
|
||||
# Build context and config for executeToolCalls
|
||||
|
||||
config = agentLoopConfig(
|
||||
beforeToolCall,
|
||||
@@ -408,23 +416,21 @@ function _processMessage(
|
||||
agentEventSink("_processMessage 12")
|
||||
|
||||
# call executeToolCalls()
|
||||
|
||||
batch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, signal,
|
||||
agentEventSink)
|
||||
|
||||
|
||||
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||
signal, agentEventSink)
|
||||
agentEventSink("_processMessage 13")
|
||||
error("debug marker")
|
||||
# save toolResults to messages
|
||||
for tool_result in batch.messages
|
||||
push!(messages, tool_result)
|
||||
end
|
||||
|
||||
if batch.terminate
|
||||
# If batch requested termination, build a final response
|
||||
# save toolResults to messages
|
||||
for toolResult in toolResultBatch.messages
|
||||
push!(agentMsgHistory, toolResult)
|
||||
end
|
||||
agentEventSink("_processMessage 14")
|
||||
if toolResultBatch.terminate
|
||||
agentEventSink("_processMessage 15")
|
||||
# If toolResultBatch requested termination, build a final response
|
||||
final_content = [textContent("Tool execution completed.")]
|
||||
for tool_result in batch.messages
|
||||
for content_block in tool_result.content
|
||||
for toolResult in toolResultBatch.messages
|
||||
for content_block in toolResult.content
|
||||
if content_block isa textContent
|
||||
append!(final_content, [content_block])
|
||||
elseif content_block isa Dict
|
||||
@@ -434,6 +440,7 @@ function _processMessage(
|
||||
end
|
||||
end
|
||||
end
|
||||
agentEventSink("_processMessage 16")
|
||||
final_response = assistantMessage(
|
||||
role="assistant",
|
||||
content=final_content,
|
||||
@@ -441,7 +448,7 @@ function _processMessage(
|
||||
model=assistant_msg.model,
|
||||
usage=assistant_msg.usage,
|
||||
stopReason="tool_use_terminated",
|
||||
errorMessage=if any(x -> x.isError, batch.messages)
|
||||
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
|
||||
"One or more tool calls failed"
|
||||
else
|
||||
nothing
|
||||
@@ -451,12 +458,13 @@ function _processMessage(
|
||||
break
|
||||
end
|
||||
else
|
||||
agentEventSink("_processMessage 17")
|
||||
# LLM did not use tool calls — this is the final response
|
||||
final_response = assistant_msg
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
agentEventSink("_processMessage 18")
|
||||
return final_response
|
||||
end
|
||||
|
||||
@@ -524,7 +532,7 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
||||
return toolResultMessage(
|
||||
"toolResult", f.toolCall.id, f.toolCall.name,
|
||||
f.result.content, f.result.details, f.result.usage,
|
||||
get(f.result, :addedToolNames, string[]), f.isError, nowMillis()
|
||||
nothing, f.isError, now()
|
||||
)
|
||||
end
|
||||
|
||||
@@ -563,8 +571,10 @@ function _extractToolCalls(response)
|
||||
toolCallList = agentToolCall[]
|
||||
|
||||
# Helper: parse args (JSON string -> Dict, or pass through)
|
||||
parse_args(raw) = raw isa AbstractDict ? Dict{String,Any}(raw) :
|
||||
raw isa String ? JSON.parse(raw) : Dict{String,Any}()
|
||||
parse_args(raw) = raw isa AbstractDict && !(raw isa Dict{String,Any}) ?
|
||||
Dict{String,Any}(raw) :
|
||||
raw isa String ? JSON.parse(raw) :
|
||||
raw isa Dict{String,Any} ? raw : Dict{String,Any}()
|
||||
|
||||
# Helper: build agentToolCall (positional)
|
||||
make_tc(tc_data, default_id=string(uuid4())) = begin
|
||||
@@ -713,12 +723,27 @@ end
|
||||
"""
|
||||
shouldTerminate(finalizedCalls::Vector{finalizedOutcome}) -> Bool
|
||||
|
||||
The `terminate` flag is set by tool implementations, not by the agent
|
||||
or the LLM. It signals that the tool itself has completed the user's
|
||||
request or encountered a fatal condition, so the agent should stop
|
||||
processing further turns without calling the LLM again.
|
||||
|
||||
Common scenarios where a tool sets `terminate: true`:
|
||||
- **Task completion**: one-shot tools like `deploy`, `submit`, or
|
||||
`send_payment` finish their work and report directly to the user
|
||||
instead of asking the LLM "what next?"
|
||||
- **Unrecoverable error**: a tool hits a fatal condition (database
|
||||
connection lost, auth token expired) and stops the agent from
|
||||
retrying endlessly.
|
||||
- **Async handoff**: a tool triggers a long-running external operation
|
||||
and wants the agent to stop now; the external system will resume
|
||||
the agent later via `continue()`.
|
||||
|
||||
Returns `true` only when every finalized call in the batch has
|
||||
`result.terminate == true`. All tools must agree — if any tool
|
||||
did not request termination, the agent continues. This prevents
|
||||
a single tool that happens to set `terminate: true` (e.g. for
|
||||
metadata purposes) from accidentally stopping the agent when
|
||||
other tools in the batch did not intend to terminate.
|
||||
a single tool that happens to set `terminate: true` from accidentally
|
||||
stopping the agent when other tools in the batch did not intend to terminate.
|
||||
|
||||
# Arguments
|
||||
- `finalizedCalls`: Vector of finalized tool call outcomes
|
||||
@@ -739,7 +764,7 @@ true
|
||||
```
|
||||
"""
|
||||
function shouldTerminate(batches::Vector{finalizedOutcome})::Bool
|
||||
return !isempty(batches) && all(b -> b.result.terminate, batches)
|
||||
return !isempty(batches) && all(b -> b.result.terminate, batches)
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -849,7 +874,7 @@ function prepareToolCall(
|
||||
agentEventSink
|
||||
)::Union{preparedToolCall,immediateOutcome}
|
||||
agentEventSink("prepareToolCall 1")
|
||||
tool = get(context.tools, toolCall.name, nothing)
|
||||
tool = get(context.tools, toolCall.name, nothing) # pick a called tool from tool store
|
||||
if tool === nothing
|
||||
agentEventSink("prepareToolCall 2")
|
||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||
@@ -859,8 +884,10 @@ function prepareToolCall(
|
||||
agentEventSink("prepareToolCall 3")
|
||||
# 1. prepare arguments (tool-specific transform)
|
||||
prepared = prepareToolCallArguments(tool, toolCall)
|
||||
agentEventSink(string(prepared.arguments))
|
||||
agentEventSink("prepareToolCall 4")
|
||||
validatedArgs = validateToolArguments(tool, prepared)
|
||||
agentEventSink(string(validatedArgs))
|
||||
agentEventSink("prepareToolCall 5")
|
||||
# 2. beforeToolCall hook — can block
|
||||
if config.beforeToolCall !== nothing
|
||||
@@ -886,12 +913,12 @@ function prepareToolCall(
|
||||
return preparedToolCall(tool, toolCall, validatedArgs)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
err_msg = sprint() do io
|
||||
errMsg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
|
||||
agentEventSink(err_msg)
|
||||
agentEventSink(errMsg)
|
||||
|
||||
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||
end
|
||||
@@ -937,36 +964,40 @@ executePreparedToolCall(prep, nothing, emit)
|
||||
# => executedOutcome(createErrorToolResult("Connection timeout"), true)
|
||||
```
|
||||
"""
|
||||
|
||||
function executePreparedToolCall(
|
||||
prep::preparedToolCall,
|
||||
signal::Union{Nothing,abortSignal},
|
||||
agentEventSink,
|
||||
)::executedOutcome
|
||||
agentEventSink("executePreparedToolCall 1")
|
||||
updateEvents = promise[]
|
||||
accepting = true
|
||||
agentEventSink(prep.toolCall.id)
|
||||
agentEventSink(prep.toolCall.name)
|
||||
agentEventSink("executePreparedToolCall 2")
|
||||
s = string(prep.args)
|
||||
agentEventSink(s)
|
||||
agentEventSink("executePreparedToolCall 3")
|
||||
t = string(fieldnames(typeof(prep.tool)))
|
||||
agentEventSink("executePreparedToolCall 3-1 " * t)
|
||||
|
||||
try
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id, prep.args, signal,
|
||||
partialResult -> begin
|
||||
if accepting
|
||||
push!(updateEvents,
|
||||
agentEventSink(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name,
|
||||
prep.toolCall.arguments, partialResult)))
|
||||
end
|
||||
end
|
||||
)
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
|
||||
agentEventSink(result.content[1].text)
|
||||
agentEventSink("executePreparedToolCall 4")
|
||||
return executedOutcome(result, false)
|
||||
catch err
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
errMsg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
agentEventSink(errMsg)
|
||||
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# ── per-call finalization ───────────────────────────────────────
|
||||
|
||||
"""
|
||||
@@ -1030,16 +1061,19 @@ function finalizeExecutedToolCall(
|
||||
executed::executedOutcome,
|
||||
config::agentLoopConfig,
|
||||
signal::Union{Nothing,abortSignal},
|
||||
agentEventSink
|
||||
)::finalizedOutcome
|
||||
|
||||
agentEventSink("finalizeExecutedToolCall 1")
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
|
||||
agentEventSink("finalizeExecutedToolCall 2")
|
||||
if config.afterToolCall !== nothing
|
||||
try
|
||||
after = config.afterToolCall(
|
||||
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
||||
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
|
||||
signal
|
||||
)
|
||||
agentEventSink("finalizeExecutedToolCall 3")
|
||||
if after !== nothing
|
||||
result = merge(result, dict(:content=>get(after,:content,result.content),
|
||||
:details=>get(after,:details,result.details),
|
||||
@@ -1047,12 +1081,19 @@ function finalizeExecutedToolCall(
|
||||
:terminate=>get(after,:terminate,result.terminate)))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
catch err
|
||||
result = createErrorToolResult(sprint(showerror, err))
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
errMsg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
agentEventSink(errMsg)
|
||||
|
||||
result = createErrorToolResult(sprint(showerror, e))
|
||||
isError = true
|
||||
end
|
||||
end
|
||||
|
||||
agentEventSink("finalizeExecutedToolCall 4")
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
end
|
||||
|
||||
@@ -1123,30 +1164,32 @@ function executeToolCallsSequential(
|
||||
messages = toolResultMessage[]
|
||||
|
||||
for tc in toolCalls
|
||||
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
agentEventSink("executeToolCallsSequential 2")
|
||||
if prep isa immediateOutcome
|
||||
agentEventSink("executeToolCallsSequential 2-1")
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
agentEventSink("executeToolCallsSequential 2-2")
|
||||
else
|
||||
agentEventSink("executeToolCallsSequential 3")
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
agentEventSink("executeToolCallsSequential 3-1")
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||
signal)
|
||||
agentEventSink("executeToolCallsSequential 3-2")
|
||||
end
|
||||
agentEventSink("executeToolCallsSequential 4")
|
||||
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
$(finalized.result), $(finalized.isError)")
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
agentEventSink("executeToolCallsSequential 5")
|
||||
if signal !== nothing && signal.aborted
|
||||
break
|
||||
end
|
||||
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
agentEventSink("executeToolCallsSequential " * string(prep.args))
|
||||
|
||||
if prep isa immediateOutcome
|
||||
agentEventSink("executeToolCallsSequential 2-1")
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
agentEventSink("executeToolCallsSequential 2-2")
|
||||
else
|
||||
agentEventSink("executeToolCallsSequential 3")
|
||||
#XXX
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
agentEventSink("executeToolCallsSequential 3-1")
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||
signal, agentEventSink)
|
||||
agentEventSink("executeToolCallsSequential 3-2")
|
||||
end
|
||||
agentEventSink("executeToolCallsSequential 4")
|
||||
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
$(finalized.result), $(finalized.isError)")
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
agentEventSink("executeToolCallsSequential 5")
|
||||
if signal !== nothing && signal.aborted
|
||||
break
|
||||
end
|
||||
end
|
||||
agentEventSink("executeToolCallsSequential 6")
|
||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||
@@ -1218,12 +1261,12 @@ function executeToolCallsParallel(
|
||||
agentEventSink,
|
||||
)::agentToolCallBatch
|
||||
|
||||
entries = union{finalizedOutcome,task{finalizedOutcome}}[]
|
||||
entries = Union{finalizedOutcome,Task}[]
|
||||
|
||||
for tc in toolCalls
|
||||
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
@@ -1231,15 +1274,15 @@ function executeToolCallsParallel(
|
||||
finalized.result, finalized.isError))
|
||||
push!(entries, finalized)
|
||||
else
|
||||
task = task() do
|
||||
t = Task() do
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
finalized.result, finalized.isError))
|
||||
return finalized
|
||||
end
|
||||
schedule(task)
|
||||
push!(entries, task)
|
||||
schedule(t)
|
||||
push!(entries, t)
|
||||
end
|
||||
|
||||
if signal !== nothing && signal.aborted
|
||||
@@ -1249,7 +1292,7 @@ function executeToolCallsParallel(
|
||||
|
||||
finalizedCalls = finalizedOutcome[]
|
||||
for entry in entries
|
||||
outcome = entry isa task ? fetch(entry) : entry
|
||||
outcome = entry isa Task ? fetch(entry) : entry
|
||||
push!(finalizedCalls, outcome)
|
||||
end
|
||||
|
||||
|
||||
+4
-96
@@ -1,6 +1,6 @@
|
||||
module toolRegistry
|
||||
|
||||
export toolStore, loadTools, registerTool, getTools, clearTools, listTool
|
||||
export toolStore, registerTool, getTools, clearTools, listTool
|
||||
|
||||
using Dates
|
||||
using JSON, DataStructures
|
||||
@@ -45,7 +45,7 @@ end
|
||||
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
|
||||
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
||||
at runtime.
|
||||
|
||||
# Arguments
|
||||
@@ -55,7 +55,7 @@ at runtime.
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1");
|
||||
|
||||
julia> loadTools(store, "src/tools") # auto-registers listTools
|
||||
julia> register_all_tools(store) # auto-registers listTools
|
||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||
[toolRegistry:agent1] Registered tool: listTools
|
||||
|
||||
@@ -97,99 +97,7 @@ function listTool(store::toolStore)::agentTool
|
||||
)
|
||||
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
|
||||
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
|
||||
|
||||
"""
|
||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using .type
|
||||
using Dates
|
||||
|
||||
"""
|
||||
@@ -15,7 +16,7 @@ Demonstrates custom validation beyond simple required-field checking:
|
||||
- `nothing` if validation passes
|
||||
- `String` error message if validation fails
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function getTimeValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
@@ -43,8 +44,8 @@ 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
|
||||
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
onPartialResult)
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
if tz !== nothing
|
||||
@@ -61,7 +62,7 @@ end
|
||||
"""
|
||||
Define and return the getTime agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function getTimeTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getTime",
|
||||
label = "Time Lookup",
|
||||
@@ -74,9 +75,9 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => []
|
||||
),
|
||||
execute = executeTool,
|
||||
execute = getTimeExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
validateRequiredArgs = getTimeValidateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
|
||||
+21
-12
@@ -1,24 +1,33 @@
|
||||
using msghandler
|
||||
using .type
|
||||
|
||||
"""
|
||||
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
|
||||
)
|
||||
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
agentEventSink)
|
||||
|
||||
agentEventSink("Getting weather...")
|
||||
|
||||
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.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function getWeatherTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
label = "Weather Lookup",
|
||||
@@ -31,7 +40,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = executeTool,
|
||||
execute = getWeatherExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
|
||||
+12
-9
@@ -1,3 +1,4 @@
|
||||
using .type
|
||||
using JSON
|
||||
|
||||
"""
|
||||
@@ -7,15 +8,17 @@ 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(agent._tool_store, "src/tools")` picks
|
||||
up the new file. The new tool is immediately available.
|
||||
After calling this tool, add the new file to `YiemAgent.jl` with an `include()`
|
||||
statement (after `include("toolRegistry.jl")`), then restart the agent.
|
||||
The new tool must be registered in `register_all_tools()` in `toolRegistry.jl`.
|
||||
|
||||
# Example
|
||||
|
||||
1. Agent calls writeTool with a spec for a "searchWine" tool
|
||||
2. writeTool generates src/tools/searchWine.jl
|
||||
3. Restart agent — loadTools() picks up the new file
|
||||
4. Agent calls searchWine with args
|
||||
3. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
||||
4. Developer adds `registerTool(store, searchWineTool())` to register_all_tools()
|
||||
5. Restart agent — new tool is available
|
||||
|
||||
# How It Works
|
||||
|
||||
@@ -24,7 +27,7 @@ tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
|
||||
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
|
||||
- Indents `executeCode` with 4 spaces
|
||||
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
||||
- Appends `getTool()` returning an `agentTool` struct
|
||||
- Appends `writeToolTool()` returning an `agentTool` struct
|
||||
- Writes the combined string to `src/tools/<name>.jl`
|
||||
|
||||
# Important Notes
|
||||
@@ -105,7 +108,7 @@ end
|
||||
"""
|
||||
Define and return the writeTool agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function writeToolTool()::agentTool
|
||||
return agentTool(
|
||||
name = "writeTool",
|
||||
label = "Create Tool",
|
||||
@@ -127,7 +130,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
||||
),
|
||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin
|
||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
|
||||
tool_name = get(args, "name", "")::String
|
||||
tool_label = get(args, "label", tool_name)::String
|
||||
tool_description = get(args, "description", "")::String
|
||||
@@ -250,13 +253,13 @@ function getTool()::agentTool
|
||||
|
||||
tool_code = join(parts)
|
||||
|
||||
# Write the file — tool is loaded on next agent restart via loadTools(store, "src/tools")
|
||||
# Write the file — tool must be included in YiemAgent.jl and registered in register_all_tools()
|
||||
write(filepath, tool_code)
|
||||
|
||||
onPartialResult(Dict("status" => "Done"))
|
||||
|
||||
return agentToolResult(
|
||||
[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.")],
|
||||
[textContent("Tool '$(tool_name)' written to $filepath. Add include(\"tools/$(tool_name).jl\") to YiemAgent.jl and registerTool(store, $(tool_name)Tool()) to register_all_tools(), then restart the agent.")],
|
||||
Dict{Any,Any}(
|
||||
"file" => filepath,
|
||||
"name" => tool_name,
|
||||
|
||||
+2
-2
@@ -264,7 +264,7 @@ struct agentTool # A tool available to the agent
|
||||
label::String # Human-readable tool name
|
||||
description::String # What the tool does
|
||||
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||
execute::Function # Tool execution function
|
||||
execute # Tool execution function
|
||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||
@@ -274,7 +274,7 @@ end
|
||||
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
|
||||
"""
|
||||
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
||||
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
|
||||
execute, prepareArguments::Union{Function, Nothing}=nothing,
|
||||
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false)
|
||||
return agentTool(name, label, description, inputSchema, execute,
|
||||
|
||||
+57
-3
@@ -241,7 +241,38 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
return openaiReadyMsg
|
||||
end
|
||||
|
||||
#TODO
|
||||
"""
|
||||
beforeToolCall(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult
|
||||
|
||||
Callback invoked before executing a tool call. Use this hook to inspect
|
||||
the tool call and decide whether to allow, block, or modify it.
|
||||
|
||||
Common use cases:
|
||||
- Request user approval via UI before running destructive tools.
|
||||
- Validate business rules that cannot be expressed in the JSON schema.
|
||||
- Check final context (e.g. session state, rate limits, permissions).
|
||||
|
||||
# Arguments
|
||||
- `context::beforeToolCallContext`: Contains the assistant message, tool call,
|
||||
validated arguments, and current conversation context.
|
||||
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||
|
||||
# Returns
|
||||
- `beforeToolCallResult(false, "N/A")` to allow the call to proceed.
|
||||
- `beforeToolCallResult(true, "Reason")` to block the call with a reason.
|
||||
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
|
||||
|
||||
# Example
|
||||
```julia
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
|
||||
if context.toolCall.name == "deleteFile"
|
||||
# Block file deletion unless explicitly approved
|
||||
return beforeToolCallResult(true, "User must approve file deletion")
|
||||
end
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
```
|
||||
"""
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
)::beforeToolCallResult
|
||||
|
||||
@@ -254,8 +285,31 @@ function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
|
||||
#TODO
|
||||
function afterToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
"""
|
||||
afterToolCall(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}
|
||||
|
||||
Callback invoked after a tool call finishes executing (before and after errors).
|
||||
Use this hook to post-process the tool result before it is fed back to the LLM.
|
||||
|
||||
Common use cases:
|
||||
- Mask sensitive data (API keys, tokens) from result content.
|
||||
- Normalize usage tracking data into a consistent format.
|
||||
- Inspect the result and set `terminate: true` based on business logic
|
||||
(e.g. "if deployment failed, stop the agent rather than retrying").
|
||||
- Wrap error results in friendlier messages for the LLM to understand.
|
||||
|
||||
# Arguments
|
||||
- `context::afterToolCallContext`: Contains the assistant message, tool call,
|
||||
arguments, raw result, error status, and current conversation context.
|
||||
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||
|
||||
# Returns
|
||||
- `nothing` to pass the result through unchanged.
|
||||
- `agentToolResult(...)` to return a modified result (content, details, usage,
|
||||
terminate flag can all be overridden).
|
||||
|
||||
"""
|
||||
function afterToolCall(context::afterToolCallContext, signal::abortSignal
|
||||
)::Union{agentToolResult, Nothing}
|
||||
|
||||
# modify context.result if needed and return agentToolResult
|
||||
|
||||
Reference in New Issue
Block a user