update
This commit is contained in:
@@ -8,7 +8,7 @@ Julia framework for building agents with tool use and MCP (Model Context Protoco
|
||||
2. Create a callable `mcpServer` struct that communicates with an MCP server via NATS
|
||||
3. Create a `yiemAgent` with an LLM callable and MCP server:
|
||||
```julia
|
||||
agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, agentEventSink=yourSink)
|
||||
agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, eventSink=yourSink)
|
||||
```
|
||||
4. Call `runAgent(agent, message)` then `takeResponse(agent)`
|
||||
|
||||
@@ -136,4 +136,4 @@ Tools are discovered dynamically via MCP server:
|
||||
| `formatMsgForLLM` | `(ctx, sink) -> dict` | Convert agent context to LLM API format |
|
||||
| `beforeToolCall` | `(context, signal) -> result` | Block/allow tool execution |
|
||||
| `afterToolCall` | `(context, signal) -> result` | Post-process tool results |
|
||||
| `agentEventSink` | `(msg) -> nothing` | Callback for agent events/debug messages |
|
||||
| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages |
|
||||
|
||||
+32
-32
@@ -89,7 +89,7 @@ using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
||||
# Create agent — tools are registered automatically via register_all_tools()
|
||||
agent = yiemAgent(
|
||||
llmCall = my_llm_call, # Function that calls the LLM API
|
||||
agentEventSink = my_event_sink, # Function for TUI/logging
|
||||
eventSink = my_event_sink, # Function for TUI/logging
|
||||
)
|
||||
```
|
||||
|
||||
@@ -98,7 +98,7 @@ agent = yiemAgent(
|
||||
| Parameter | Type | Required | Purpose |
|
||||
|-----------|------|----------|---------|
|
||||
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
||||
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
||||
| `eventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
||||
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
|
||||
| `model` | `llmModel` | No | LLM model config |
|
||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
||||
@@ -179,7 +179,7 @@ Each phase has a single responsibility and produces an intermediate result:
|
||||
| Phase | Function | Input | Output | Purpose |
|
||||
|-------|----------|-------|--------|---------|
|
||||
| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook |
|
||||
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `agentEventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
|
||||
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `eventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
|
||||
| Finalize | `finalizeExecutedToolCall()` | `agentContext`, `assistantMessage`, `preparedToolCall`, `executedOutcome`, `agentLoopConfig`, `abortSignal` | `finalizedOutcome` | Run post-hook, emit end event |
|
||||
|
||||
The pipeline ensures that **every tool call produces a result**, even on failure. Errors are captured as `immediateOutcome`, `executedOutcome`, or `finalizedOutcome` with `isError=true`, then converted to `toolResultMessage` objects that are fed back to the LLM conversation history.
|
||||
@@ -222,7 +222,7 @@ end
|
||||
|
||||
```julia
|
||||
execute(toolCallId::String,
|
||||
args::Dict{String,Any},
|
||||
args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
```
|
||||
@@ -430,12 +430,12 @@ function _processMessage(agent::yiemAgent)::assistantMessage
|
||||
|
||||
# ── Step 2: Prepare context ─────────────────────────────────
|
||||
state = agentState(systemPrompt, nothing, tools, messages)
|
||||
preparedContext = prepareContext(state, agentEventSink)
|
||||
preparedContext = prepareContext(state, eventSink)
|
||||
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
|
||||
# Override point: filter tools, inject context, modify system prompt
|
||||
|
||||
# ── Step 3: Format for LLM ──────────────────────────────────
|
||||
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
|
||||
formattedMessages = formatMsgForLLM(preparedContext, eventSink)
|
||||
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
||||
# Wraps systemPrompt as system role, converts each messageContent block
|
||||
|
||||
@@ -456,7 +456,7 @@ function _processMessage(agent::yiemAgent)::assistantMessage
|
||||
signal = abortSignal(false)
|
||||
|
||||
# Execute tool calls (sequential or parallel)
|
||||
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, agentEventSink)
|
||||
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, eventSink)
|
||||
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
@@ -611,7 +611,7 @@ function prepareToolCall(
|
||||
function executePreparedToolCall(
|
||||
prep::preparedToolCall,
|
||||
signal::Union{Nothing, abortSignal},
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
)::executedOutcome
|
||||
```
|
||||
|
||||
@@ -623,7 +623,7 @@ function executePreparedToolCall(
|
||||
prep.toolCall.id,
|
||||
prep.args,
|
||||
signal,
|
||||
agentEventSink # serves as onPartialResult callback
|
||||
eventSink # serves as onPartialResult callback
|
||||
)
|
||||
return executedOutcome(result, false)
|
||||
```
|
||||
@@ -698,7 +698,7 @@ function executeToolCalls(
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::Union{Nothing, abortSignal},
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
)::agentToolCallBatch
|
||||
```
|
||||
|
||||
@@ -733,12 +733,12 @@ function executeToolCallsSequential(...)::agentToolCallBatch
|
||||
messages = toolResultMessage[]
|
||||
|
||||
for tc in toolCalls
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
else
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
end
|
||||
|
||||
@@ -763,14 +763,14 @@ function executeToolCallsParallel(...)::agentToolCallBatch
|
||||
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
||||
|
||||
for tc in toolCalls
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
push!(entries, finalized) # immediate outcome — no task
|
||||
else
|
||||
task = task() do
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
return finalized
|
||||
end
|
||||
@@ -845,7 +845,7 @@ From the type documentation (`type.jl:803-815`):
|
||||
**Source:** `agentCore.jl:266-307`
|
||||
|
||||
```julia
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink)
|
||||
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
@@ -1037,13 +1037,13 @@ end
|
||||
|
||||
### Event Sink
|
||||
|
||||
The `agentEventSink` function is passed through the entire call chain:
|
||||
The `eventSink` function is passed through the entire call chain:
|
||||
|
||||
```julia
|
||||
agentEventSink = agent.agentEventSink # set during yiemAgent construction
|
||||
eventSink = agent.eventSink # set during yiemAgent construction
|
||||
```
|
||||
|
||||
The `agentEventSink` function is a user-provided callback that receives all events. This is typically used by:
|
||||
The `eventSink` function is a user-provided callback that receives all events. This is typically used by:
|
||||
- **TUI (Terminal UI):** Display real-time progress, tool names, results
|
||||
- **Logging systems:** Record tool execution history
|
||||
- **Monitoring:** Track tool usage, execution times, error rates
|
||||
@@ -1057,12 +1057,12 @@ The `agentEventSink` function is a user-provided callback that receives all even
|
||||
|
||||
| Hook | Signature | Called | Purpose |
|
||||
|------|-----------|--------|---------|
|
||||
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||
| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
||||
| `beforeToolCall` | `(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
|
||||
| `afterToolCall` | `(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
|
||||
| `agentEventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
|
||||
| `eventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
|
||||
|
||||
### `beforeToolCall` Hook
|
||||
|
||||
@@ -1125,7 +1125,7 @@ end
|
||||
**Source:** `utils.jl:111-125`
|
||||
|
||||
```julia
|
||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
||||
function prepareContext(state::agentState, eventSink)::agentContext
|
||||
# TODO: filter tools from state.tools based on user intent
|
||||
filteredTools = state.tools
|
||||
|
||||
@@ -1152,7 +1152,7 @@ end
|
||||
Default implementation converts `agentContext` to OpenAI-compatible format:
|
||||
|
||||
```julia
|
||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
|
||||
messages = Vector{Dict{String, Any}}()
|
||||
|
||||
# System prompt as system message
|
||||
@@ -1284,7 +1284,7 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
│ Step 6: Execute tool calls
|
||||
│ context = agentContext(systemPrompt, messages, tools)
|
||||
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink)
|
||||
|
||||
|
||||
LOOP ITERATION 1 — executeToolCallsSequential
|
||||
@@ -1299,7 +1299,7 @@ LOOP ITERATION 1 — executeToolCallsSequential
|
||||
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||
│
|
||||
│ EXECUTE:
|
||||
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, agentEventSink)
|
||||
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, eventSink)
|
||||
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||
│ → executedOutcome(result, false)
|
||||
│
|
||||
@@ -1365,20 +1365,20 @@ end
|
||||
|
||||
```julia
|
||||
# Argument preparation (before validation)
|
||||
function <name>PrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
function <name>PrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
|
||||
# Return modified args, or args unchanged
|
||||
return args
|
||||
end
|
||||
|
||||
# Custom validation (before execution)
|
||||
function <name>ValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function <name>ValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
|
||||
# Return nothing to pass, or error string to fail
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Core execution
|
||||
function <name>Execute(toolCallId::String,
|
||||
args::Dict{String,Any},
|
||||
args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
# Return agentToolResult with content, details, usage, terminate
|
||||
@@ -1400,17 +1400,17 @@ function helper_function(...)
|
||||
end
|
||||
|
||||
# Optional: prepareArguments
|
||||
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
function myToolPrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
|
||||
return args
|
||||
end
|
||||
|
||||
# Optional: validateRequiredArgs
|
||||
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function myToolValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Required: execute function
|
||||
function myToolExecute(toolCallId::String, args::Dict{String,Any},
|
||||
function myToolExecute(toolCallId::String, args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
...
|
||||
@@ -1481,7 +1481,7 @@ To add a new tool (e.g., `searchWine.jl`):
|
||||
using .type
|
||||
# using AdditionalPkg # add if needed
|
||||
|
||||
function searchWineExecute(toolCallId::String, args::Dict{String,Any},
|
||||
function searchWineExecute(toolCallId::String, args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal}, onPartialResult)
|
||||
query = get(args, "query", "")
|
||||
result = search_wine_db(query)
|
||||
|
||||
+3
-12
@@ -1,10 +1,7 @@
|
||||
module YiemAgent
|
||||
|
||||
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.
|
||||
"""
|
||||
"""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."""
|
||||
|
||||
include("type.jl")
|
||||
using .type
|
||||
@@ -15,12 +12,6 @@ module YiemAgent
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
function register_all_tools(store::toolRegistry.toolStore, mcpserver=nothing)
|
||||
# Only register listTools — all other tools come from MCP server at runtime
|
||||
registerTool(store, listTool(store, mcpserver))
|
||||
return store.tools
|
||||
end
|
||||
|
||||
# include("llmfunction.jl")
|
||||
# using .llmfunction
|
||||
|
||||
@@ -31,10 +22,10 @@ module YiemAgent
|
||||
using .api
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
end # module YiemAgent_v1
|
||||
|
||||
+101
-106
@@ -9,11 +9,6 @@ using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serializ
|
||||
using GeneralUtils
|
||||
using ..type, ..utils, ..toolRegistry
|
||||
|
||||
function register_all_tools(store::toolRegistry.toolStore, mcpServer=nothing)
|
||||
# Call parent module's version which has access to tool functions
|
||||
parentmodule(@__MODULE__).register_all_tools(store, mcpServer)
|
||||
end
|
||||
|
||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||
|
||||
"""
|
||||
@@ -74,7 +69,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
# return incoming_env["payloads"][1][2]
|
||||
# end
|
||||
#
|
||||
# function (c::MyMCPClient)(method::String, toolName::String, arguments::Dict{String,Any})
|
||||
# function (c::MyMCPClient)(method::String, toolName::String, arguments::AbstractDict{String, Any})
|
||||
# payload = Dict("jsonrpc" => "2.0", "id" => 2, "method" => method,
|
||||
# "params" => Dict("name" => toolName, "arguments" => arguments))
|
||||
# payloads = [("payload", payload, "dictionary"),]
|
||||
@@ -137,7 +132,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
sessionId::Union{String, Nothing} # Optional session identifier
|
||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||
parallelToolExecute::Bool # Default: false
|
||||
agentEventSink # agent emits its status via this function
|
||||
eventSink # agent emits its status via this function
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -161,7 +156,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
||||
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
|
||||
- `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
|
||||
- `eventSink::Function`: Callback to receive agent events
|
||||
- `mcpServer`: Callable struct for MCP server communication. Called as
|
||||
`mcpServer("tools/list")` to discover tools, or `mcpServer("tools/call", args)`
|
||||
to execute a tool. Returns parsed JSON dicts. (default: `nothing`)
|
||||
@@ -184,7 +179,7 @@ function yiemAgent(
|
||||
sessionId::Union{String, Nothing}=nothing,
|
||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false,
|
||||
agentEventSink=agentEventSink,
|
||||
eventSink=eventSink,
|
||||
mcpServer=nothing,
|
||||
)
|
||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||
@@ -194,7 +189,7 @@ function yiemAgent(
|
||||
|
||||
# load tools (statically registered at module init)
|
||||
toolStore1 = toolStore(name="myagent")
|
||||
register_all_tools(toolStore1, mcpServer)
|
||||
registerAllTools(toolStore1, mcpServer; eventSink=eventSink)
|
||||
|
||||
# Create struct with a placeholder task, then spawn and replace it
|
||||
agent = yiemAgent(
|
||||
@@ -214,7 +209,7 @@ function yiemAgent(
|
||||
sessionId,
|
||||
maxRetryDelayMs,
|
||||
parallelToolExecute,
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
)
|
||||
|
||||
# Spawn the background loop and attach it
|
||||
@@ -289,18 +284,18 @@ function _agentLoop(agent::yiemAgent)
|
||||
while true
|
||||
while newUserMsg === nothing
|
||||
if isready(agent.inputChannel)
|
||||
agent.agentEventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))")
|
||||
# agent process new user msg immediately after the current tool call finished.
|
||||
newUserMsg = take!(agent.inputChannel)
|
||||
agent.agentEventSink("new user msg")
|
||||
agent.eventSink("new user msg")
|
||||
else
|
||||
# check followUp message after _processMessage() is done
|
||||
if typeof(processingTask) == Task && istaskdone(processingTask) == true
|
||||
agent.agentEventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))")
|
||||
# if agent runs is done but followUpChannel has messages,
|
||||
# put new message in inputChannel instead
|
||||
if isready(agent.followUpChannel)
|
||||
agent.agentEventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))")
|
||||
while isready(agent.followUpChannel)
|
||||
followUpMsg = take!(agent.followUpChannel)
|
||||
put!(agent.inputChannel, followUpMsg)
|
||||
@@ -308,18 +303,18 @@ function _agentLoop(agent::yiemAgent)
|
||||
processingTask = nothing # reset
|
||||
result = nothing # reset
|
||||
else # _processMessage() done and no followUp message.
|
||||
agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
|
||||
result = deepcopy(agent._state.messages[end])
|
||||
agent.agentEventSink("_agentLoop 4-1 ")
|
||||
agent.eventSink("_agentLoop 4-1 ")
|
||||
# filter out reasoningContent in-place
|
||||
filter!(c -> !(c isa reasoningContent), result.content)
|
||||
agent.agentEventSink("_agentLoop 5 ")
|
||||
agent.eventSink("_agentLoop 5 ")
|
||||
# format output
|
||||
respondToUI = _assistantMessageToOpenAI(result)
|
||||
agent.agentEventSink("_agentLoop 6 ")
|
||||
agent.eventSink("_agentLoop 6 ")
|
||||
put!(agent.outputChannel, respondToUI)
|
||||
if !isempty(result.content) && result.content[1] isa textContent
|
||||
agent.agentEventSink(result.content[1].text)
|
||||
agent.eventSink(result.content[1].text)
|
||||
end
|
||||
processingTask = nothing # reset
|
||||
result = nothing # reset
|
||||
@@ -350,7 +345,7 @@ function _agentLoop(agent::yiemAgent)
|
||||
else
|
||||
# spawn new _processMessage() if it is not already running.
|
||||
if processingTask === nothing
|
||||
agent.agentEventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))")
|
||||
# discard all messages in followUpChannel
|
||||
while isready(agent.followUpChannel)
|
||||
_ = take!(agent.followUpChannel)
|
||||
@@ -359,7 +354,7 @@ function _agentLoop(agent::yiemAgent)
|
||||
# Dispatch message through the processing pipeline
|
||||
processingTask = @spawn _processMessage(
|
||||
processMessageInputCh,
|
||||
agent.agentEventSink,
|
||||
agent.eventSink,
|
||||
agent._state.messages,
|
||||
agent._state.systemPrompt,
|
||||
agent._state.tools,
|
||||
@@ -381,7 +376,7 @@ function _agentLoop(agent::yiemAgent)
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
agentEventSink(errMsg)
|
||||
eventSink(errMsg)
|
||||
|
||||
# On any error, send error response and exit the loop
|
||||
@error "Agent loop failed" error=e
|
||||
@@ -418,7 +413,7 @@ julia> # Currently returns a placeholder echo response
|
||||
"""
|
||||
function _processMessage(
|
||||
inputChannel::Channel,
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
agentMsgHistory::Vector{agentMessage},
|
||||
systemPrompt::String,
|
||||
tools::OrderedDict{String, agentTool},
|
||||
@@ -429,7 +424,7 @@ function _processMessage(
|
||||
afterToolCall::Union{Function, Nothing},
|
||||
parallelToolExecute::Bool,
|
||||
)::Nothing
|
||||
agentEventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
|
||||
# loop until llmCall() response didn't use tool calls
|
||||
final_response = nothing
|
||||
|
||||
@@ -449,43 +444,43 @@ function _processMessage(
|
||||
while true
|
||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||
while isready(inputChannel)
|
||||
agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||
newUserMsg_openai = take!(inputChannel)
|
||||
agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||
if newUserMsg_openai === :shutdown
|
||||
agentEventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
||||
# Re-emit shutdown signal for the loop to handle
|
||||
put!(inputChannel, :shutdown)
|
||||
break
|
||||
end
|
||||
agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
||||
push!(agentMsgHistory, newUserMsg)
|
||||
agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||
end
|
||||
agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||
# call prepareContext()
|
||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||
preparedContext = prepareContext(state, agentEventSink, llmCall)
|
||||
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||
preparedContext = prepareContext(state, eventSink, llmCall)
|
||||
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||
# Call formatMessagesForLLM() to format for LLM
|
||||
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
|
||||
formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
|
||||
|
||||
agentEventSink("_processMessage 10 formattedMessages $formattedMessages")
|
||||
eventSink("_processMessage 10 formattedMessages $formattedMessages")
|
||||
|
||||
""" response example
|
||||
response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")]))
|
||||
"""
|
||||
response = llmCall(formattedMessages)
|
||||
agentEventSink(" llmCall " * string(response))
|
||||
eventSink(" llmCall " * string(response))
|
||||
|
||||
agentEventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
|
||||
# Extract tool calls from LLM response content blocks
|
||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
||||
agentEventSink("assistant_msg " * string(assistant_msg))
|
||||
agentEventSink("_processMessage 11-1")
|
||||
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("assistant_msg " * string(assistant_msg))
|
||||
eventSink("_processMessage 11-1")
|
||||
|
||||
# Add assistant message (tool calls or text) to history for next LLM turn
|
||||
push!(agentMsgHistory, assistant_msg)
|
||||
@@ -501,21 +496,21 @@ function _processMessage(
|
||||
)
|
||||
|
||||
signal = abortSignal(false)
|
||||
agentEventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
||||
|
||||
# call executeToolCalls()
|
||||
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||
signal, agentEventSink)
|
||||
agentEventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
||||
signal, eventSink)
|
||||
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
||||
|
||||
# save toolResults to messages
|
||||
for toolResult in toolResultBatch.messages
|
||||
agentEventSink("toolResult " * string(toolResult))
|
||||
eventSink("toolResult " * string(toolResult))
|
||||
push!(agentMsgHistory, toolResult)
|
||||
end
|
||||
agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||
if toolResultBatch.terminate
|
||||
agentEventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
||||
# If toolResultBatch requested termination, build a final response
|
||||
final_content = [textContent("Tool execution completed.")]
|
||||
for toolResult in toolResultBatch.messages
|
||||
@@ -529,7 +524,7 @@ function _processMessage(
|
||||
end
|
||||
end
|
||||
end
|
||||
agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||
final_response = assistantMessage(
|
||||
role="assistant",
|
||||
content=final_content,
|
||||
@@ -548,12 +543,12 @@ function _processMessage(
|
||||
break
|
||||
end
|
||||
else
|
||||
agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||
# LLM did not use tool calls —
|
||||
break
|
||||
end
|
||||
end
|
||||
agentEventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
||||
return nothing
|
||||
end
|
||||
|
||||
@@ -991,48 +986,48 @@ function prepareToolCall(
|
||||
toolCall::agentToolCall,
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
agentEventSink
|
||||
eventSink
|
||||
)::Union{preparedToolCall,immediateOutcome}
|
||||
agentEventSink("prepareToolCall 1")
|
||||
eventSink("prepareToolCall 1")
|
||||
|
||||
# pick a called tool from tool store
|
||||
tool = get(context.tools, toolCall.name, nothing)
|
||||
|
||||
if tool === nothing
|
||||
agentEventSink("prepareToolCall 2")
|
||||
eventSink("prepareToolCall 2")
|
||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||
end
|
||||
|
||||
try
|
||||
agentEventSink("prepareToolCall 3")
|
||||
eventSink("prepareToolCall 3")
|
||||
# 1. prepare arguments (tool-specific transform)
|
||||
prepared = prepareToolCallArguments(tool, toolCall)
|
||||
agentEventSink("prepared " * string(prepared.arguments))
|
||||
agentEventSink("prepareToolCall 4")
|
||||
eventSink("prepared " * string(prepared.arguments))
|
||||
eventSink("prepareToolCall 4")
|
||||
validatedArgs = validateToolArguments(tool, prepared)
|
||||
agentEventSink("validatedArgs " * string(validatedArgs))
|
||||
agentEventSink("prepareToolCall 5")
|
||||
eventSink("validatedArgs " * string(validatedArgs))
|
||||
eventSink("prepareToolCall 5")
|
||||
# 2. beforeToolCall hook — can block
|
||||
if config.beforeToolCall !== nothing
|
||||
agentEventSink("prepareToolCall 6")
|
||||
eventSink("prepareToolCall 6")
|
||||
|
||||
before = config.beforeToolCall(
|
||||
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
|
||||
signal
|
||||
)
|
||||
agentEventSink("prepareToolCall 7")
|
||||
eventSink("prepareToolCall 7")
|
||||
if signal.aborted
|
||||
agentEventSink("prepareToolCall 8")
|
||||
eventSink("prepareToolCall 8")
|
||||
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
||||
end
|
||||
|
||||
if before !== nothing && before.block
|
||||
agentEventSink("prepareToolCall 9")
|
||||
eventSink("prepareToolCall 9")
|
||||
return immediateOutcome(
|
||||
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
||||
end
|
||||
end
|
||||
agentEventSink("prepareToolCall 10")
|
||||
eventSink("prepareToolCall 10")
|
||||
return preparedToolCall(tool, toolCall, validatedArgs)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
@@ -1041,7 +1036,7 @@ function prepareToolCall(
|
||||
println(io)
|
||||
end
|
||||
|
||||
agentEventSink(errMsg)
|
||||
eventSink(errMsg)
|
||||
|
||||
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||
end
|
||||
@@ -1091,16 +1086,16 @@ executePreparedToolCall(prep, nothing, emit)
|
||||
function executePreparedToolCall(
|
||||
prep::preparedToolCall,
|
||||
signal::Union{Nothing,abortSignal},
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
llmCall::Union{Any,Nothing}=nothing,
|
||||
)::executedOutcome
|
||||
agentEventSink("executePreparedToolCall 1")
|
||||
eventSink("executePreparedToolCall 1")
|
||||
|
||||
try #WORKING
|
||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink, llmCall)
|
||||
agentEventSink("executePreparedToolCall 2")
|
||||
agentEventSink(result.content[1].text)
|
||||
agentEventSink("executePreparedToolCall 3")
|
||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink, llmCall)
|
||||
eventSink("executePreparedToolCall 2")
|
||||
eventSink(result.content[1].text)
|
||||
eventSink("executePreparedToolCall 3")
|
||||
return executedOutcome(result, false)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
@@ -1108,7 +1103,7 @@ function executePreparedToolCall(
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
agentEventSink(errMsg)
|
||||
eventSink(errMsg)
|
||||
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||
end
|
||||
@@ -1178,19 +1173,19 @@ function finalizeExecutedToolCall(
|
||||
executed::executedOutcome,
|
||||
config::agentLoopConfig,
|
||||
signal::Union{Nothing,abortSignal},
|
||||
agentEventSink
|
||||
eventSink
|
||||
)::finalizedOutcome
|
||||
agentEventSink("finalizeExecutedToolCall 1")
|
||||
eventSink("finalizeExecutedToolCall 1")
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
agentEventSink("finalizeExecutedToolCall 2")
|
||||
eventSink("finalizeExecutedToolCall 2")
|
||||
if config.afterToolCall !== nothing
|
||||
try
|
||||
after = config.afterToolCall(
|
||||
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
|
||||
signal
|
||||
)
|
||||
agentEventSink("finalizeExecutedToolCall 3")
|
||||
eventSink("finalizeExecutedToolCall 3")
|
||||
if after !== nothing
|
||||
result = merge(result, dict(:content=>get(after,:content,result.content),
|
||||
:details=>get(after,:details,result.details),
|
||||
@@ -1204,13 +1199,13 @@ function finalizeExecutedToolCall(
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
agentEventSink(errMsg)
|
||||
eventSink(errMsg)
|
||||
|
||||
result = createErrorToolResult(sprint(showerror, e))
|
||||
isError = true
|
||||
end
|
||||
end
|
||||
agentEventSink("finalizeExecutedToolCall 4")
|
||||
eventSink("finalizeExecutedToolCall 4")
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
end
|
||||
|
||||
@@ -1274,41 +1269,41 @@ function executeToolCallsSequential(
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
)::agentToolCallBatch
|
||||
llmCall = config.llmCall
|
||||
agentEventSink("executeToolCallsSequential 1")
|
||||
eventSink("executeToolCallsSequential 1")
|
||||
finalizedCalls = finalizedOutcome[]
|
||||
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 " * string(prep.args))
|
||||
eventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||
eventSink("executeToolCallsSequential " * string(prep.args))
|
||||
|
||||
if prep isa immediateOutcome
|
||||
agentEventSink("executeToolCallsSequential 2-1")
|
||||
eventSink("executeToolCallsSequential 2-1")
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
agentEventSink("executeToolCallsSequential 2-2")
|
||||
eventSink("executeToolCallsSequential 2-2")
|
||||
else
|
||||
agentEventSink("executeToolCallsSequential 3")
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
|
||||
agentEventSink("executeToolCallsSequential 3-1")
|
||||
eventSink("executeToolCallsSequential 3")
|
||||
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
|
||||
eventSink("executeToolCallsSequential 3-1")
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||
signal, agentEventSink)
|
||||
agentEventSink("executeToolCallsSequential 3-2")
|
||||
signal, eventSink)
|
||||
eventSink("executeToolCallsSequential 3-2")
|
||||
end
|
||||
agentEventSink("executeToolCallsSequential 4")
|
||||
agentEventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
eventSink("executeToolCallsSequential 4")
|
||||
eventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
$(finalized.result), $(finalized.isError)")
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
agentEventSink("executeToolCallsSequential 5")
|
||||
eventSink("executeToolCallsSequential 5")
|
||||
if signal !== nothing && signal.aborted
|
||||
break
|
||||
end
|
||||
end
|
||||
agentEventSink("executeToolCallsSequential 6")
|
||||
eventSink("executeToolCallsSequential 6")
|
||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||
end
|
||||
|
||||
@@ -1375,27 +1370,27 @@ function executeToolCallsParallel(
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
)::agentToolCallBatch
|
||||
|
||||
entries = Union{finalizedOutcome,Task}[]
|
||||
llmCall = config.llmCall
|
||||
|
||||
for tc in toolCalls
|
||||
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
finalized.result, finalized.isError))
|
||||
push!(entries, finalized)
|
||||
else
|
||||
t = Task() do
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
|
||||
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
finalized.result, finalized.isError))
|
||||
return finalized
|
||||
end
|
||||
@@ -1478,11 +1473,11 @@ function executeToolCalls(
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
)::agentToolCallBatch
|
||||
|
||||
llmCall = config.llmCall
|
||||
agentEventSink("_executeToolCalls 1")
|
||||
eventSink("_executeToolCalls 1")
|
||||
hasSequential = false
|
||||
for tc in toolCalls
|
||||
t = get(context.tools, tc.name, nothing)
|
||||
@@ -1491,15 +1486,15 @@ function executeToolCalls(
|
||||
break
|
||||
end
|
||||
end
|
||||
agentEventSink("_executeToolCalls 2")
|
||||
eventSink("_executeToolCalls 2")
|
||||
if config.toolExecution == "sequential" || hasSequential
|
||||
agentEventSink("_executeToolCalls 3")
|
||||
eventSink("_executeToolCalls 3")
|
||||
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
|
||||
agentEventSink)
|
||||
eventSink)
|
||||
else
|
||||
agentEventSink("_executeToolCalls 4")
|
||||
eventSink("_executeToolCalls 4")
|
||||
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
|
||||
agentEventSink)
|
||||
eventSink)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+106
-139
@@ -1,6 +1,6 @@
|
||||
module toolRegistry
|
||||
|
||||
export toolStore, registerTool, getTools, clearTools, listTool
|
||||
export toolStore, registerTool, registerAllTools, getTools, clearTools, listTool
|
||||
|
||||
using Dates
|
||||
using JSON, DataStructures
|
||||
@@ -9,8 +9,7 @@ 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.
|
||||
Each agent gets its own `toolStore` so tool registration is independent.
|
||||
|
||||
# Fields
|
||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
||||
@@ -28,12 +27,6 @@ Create a new empty tool store.
|
||||
|
||||
# Keyword Arguments
|
||||
- `name::String`: Display name for logging (default: `"default"`)
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1")
|
||||
toolStore(OrderedDict{String, agentTool}(), "agent1")
|
||||
```
|
||||
"""
|
||||
function toolStore(; name::String="default")::toolStore
|
||||
toolStore(OrderedDict{String, agentTool}(), name)
|
||||
@@ -46,8 +39,6 @@ Extract text from MCP tool result content array.
|
||||
|
||||
Handles JSON-RPC 2.0 result content format:
|
||||
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
||||
|
||||
Returns the joined text content, or JSON-serialized fallback if no text blocks found.
|
||||
"""
|
||||
function _extract_text_content(result::Dict)::String
|
||||
content = get(result, "content", Any[])
|
||||
@@ -69,10 +60,9 @@ end
|
||||
Wrap an MCP tool definition as an `agentTool`.
|
||||
|
||||
The returned tool's `execute` function calls the MCP server's "tools/call"
|
||||
method with the validated arguments. Responses are in JSON-RPC 2.0 format
|
||||
with result/error envelopes handled by the execute function.
|
||||
method with the validated arguments.
|
||||
"""
|
||||
function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
|
||||
function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSink=nothing)::agentTool
|
||||
name = tool_def["name"]
|
||||
title = get(tool_def, "title", get(tool_def, "label", name))
|
||||
desc = get(tool_def, "description", "")
|
||||
@@ -98,9 +88,9 @@ function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
|
||||
label=title,
|
||||
description=desc,
|
||||
inputSchema=params,
|
||||
execute=(toolCallId::String, args::Dict{String,Any},
|
||||
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
agentEventSink,
|
||||
eventSink,
|
||||
llmCall=nothing) -> begin
|
||||
try
|
||||
response = mcpserver("tools/call", name, args)
|
||||
@@ -145,14 +135,18 @@ Discover and register MCP tools into `store.tools`.
|
||||
|
||||
Queries the MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0 format),
|
||||
parses the response envelope, and registers each discovered tool.
|
||||
Handles pagination via `nextCursor` — keeps fetching until cursor is empty.
|
||||
Skips tools already registered. Returns `(new_count, tool_list_text)`.
|
||||
Handles pagination via `nextCursor`. Skips tools already registered.
|
||||
|
||||
# Returns
|
||||
- `Int`: number of new tools registered
|
||||
"""
|
||||
function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
|
||||
function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
||||
if mcpserver === nothing
|
||||
return (0, "")
|
||||
return 0
|
||||
end
|
||||
|
||||
new_count = 0
|
||||
|
||||
try
|
||||
response = mcpserver("tools/list")
|
||||
|
||||
@@ -160,7 +154,8 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
|
||||
if haskey(response, "error")
|
||||
rpc_error = response["error"]
|
||||
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
||||
return (0, "MCP tools/list failed: $err_msg")
|
||||
println("[toolRegistry:$(store.name)] MCP tools/list failed: $err_msg")
|
||||
return 0
|
||||
end
|
||||
if haskey(response, "result")
|
||||
response = response["result"]
|
||||
@@ -169,13 +164,13 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
|
||||
tools_array = response["tools"]
|
||||
cursor = get(response, "nextCursor", nothing)
|
||||
|
||||
new_count = 0
|
||||
for tool_def in tools_array
|
||||
name = tool_def["name"]
|
||||
if haskey(store.tools, name)
|
||||
continue
|
||||
end
|
||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
|
||||
@show "tool_def $(typeof(tool_def))"
|
||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
||||
store.tools[name] = wrapped
|
||||
new_count += 1
|
||||
end
|
||||
@@ -193,101 +188,29 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
|
||||
if haskey(store.tools, name)
|
||||
continue
|
||||
end
|
||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
|
||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
||||
store.tools[name] = wrapped
|
||||
new_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Build readable tool list
|
||||
lines = String[
|
||||
"- $(t.name): $(t.label) — $(t.description)"
|
||||
for (k, t) in store.tools
|
||||
]
|
||||
tool_list_text = "Discovered $(new_count) MCP tools. Total registered: $(length(store.tools)).\nAvailable tools:\n" * join(lines, "\n")
|
||||
|
||||
return (new_count, tool_list_text)
|
||||
println("[toolRegistry:$(store.name)] Discovered $new_count MCP tools. Total: $(length(store.tools))")
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
err_msg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
|
||||
eventSink(err_msg)
|
||||
|
||||
errMsg = sprint(showerror, e)
|
||||
return (0, "MCP tools/list failed: $errMsg")
|
||||
println("[toolRegistry:$(store.name)] MCP tools/list failed: $errMsg")
|
||||
end
|
||||
|
||||
return new_count
|
||||
end
|
||||
|
||||
"""
|
||||
listTool(store::toolStore, mcpserver) -> agentTool
|
||||
|
||||
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
|
||||
|
||||
First call: queries the MCP server via `mcpserver("tools/list")`, registers
|
||||
all discovered tools into the shared `store.tools` (in-place mutation, with
|
||||
pagination via nextCursor), then returns the full tool list.
|
||||
|
||||
Subsequent calls: returns the current list (tools remain registered).
|
||||
|
||||
This is the only pre-registered tool. All other tools come from the
|
||||
MCP server and are loaded at runtime when the LLM calls listTools().
|
||||
|
||||
# Arguments
|
||||
- `store`: The tool store to populate with MCP tools
|
||||
- `mcpserver`: A callable struct that communicates with the MCP server.
|
||||
Called as `mcpserver("tools/list")` or `mcpserver("tools/call", args)`.
|
||||
Returns parsed JSON dicts.
|
||||
|
||||
# Example
|
||||
```julia
|
||||
# User provides an MCP server client (callable struct)
|
||||
mcp = MyMCPClient("nats://localhost:4222")
|
||||
store = toolStore(name="agent1")
|
||||
registerTool(store, listTool(store, mcp))
|
||||
|
||||
# When agent calls listTools(), tools are discovered from MCP server
|
||||
# and registered into store.tools in real time.
|
||||
```
|
||||
"""
|
||||
function listTool(store::toolStore, mcpserver)::agentTool
|
||||
return agentTool(
|
||||
name="listTools",
|
||||
label="List Tools",
|
||||
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
|
||||
inputSchema=Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict{String,Any}(),
|
||||
"required" => Any[]
|
||||
),
|
||||
execute=(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
agentEventSink, llmCall=nothing) -> begin
|
||||
# Discover and register MCP tools (idempotent — skips already registered)
|
||||
new_count, tool_list = _register_mcp_tools(mcpserver, store)
|
||||
|
||||
# Always include listTools itself in the count
|
||||
total = length(store.tools)
|
||||
|
||||
if new_count > 0
|
||||
result_text = tool_list
|
||||
else
|
||||
# Already discovered — just return current list
|
||||
lines = String[
|
||||
"- $(t.name): $(t.label) — $(t.description)"
|
||||
for (k, t) in store.tools
|
||||
]
|
||||
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
||||
end
|
||||
|
||||
return agentToolResult(
|
||||
[textContent(result_text)],
|
||||
Dict{Any,Any}("count" => total),
|
||||
nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments=nothing,
|
||||
validateRequiredArgs=nothing,
|
||||
parallelToolExecute=false,
|
||||
)
|
||||
end
|
||||
|
||||
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
|
||||
|
||||
"""
|
||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||
|
||||
@@ -299,18 +222,9 @@ Add `tool` to `store`, overwriting any existing tool with the same name.
|
||||
|
||||
# Returns
|
||||
- The same `store.tools` dict (modified in place)
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1");
|
||||
|
||||
julia> registerTool(store, listTool(store, nothing))
|
||||
[toolRegistry:agent1] Registered tool: listTools
|
||||
OrderedDict{String, agentTool} with 1 entry:
|
||||
"listTools" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
|
||||
function registerTool(store::toolStore, tool::agentTool; eventSink=nothing
|
||||
)::OrderedDict{String, agentTool}
|
||||
store.tools[tool.name] = tool
|
||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||
return store.tools
|
||||
@@ -319,22 +233,13 @@ end
|
||||
"""
|
||||
Return the tools registered in `store`.
|
||||
|
||||
The returned dict is the **same object** stored inside `store` — mutations
|
||||
to it (e.g. via `registerTool`) are visible through subsequent calls.
|
||||
The returned dict is the **same object** stored inside `store`.
|
||||
|
||||
# Arguments
|
||||
- `store`: Tool store to query
|
||||
|
||||
# Returns
|
||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> tools = getTools(store)
|
||||
OrderedDict{String, agentTool} with 2 entries:
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
||||
return store.tools
|
||||
@@ -348,16 +253,6 @@ Remove all tools from `store`.
|
||||
|
||||
# Returns
|
||||
- `nothing`
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> clearTools(store)
|
||||
[toolRegistry:agent1] Registry cleared
|
||||
nothing
|
||||
|
||||
julia> getTools(store)
|
||||
OrderedDict{String, agentTool} with 0 entries
|
||||
```
|
||||
"""
|
||||
function clearTools(store::toolStore)::Nothing
|
||||
empty!(store.tools)
|
||||
@@ -365,4 +260,76 @@ function clearTools(store::toolStore)::Nothing
|
||||
return nothing
|
||||
end
|
||||
|
||||
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
|
||||
|
||||
"""
|
||||
listTool(store::toolStore, mcpserver) -> agentTool
|
||||
|
||||
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
|
||||
|
||||
First call: queries the MCP server via `mcpserver("tools/list")`, registers
|
||||
all discovered tools into the shared `store.tools` (in-place mutation, with
|
||||
pagination via nextCursor), then returns the full tool list.
|
||||
|
||||
Subsequent calls: returns the current list (tools remain registered).
|
||||
"""
|
||||
function listTool(store::toolStore, mcpserver)::agentTool
|
||||
return agentTool(
|
||||
name="listTools",
|
||||
label="List Tools",
|
||||
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
|
||||
inputSchema=Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict{String,Any}(),
|
||||
"required" => Any[]
|
||||
),
|
||||
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
eventSink, llmCall=nothing) -> begin
|
||||
# Discover and register MCP tools (idempotent — skips already registered)
|
||||
new_count = register_mcp_tools(mcpserver, store)
|
||||
|
||||
# Always include listTools itself in the count
|
||||
total = length(store.tools)
|
||||
|
||||
lines = String[
|
||||
"- $(t.name): $(t.label) — $(t.description)"
|
||||
for (k, t) in store.tools
|
||||
]
|
||||
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
||||
|
||||
return agentToolResult(
|
||||
[textContent(result_text)],
|
||||
Dict{Any,Any}("count" => total),
|
||||
nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments=nothing,
|
||||
validateRequiredArgs=nothing,
|
||||
parallelToolExecute=false,
|
||||
)
|
||||
end
|
||||
|
||||
# ── High-level API ──────────────────────────────────────────────────
|
||||
|
||||
"""
|
||||
registerAllTools(store::toolStore, mcpserver) -> toolStore
|
||||
|
||||
Register all tools for an agent:
|
||||
1. Auto-discover existing tools from the MCP server
|
||||
2. Register the `listTools` tool so the agent can discover new tools at runtime
|
||||
|
||||
# Arguments
|
||||
- `store`: The tool store to populate
|
||||
- `mcpserver`: A callable struct that communicates with the MCP server
|
||||
|
||||
# Returns
|
||||
- The populated `toolStore`
|
||||
"""
|
||||
function registerAllTools(store::toolStore, mcpserver=nothing; eventSink=nothing)::toolStore
|
||||
register_mcp_tools(mcpserver, store; eventSink=eventSink)
|
||||
registerTool(store, listTool(store, mcpserver); eventSink=eventSink)
|
||||
return store
|
||||
end
|
||||
|
||||
end # module
|
||||
|
||||
+8
-8
@@ -487,13 +487,13 @@ Context passed to the `beforeToolCall` hook.
|
||||
# Arguments
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call being prepared
|
||||
- `args::Dict{String,Any}`: Validated tool arguments
|
||||
- `args::AbstractDict{String, Any}`: Validated tool arguments
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct beforeToolCallContext
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
args::AbstractDict{String, Any}
|
||||
context::agentContext
|
||||
end
|
||||
|
||||
@@ -508,7 +508,7 @@ Context passed to the `afterToolCall` hook.
|
||||
# Arguments
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call that was executed
|
||||
- `args::Dict{String,Any}`: Tool arguments
|
||||
- `args::AbstractDict{String, Any}`: Tool arguments
|
||||
- `result::agentToolResult`: The raw tool result
|
||||
- `isError::Bool`: Whether execution resulted in an error
|
||||
- `context::agentContext`: Current conversation context
|
||||
@@ -516,7 +516,7 @@ Context passed to the `afterToolCall` hook.
|
||||
struct afterToolCallContext
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
args::AbstractDict{String, Any}
|
||||
result::agentToolResult
|
||||
isError::Bool
|
||||
context::agentContext
|
||||
@@ -528,12 +528,12 @@ Event emitted when a tool call execution starts.
|
||||
# Arguments
|
||||
- `toolCallId::String`: ID of the tool call
|
||||
- `toolName::String`: Name of the tool
|
||||
- `arguments::Dict{String,Any}`: Tool arguments
|
||||
- `arguments::AbstractDict{String, Any}`: Tool arguments
|
||||
"""
|
||||
struct toolExecStartEvent
|
||||
toolCallId::String
|
||||
toolName::String
|
||||
arguments::Dict{String,Any}
|
||||
arguments::AbstractDict{String, Any}
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -542,13 +542,13 @@ Event emitted with partial results during tool execution.
|
||||
# Arguments
|
||||
- `toolCallId::String`: ID of the tool call
|
||||
- `toolName::String`: Name of the tool
|
||||
- `arguments::Dict{String,Any}`: Tool arguments
|
||||
- `arguments::AbstractDict{String, Any}`: Tool arguments
|
||||
- `partialResult::Any`: The partial result data
|
||||
"""
|
||||
struct toolExecUpdateEvent
|
||||
toolCallId::String
|
||||
toolName::String
|
||||
arguments::Dict{String,Any}
|
||||
arguments::AbstractDict{String, Any}
|
||||
partialResult::Any
|
||||
end
|
||||
|
||||
|
||||
+19
-19
@@ -3,7 +3,7 @@ module utils
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||
validateToolArguments, _userMessageToOpenAI,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI,
|
||||
beforeToolCall, afterToolCall, agentEventSink
|
||||
beforeToolCall, afterToolCall, eventSink
|
||||
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
|
||||
using GeneralUtils
|
||||
@@ -109,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages)
|
||||
# end
|
||||
```
|
||||
"""
|
||||
function prepareContext(state::agentState, agentEventSink, llmCall=nothing)::agentContext
|
||||
function prepareContext(state::agentState, eventSink, llmCall=nothing)::agentContext
|
||||
|
||||
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||
filteredTools = state.tools
|
||||
@@ -156,7 +156,7 @@ formatMsgForLLm(ctx) == Dict("messages" => [
|
||||
])
|
||||
```
|
||||
"""
|
||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
|
||||
|
||||
""" openai message format example
|
||||
msg = Dict(
|
||||
@@ -208,7 +208,7 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
openaiReadyMsg = Dict{String, Any}()
|
||||
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
|
||||
messages = Vector{Dict{String, Any}}()
|
||||
agentEventSink("formatMsgForLLM 1")
|
||||
eventSink("formatMsgForLLM 1")
|
||||
# System prompt as system message
|
||||
if !isempty(ctx.systemPrompt)
|
||||
push!(messages, Dict(
|
||||
@@ -216,7 +216,7 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||
))
|
||||
end
|
||||
agentEventSink("formatMsgForLLM 2")
|
||||
eventSink("formatMsgForLLM 2")
|
||||
# Conversation messages
|
||||
for msg in ctx.messages
|
||||
if msg isa userMessage
|
||||
@@ -229,10 +229,10 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
push!(messages, _toolResultMessageToOpenAI(msg))
|
||||
end
|
||||
end
|
||||
agentEventSink("formatMsgForLLM 3")
|
||||
eventSink("formatMsgForLLM 3")
|
||||
# Convert ctx.tools into OpenAI tools format
|
||||
tools_array = _toolsToOpenAI(ctx.tools, agentEventSink)
|
||||
agentEventSink("formatMsgForLLM 4")
|
||||
tools_array = _toolsToOpenAI(ctx.tools, eventSink)
|
||||
eventSink("formatMsgForLLM 4")
|
||||
openaiReadyMsg["messages"] = messages
|
||||
openaiReadyMsg["temperature"] = 0.7
|
||||
|
||||
@@ -321,7 +321,7 @@ end
|
||||
|
||||
|
||||
#TODO
|
||||
function agentEventSink(x)
|
||||
function eventSink(x)
|
||||
|
||||
end
|
||||
|
||||
@@ -375,7 +375,7 @@ function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
"""
|
||||
Convert an assistantMessage to OpenAI message format.
|
||||
"""
|
||||
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
|
||||
@@ -437,10 +437,10 @@ _toolsToOpenAI(nothing) # => Dict{String, Any}[]
|
||||
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
|
||||
```
|
||||
"""
|
||||
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, agentEventSink)::Vector{Dict{String, Any}}
|
||||
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, eventSink)::Vector{Dict{String, Any}}
|
||||
tools_array = Vector{Dict{String, Any}}()
|
||||
agentEventSink("_toolsToOpenAI 1")
|
||||
agentEventSink(string(typeof(tools)))
|
||||
eventSink("_toolsToOpenAI 1")
|
||||
eventSink(string(typeof(tools)))
|
||||
if tools !== nothing
|
||||
for (_, tool) in tools
|
||||
push!(tools_array, Dict(
|
||||
@@ -453,13 +453,13 @@ function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, a
|
||||
))
|
||||
end
|
||||
end
|
||||
agentEventSink("_toolsToOpenAI 2")
|
||||
eventSink("_toolsToOpenAI 2")
|
||||
return tools_array
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String}
|
||||
validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any}) -> Union{Nothing,String}
|
||||
|
||||
Validates that all required fields listed in the tool's JSON Schema are present
|
||||
in `args`. Returns `nothing` if validation passes, or a descriptive error string
|
||||
@@ -470,8 +470,8 @@ with a custom validation function that performs additional checks (e.g. type
|
||||
coercion, format validation, cross-field constraints).
|
||||
|
||||
# Arguments
|
||||
- `args::Dict{String,Any}`: The arguments provided by the LLM
|
||||
- `inputSchema::Dict{String,Any}`: The tool's `inputSchema` (JSON Schema format)
|
||||
- `args::AbstractDict{String, Any}`: The arguments provided by the LLM
|
||||
- `inputSchema::AbstractDict{String, Any}`: The tool's `inputSchema` (JSON Schema format)
|
||||
|
||||
# Returns
|
||||
- `nothing` if all required args are present
|
||||
@@ -487,7 +487,7 @@ args2 = Dict("city" => "Tokyo")
|
||||
validateRequiredArgs(args2, schema) # => nothing
|
||||
```
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any})::Union{Nothing,String}
|
||||
function validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing,String}
|
||||
required = get(inputSchema, "required", Any[])
|
||||
if isempty(required)
|
||||
return nothing
|
||||
@@ -541,7 +541,7 @@ validateToolArguments(toolWithHook, tc) # => validated args or throws
|
||||
validateToolArguments(toolDefault, tc) # => args or throws
|
||||
```
|
||||
"""
|
||||
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{String,Any}
|
||||
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}
|
||||
# Use default (2-arg: args + schema) or tool-specific hook (1-arg: args only)
|
||||
if isnothing(tool.validateRequiredArgs)
|
||||
result = validateRequiredArgs(prepared.arguments, tool.inputSchema)
|
||||
|
||||
+5
-5
@@ -32,7 +32,7 @@ catch e
|
||||
println(io)
|
||||
end
|
||||
|
||||
agentEventSink(err_msg)
|
||||
eventSink(err_msg)
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -66,13 +66,13 @@ end
|
||||
|
||||
|
||||
|
||||
struct agentEventSink
|
||||
struct eventSink
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
function (aes::agentEventSink)(msg::String)
|
||||
function (aes::eventSink)(msg::String)
|
||||
NATS.publish(aes.natsConn, aes.topic, msg)
|
||||
end
|
||||
|
||||
@@ -88,11 +88,11 @@ text2text_llm = text2textInstructLLM(agent_conn,
|
||||
"sender",
|
||||
config["externalservice"]["fileserver"]["url"])
|
||||
|
||||
debugNats = agentEventSink(agent_conn, "sommanion.debug", "sender")
|
||||
debugNats = eventSink(agent_conn, "sommanion.debug", "sender")
|
||||
|
||||
agent = YiemAgent.yiemAgent(
|
||||
text2text_llm;
|
||||
agentEventSink=debugNats
|
||||
eventSink=debugNats
|
||||
)
|
||||
|
||||
msg = Dict(
|
||||
|
||||
Reference in New Issue
Block a user