V0.8.0 async think loop #40
@@ -0,0 +1,383 @@
|
||||
# Tools
|
||||
|
||||
Tools allow the agent to perform actions and fetch data. Each tool defines a **schema** (what arguments it accepts) and an **execution function** (what it does).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Add a new tool by creating a `.jl` file in `src/tools/`. The file must define a `getTool()` function that returns an `agentTool`:
|
||||
|
||||
```julia
|
||||
# src/tools/my_tool.jl
|
||||
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Hello from $(city)!")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "my_tool",
|
||||
label = "My Tool",
|
||||
description = "Says hello to a city.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string", "description" => "City name")
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
When `loadTools()` or `registerTool()` is called, the tool becomes available to the agent.
|
||||
|
||||
## Tool Anatomy
|
||||
|
||||
Each tool has 3 main parts:
|
||||
|
||||
### 1. Schema (`inputSchema`)
|
||||
|
||||
JSON Schema (MCP format) describing the tool's arguments. The `"required"` array lists mandatory fields:
|
||||
|
||||
```julia
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string", "description" => "City name"),
|
||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
|
||||
),
|
||||
"required" => ["city"]
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Execution Function (`execute`)
|
||||
|
||||
A function with the signature:
|
||||
|
||||
```julia
|
||||
execute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
```
|
||||
|
||||
- **`toolCallId`** — unique ID for this invocation (from the LLM's tool call)
|
||||
- **`args`** — validated arguments provided by the LLM
|
||||
- **`signal`** — abort signal for cancellable operations
|
||||
- **`onPartialResult`** — callback for streaming progress updates
|
||||
- **Returns** — `agentToolResult` with content, details, usage, and termination flag
|
||||
|
||||
```julia
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
# Optional: stream progress updates
|
||||
onPartialResult(Dict("status" => "Fetching data..."))
|
||||
|
||||
# Do work
|
||||
result = "Weather in $(args["city"]): Sunny, 22°C"
|
||||
|
||||
# Return result
|
||||
return agentToolResult(
|
||||
[textContent(result)],
|
||||
Dict{Any,Any}(), # details
|
||||
nothing, # usage
|
||||
false # terminate (true to stop agent loop)
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Tool Definition (`getTool()`)
|
||||
|
||||
Returns an `agentTool` struct:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | `String` | Unique identifier (e.g. `"getWeather"`) |
|
||||
| `label` | `String` | Human-readable name (e.g. `"Weather Lookup"`) |
|
||||
| `description` | `String` | What the tool does (shown to the LLM) |
|
||||
| `inputSchema` | `Any` | JSON Schema (MCP format) |
|
||||
| `execute` | `Function` | The execution function |
|
||||
| `prepareArguments` | `Union{Function,Nothing}` | Optional argument transform before validation |
|
||||
| `validateRequiredArgs` | `Union{Function,Nothing}` | Optional custom validation |
|
||||
| `parallelToolExecute` | `Bool` | Run this tool in parallel with others |
|
||||
|
||||
## Argument Validation
|
||||
|
||||
Validation happens **before** tool execution, in the `prepareToolCall` phase. Invalid calls return an error immediately without invoking `execute`, `beforeToolCall`, or logging `toolExecutionStart`.
|
||||
|
||||
### Default: JSON Schema Required Fields
|
||||
|
||||
Set `validateRequiredArgs = nothing` to use the default validator, which checks that all fields in `inputSchema["required"]` are present:
|
||||
|
||||
```julia
|
||||
# src/tools/getWeather.jl — uses default validation
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
# ...
|
||||
validateRequiredArgs = nothing, # uses default
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Custom Validation Hook
|
||||
|
||||
Override `validateRequiredArgs` when you need:
|
||||
- **Cross-field constraints** (e.g. "at least one of X or Y")
|
||||
- **Format validation** (e.g. regex patterns, date parsing)
|
||||
- **Domain rules** (e.g. value ranges, business logic)
|
||||
|
||||
The hook signature takes only `args`:
|
||||
|
||||
```julia
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
if !haskey(args, "timezone") && isempty(city)
|
||||
return "Missing required argument: provide at least one of 'timezone' or 'city'"
|
||||
end
|
||||
|
||||
if tz !== nothing
|
||||
tz_str = string(tz)
|
||||
if !occursin(r"^[A-Za-z]+\/[A-Za-z]+(/[A-Za-z]+)*$", tz_str)
|
||||
return "Invalid timezone format: '$tz_str'. Use IANA format, e.g. 'America/New_York'"
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
```
|
||||
|
||||
Return `nothing` to pass, or an error `String` to fail. The error is fed back to the LLM so it can retry with corrected arguments.
|
||||
|
||||
## Tool Call Lifecycle
|
||||
|
||||
```
|
||||
LLM requests tool call
|
||||
└── prepareToolCall (agentCore.jl:511)
|
||||
├── Tool lookup by name
|
||||
├── prepareArguments (tool-specific transform, if defined)
|
||||
├── validateToolArguments (validateRequiredArgs hook or default)
|
||||
│ └── on failure → immediateOutcome (no execution)
|
||||
├── beforeToolCall hook (if defined)
|
||||
│ └── on block → immediateOutcome (no execution)
|
||||
└── returns preparedToolCall
|
||||
|
||||
executed by executeToolCallsSequential or executeToolCallsParallel
|
||||
└── executePreparedToolCall (agentCore.jl:589)
|
||||
├── emit toolExecutionStart
|
||||
├── call tool.execute()
|
||||
│ └── on error → executedOutcome(isError=true)
|
||||
└── returns executedOutcome
|
||||
|
||||
finalizeExecutedToolCall (agentCore.jl:675)
|
||||
├── afterToolCall hook (if defined)
|
||||
│ └── can mutate result content, usage, terminate, isError
|
||||
└── returns finalizedOutcome
|
||||
|
||||
emit toolExecutionEnd
|
||||
└── createToolResultMessage → added to conversation history
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
### Sequential
|
||||
|
||||
Tools execute one at a time in order. Required when:
|
||||
- Tools have implicit dependencies
|
||||
- Tools share state (e.g. writing to the same file)
|
||||
- Tools have `parallelToolExecute = false`
|
||||
|
||||
Set globally via `agentLoopConfig.toolExecution = "sequential"`, or per-tool via `parallelToolExecute = false`.
|
||||
|
||||
### Parallel
|
||||
|
||||
Tools execute concurrently when all are independent. Reduces wall-clock time. Set `parallelToolExecute = true` on individual tools, or set `agentLoopConfig.toolExecution = "parallel"`.
|
||||
|
||||
## Streaming Partial Results
|
||||
|
||||
For long-running tools (API calls, file uploads, training), use `onPartialResult` to stream progress:
|
||||
|
||||
```julia
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
onPartialResult(Dict("status" => "Step 1: Fetching data..."))
|
||||
sleep(1)
|
||||
|
||||
onPartialResult(Dict("status" => "Step 2: Processing..."))
|
||||
sleep(1)
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Done!")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
UI listeners and the TUI consume these events in real time via `toolExecutionUpdate`.
|
||||
|
||||
## Loading Tools
|
||||
|
||||
### Auto-load from Directory
|
||||
|
||||
```julia
|
||||
using .toolRegistry
|
||||
|
||||
tools = loadTools("src/tools") # scans for *.jl files with getTool()
|
||||
```
|
||||
|
||||
Files are loaded alphabetically for deterministic registration order.
|
||||
|
||||
### Manual Registration
|
||||
|
||||
```julia
|
||||
tool = getTool() # from your tool module
|
||||
registerTool(tool)
|
||||
```
|
||||
|
||||
## Using Tools with an Agent
|
||||
|
||||
Loading tools only registers them — you must pass them to the `yiemAgent` and provide an `llmCall` function. Here is the complete flow:
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .toolRegistry
|
||||
|
||||
# 1. Load tools from the tools directory
|
||||
tools = loadTools("src/tools")
|
||||
# [toolRegistry] Loading tool from: src/tools/getTime.jl
|
||||
# [toolRegistry] Loaded tool: getTime — Time Lookup
|
||||
# [toolRegistry] Loading tool from: src/tools/getWeather.jl
|
||||
# [toolRegistry] Loaded tool: getWeather — Weather Lookup
|
||||
|
||||
# 2. Define your LLM call function
|
||||
function my_llm_call(messages::Dict)::assistantMessage
|
||||
# Call your LLM API here (OpenAI, Anthropic, local model, etc.)
|
||||
# Return an assistantMessage with the response content
|
||||
# If the LLM wants to call a tool, include tool_call content blocks
|
||||
...
|
||||
end
|
||||
|
||||
# 3. Define your event sink (optional, for logging/debugging)
|
||||
function my_event_sink(event)
|
||||
if event isa toolExecStartEvent
|
||||
println("[EVENT] Tool start: $(event.toolName)")
|
||||
elseif event isa toolExecEndEvent
|
||||
status = event.isError ? "ERROR" : "OK"
|
||||
println("[EVENT] Tool end: $(event.toolName) — $status")
|
||||
end
|
||||
end
|
||||
|
||||
# 4. Create the agent with tools
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant that can check weather and time.",
|
||||
model = my_model,
|
||||
tools = tools, # pass loaded tools
|
||||
llmCall = my_llm_call, # your LLM function
|
||||
agentEventSink = my_event_sink, # event handler
|
||||
)
|
||||
|
||||
# 5. Send a message and get a response
|
||||
run_agent(agent, "What's the weather in Tokyo?")
|
||||
response = take_response(agent)
|
||||
|
||||
# response.content contains the LLM's reply (with tool results if applicable)
|
||||
println(response.content)
|
||||
|
||||
# 6. When done, stop the agent
|
||||
stop_agent(agent)
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **User sends a message** via `run_agent(agent, "What's the weather in Tokyo?")`. The message goes into `inputChannel`.
|
||||
|
||||
2. **Agent loop** (`_agent_loop`) picks it up, converts it to a `userMessage`, and adds it to `agent._state.messages`.
|
||||
|
||||
3. **LLM is called** via `agent.llmCall(formatted_messages)`. The LLM sees the system prompt, conversation history, and the tool definitions in the prompt (via `formatMsgForLLM`).
|
||||
|
||||
4. **If the LLM uses a tool**, it returns a response with `tool_call` content blocks. The agent:
|
||||
- Extracts each tool call (name, arguments)
|
||||
- Runs validation (`validateRequiredArgs` or default)
|
||||
- Executes the tool (or returns an error if validation fails)
|
||||
- Feeds the result back as a `toolResultMessage` in the conversation
|
||||
|
||||
5. **LLM is called again** with the tool results. This repeats until the LLM returns a text response with no tool calls.
|
||||
|
||||
6. **Final response** is sent to `outputChannel` — retrieve it with `take_response(agent)`.
|
||||
|
||||
### Minimal Working Example
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .toolRegistry
|
||||
|
||||
# Load tools
|
||||
tools = loadTools("src/tools")
|
||||
|
||||
# Mock LLM that echoes back a tool call, then a text response
|
||||
call_count = 0
|
||||
function mock_llm_call(messages::Dict)::assistantMessage
|
||||
global call_count += 1
|
||||
if call_count == 1
|
||||
# First call: LLM decides to use getWeather
|
||||
return assistantMessage(
|
||||
content=[
|
||||
Dict("type" => "tool_calls",
|
||||
"tool_calls" => [Dict("id" => "call_1", "name" => "getWeather",
|
||||
"arguments" => Dict("city" => "Tokyo"))])
|
||||
],
|
||||
model = "mock",
|
||||
usage = llmUsage(0, 0)
|
||||
)
|
||||
else
|
||||
# Second call: LLM returns text (after tool result)
|
||||
return assistantMessage(
|
||||
content = [textContent("The weather in Tokyo is sunny, 22°C.")],
|
||||
model = "mock",
|
||||
usage = llmUsage(0, 0)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Create agent
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant.",
|
||||
tools = tools,
|
||||
llmCall = mock_llm_call,
|
||||
agentEventSink = e -> nothing, # no events
|
||||
)
|
||||
|
||||
# Run
|
||||
run_agent(agent, "What's the weather in Tokyo?")
|
||||
response = take_response(agent)
|
||||
|
||||
stop_agent(agent)
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description | Validation |
|
||||
|---|---|---|
|
||||
| `getWeather` | Fetch weather for a city | Default (JSON Schema required) |
|
||||
| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) |
|
||||
|
||||
## Example: Error Flow
|
||||
|
||||
When the LLM calls a tool with invalid arguments:
|
||||
|
||||
```
|
||||
User: "What's the weather?"
|
||||
└── LLM: call getWeather() with no arguments
|
||||
└── prepareToolCall → validateRequiredArgs → "Missing required arguments: city"
|
||||
└── immediateOutcome → error tool result
|
||||
└── LLM sees: "Missing required arguments: city"
|
||||
└── LLM retries: call getWeather(city="Tokyo")
|
||||
└── executeTool → "Weather in Tokyo: Sunny, 22°C"
|
||||
```
|
||||
|
||||
The agent feeds the error back to the LLM as a tool result message, allowing it to self-correct.
|
||||
+54
-14
@@ -1,5 +1,43 @@
|
||||
"""
|
||||
Execute the get_time tool.
|
||||
Validate required arguments for the getTime tool.
|
||||
|
||||
Demonstrates custom validation beyond simple required-field checking:
|
||||
- Ensures at least one time source (timezone or city) is provided
|
||||
- Validates timezone is in IANA format if specified
|
||||
- Validates city name is not empty if specified
|
||||
|
||||
# Arguments
|
||||
- `args::Dict{String,Any}`: Arguments from the LLM
|
||||
|
||||
# Returns
|
||||
- `nothing` if validation passes
|
||||
- `String` error message if validation fails
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
hasTz = tz !== nothing && !isempty(tz)
|
||||
hasCity = !isempty(city)
|
||||
|
||||
# At least one of timezone or city is required
|
||||
if !hasTz && !hasCity
|
||||
return "Missing required argument: provide at least one of 'timezone' or 'city'"
|
||||
end
|
||||
|
||||
# Validate timezone format (IANA tz database: "Continent/City" or "Continent/City/SubCity")
|
||||
if hasTz
|
||||
tz_str = string(tz)
|
||||
if !occursin(r"^[A-Za-z]+\/[A-Za-z]+(/[A-Za-z]+)*$", tz_str)
|
||||
return "Invalid timezone format: '$tz_str'. Use IANA format, e.g. 'America/New_York' or 'Asia/Tokyo'"
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
Execute the getTime tool.
|
||||
|
||||
# Arguments
|
||||
- `toolCallId::String`: Unique identifier for this tool call
|
||||
@@ -8,42 +46,44 @@ Execute the get_time tool.
|
||||
- `onPartialResult::Function`: Callback for streaming partial results
|
||||
|
||||
# Returns
|
||||
- `agentToolResult`: Result content with current time
|
||||
- `agentToolResult`: Result content with current time data
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
tz = get(args, "timezone", "local")
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
t = now()
|
||||
|
||||
if tz == "local"
|
||||
timeStr = string(t)
|
||||
# Simulate time lookup — replace with actual timezone API call
|
||||
if tz !== nothing
|
||||
result = "Current time in $(tz): $(now())"
|
||||
else
|
||||
timeStr = string(t)
|
||||
result = "Current time in $(city): $(now())"
|
||||
end
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Current time: $(timeStr)")],
|
||||
[textContent(result)],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
Define and return the get_time agentTool.
|
||||
Define and return the getTime agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "get_time",
|
||||
label = "Get Current Time",
|
||||
description = "Get the current date and time.",
|
||||
name = "getTime",
|
||||
label = "Time Lookup",
|
||||
description = "Get current local time for a timezone or city.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"timezone" => Dict("type" => "string", "description" => "Timezone (currently only 'local' is supported)")
|
||||
"timezone" => Dict("type" => "string", "description", "IANA timezone, e.g. 'America/New_York'"),
|
||||
"city" => Dict("type" => "string", "description", "City name as fallback")
|
||||
),
|
||||
"required" => []
|
||||
),
|
||||
execute = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
|
||||
@@ -14,14 +14,6 @@ function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{N
|
||||
city = get(args, "city", "")
|
||||
units = get(args, "units", "celsius")
|
||||
|
||||
# Validate required arguments
|
||||
if isempty(city)
|
||||
return agentToolResult(
|
||||
[textContent("Error: 'city' argument is required.")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
# Simulate weather fetch — replace with actual API call
|
||||
# You can call onPartialResult() here for streaming progress updates:
|
||||
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
||||
@@ -54,6 +46,7 @@ function getTool()::agentTool
|
||||
),
|
||||
execute = executeTool, # reference the function defined above
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
|
||||
+9
-3
@@ -8,8 +8,8 @@
|
||||
textContent, imageContent,
|
||||
# Message types
|
||||
userMessage, assistantMessage, toolResultMessage,
|
||||
# Tool types
|
||||
agentTool,
|
||||
# Tool types
|
||||
agentTool, validateRequiredArgs
|
||||
# Context types
|
||||
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
||||
# Loop & execution types
|
||||
@@ -207,6 +207,8 @@ Maps MCP server tool definitions to an executable Julia tool.
|
||||
- `execute::Function`: Tool execution function, signature:
|
||||
`execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)`
|
||||
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
|
||||
- `validateRequiredArgs::Union{Function, Nothing}`: Optional validation hook, signature:
|
||||
`validateRequiredArgs(args::Dict) -> Union{Nothing, String}` where `String` is an error message
|
||||
- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel
|
||||
|
||||
# Returns
|
||||
@@ -249,7 +251,10 @@ tool = agentTool(
|
||||
[textContent("Sunny, 22C in $(city)")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
```
|
||||
"""
|
||||
@@ -260,6 +265,7 @@ struct agentTool # A tool available to the agent
|
||||
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||
execute::Function # 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
|
||||
end
|
||||
|
||||
|
||||
+101
-2
@@ -1,6 +1,6 @@
|
||||
module utils
|
||||
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, _userMessageToOpenAI,
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
|
||||
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON
|
||||
@@ -275,10 +275,109 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{
|
||||
end
|
||||
end
|
||||
|
||||
return blocks
|
||||
return blocks
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{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
|
||||
listing the missing required fields.
|
||||
|
||||
This is the default `validateRequiredArgs` hook. Tool authors can override it
|
||||
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)
|
||||
|
||||
# Returns
|
||||
- `nothing` if all required args are present
|
||||
- `String` error message listing missing fields otherwise
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
schema = Dict("required" => ["city"])
|
||||
args = Dict{String,Any}()
|
||||
validateRequiredArgs(args, schema) # => "Missing required arguments: city"
|
||||
|
||||
args2 = Dict("city" => "Tokyo")
|
||||
validateRequiredArgs(args2, schema) # => nothing
|
||||
```
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any})::Union{Nothing,String}
|
||||
required = get(inputSchema, "required", Any[])
|
||||
if isempty(required)
|
||||
return nothing
|
||||
end
|
||||
|
||||
missing = String[]
|
||||
for field in required
|
||||
if !(field in keys(args))
|
||||
push!(missing, field)
|
||||
end
|
||||
end
|
||||
|
||||
if !isempty(missing)
|
||||
return "Missing required arguments: $(join(missing, ", "))"
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
validateToolArguments(tool::agentTool, prepared::agentToolCall) -> Dict{String,Any}
|
||||
|
||||
Validates the prepared tool call arguments by calling the tool's
|
||||
`validateRequiredArgs` hook (or the default implementation). If validation
|
||||
fails, returns a modified `agentToolCall` with an empty arguments dict
|
||||
so downstream code can detect the failure. If the hook exists on the tool
|
||||
and returns an error string, that error is returned.
|
||||
|
||||
This runs **before** the `beforeToolCall` hook, allowing the agent to
|
||||
reject invalid calls without invoking lifecycle callbacks or logging
|
||||
false `toolExecutionStart` events.
|
||||
|
||||
# Arguments
|
||||
- `tool::agentTool`: The resolved tool definition
|
||||
- `prepared::agentToolCall`: The prepared tool call with potentially transformed arguments
|
||||
|
||||
# Returns
|
||||
- `Dict{String,Any}`: The validated arguments if successful
|
||||
|
||||
# Errors
|
||||
- Throws `ArgumentError` if validation fails — this is caught by `prepareToolCall`
|
||||
and converted to an `immediateOutcome`
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
# With validateRequiredArgs hook set on the tool
|
||||
validateToolArguments(toolWithHook, tc) # => validated args or throws
|
||||
|
||||
# With default validation (nothing on tool)
|
||||
validateToolArguments(toolDefault, tc) # => args or throws
|
||||
```
|
||||
"""
|
||||
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{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)
|
||||
else
|
||||
result = tool.validateRequiredArgs(prepared.arguments)
|
||||
end
|
||||
|
||||
if result !== nothing
|
||||
throw(ArgumentError(result))
|
||||
end
|
||||
|
||||
return prepared.arguments
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user