update
This commit is contained in:
@@ -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
|
# Arguments
|
||||||
- `toolCallId::String`: Unique identifier for this tool call
|
- `toolCallId::String`: Unique identifier for this tool call
|
||||||
@@ -8,42 +46,44 @@ Execute the get_time tool.
|
|||||||
- `onPartialResult::Function`: Callback for streaming partial results
|
- `onPartialResult::Function`: Callback for streaming partial results
|
||||||
|
|
||||||
# Returns
|
# 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
|
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()
|
# Simulate time lookup — replace with actual timezone API call
|
||||||
|
if tz !== nothing
|
||||||
if tz == "local"
|
result = "Current time in $(tz): $(now())"
|
||||||
timeStr = string(t)
|
|
||||||
else
|
else
|
||||||
timeStr = string(t)
|
result = "Current time in $(city): $(now())"
|
||||||
end
|
end
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent("Current time: $(timeStr)")],
|
[textContent(result)],
|
||||||
Dict{Any,Any}(), nothing, false
|
Dict{Any,Any}(), nothing, false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Define and return the get_time agentTool.
|
Define and return the getTime agentTool.
|
||||||
"""
|
"""
|
||||||
function getTool()::agentTool
|
function getTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "get_time",
|
name = "getTime",
|
||||||
label = "Get Current Time",
|
label = "Time Lookup",
|
||||||
description = "Get the current date and time.",
|
description = "Get current local time for a timezone or city.",
|
||||||
inputSchema = Dict{String,Any}(
|
inputSchema = Dict{String,Any}(
|
||||||
"type" => "object",
|
"type" => "object",
|
||||||
"properties" => Dict(
|
"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" => []
|
"required" => []
|
||||||
),
|
),
|
||||||
execute = executeTool,
|
execute = executeTool,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
|
validateRequiredArgs = validateRequiredArgs,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,14 +14,6 @@ function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{N
|
|||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
units = get(args, "units", "celsius")
|
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
|
# Simulate weather fetch — replace with actual API call
|
||||||
# You can call onPartialResult() here for streaming progress updates:
|
# You can call onPartialResult() here for streaming progress updates:
|
||||||
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
||||||
@@ -54,6 +46,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
execute = executeTool, # reference the function defined above
|
execute = executeTool, # reference the function defined above
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
+9
-3
@@ -8,8 +8,8 @@
|
|||||||
textContent, imageContent,
|
textContent, imageContent,
|
||||||
# Message types
|
# Message types
|
||||||
userMessage, assistantMessage, toolResultMessage,
|
userMessage, assistantMessage, toolResultMessage,
|
||||||
# Tool types
|
# Tool types
|
||||||
agentTool,
|
agentTool, validateRequiredArgs
|
||||||
# Context types
|
# Context types
|
||||||
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
||||||
# Loop & execution types
|
# Loop & execution types
|
||||||
@@ -207,6 +207,8 @@ Maps MCP server tool definitions to an executable Julia tool.
|
|||||||
- `execute::Function`: Tool execution function, signature:
|
- `execute::Function`: Tool execution function, signature:
|
||||||
`execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)`
|
`execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)`
|
||||||
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
|
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
|
||||||
|
- `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
|
- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
@@ -249,7 +251,10 @@ tool = agentTool(
|
|||||||
[textContent("Sunny, 22C in $(city)")],
|
[textContent("Sunny, 22C in $(city)")],
|
||||||
Dict{Any,Any}(), nothing, false
|
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)
|
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||||
execute::Function # Tool execution function
|
execute::Function # Tool execution function
|
||||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||||
|
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+101
-2
@@ -1,6 +1,6 @@
|
|||||||
module utils
|
module utils
|
||||||
|
|
||||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, _userMessageToOpenAI,
|
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI,
|
||||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
|
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
|
||||||
|
|
||||||
using UUIDs, Dates, DataStructures, HTTP, JSON
|
using UUIDs, Dates, DataStructures, HTTP, JSON
|
||||||
@@ -275,10 +275,109 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
end
|
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