Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da21790263 | |||
| c59f6bfa61 | |||
| 3c91222462 | |||
| f62b8f14e7 | |||
| 7ffb720f86 | |||
| 5829c82d05 | |||
| c7a98f1710 |
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
julia_version = "1.12.6"
|
julia_version = "1.12.6"
|
||||||
manifest_format = "2.0"
|
manifest_format = "2.0"
|
||||||
project_hash = "aa163e2bf572632825162936e107be18384fd40f"
|
project_hash = "3ff1783eadf40ccb51801954aa0a8df935689752"
|
||||||
|
|
||||||
[[deps.Accessors]]
|
[[deps.Accessors]]
|
||||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
|||||||
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||||
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241"
|
|
||||||
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
|
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
|
||||||
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
|
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
|
||||||
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
|
||||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||||
@@ -31,7 +29,5 @@ DataFrames = "1.7.0"
|
|||||||
GeneralUtils = "0.5.10"
|
GeneralUtils = "0.5.10"
|
||||||
HTTP = "2.4.0"
|
HTTP = "2.4.0"
|
||||||
JSON = "1.6.1"
|
JSON = "1.6.1"
|
||||||
LLMMCTS = "0.1.5"
|
|
||||||
NATS = "0.1.0"
|
NATS = "0.1.0"
|
||||||
SQLLLM = "0.2.8"
|
|
||||||
msghandler = "1.2.1"
|
msghandler = "1.2.1"
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
# YiemAgent
|
# YiemAgent
|
||||||
|
|
||||||
Julia framework for building agents with tool use.
|
Julia framework for building agents with tool use and MCP (Model Context Protocol) support.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
|
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, NATS, DataFrames`
|
||||||
2. Create a `yiemAgent` with `loadTools("src/tools")`
|
2. Create a callable `mcpServer` struct that communicates with an MCP server via NATS
|
||||||
3. Call `runAgent(agent, "message")` then `takeResponse(agent)`
|
3. Create a `yiemAgent` with an LLM callable and MCP server:
|
||||||
|
```julia
|
||||||
|
agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, eventSink=yourSink)
|
||||||
|
```
|
||||||
|
4. Call `runAgent(agent, message)` then `takeResponse(agent)`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -14,21 +18,122 @@ Julia framework for building agents with tool use.
|
|||||||
src/
|
src/
|
||||||
├── YiemAgent.jl # Module entry point
|
├── YiemAgent.jl # Module entry point
|
||||||
├── type.jl # Core types (messages, tools, agent state)
|
├── type.jl # Core types (messages, tools, agent state)
|
||||||
├── utils.jl # Message formatting, validation
|
├── utils.jl # Message formatting, context preparation, validation
|
||||||
├── agentCore.jl # Agent loop, tool execution pipeline
|
├── agentCore.jl # Agent loop, tool execution pipeline
|
||||||
├── api.jl # Public API (runAgent, takeResponse, etc.)
|
├── api.jl # Public API (runAgent, takeResponse, followUp, stopAgent)
|
||||||
└── tools/
|
└── toolRegistry.jl # Tool store, MCP discovery, listTools registration
|
||||||
├── registry.jl # Tool registry (loadTools, registerTool, listTools)
|
|
||||||
├── getWeather.jl # Weather lookup tool
|
|
||||||
├── getTime.jl # Time lookup tool
|
|
||||||
├── writeTool.jl # Create new tool files (self-modifying)
|
|
||||||
└── README.md # Tool development guide
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tool Development
|
## MCP Protocol
|
||||||
|
|
||||||
See `src/tools/README.md` for:
|
YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement:
|
||||||
- Tool anatomy (schema, execute, getTool)
|
|
||||||
- Validation hooks
|
```julia
|
||||||
- Agent loop lifecycle
|
# tools/list — returns tool definitions
|
||||||
- Self-modifying tools (`writeTool`)
|
mcpServer("tools/list") # → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...))
|
||||||
|
|
||||||
|
# tools/call — executes a tool
|
||||||
|
mcpServer("tools/call", toolName, arguments) # → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...))
|
||||||
|
```
|
||||||
|
|
||||||
|
Protocol error responses include `"error"` instead of `"result"`:
|
||||||
|
```julia
|
||||||
|
Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"..."))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementing the MCP Server Client
|
||||||
|
|
||||||
|
You must provide a callable struct that communicates with your MCP server. Example using NATS:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
struct mcpServer
|
||||||
|
natsConn::NATS.Connection
|
||||||
|
topic::String
|
||||||
|
senderID::String
|
||||||
|
fileserver_url::String
|
||||||
|
end
|
||||||
|
|
||||||
|
# tools/list implementation
|
||||||
|
function (c::mcpServer)(method::String)
|
||||||
|
if method != "tools/list"
|
||||||
|
error("mcpServer: unexpected method '$method' (expected 'tools/list')")
|
||||||
|
end
|
||||||
|
|
||||||
|
payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}())
|
||||||
|
payloads = [("payload", payload, "dictionary")]
|
||||||
|
_, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
c.topic, payloads;
|
||||||
|
sender_id=c.senderID,
|
||||||
|
msg_purpose="mcp_tools_list",
|
||||||
|
fileserver_url=c.fileserver_url)
|
||||||
|
|
||||||
|
reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
incoming_env = msghandler.smartunpack(String(reply.payload))
|
||||||
|
return incoming_env["payloads"][1][2]
|
||||||
|
end
|
||||||
|
|
||||||
|
# tools/call implementation
|
||||||
|
function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any})
|
||||||
|
if method != "tools/call"
|
||||||
|
error("mcpServer: unexpected method '$method' (expected 'tools/call')")
|
||||||
|
end
|
||||||
|
|
||||||
|
payload = Dict(
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => 2,
|
||||||
|
"method" => method,
|
||||||
|
"params" => Dict(
|
||||||
|
"name" => toolName,
|
||||||
|
"arguments" => arguments
|
||||||
|
)
|
||||||
|
)
|
||||||
|
payloads = [("payload", payload, "dictionary")]
|
||||||
|
_, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
c.topic, payloads;
|
||||||
|
sender_id=c.senderID,
|
||||||
|
msg_purpose="mcp_tool_call",
|
||||||
|
fileserver_url=c.fileserver_url)
|
||||||
|
|
||||||
|
reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
incoming_env = msghandler.smartunpack(String(reply.payload))
|
||||||
|
return incoming_env["payloads"][1][2]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
See `test/runtest.jl` for the complete working example.
|
||||||
|
|
||||||
|
## Tool Discovery
|
||||||
|
|
||||||
|
Tools are discovered dynamically via MCP server:
|
||||||
|
|
||||||
|
1. `listTool` is the only pre-registered tool
|
||||||
|
2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime
|
||||||
|
3. Supports pagination via `nextCursor` for large tool sets
|
||||||
|
|
||||||
|
## Agent API
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `runAgent(agent, msg)` | Send a message to the agent's input channel |
|
||||||
|
| `takeResponse(agent)` | Block and take the agent's response from output channel |
|
||||||
|
| `followUp(agent, msg)` | Send a follow-up message while agent is still processing |
|
||||||
|
| `stopAgent(agent)` | Gracefully stop the agent and close channels |
|
||||||
|
|
||||||
|
## Agent Lifecycle
|
||||||
|
|
||||||
|
1. `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel`
|
||||||
|
2. User messages enter via `runAgent()` or `followUp()`
|
||||||
|
3. `_processMessage()` handles LLM calls, tool execution, and conversation history
|
||||||
|
4. Tool execution follows three phases: `prepareToolCall` → `executePreparedToolCall` → `finalizeExecutedToolCall`
|
||||||
|
5. `beforeToolCall`/`afterToolCall` hooks allow pre/post-processing of tool calls
|
||||||
|
6. Tool `execute` functions can set `terminate=true` to stop the agent loop
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
| Hook | Signature | Purpose |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| `prepareContext` | `(state, sink, llmCall) -> ctx` | Transform messages/context before LLM call |
|
||||||
|
| `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 |
|
||||||
|
| `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()
|
# Create agent — tools are registered automatically via register_all_tools()
|
||||||
agent = yiemAgent(
|
agent = yiemAgent(
|
||||||
llmCall = my_llm_call, # Function that calls the LLM API
|
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 |
|
| Parameter | Type | Required | Purpose |
|
||||||
|-----------|------|----------|---------|
|
|-----------|------|----------|---------|
|
||||||
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
| `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 |
|
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
|
||||||
| `model` | `llmModel` | No | LLM model config |
|
| `model` | `llmModel` | No | LLM model config |
|
||||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
| `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 |
|
| Phase | Function | Input | Output | Purpose |
|
||||||
|-------|----------|-------|--------|---------|
|
|-------|----------|-------|--------|---------|
|
||||||
| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook |
|
| 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 |
|
| 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.
|
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
|
```julia
|
||||||
execute(toolCallId::String,
|
execute(toolCallId::String,
|
||||||
args::Dict{String,Any},
|
args::AbstractDict{String, Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
onPartialResult::Function)::agentToolResult
|
||||||
```
|
```
|
||||||
@@ -430,12 +430,12 @@ function _processMessage(agent::yiemAgent)::assistantMessage
|
|||||||
|
|
||||||
# ── Step 2: Prepare context ─────────────────────────────────
|
# ── Step 2: Prepare context ─────────────────────────────────
|
||||||
state = agentState(systemPrompt, nothing, tools, messages)
|
state = agentState(systemPrompt, nothing, tools, messages)
|
||||||
preparedContext = prepareContext(state, agentEventSink)
|
preparedContext = prepareContext(state, eventSink)
|
||||||
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
|
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
|
||||||
# Override point: filter tools, inject context, modify system prompt
|
# Override point: filter tools, inject context, modify system prompt
|
||||||
|
|
||||||
# ── Step 3: Format for LLM ──────────────────────────────────
|
# ── Step 3: Format for LLM ──────────────────────────────────
|
||||||
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
|
formattedMessages = formatMsgForLLM(preparedContext, eventSink)
|
||||||
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
||||||
# Wraps systemPrompt as system role, converts each messageContent block
|
# Wraps systemPrompt as system role, converts each messageContent block
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ function _processMessage(agent::yiemAgent)::assistantMessage
|
|||||||
signal = abortSignal(false)
|
signal = abortSignal(false)
|
||||||
|
|
||||||
# Execute tool calls (sequential or parallel)
|
# 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
|
# Save results to conversation history
|
||||||
for tool_result in batch.messages
|
for tool_result in batch.messages
|
||||||
@@ -611,7 +611,7 @@ function prepareToolCall(
|
|||||||
function executePreparedToolCall(
|
function executePreparedToolCall(
|
||||||
prep::preparedToolCall,
|
prep::preparedToolCall,
|
||||||
signal::Union{Nothing, abortSignal},
|
signal::Union{Nothing, abortSignal},
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -623,7 +623,7 @@ function executePreparedToolCall(
|
|||||||
prep.toolCall.id,
|
prep.toolCall.id,
|
||||||
prep.args,
|
prep.args,
|
||||||
signal,
|
signal,
|
||||||
agentEventSink # serves as onPartialResult callback
|
eventSink # serves as onPartialResult callback
|
||||||
)
|
)
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
```
|
```
|
||||||
@@ -698,7 +698,7 @@ function executeToolCalls(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::Union{Nothing, abortSignal},
|
signal::Union{Nothing, abortSignal},
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -733,12 +733,12 @@ function executeToolCallsSequential(...)::agentToolCallBatch
|
|||||||
messages = toolResultMessage[]
|
messages = toolResultMessage[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||||
|
|
||||||
if prep isa immediateOutcome
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
else
|
else
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -763,14 +763,14 @@ function executeToolCallsParallel(...)::agentToolCallBatch
|
|||||||
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||||
|
|
||||||
if prep isa immediateOutcome
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
push!(entries, finalized) # immediate outcome — no task
|
push!(entries, finalized) # immediate outcome — no task
|
||||||
else
|
else
|
||||||
task = task() do
|
task = task() do
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
return finalized
|
return finalized
|
||||||
end
|
end
|
||||||
@@ -845,7 +845,7 @@ From the type documentation (`type.jl:803-815`):
|
|||||||
**Source:** `agentCore.jl:266-307`
|
**Source:** `agentCore.jl:266-307`
|
||||||
|
|
||||||
```julia
|
```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
|
# Save results to conversation history
|
||||||
for tool_result in batch.messages
|
for tool_result in batch.messages
|
||||||
@@ -1037,13 +1037,13 @@ end
|
|||||||
|
|
||||||
### Event Sink
|
### Event Sink
|
||||||
|
|
||||||
The `agentEventSink` function is passed through the entire call chain:
|
The `eventSink` function is passed through the entire call chain:
|
||||||
|
|
||||||
```julia
|
```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
|
- **TUI (Terminal UI):** Display real-time progress, tool names, results
|
||||||
- **Logging systems:** Record tool execution history
|
- **Logging systems:** Record tool execution history
|
||||||
- **Monitoring:** Track tool usage, execution times, error rates
|
- **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 |
|
| Hook | Signature | Called | Purpose |
|
||||||
|------|-----------|--------|---------|
|
|------|-----------|--------|---------|
|
||||||
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||||
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||||
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
| `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 |
|
| `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` |
|
| `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
|
### `beforeToolCall` Hook
|
||||||
|
|
||||||
@@ -1125,7 +1125,7 @@ end
|
|||||||
**Source:** `utils.jl:111-125`
|
**Source:** `utils.jl:111-125`
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
function prepareContext(state::agentState, eventSink)::agentContext
|
||||||
# TODO: filter tools from state.tools based on user intent
|
# TODO: filter tools from state.tools based on user intent
|
||||||
filteredTools = state.tools
|
filteredTools = state.tools
|
||||||
|
|
||||||
@@ -1152,7 +1152,7 @@ end
|
|||||||
Default implementation converts `agentContext` to OpenAI-compatible format:
|
Default implementation converts `agentContext` to OpenAI-compatible format:
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
|
||||||
messages = Vector{Dict{String, Any}}()
|
messages = Vector{Dict{String, Any}}()
|
||||||
|
|
||||||
# System prompt as system message
|
# System prompt as system message
|
||||||
@@ -1284,7 +1284,7 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
|||||||
│ Step 6: Execute tool calls
|
│ Step 6: Execute tool calls
|
||||||
│ context = agentContext(systemPrompt, messages, tools)
|
│ context = agentContext(systemPrompt, messages, tools)
|
||||||
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
│ 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
|
LOOP ITERATION 1 — executeToolCallsSequential
|
||||||
@@ -1299,7 +1299,7 @@ LOOP ITERATION 1 — executeToolCallsSequential
|
|||||||
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||||
│
|
│
|
||||||
│ EXECUTE:
|
│ 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)
|
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||||
│ → executedOutcome(result, false)
|
│ → executedOutcome(result, false)
|
||||||
│
|
│
|
||||||
@@ -1365,20 +1365,20 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Argument preparation (before validation)
|
# 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 modified args, or args unchanged
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
# Custom validation (before execution)
|
# 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 to pass, or error string to fail
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
# Core execution
|
# Core execution
|
||||||
function <name>Execute(toolCallId::String,
|
function <name>Execute(toolCallId::String,
|
||||||
args::Dict{String,Any},
|
args::AbstractDict{String, Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
onPartialResult::Function)::agentToolResult
|
||||||
# Return agentToolResult with content, details, usage, terminate
|
# Return agentToolResult with content, details, usage, terminate
|
||||||
@@ -1400,17 +1400,17 @@ function helper_function(...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Optional: prepareArguments
|
# Optional: prepareArguments
|
||||||
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
function myToolPrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
# Optional: validateRequiredArgs
|
# Optional: validateRequiredArgs
|
||||||
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
function myToolValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
# Required: execute function
|
# Required: execute function
|
||||||
function myToolExecute(toolCallId::String, args::Dict{String,Any},
|
function myToolExecute(toolCallId::String, args::AbstractDict{String, Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
onPartialResult::Function)::agentToolResult
|
||||||
...
|
...
|
||||||
@@ -1481,7 +1481,7 @@ To add a new tool (e.g., `searchWine.jl`):
|
|||||||
using .type
|
using .type
|
||||||
# using AdditionalPkg # add if needed
|
# 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)
|
signal::Union{Nothing,abortSignal}, onPartialResult)
|
||||||
query = get(args, "query", "")
|
query = get(args, "query", "")
|
||||||
result = search_wine_db(query)
|
result = search_wine_db(query)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
check my understand:
|
|
||||||
1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and
|
|
||||||
it will be the latest message in agent._state.messages. then _agentLoop() can pick it as
|
|
||||||
the output to outputChannel
|
|
||||||
2) if LLM use tool calls but toolResultBatch.terminate is false, assistantMessageToolCall
|
|
||||||
@@ -6,3 +6,6 @@ check my understanding
|
|||||||
Is my understanding correct?
|
Is my understanding correct?
|
||||||
|
|
||||||
|
|
||||||
|
the user can provide NATS connection to MCP server by adding agent.mcpserver (a callable struct) for communication with MCP server just like agent.llmCall (also a callable struct). I think communicating with MCP server is just send/receive JSON text right?
|
||||||
|
Moreover, for simplicity I want to all tools into an MCP server so an agent can be instantiated with only listTools() in tool store then populate tools from MCP server later.
|
||||||
|
what do you think?
|
||||||
+3
-36
@@ -1,10 +1,7 @@
|
|||||||
module YiemAgent
|
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")
|
include("type.jl")
|
||||||
using .type
|
using .type
|
||||||
@@ -12,21 +9,9 @@ module YiemAgent
|
|||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
using .utils
|
using .utils
|
||||||
|
|
||||||
include("tools/getWeather.jl")
|
|
||||||
include("tools/getTime.jl")
|
|
||||||
include("tools/writeTool.jl")
|
|
||||||
|
|
||||||
include("toolRegistry.jl")
|
include("toolRegistry.jl")
|
||||||
using .toolRegistry
|
using .toolRegistry
|
||||||
|
|
||||||
function register_all_tools(store::toolRegistry.toolStore)
|
|
||||||
registerTool(store, getWeatherTool())
|
|
||||||
registerTool(store, getTimeTool())
|
|
||||||
registerTool(store, writeToolTool())
|
|
||||||
registerTool(store, listTool(store))
|
|
||||||
return store.tools
|
|
||||||
end
|
|
||||||
|
|
||||||
# include("llmfunction.jl")
|
# include("llmfunction.jl")
|
||||||
# using .llmfunction
|
# using .llmfunction
|
||||||
|
|
||||||
@@ -37,28 +22,10 @@ module YiemAgent
|
|||||||
using .api
|
using .api
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # module YiemAgent_v1
|
end # module YiemAgent_v1
|
||||||
|
|||||||
+198
-109
@@ -5,15 +5,10 @@ export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls,
|
|||||||
executeToolCallsParallel, executeToolCalls
|
executeToolCallsParallel, executeToolCalls
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Base.Threads, NATS
|
DataFrames, Base.Threads, NATS, LibPQ
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils, ..toolRegistry
|
using ..type, ..utils, ..toolRegistry
|
||||||
|
|
||||||
function register_all_tools(store::toolRegistry.toolStore)
|
|
||||||
# Call parent module's version which has access to tool functions
|
|
||||||
parentmodule(@__MODULE__).register_all_tools(store)
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -50,6 +45,82 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
|||||||
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
||||||
llmCall
|
llmCall
|
||||||
|
|
||||||
|
# Callable struct for MCP server communication.
|
||||||
|
# Called as: mcpServer("tools/list") → returns JSON-RPC 2.0 parsed response
|
||||||
|
# mcpServer("tools/call", toolName, arguments) → returns JSON-RPC 2.0 parsed response
|
||||||
|
#
|
||||||
|
# # Example (weather tool)
|
||||||
|
# # User provides a callable struct
|
||||||
|
# struct MyMCPClient
|
||||||
|
# natsConn::NATS.Connection
|
||||||
|
# topic::String
|
||||||
|
# senderID::String
|
||||||
|
# fileserver_url::String
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# function (c::MyMCPClient)(method::String)
|
||||||
|
# payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}())
|
||||||
|
# payloads = [("payload", payload, "dictionary")]
|
||||||
|
# _, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
# c.topic, payloads; sender_id=c.senderID,
|
||||||
|
# msg_purpose="mcp_tools_list", fileserver_url=c.fileserver_url)
|
||||||
|
# reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
# incoming_env = msghandler.smartunpack(String(reply.payload))
|
||||||
|
# return incoming_env["payloads"][1][2]
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# 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"),]
|
||||||
|
# _, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
# c.topic, payloads; sender_id=c.senderID,
|
||||||
|
# msg_purpose="mcp_tool_call", fileserver_url=c.fileserver_url)
|
||||||
|
# reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
# incoming_env = msghandler.smartunpack(String(reply.payload))
|
||||||
|
# return incoming_env["payloads"][1][2]
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # "tools/list" input:
|
||||||
|
# mcpServer("tools/list")
|
||||||
|
# # sending out payload before smart packed by msghandler:
|
||||||
|
# Dict("jsonrpc" => "2.0", "id" => 1, "method" => "tools/list", "params" => Dict{String, Any}())
|
||||||
|
# # expected return after smart unpacked by msghandler:
|
||||||
|
# Dict(
|
||||||
|
# "jsonrpc" => "2.0", "id" => 1,
|
||||||
|
# "result" => Dict(
|
||||||
|
# "tools" => [
|
||||||
|
# Dict(
|
||||||
|
# "name" => "getWeather",
|
||||||
|
# "description" => "Fetch current weather for a city.",
|
||||||
|
# "inputSchema" => Dict("type"=>"object",
|
||||||
|
# "properties" => Dict("city"=>Dict("type"=>"string", "description"=>"City name"),
|
||||||
|
# "units"=>Dict("type"=>"string", "enum"=>["celsius","fahrenheit"], "default"=>"celsius")),
|
||||||
|
# "required" => ["city"])
|
||||||
|
# )
|
||||||
|
# ],
|
||||||
|
# "nextCursor" => nothing
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # "tools/call" input:
|
||||||
|
# mcpServer("tools/call", "getWeather", Dict("city"=>"Tokyo", "units"=>"celsius"))
|
||||||
|
# # sending out payload before smart packed by msghandler:
|
||||||
|
# Dict("jsonrpc" => "2.0", "id" => 2, "method" => "tools/call",
|
||||||
|
# "params" => Dict("name" => "getWeather", "arguments" => Dict("city"=>"Tokyo", "units"=>"celsius")))
|
||||||
|
# # expected return after smart unpacked by msghandler (success):
|
||||||
|
# Dict("jsonrpc" => "2.0", "id" => 2,
|
||||||
|
# "result" => Dict("content" => [{"type" => "text", "text" => "Weather in Tokyo: Sunny, 22°C"}],
|
||||||
|
# "isError" => false))
|
||||||
|
# # expected return after smart unpacked by msghandler (tool error):
|
||||||
|
# Dict("jsonrpc" => "2.0", "id" => 2,
|
||||||
|
# "result" => Dict("content" => [{"type" => "text", "text" => "Error: City 'Atlantis' not found."}],
|
||||||
|
# "isError" => true))
|
||||||
|
# # expected return after smart unpacked by msghandler (protocol error):
|
||||||
|
# Dict("jsonrpc" => "2.0", "id" => 2,
|
||||||
|
# "error" => Dict("code" => -32602, "message" => "Missing required argument 'city'"))
|
||||||
|
mcpServer
|
||||||
|
|
||||||
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
||||||
beforeToolCall::Union{Function, Nothing}
|
beforeToolCall::Union{Function, Nothing}
|
||||||
|
|
||||||
@@ -61,7 +132,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
|||||||
sessionId::Union{String, Nothing} # Optional session identifier
|
sessionId::Union{String, Nothing} # Optional session identifier
|
||||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||||
parallelToolExecute::Bool # Default: false
|
parallelToolExecute::Bool # Default: false
|
||||||
agentEventSink # agent emits its status via this function
|
eventSink # agent emits its status via this function
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -85,7 +156,10 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
|||||||
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
|
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
|
||||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
- `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`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A new `yiemAgent` instance with an active background task
|
- A new `yiemAgent` instance with an active background task
|
||||||
@@ -105,7 +179,8 @@ function yiemAgent(
|
|||||||
sessionId::Union{String, Nothing}=nothing,
|
sessionId::Union{String, Nothing}=nothing,
|
||||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||||
parallelToolExecute::Bool=false,
|
parallelToolExecute::Bool=false,
|
||||||
agentEventSink=agentEventSink,
|
eventSink=eventSink,
|
||||||
|
mcpServer=nothing,
|
||||||
)
|
)
|
||||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||||
inputChannel = Channel(16)
|
inputChannel = Channel(16)
|
||||||
@@ -114,7 +189,7 @@ function yiemAgent(
|
|||||||
|
|
||||||
# load tools (statically registered at module init)
|
# load tools (statically registered at module init)
|
||||||
toolStore1 = toolStore(name="myagent")
|
toolStore1 = toolStore(name="myagent")
|
||||||
register_all_tools(toolStore1)
|
registerAllTools(toolStore1, mcpServer; eventSink=eventSink)
|
||||||
|
|
||||||
# Create struct with a placeholder task, then spawn and replace it
|
# Create struct with a placeholder task, then spawn and replace it
|
||||||
agent = yiemAgent(
|
agent = yiemAgent(
|
||||||
@@ -126,6 +201,7 @@ function yiemAgent(
|
|||||||
prepareContext,
|
prepareContext,
|
||||||
formatMsgForLLM,
|
formatMsgForLLM,
|
||||||
llmCall,
|
llmCall,
|
||||||
|
mcpServer,
|
||||||
beforeToolCall,
|
beforeToolCall,
|
||||||
afterToolCall,
|
afterToolCall,
|
||||||
# prepareNextTurn,
|
# prepareNextTurn,
|
||||||
@@ -133,7 +209,7 @@ function yiemAgent(
|
|||||||
sessionId,
|
sessionId,
|
||||||
maxRetryDelayMs,
|
maxRetryDelayMs,
|
||||||
parallelToolExecute,
|
parallelToolExecute,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Spawn the background loop and attach it
|
# Spawn the background loop and attach it
|
||||||
@@ -208,18 +284,18 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
while true
|
while true
|
||||||
while newUserMsg === nothing
|
while newUserMsg === nothing
|
||||||
if isready(agent.inputChannel)
|
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.
|
# agent process new user msg immediately after the current tool call finished.
|
||||||
newUserMsg = take!(agent.inputChannel)
|
newUserMsg = take!(agent.inputChannel)
|
||||||
agent.agentEventSink("new user msg")
|
agent.eventSink("new user msg")
|
||||||
else
|
else
|
||||||
# check followUp message after _processMessage() is done
|
# check followUp message after _processMessage() is done
|
||||||
if typeof(processingTask) == Task && istaskdone(processingTask) == true
|
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,
|
# if agent runs is done but followUpChannel has messages,
|
||||||
# put new message in inputChannel instead
|
# put new message in inputChannel instead
|
||||||
if isready(agent.followUpChannel)
|
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)
|
while isready(agent.followUpChannel)
|
||||||
followUpMsg = take!(agent.followUpChannel)
|
followUpMsg = take!(agent.followUpChannel)
|
||||||
put!(agent.inputChannel, followUpMsg)
|
put!(agent.inputChannel, followUpMsg)
|
||||||
@@ -227,18 +303,18 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
processingTask = nothing # reset
|
processingTask = nothing # reset
|
||||||
result = nothing # reset
|
result = nothing # reset
|
||||||
else # _processMessage() done and no followUp message.
|
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])
|
result = deepcopy(agent._state.messages[end])
|
||||||
|
agent.eventSink("_agentLoop 4-1 ")
|
||||||
# filter out reasoningContent in-place
|
# filter out reasoningContent in-place
|
||||||
filter!(c -> !(c isa reasoningContent), result.content)
|
filter!(c -> !(c isa reasoningContent), result.content)
|
||||||
|
agent.eventSink("_agentLoop 5 ")
|
||||||
# format output
|
# format output
|
||||||
respondToUI = _assistantMessageToOpenAI(result)
|
respondToUI = _assistantMessageToOpenAI(result)
|
||||||
|
agent.eventSink("_agentLoop 6 ")
|
||||||
put!(agent.outputChannel, respondToUI)
|
put!(agent.outputChannel, respondToUI)
|
||||||
if !isempty(result.content) && result.content[1] isa textContent
|
if !isempty(result.content) && result.content[1] isa textContent
|
||||||
agent.agentEventSink(result.content[1].text)
|
agent.eventSink(result.content[1].text)
|
||||||
end
|
end
|
||||||
processingTask = nothing # reset
|
processingTask = nothing # reset
|
||||||
result = nothing # reset
|
result = nothing # reset
|
||||||
@@ -269,7 +345,7 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
else
|
else
|
||||||
# spawn new _processMessage() if it is not already running.
|
# spawn new _processMessage() if it is not already running.
|
||||||
if processingTask === nothing
|
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
|
# discard all messages in followUpChannel
|
||||||
while isready(agent.followUpChannel)
|
while isready(agent.followUpChannel)
|
||||||
_ = take!(agent.followUpChannel)
|
_ = take!(agent.followUpChannel)
|
||||||
@@ -278,7 +354,7 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
# Dispatch message through the processing pipeline
|
# Dispatch message through the processing pipeline
|
||||||
processingTask = @spawn _processMessage(
|
processingTask = @spawn _processMessage(
|
||||||
processMessageInputCh,
|
processMessageInputCh,
|
||||||
agent.agentEventSink,
|
agent.eventSink,
|
||||||
agent._state.messages,
|
agent._state.messages,
|
||||||
agent._state.systemPrompt,
|
agent._state.systemPrompt,
|
||||||
agent._state.tools,
|
agent._state.tools,
|
||||||
@@ -295,6 +371,13 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
catch e
|
catch e
|
||||||
|
bt = catch_backtrace()
|
||||||
|
errMsg = sprint() do io
|
||||||
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
eventSink(errMsg)
|
||||||
|
|
||||||
# On any error, send error response and exit the loop
|
# On any error, send error response and exit the loop
|
||||||
@error "Agent loop failed" error=e
|
@error "Agent loop failed" error=e
|
||||||
end
|
end
|
||||||
@@ -330,7 +413,7 @@ julia> # Currently returns a placeholder echo response
|
|||||||
"""
|
"""
|
||||||
function _processMessage(
|
function _processMessage(
|
||||||
inputChannel::Channel,
|
inputChannel::Channel,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
agentMsgHistory::Vector{agentMessage},
|
agentMsgHistory::Vector{agentMessage},
|
||||||
systemPrompt::String,
|
systemPrompt::String,
|
||||||
tools::OrderedDict{String, agentTool},
|
tools::OrderedDict{String, agentTool},
|
||||||
@@ -341,7 +424,7 @@ function _processMessage(
|
|||||||
afterToolCall::Union{Function, Nothing},
|
afterToolCall::Union{Function, Nothing},
|
||||||
parallelToolExecute::Bool,
|
parallelToolExecute::Bool,
|
||||||
)::Nothing
|
)::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
|
# loop until llmCall() response didn't use tool calls
|
||||||
final_response = nothing
|
final_response = nothing
|
||||||
|
|
||||||
@@ -361,43 +444,43 @@ function _processMessage(
|
|||||||
while true
|
while true
|
||||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||||
while isready(inputChannel)
|
while isready(inputChannel)
|
||||||
agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||||
newUserMsg_openai = take!(inputChannel)
|
newUserMsg_openai = take!(inputChannel)
|
||||||
agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||||
if newUserMsg_openai === :shutdown
|
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
|
# Re-emit shutdown signal for the loop to handle
|
||||||
put!(inputChannel, :shutdown)
|
put!(inputChannel, :shutdown)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||||
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
||||||
push!(agentMsgHistory, newUserMsg)
|
push!(agentMsgHistory, newUserMsg)
|
||||||
agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||||
# call prepareContext()
|
# call prepareContext()
|
||||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||||
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||||
preparedContext = prepareContext(state, agentEventSink)
|
preparedContext = prepareContext(state, eventSink, llmCall)
|
||||||
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||||
# Call formatMessagesForLLM() to format for LLM
|
# 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 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 = 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)
|
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
|
# Extract tool calls from LLM response content blocks
|
||||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||||
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
||||||
agentEventSink("assistant_msg " * string(assistant_msg))
|
eventSink("assistant_msg " * string(assistant_msg))
|
||||||
agentEventSink("_processMessage 11-1")
|
eventSink("_processMessage 11-1")
|
||||||
|
|
||||||
# Add assistant message (tool calls or text) to history for next LLM turn
|
# Add assistant message (tool calls or text) to history for next LLM turn
|
||||||
push!(agentMsgHistory, assistant_msg)
|
push!(agentMsgHistory, assistant_msg)
|
||||||
@@ -409,24 +492,25 @@ function _processMessage(
|
|||||||
beforeToolCall,
|
beforeToolCall,
|
||||||
afterToolCall,
|
afterToolCall,
|
||||||
parallelToolExecute ? "parallel" : "sequential",
|
parallelToolExecute ? "parallel" : "sequential",
|
||||||
|
llmCall,
|
||||||
)
|
)
|
||||||
|
|
||||||
signal = abortSignal(false)
|
signal = abortSignal(false)
|
||||||
agentEventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
|
||||||
# call executeToolCalls()
|
# call executeToolCalls()
|
||||||
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||||
signal, agentEventSink)
|
signal, eventSink)
|
||||||
agentEventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
|
||||||
# save toolResults to messages
|
# save toolResults to messages
|
||||||
for toolResult in toolResultBatch.messages
|
for toolResult in toolResultBatch.messages
|
||||||
agentEventSink("toolResult " * string(toolResult))
|
eventSink("toolResult " * string(toolResult))
|
||||||
push!(agentMsgHistory, toolResult)
|
push!(agentMsgHistory, toolResult)
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||||
if toolResultBatch.terminate
|
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
|
# If toolResultBatch requested termination, build a final response
|
||||||
final_content = [textContent("Tool execution completed.")]
|
final_content = [textContent("Tool execution completed.")]
|
||||||
for toolResult in toolResultBatch.messages
|
for toolResult in toolResultBatch.messages
|
||||||
@@ -440,7 +524,7 @@ function _processMessage(
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||||
final_response = assistantMessage(
|
final_response = assistantMessage(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=final_content,
|
content=final_content,
|
||||||
@@ -459,12 +543,12 @@ function _processMessage(
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||||
# LLM did not use tool calls —
|
# LLM did not use tool calls —
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -902,45 +986,48 @@ function prepareToolCall(
|
|||||||
toolCall::agentToolCall,
|
toolCall::agentToolCall,
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink
|
eventSink
|
||||||
)::Union{preparedToolCall,immediateOutcome}
|
)::Union{preparedToolCall,immediateOutcome}
|
||||||
agentEventSink("prepareToolCall 1")
|
eventSink("prepareToolCall 1")
|
||||||
tool = get(context.tools, toolCall.name, nothing) # pick a called tool from tool store
|
|
||||||
|
# pick a called tool from tool store
|
||||||
|
tool = get(context.tools, toolCall.name, nothing)
|
||||||
|
|
||||||
if tool === nothing
|
if tool === nothing
|
||||||
agentEventSink("prepareToolCall 2")
|
eventSink("prepareToolCall 2")
|
||||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||||
end
|
end
|
||||||
|
|
||||||
try
|
try
|
||||||
agentEventSink("prepareToolCall 3")
|
eventSink("prepareToolCall 3")
|
||||||
# 1. prepare arguments (tool-specific transform)
|
# 1. prepare arguments (tool-specific transform)
|
||||||
prepared = prepareToolCallArguments(tool, toolCall)
|
prepared = prepareToolCallArguments(tool, toolCall)
|
||||||
agentEventSink("prepared " * string(prepared.arguments))
|
eventSink("prepared " * string(prepared.arguments))
|
||||||
agentEventSink("prepareToolCall 4")
|
eventSink("prepareToolCall 4")
|
||||||
validatedArgs = validateToolArguments(tool, prepared)
|
validatedArgs = validateToolArguments(tool, prepared)
|
||||||
agentEventSink("validatedArgs " * string(validatedArgs))
|
eventSink("validatedArgs " * string(validatedArgs))
|
||||||
agentEventSink("prepareToolCall 5")
|
eventSink("prepareToolCall 5")
|
||||||
# 2. beforeToolCall hook — can block
|
# 2. beforeToolCall hook — can block
|
||||||
if config.beforeToolCall !== nothing
|
if config.beforeToolCall !== nothing
|
||||||
agentEventSink("prepareToolCall 6")
|
eventSink("prepareToolCall 6")
|
||||||
|
|
||||||
before = config.beforeToolCall(
|
before = config.beforeToolCall(
|
||||||
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
|
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
|
||||||
signal
|
signal
|
||||||
)
|
)
|
||||||
agentEventSink("prepareToolCall 7")
|
eventSink("prepareToolCall 7")
|
||||||
if signal.aborted
|
if signal.aborted
|
||||||
agentEventSink("prepareToolCall 8")
|
eventSink("prepareToolCall 8")
|
||||||
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
||||||
end
|
end
|
||||||
|
|
||||||
if before !== nothing && before.block
|
if before !== nothing && before.block
|
||||||
agentEventSink("prepareToolCall 9")
|
eventSink("prepareToolCall 9")
|
||||||
return immediateOutcome(
|
return immediateOutcome(
|
||||||
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("prepareToolCall 10")
|
eventSink("prepareToolCall 10")
|
||||||
return preparedToolCall(tool, toolCall, validatedArgs)
|
return preparedToolCall(tool, toolCall, validatedArgs)
|
||||||
catch e
|
catch e
|
||||||
bt = catch_backtrace()
|
bt = catch_backtrace()
|
||||||
@@ -949,7 +1036,7 @@ function prepareToolCall(
|
|||||||
println(io)
|
println(io)
|
||||||
end
|
end
|
||||||
|
|
||||||
agentEventSink(errMsg)
|
eventSink(errMsg)
|
||||||
|
|
||||||
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||||
end
|
end
|
||||||
@@ -999,16 +1086,16 @@ executePreparedToolCall(prep, nothing, emit)
|
|||||||
function executePreparedToolCall(
|
function executePreparedToolCall(
|
||||||
prep::preparedToolCall,
|
prep::preparedToolCall,
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
agentEventSink,
|
eventSink,
|
||||||
|
llmCall::Union{Any,Nothing}=nothing,
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
agentEventSink("executePreparedToolCall 1")
|
eventSink("executePreparedToolCall 1")
|
||||||
agentEventSink("executePreparedToolCall 2")
|
|
||||||
agentEventSink("executePreparedToolCall 3")
|
|
||||||
|
|
||||||
try
|
try #WORKING
|
||||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
|
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink, llmCall)
|
||||||
agentEventSink(result.content[1].text)
|
eventSink("executePreparedToolCall 2")
|
||||||
agentEventSink("executePreparedToolCall 4")
|
eventSink(result.content[1].text)
|
||||||
|
eventSink("executePreparedToolCall 3")
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
catch e
|
catch e
|
||||||
bt = catch_backtrace()
|
bt = catch_backtrace()
|
||||||
@@ -1016,7 +1103,7 @@ function executePreparedToolCall(
|
|||||||
showerror(io, e, bt)
|
showerror(io, e, bt)
|
||||||
println(io)
|
println(io)
|
||||||
end
|
end
|
||||||
agentEventSink(errMsg)
|
eventSink(errMsg)
|
||||||
|
|
||||||
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||||
end
|
end
|
||||||
@@ -1086,19 +1173,19 @@ function finalizeExecutedToolCall(
|
|||||||
executed::executedOutcome,
|
executed::executedOutcome,
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
agentEventSink
|
eventSink
|
||||||
)::finalizedOutcome
|
)::finalizedOutcome
|
||||||
agentEventSink("finalizeExecutedToolCall 1")
|
eventSink("finalizeExecutedToolCall 1")
|
||||||
result = executed.result
|
result = executed.result
|
||||||
isError = executed.isError
|
isError = executed.isError
|
||||||
agentEventSink("finalizeExecutedToolCall 2")
|
eventSink("finalizeExecutedToolCall 2")
|
||||||
if config.afterToolCall !== nothing
|
if config.afterToolCall !== nothing
|
||||||
try
|
try
|
||||||
after = config.afterToolCall(
|
after = config.afterToolCall(
|
||||||
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
|
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
|
||||||
signal
|
signal
|
||||||
)
|
)
|
||||||
agentEventSink("finalizeExecutedToolCall 3")
|
eventSink("finalizeExecutedToolCall 3")
|
||||||
if after !== nothing
|
if after !== nothing
|
||||||
result = merge(result, dict(:content=>get(after,:content,result.content),
|
result = merge(result, dict(:content=>get(after,:content,result.content),
|
||||||
:details=>get(after,:details,result.details),
|
:details=>get(after,:details,result.details),
|
||||||
@@ -1112,13 +1199,13 @@ function finalizeExecutedToolCall(
|
|||||||
showerror(io, e, bt)
|
showerror(io, e, bt)
|
||||||
println(io)
|
println(io)
|
||||||
end
|
end
|
||||||
agentEventSink(errMsg)
|
eventSink(errMsg)
|
||||||
|
|
||||||
result = createErrorToolResult(sprint(showerror, e))
|
result = createErrorToolResult(sprint(showerror, e))
|
||||||
isError = true
|
isError = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("finalizeExecutedToolCall 4")
|
eventSink("finalizeExecutedToolCall 4")
|
||||||
return finalizedOutcome(prep.toolCall, result, isError)
|
return finalizedOutcome(prep.toolCall, result, isError)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1182,41 +1269,41 @@ function executeToolCallsSequential(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
agentEventSink("executeToolCallsSequential 1")
|
llmCall = config.llmCall
|
||||||
|
eventSink("executeToolCallsSequential 1")
|
||||||
finalizedCalls = finalizedOutcome[]
|
finalizedCalls = finalizedOutcome[]
|
||||||
messages = toolResultMessage[]
|
messages = toolResultMessage[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
eventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
||||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||||
agentEventSink("executeToolCallsSequential " * string(prep.args))
|
eventSink("executeToolCallsSequential " * string(prep.args))
|
||||||
|
|
||||||
if prep isa immediateOutcome
|
if prep isa immediateOutcome
|
||||||
agentEventSink("executeToolCallsSequential 2-1")
|
eventSink("executeToolCallsSequential 2-1")
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
agentEventSink("executeToolCallsSequential 2-2")
|
eventSink("executeToolCallsSequential 2-2")
|
||||||
else
|
else
|
||||||
agentEventSink("executeToolCallsSequential 3")
|
eventSink("executeToolCallsSequential 3")
|
||||||
#XXX
|
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
eventSink("executeToolCallsSequential 3-1")
|
||||||
agentEventSink("executeToolCallsSequential 3-1")
|
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||||
signal, agentEventSink)
|
signal, eventSink)
|
||||||
agentEventSink("executeToolCallsSequential 3-2")
|
eventSink("executeToolCallsSequential 3-2")
|
||||||
end
|
end
|
||||||
agentEventSink("executeToolCallsSequential 4")
|
eventSink("executeToolCallsSequential 4")
|
||||||
agentEventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
eventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||||
$(finalized.result), $(finalized.isError)")
|
$(finalized.result), $(finalized.isError)")
|
||||||
push!(messages, createToolResultMessage(finalized))
|
push!(messages, createToolResultMessage(finalized))
|
||||||
push!(finalizedCalls, finalized)
|
push!(finalizedCalls, finalized)
|
||||||
agentEventSink("executeToolCallsSequential 5")
|
eventSink("executeToolCallsSequential 5")
|
||||||
if signal !== nothing && signal.aborted
|
if signal !== nothing && signal.aborted
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("executeToolCallsSequential 6")
|
eventSink("executeToolCallsSequential 6")
|
||||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1283,26 +1370,27 @@ function executeToolCallsParallel(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
entries = Union{finalizedOutcome,Task}[]
|
entries = Union{finalizedOutcome,Task}[]
|
||||||
|
llmCall = config.llmCall
|
||||||
|
|
||||||
for tc in toolCalls
|
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
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
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))
|
finalized.result, finalized.isError))
|
||||||
push!(entries, finalized)
|
push!(entries, finalized)
|
||||||
else
|
else
|
||||||
t = Task() do
|
t = Task() do
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
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))
|
finalized.result, finalized.isError))
|
||||||
return finalized
|
return finalized
|
||||||
end
|
end
|
||||||
@@ -1385,10 +1473,11 @@ function executeToolCalls(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
agentEventSink("_executeToolCalls 1")
|
llmCall = config.llmCall
|
||||||
|
eventSink("_executeToolCalls 1")
|
||||||
hasSequential = false
|
hasSequential = false
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
t = get(context.tools, tc.name, nothing)
|
t = get(context.tools, tc.name, nothing)
|
||||||
@@ -1397,15 +1486,15 @@ function executeToolCalls(
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("_executeToolCalls 2")
|
eventSink("_executeToolCalls 2")
|
||||||
if config.toolExecution == "sequential" || hasSequential
|
if config.toolExecution == "sequential" || hasSequential
|
||||||
agentEventSink("_executeToolCalls 3")
|
eventSink("_executeToolCalls 3")
|
||||||
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
|
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
|
||||||
agentEventSink)
|
eventSink)
|
||||||
else
|
else
|
||||||
agentEventSink("_executeToolCalls 4")
|
eventSink("_executeToolCalls 4")
|
||||||
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
|
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
|
||||||
agentEventSink)
|
eventSink)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+243
-87
@@ -1,16 +1,15 @@
|
|||||||
module toolRegistry
|
module toolRegistry
|
||||||
|
|
||||||
export toolStore, registerTool, getTools, clearTools, listTool
|
export toolStore, registerTool, registerAllTools, getTools, clearTools, listTool
|
||||||
|
|
||||||
using Dates
|
using Dates
|
||||||
using JSON, DataStructures
|
using JSON, DataStructures
|
||||||
using ..type
|
using ..type
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Per-agent isolated tool storage.
|
Per-agent isolated tool storage.
|
||||||
|
|
||||||
Each agent gets its own `toolStore` so tool registration is independent —
|
Each agent gets its own `toolStore` so tool registration is independent.
|
||||||
`registerTool(store, tool)` only affects that agent's tool set.
|
|
||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
||||||
@@ -28,76 +27,189 @@ Create a new empty tool store.
|
|||||||
|
|
||||||
# Keyword Arguments
|
# Keyword Arguments
|
||||||
- `name::String`: Display name for logging (default: `"default"`)
|
- `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
|
function toolStore(; name::String="default")::toolStore
|
||||||
toolStore(OrderedDict{String, agentTool}(), name)
|
toolStore(OrderedDict{String, agentTool}(), name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── MCP helper functions ────────────────────────────────────────────
|
||||||
|
|
||||||
"""
|
"""
|
||||||
listTool(store::toolStore) -> agentTool
|
Extract text from MCP tool result content array.
|
||||||
|
|
||||||
Return an `agentTool` definition for listing registered tools.
|
Handles JSON-RPC 2.0 result content format:
|
||||||
|
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
||||||
Each call produces a **new** tool object that captures (closes over)
|
|
||||||
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
|
||||||
at runtime.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `store`: The tool store whose tools will be listed when the tool runs
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> store = toolStore(name="agent1");
|
|
||||||
|
|
||||||
julia> register_all_tools(store) # auto-registers listTools
|
|
||||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
|
||||||
[toolRegistry:agent1] Registered tool: listTools
|
|
||||||
|
|
||||||
julia> tools = getTools(store)
|
|
||||||
OrderedDict{String, agentTool} with 4 entries:
|
|
||||||
"getWeather" => agentTool(...)
|
|
||||||
"getTime" => agentTool(...)
|
|
||||||
"writeTool" => agentTool(...)
|
|
||||||
"listTools" => agentTool(...)
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
function listTool(store::toolStore)::agentTool
|
function _extract_text_content(result::Dict)::String
|
||||||
return agentTool(
|
content = get(result, "content", Any[])
|
||||||
name = "listTools",
|
if content isa Vector && !isempty(content)
|
||||||
label = "List Tools",
|
lines = String[]
|
||||||
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
|
for block in content
|
||||||
inputSchema = Dict{String,Any}(
|
if block isa Dict && get(block, "type", "") == "text"
|
||||||
|
push!(lines, string(get(block, "text", "")))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if !isempty(lines)
|
||||||
|
return join(lines, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return JSON.json(result)
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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", "")
|
||||||
|
input_schema = get(tool_def, "inputSchema", Dict{String,Any}())
|
||||||
|
|
||||||
|
# Normalize inputSchema to OpenAI function format
|
||||||
|
if haskey(input_schema, "properties") && input_schema["type"] == "object"
|
||||||
|
params = Dict(
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => input_schema["properties"],
|
||||||
|
"required" => get(input_schema, "required", Any[]),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
params = Dict(
|
||||||
"type" => "object",
|
"type" => "object",
|
||||||
"properties" => Dict{String,Any}(),
|
"properties" => Dict{String,Any}(),
|
||||||
"required" => Any[]
|
"required" => Any[],
|
||||||
),
|
)
|
||||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
|
||||||
tools = getTools(store)
|
|
||||||
if isempty(tools)
|
|
||||||
result_text = "No tools registered."
|
|
||||||
else
|
|
||||||
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, t) in tools]
|
|
||||||
result_text = "Available tools:\n" * join(lines, "\n")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return agentTool(
|
||||||
|
name=name,
|
||||||
|
label=title,
|
||||||
|
description=desc,
|
||||||
|
inputSchema=params,
|
||||||
|
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||||
|
signal::Union{Nothing,abortSignal},
|
||||||
|
eventSink,
|
||||||
|
llmCall=nothing) -> begin
|
||||||
|
try
|
||||||
|
response = mcpserver("tools/call", name, args)
|
||||||
|
|
||||||
|
# Parse JSON-RPC 2.0 response envelope
|
||||||
|
if haskey(response, "error")
|
||||||
|
rpc_error = response["error"]
|
||||||
|
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent(result_text)],
|
[textContent("MCP error: $err_msg")],
|
||||||
Dict{Any,Any}("count" => length(tools)),
|
Dict{Any,Any}("error" => err_msg),
|
||||||
nothing, false
|
nothing, false
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
result_data = get(response, "result", response)
|
||||||
|
content_text = _extract_text_content(result_data)
|
||||||
|
is_error = get(result_data, "isError", false)
|
||||||
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent(content_text)],
|
||||||
|
Dict{Any,Any}("isError" => is_error),
|
||||||
|
nothing, false
|
||||||
|
)
|
||||||
|
catch e
|
||||||
|
errMsg = sprint(showerror, e)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("MCP call error: $errMsg")],
|
||||||
|
Dict{Any,Any}("error" => errMsg),
|
||||||
|
nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
prepareArguments = nothing,
|
prepareArguments=nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs=nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute=false,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
|
"""
|
||||||
|
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`. Skips tools already registered.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `Int`: number of new tools registered
|
||||||
|
"""
|
||||||
|
function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
||||||
|
if mcpserver === nothing
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
new_count = 0
|
||||||
|
|
||||||
|
try
|
||||||
|
response = mcpserver("tools/list")
|
||||||
|
|
||||||
|
# Parse JSON-RPC 2.0 response envelope
|
||||||
|
if haskey(response, "error")
|
||||||
|
rpc_error = response["error"]
|
||||||
|
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
||||||
|
println("[toolRegistry:$(store.name)] MCP tools/list failed: $err_msg")
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
if haskey(response, "result")
|
||||||
|
response = response["result"]
|
||||||
|
end
|
||||||
|
|
||||||
|
tools_array = response["tools"]
|
||||||
|
cursor = get(response, "nextCursor", nothing)
|
||||||
|
|
||||||
|
for tool_def in tools_array
|
||||||
|
name = tool_def["name"]
|
||||||
|
if haskey(store.tools, name)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
@show "tool_def $(typeof(tool_def))"
|
||||||
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
||||||
|
store.tools[name] = wrapped
|
||||||
|
new_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Paginate: fetch remaining tools if nextCursor is present
|
||||||
|
while cursor !== nothing && cursor !== ""
|
||||||
|
response = mcpserver("tools/list")
|
||||||
|
if haskey(response, "result")
|
||||||
|
response = response["result"]
|
||||||
|
end
|
||||||
|
tools_array = get(response, "tools", Any[])
|
||||||
|
cursor = get(response, "nextCursor", nothing)
|
||||||
|
for tool_def in tools_array
|
||||||
|
name = tool_def["name"]
|
||||||
|
if haskey(store.tools, name)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
||||||
|
store.tools[name] = wrapped
|
||||||
|
new_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
println("[toolRegistry:$(store.name)] MCP tools/list failed: $errMsg")
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_count
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||||
@@ -110,18 +222,9 @@ Add `tool` to `store`, overwriting any existing tool with the same name.
|
|||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- The same `store.tools` dict (modified in place)
|
- The same `store.tools` dict (modified in place)
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> store = toolStore(name="agent1");
|
|
||||||
|
|
||||||
julia> registerTool(store, listTool(store))
|
|
||||||
[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
|
store.tools[tool.name] = tool
|
||||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||||
return store.tools
|
return store.tools
|
||||||
@@ -130,22 +233,13 @@ end
|
|||||||
"""
|
"""
|
||||||
Return the tools registered in `store`.
|
Return the tools registered in `store`.
|
||||||
|
|
||||||
The returned dict is the **same object** stored inside `store` — mutations
|
The returned dict is the **same object** stored inside `store`.
|
||||||
to it (e.g. via `registerTool`) are visible through subsequent calls.
|
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store`: Tool store to query
|
- `store`: Tool store to query
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
- `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}
|
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
||||||
return store.tools
|
return store.tools
|
||||||
@@ -159,16 +253,6 @@ Remove all tools from `store`.
|
|||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `nothing`
|
- `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
|
function clearTools(store::toolStore)::Nothing
|
||||||
empty!(store.tools)
|
empty!(store.tools)
|
||||||
@@ -176,4 +260,76 @@ function clearTools(store::toolStore)::Nothing
|
|||||||
return nothing
|
return nothing
|
||||||
end
|
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
|
end # module
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
using .type
|
|
||||||
using Dates
|
|
||||||
|
|
||||||
"""
|
|
||||||
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 getTimeValidateRequiredArgs(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.
|
|
||||||
|
|
||||||
Returns mock time data for the given timezone or city.
|
|
||||||
"""
|
|
||||||
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
|
||||||
onPartialResult)
|
|
||||||
tz = get(args, "timezone", nothing)
|
|
||||||
city = get(args, "city", "")
|
|
||||||
if tz !== nothing
|
|
||||||
result = "Current time in $(tz): $(now())"
|
|
||||||
else
|
|
||||||
result = "Current time in $(city): $(now())"
|
|
||||||
end
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(result)],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the getTime agentTool.
|
|
||||||
"""
|
|
||||||
function getTimeTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
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" => "IANA timezone, e.g. 'America/New_York'"),
|
|
||||||
"city" => Dict("type" => "string", "description" => "City name as fallback")
|
|
||||||
),
|
|
||||||
"required" => []
|
|
||||||
),
|
|
||||||
execute = getTimeExecute,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = getTimeValidateRequiredArgs,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using msghandler
|
|
||||||
using .type
|
|
||||||
|
|
||||||
"""
|
|
||||||
Execute the getWeather tool.
|
|
||||||
|
|
||||||
Returns mock weather data for the given city and temperature units.
|
|
||||||
"""
|
|
||||||
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
|
||||||
agentEventSink)
|
|
||||||
|
|
||||||
agentEventSink("Getting weather...")
|
|
||||||
|
|
||||||
city = get(args, "city", "")
|
|
||||||
units = get(args, "units", "celsius")
|
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
|
||||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
|
||||||
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
|
||||||
Dict{Any,Any}(),
|
|
||||||
nothing,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the getWeather agentTool.
|
|
||||||
"""
|
|
||||||
function getWeatherTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "getWeather",
|
|
||||||
label = "Weather Lookup",
|
|
||||||
description = "Fetch current weather and forecast for a given city.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"city" => Dict("type" => "string", "description" => "City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'"),
|
|
||||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius", "description" => "Temperature scale")
|
|
||||||
),
|
|
||||||
"required" => ["city"]
|
|
||||||
),
|
|
||||||
execute = getWeatherExecute,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = nothing,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
using .type
|
|
||||||
using JSON
|
|
||||||
|
|
||||||
"""
|
|
||||||
Tool that writes new Julia tool module files to disk.
|
|
||||||
|
|
||||||
The agent can use this tool when it encounters a task that no existing tool
|
|
||||||
can handle. Provide the tool's name, label, description, inputSchema, and
|
|
||||||
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
|
|
||||||
|
|
||||||
After calling this tool, add the new file to `YiemAgent.jl` with an `include()`
|
|
||||||
statement (after `include("toolRegistry.jl")`), then restart the agent.
|
|
||||||
The new tool must be registered in `register_all_tools()` in `toolRegistry.jl`.
|
|
||||||
|
|
||||||
# Example
|
|
||||||
|
|
||||||
1. Agent calls writeTool with a spec for a "searchWine" tool
|
|
||||||
2. writeTool generates src/tools/searchWine.jl
|
|
||||||
3. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
|
||||||
4. Developer adds `registerTool(store, searchWineTool())` to register_all_tools()
|
|
||||||
5. Restart agent — new tool is available
|
|
||||||
|
|
||||||
# How It Works
|
|
||||||
|
|
||||||
writeTool is a **file writer**, not a code generator. The LLM provides the
|
|
||||||
tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
|
|
||||||
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
|
|
||||||
- Indents `executeCode` with 4 spaces
|
|
||||||
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
|
||||||
- Appends `writeToolTool()` returning an `agentTool` struct
|
|
||||||
- Writes the combined string to `src/tools/<name>.jl`
|
|
||||||
|
|
||||||
# Important Notes
|
|
||||||
|
|
||||||
- The `executeCode` string is embedded literally into the generated tool.
|
|
||||||
Use `args["param_name"]` to access input parameters.
|
|
||||||
- The code string should be the function body (NOT wrapped in a function).
|
|
||||||
Lines will be indented with 4 spaces inside the execute function.
|
|
||||||
- Tool names must be valid Julia identifiers (lowercase letters, digits, underscores,
|
|
||||||
no leading digits or special characters).
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
Validate that a tool name is a valid Julia identifier.
|
|
||||||
"""
|
|
||||||
function validateToolName(name::String)::Union{Nothing,String}
|
|
||||||
if !occursin(r"^[a-zA-Z_][a-zA-Z0-9_!]*$", name)
|
|
||||||
return "Invalid tool name: '$name'. Tool names must be valid Julia identifiers (letters, digits, underscores, starting with a letter or underscore)."
|
|
||||||
end
|
|
||||||
return nothing
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Indent a multi-line code string by the specified number of spaces.
|
|
||||||
"""
|
|
||||||
function indent_code(code::String, n::Int)::String
|
|
||||||
prefix = " "^n
|
|
||||||
lines = split(code, '\n')
|
|
||||||
result_lines = String[prefix * line for line in lines]
|
|
||||||
return join(result_lines, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Convert a Julia Dict to a valid Julia Dict{String,Any}(...) literal string.
|
|
||||||
"""
|
|
||||||
function dict_to_julia_literal(d)::String
|
|
||||||
if d isa Dict
|
|
||||||
items = String[]
|
|
||||||
for (k, v) in d
|
|
||||||
key_str = json_string(k)
|
|
||||||
val_str = value_to_julia(v)
|
|
||||||
push!(items, "$key_str => $val_str")
|
|
||||||
end
|
|
||||||
return "Dict{String,Any}(" * join(items, ", ") * ")"
|
|
||||||
else
|
|
||||||
return value_to_julia(d)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function value_to_julia(v)::String
|
|
||||||
if v isa Dict
|
|
||||||
return dict_to_julia_literal(v)
|
|
||||||
elseif v isa Vector
|
|
||||||
items = [value_to_julia(x) for x in v]
|
|
||||||
return "[" * join(items, ", ") * "]"
|
|
||||||
elseif v isa String
|
|
||||||
escaped = replace(v, "\\" => "\\\\")
|
|
||||||
escaped = replace(escaped, "\"" => "\\\"")
|
|
||||||
return "\"$escaped\""
|
|
||||||
elseif v isa Number
|
|
||||||
return string(v)
|
|
||||||
elseif v isa Bool
|
|
||||||
return string(v)
|
|
||||||
elseif v === nothing
|
|
||||||
return "nothing"
|
|
||||||
else
|
|
||||||
return "\"$(v)\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Convert any Julia value to a JSON string.
|
|
||||||
"""
|
|
||||||
function json_string(v)::String
|
|
||||||
return JSON.json(v)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the writeTool agentTool.
|
|
||||||
"""
|
|
||||||
function writeToolTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "writeTool",
|
|
||||||
label = "Create Tool",
|
|
||||||
description = "Write a new Julia tool module file to src/tools/<name>.jl. The LLM provides the tool logic as executeCode; writeTool wraps it in Julia boilerplate and writes the file. Restart the agent to load the new tool.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"name" => Dict("type" => "string", "description" => "Unique tool name (valid Julia identifier, no spaces or special chars)"),
|
|
||||||
"label" => Dict("type" => "string", "description" => "Human-readable tool name shown in tool descriptions"),
|
|
||||||
"description" => Dict("type" => "string", "description" => "What the tool does (shown to LLM for tool selection decisions)"),
|
|
||||||
"inputSchema" => Dict(
|
|
||||||
"type" => "object",
|
|
||||||
"description" => "JSON Schema describing tool parameters in MCP format"
|
|
||||||
),
|
|
||||||
"executeCode" => Dict("type" => "string", "description" => "Julia code for the execute function body. Use args[\"key\"] to access parameters. Do NOT wrap in a function definition."),
|
|
||||||
"validateCode" => Dict("type" => "string", "optional" => true, "description" => "Optional custom validation Julia code (runs before execute). Use args[\"key\"] to access parameters. Return nothing to pass, or a string error message to fail."),
|
|
||||||
"prepareCode" => Dict("type" => "string", "optional" => true, "description" => "Optional argument preparation code (runs before validation). Return modified args dict."),
|
|
||||||
"parallel" => Dict("type" => "boolean", "default" => false, "description" => "Whether this tool can run in parallel with other tools")
|
|
||||||
),
|
|
||||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
|
||||||
),
|
|
||||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
|
|
||||||
tool_name = get(args, "name", "")::String
|
|
||||||
tool_label = get(args, "label", tool_name)::String
|
|
||||||
tool_description = get(args, "description", "")::String
|
|
||||||
tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any}
|
|
||||||
execute_code = get(args, "executeCode", "")::String
|
|
||||||
validate_code = get(args, "validateCode", nothing)::Union{String,Nothing}
|
|
||||||
prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing}
|
|
||||||
parallel = get(args, "parallel", false)::Bool
|
|
||||||
|
|
||||||
# Validate tool name
|
|
||||||
name_err = validateToolName(tool_name)
|
|
||||||
if name_err !== nothing
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(name_err)],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validate required fields
|
|
||||||
if isempty(tool_name)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'name'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if isempty(tool_description)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'description'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if isempty(execute_code)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'executeCode'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Generating tool: $tool_name"))
|
|
||||||
|
|
||||||
# Build the tool file path
|
|
||||||
script_dir = dirname(@__FILE__)
|
|
||||||
tools_dir = dirname(script_dir)
|
|
||||||
filepath = joinpath(tools_dir, "$(tool_name).jl")
|
|
||||||
|
|
||||||
# Check for naming conflicts
|
|
||||||
if isfile(filepath)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Writing file: $(basename(filepath))"))
|
|
||||||
|
|
||||||
# Convert schema Dict to a Julia Dict literal string
|
|
||||||
schema_literal = dict_to_julia_literal(tool_schema)
|
|
||||||
|
|
||||||
# Build optional validation function
|
|
||||||
validate_section = if validate_code !== nothing && !isempty(validate_code)
|
|
||||||
indented = indent_code(validate_code, 4)
|
|
||||||
"function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build optional prepare function
|
|
||||||
prepare_section = if prepare_code !== nothing && !isempty(prepare_code)
|
|
||||||
indented = indent_code(prepare_code, 4)
|
|
||||||
"function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Indent user's execute code for embedding inside execute function body
|
|
||||||
indented_exec = indent_code(execute_code, 4)
|
|
||||||
|
|
||||||
# Escape description for Julia string literal
|
|
||||||
escaped_desc = replace(tool_description, "\\" => "\\\\")
|
|
||||||
escaped_desc = replace(escaped_desc, "\"" => "\\\"")
|
|
||||||
|
|
||||||
# Build the complete tool file content
|
|
||||||
parts = String[]
|
|
||||||
push!(parts, "# Auto-generated tool: $tool_name\n")
|
|
||||||
push!(parts, "# Generated by writeTool at $(now())\n\n")
|
|
||||||
if !isempty(validate_section)
|
|
||||||
push!(parts, validate_section)
|
|
||||||
push!(parts, "\n")
|
|
||||||
end
|
|
||||||
if !isempty(prepare_section)
|
|
||||||
push!(parts, prepare_section)
|
|
||||||
push!(parts, "\n")
|
|
||||||
end
|
|
||||||
push!(parts, "\n")
|
|
||||||
push!(parts, "# Execute function\n")
|
|
||||||
push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n")
|
|
||||||
push!(parts, "$indented_exec\n")
|
|
||||||
push!(parts, "end\n\n")
|
|
||||||
push!(parts, "# Tool definition\n")
|
|
||||||
push!(parts, "function getTool()::agentTool\n")
|
|
||||||
push!(parts, " return agentTool(\n")
|
|
||||||
push!(parts, " name = \"$(tool_name)\",\n")
|
|
||||||
push!(parts, " label = \"$(tool_label)\",\n")
|
|
||||||
push!(parts, " description = \"$(escaped_desc)\",\n")
|
|
||||||
push!(parts, " inputSchema = $schema_literal,\n")
|
|
||||||
push!(parts, " execute = executeTool,\n")
|
|
||||||
if validate_code !== nothing && !isempty(validate_code)
|
|
||||||
push!(parts, " validateRequiredArgs = validateRequiredArgs,\n")
|
|
||||||
else
|
|
||||||
push!(parts, " validateRequiredArgs = nothing,\n")
|
|
||||||
end
|
|
||||||
if prepare_code !== nothing && !isempty(prepare_code)
|
|
||||||
push!(parts, " prepareArguments = prepareArguments,\n")
|
|
||||||
else
|
|
||||||
push!(parts, " prepareArguments = nothing,\n")
|
|
||||||
end
|
|
||||||
push!(parts, " parallelToolExecute = $parallel\n")
|
|
||||||
push!(parts, " )\n")
|
|
||||||
push!(parts, "end\n")
|
|
||||||
|
|
||||||
tool_code = join(parts)
|
|
||||||
|
|
||||||
# Write the file — tool must be included in YiemAgent.jl and registered in register_all_tools()
|
|
||||||
write(filepath, tool_code)
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Done"))
|
|
||||||
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Tool '$(tool_name)' written to $filepath. Add include(\"tools/$(tool_name).jl\") to YiemAgent.jl and registerTool(store, $(tool_name)Tool()) to register_all_tools(), then restart the agent.")],
|
|
||||||
Dict{Any,Any}(
|
|
||||||
"file" => filepath,
|
|
||||||
"name" => tool_name,
|
|
||||||
"label" => tool_label,
|
|
||||||
"description" => tool_description,
|
|
||||||
),
|
|
||||||
nothing, false
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = nothing,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
+10
-8
@@ -358,6 +358,7 @@ struct agentContext # Snapshot of the agent's conversa
|
|||||||
systemPrompt::String # System prompt for the agent
|
systemPrompt::String # System prompt for the agent
|
||||||
messages::Vector{agentMessage} # Conversation messages
|
messages::Vector{agentMessage} # Conversation messages
|
||||||
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||||
|
llmCall::Union{Any, Nothing} # LLM call function (for tools that need it)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -451,6 +452,7 @@ struct agentLoopConfig
|
|||||||
beforeToolCall::Union{Function, Nothing}
|
beforeToolCall::Union{Function, Nothing}
|
||||||
afterToolCall::Union{Function, Nothing}
|
afterToolCall::Union{Function, Nothing}
|
||||||
toolExecution::String
|
toolExecution::String
|
||||||
|
llmCall::Union{Any, Nothing} # LLM call function (for tools like searchWine)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -485,13 +487,13 @@ Context passed to the `beforeToolCall` hook.
|
|||||||
# Arguments
|
# Arguments
|
||||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||||
- `toolCall::agentToolCall`: The tool call being prepared
|
- `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
|
- `context::agentContext`: Current conversation context
|
||||||
"""
|
"""
|
||||||
struct beforeToolCallContext
|
struct beforeToolCallContext
|
||||||
message::assistantMessageToolCall
|
message::assistantMessageToolCall
|
||||||
toolCall::agentToolCall
|
toolCall::agentToolCall
|
||||||
args::Dict{String,Any}
|
args::AbstractDict{String, Any}
|
||||||
context::agentContext
|
context::agentContext
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -506,7 +508,7 @@ Context passed to the `afterToolCall` hook.
|
|||||||
# Arguments
|
# Arguments
|
||||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||||
- `toolCall::agentToolCall`: The tool call that was executed
|
- `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
|
- `result::agentToolResult`: The raw tool result
|
||||||
- `isError::Bool`: Whether execution resulted in an error
|
- `isError::Bool`: Whether execution resulted in an error
|
||||||
- `context::agentContext`: Current conversation context
|
- `context::agentContext`: Current conversation context
|
||||||
@@ -514,7 +516,7 @@ Context passed to the `afterToolCall` hook.
|
|||||||
struct afterToolCallContext
|
struct afterToolCallContext
|
||||||
message::assistantMessageToolCall
|
message::assistantMessageToolCall
|
||||||
toolCall::agentToolCall
|
toolCall::agentToolCall
|
||||||
args::Dict{String,Any}
|
args::AbstractDict{String, Any}
|
||||||
result::agentToolResult
|
result::agentToolResult
|
||||||
isError::Bool
|
isError::Bool
|
||||||
context::agentContext
|
context::agentContext
|
||||||
@@ -526,12 +528,12 @@ Event emitted when a tool call execution starts.
|
|||||||
# Arguments
|
# Arguments
|
||||||
- `toolCallId::String`: ID of the tool call
|
- `toolCallId::String`: ID of the tool call
|
||||||
- `toolName::String`: Name of the tool
|
- `toolName::String`: Name of the tool
|
||||||
- `arguments::Dict{String,Any}`: Tool arguments
|
- `arguments::AbstractDict{String, Any}`: Tool arguments
|
||||||
"""
|
"""
|
||||||
struct toolExecStartEvent
|
struct toolExecStartEvent
|
||||||
toolCallId::String
|
toolCallId::String
|
||||||
toolName::String
|
toolName::String
|
||||||
arguments::Dict{String,Any}
|
arguments::AbstractDict{String, Any}
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -540,13 +542,13 @@ Event emitted with partial results during tool execution.
|
|||||||
# Arguments
|
# Arguments
|
||||||
- `toolCallId::String`: ID of the tool call
|
- `toolCallId::String`: ID of the tool call
|
||||||
- `toolName::String`: Name of the tool
|
- `toolName::String`: Name of the tool
|
||||||
- `arguments::Dict{String,Any}`: Tool arguments
|
- `arguments::AbstractDict{String, Any}`: Tool arguments
|
||||||
- `partialResult::Any`: The partial result data
|
- `partialResult::Any`: The partial result data
|
||||||
"""
|
"""
|
||||||
struct toolExecUpdateEvent
|
struct toolExecUpdateEvent
|
||||||
toolCallId::String
|
toolCallId::String
|
||||||
toolName::String
|
toolName::String
|
||||||
arguments::Dict{String,Any}
|
arguments::AbstractDict{String, Any}
|
||||||
partialResult::Any
|
partialResult::Any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+19
-19
@@ -3,7 +3,7 @@ module utils
|
|||||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||||
validateToolArguments, _userMessageToOpenAI,
|
validateToolArguments, _userMessageToOpenAI,
|
||||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI,
|
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI,
|
||||||
beforeToolCall, afterToolCall, agentEventSink
|
beforeToolCall, afterToolCall, eventSink
|
||||||
|
|
||||||
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
|
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
@@ -109,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages)
|
|||||||
# end
|
# end
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function prepareContext(state::agentState, agentEventSink)::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
|
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||||
filteredTools = state.tools
|
filteredTools = state.tools
|
||||||
@@ -120,7 +120,7 @@ function prepareContext(state::agentState, agentEventSink)::agentContext
|
|||||||
#TODO add system prompt, adjust/modify and inject additional context into messages
|
#TODO add system prompt, adjust/modify and inject additional context into messages
|
||||||
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
|
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
|
||||||
|
|
||||||
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
|
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools, llmCall)
|
||||||
|
|
||||||
return agentCtx
|
return agentCtx
|
||||||
end
|
end
|
||||||
@@ -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
|
""" openai message format example
|
||||||
msg = Dict(
|
msg = Dict(
|
||||||
@@ -208,7 +208,7 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
|||||||
openaiReadyMsg = Dict{String, Any}()
|
openaiReadyMsg = Dict{String, Any}()
|
||||||
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
|
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
|
||||||
messages = Vector{Dict{String, Any}}()
|
messages = Vector{Dict{String, Any}}()
|
||||||
agentEventSink("formatMsgForLLM 1")
|
eventSink("formatMsgForLLM 1")
|
||||||
# System prompt as system message
|
# System prompt as system message
|
||||||
if !isempty(ctx.systemPrompt)
|
if !isempty(ctx.systemPrompt)
|
||||||
push!(messages, Dict(
|
push!(messages, Dict(
|
||||||
@@ -216,7 +216,7 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
|||||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
agentEventSink("formatMsgForLLM 2")
|
eventSink("formatMsgForLLM 2")
|
||||||
# Conversation messages
|
# Conversation messages
|
||||||
for msg in ctx.messages
|
for msg in ctx.messages
|
||||||
if msg isa userMessage
|
if msg isa userMessage
|
||||||
@@ -229,10 +229,10 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
|||||||
push!(messages, _toolResultMessageToOpenAI(msg))
|
push!(messages, _toolResultMessageToOpenAI(msg))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("formatMsgForLLM 3")
|
eventSink("formatMsgForLLM 3")
|
||||||
# Convert ctx.tools into OpenAI tools format
|
# Convert ctx.tools into OpenAI tools format
|
||||||
tools_array = _toolsToOpenAI(ctx.tools, agentEventSink)
|
tools_array = _toolsToOpenAI(ctx.tools, eventSink)
|
||||||
agentEventSink("formatMsgForLLM 4")
|
eventSink("formatMsgForLLM 4")
|
||||||
openaiReadyMsg["messages"] = messages
|
openaiReadyMsg["messages"] = messages
|
||||||
openaiReadyMsg["temperature"] = 0.7
|
openaiReadyMsg["temperature"] = 0.7
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ end
|
|||||||
|
|
||||||
|
|
||||||
#TODO
|
#TODO
|
||||||
function agentEventSink(x)
|
function eventSink(x)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -437,10 +437,10 @@ _toolsToOpenAI(nothing) # => Dict{String, Any}[]
|
|||||||
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
|
_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}}()
|
tools_array = Vector{Dict{String, Any}}()
|
||||||
agentEventSink("_toolsToOpenAI 1")
|
eventSink("_toolsToOpenAI 1")
|
||||||
agentEventSink(string(typeof(tools)))
|
eventSink(string(typeof(tools)))
|
||||||
if tools !== nothing
|
if tools !== nothing
|
||||||
for (_, tool) in tools
|
for (_, tool) in tools
|
||||||
push!(tools_array, Dict(
|
push!(tools_array, Dict(
|
||||||
@@ -453,13 +453,13 @@ function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, a
|
|||||||
))
|
))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("_toolsToOpenAI 2")
|
eventSink("_toolsToOpenAI 2")
|
||||||
return tools_array
|
return tools_array
|
||||||
end
|
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
|
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
|
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).
|
coercion, format validation, cross-field constraints).
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `args::Dict{String,Any}`: The arguments provided by the LLM
|
- `args::AbstractDict{String, Any}`: The arguments provided by the LLM
|
||||||
- `inputSchema::Dict{String,Any}`: The tool's `inputSchema` (JSON Schema format)
|
- `inputSchema::AbstractDict{String, Any}`: The tool's `inputSchema` (JSON Schema format)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `nothing` if all required args are present
|
- `nothing` if all required args are present
|
||||||
@@ -487,7 +487,7 @@ args2 = Dict("city" => "Tokyo")
|
|||||||
validateRequiredArgs(args2, schema) # => nothing
|
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[])
|
required = get(inputSchema, "required", Any[])
|
||||||
if isempty(required)
|
if isempty(required)
|
||||||
return nothing
|
return nothing
|
||||||
@@ -541,7 +541,7 @@ validateToolArguments(toolWithHook, tc) # => validated args or throws
|
|||||||
validateToolArguments(toolDefault, tc) # => 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)
|
# Use default (2-arg: args + schema) or tool-specific hook (1-arg: args only)
|
||||||
if isnothing(tool.validateRequiredArgs)
|
if isnothing(tool.validateRequiredArgs)
|
||||||
result = validateRequiredArgs(prepared.arguments, tool.inputSchema)
|
result = validateRequiredArgs(prepared.arguments, tool.inputSchema)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,375 +0,0 @@
|
|||||||
module type
|
|
||||||
|
|
||||||
export agent, sommelier, companion, virtualcustomer, agentcontext
|
|
||||||
|
|
||||||
using Dates, UUIDs, DataStructures, JSON, NATS
|
|
||||||
using GeneralUtils
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
|
||||||
|
|
||||||
|
|
||||||
mutable struct agentcontext
|
|
||||||
text2textInstructLLM::Function
|
|
||||||
getTextEmbedding::Function
|
|
||||||
executeSQL::Function
|
|
||||||
similarSQLVectorDB::Function
|
|
||||||
insertSQLVectorDB::Function
|
|
||||||
similarSommelierDecision::Function
|
|
||||||
insertSommelierDecision::Function
|
|
||||||
find_related_tables_for_user_question::Function
|
|
||||||
pg_conn_str::String
|
|
||||||
agentconfig::AbstractDict
|
|
||||||
end
|
|
||||||
|
|
||||||
abstract type agent end
|
|
||||||
|
|
||||||
mutable struct sommelier <: agent
|
|
||||||
name::String # agent name
|
|
||||||
id::String # agent id
|
|
||||||
retailername::String
|
|
||||||
retailerid::String
|
|
||||||
tools::Dict
|
|
||||||
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
|
|
||||||
chathistory::Vector{Dict{String, Any}}
|
|
||||||
memory::Dict{String, Any}
|
|
||||||
context::agentcontext
|
|
||||||
llmFormatName::String
|
|
||||||
end
|
|
||||||
|
|
||||||
""" A sommelier agent.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `context::agentcontext`
|
|
||||||
Application context containing shared functions for LLM, SQL, and vector database operations.
|
|
||||||
|
|
||||||
# Keyword Arguments
|
|
||||||
- `name::String`
|
|
||||||
Agent's name. Default: `"Assistant"`
|
|
||||||
- `id::String`
|
|
||||||
Agent's ID. Default: generated UUID string.
|
|
||||||
- `retailername::String`
|
|
||||||
Retailer name associated with the sommelier. Default: `"retailer_name"`
|
|
||||||
- `maxHistoryMsg::Integer`
|
|
||||||
Maximum history messages. Default: `20`
|
|
||||||
- `chathistory::Vector{Dict{String, String}}`
|
|
||||||
Chat history. Default: empty vector.
|
|
||||||
- `llmFormatName::String`
|
|
||||||
LLM format name. Default: `"granite3"`
|
|
||||||
|
|
||||||
# Return
|
|
||||||
- `sommelier`: An instantiated sommelier agent.
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> using YiemAgent
|
|
||||||
julia> context = agentcontext(
|
|
||||||
text2textInstructLLM,
|
|
||||||
getTextEmbedding,
|
|
||||||
executeSQL,
|
|
||||||
similarSQLVectorDB,
|
|
||||||
insertSQLVectorDB,
|
|
||||||
similarSommelierDecision,
|
|
||||||
insertSommelierDecision
|
|
||||||
)
|
|
||||||
julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop")
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function sommelier(
|
|
||||||
context::agentcontext, # agent functions, db connect and other context
|
|
||||||
;
|
|
||||||
name::String= "Assistant",
|
|
||||||
id::String= string(uuid4()),
|
|
||||||
retailername::String= "not specified",
|
|
||||||
retailerid::String= "not specified",
|
|
||||||
maxHistoryMsg::Integer= 20,
|
|
||||||
chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(),
|
|
||||||
llmFormatName::String= "granite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = Dict( # update input format
|
|
||||||
"chatbox"=> Dict(
|
|
||||||
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
|
|
||||||
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
|
|
||||||
"output" => "" ,
|
|
||||||
),
|
|
||||||
"winestock"=> Dict(
|
|
||||||
"description" => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
|
|
||||||
"input" => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
|
|
||||||
"output" => """<output>Output are wines that match the search query in JSON format.""",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Memory
|
|
||||||
|
|
||||||
Chat history use openai format as follow:
|
|
||||||
|
|
||||||
image1_path = "test/large_image.png" ---
|
|
||||||
image1_bytes = read(image1_path) | this part must be done
|
|
||||||
image1_base64_string = base64encode(image1_bytes) | in frontend
|
|
||||||
mime_type = "image/png" | not in agent code
|
|
||||||
data1_uri = "data:<mime_type>;base64,<image1_base64_string>" ---
|
|
||||||
|
|
||||||
chathistory= [
|
|
||||||
Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "You are a helpful assistant"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"role" => "user",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "<internal_context_for_assistant>
|
|
||||||
LLM context here...
|
|
||||||
</internal_context_for_assistant>
|
|
||||||
Do you know this wine? Just give me brief intro."
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"type" => "image_url",
|
|
||||||
"image_url" => Dict("url" => data1_uri)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
shortmem = Dict(
|
|
||||||
"1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."),
|
|
||||||
"2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."),
|
|
||||||
...
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
memory = Dict{String, Any}(
|
|
||||||
"shortmem"=> OrderedDict{String, Any}(),
|
|
||||||
"scratchpad"=> "",
|
|
||||||
"recap"=> OrderedDict{String, Any}(),
|
|
||||||
)
|
|
||||||
|
|
||||||
newAgent = sommelier(
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
retailername,
|
|
||||||
retailerid,
|
|
||||||
tools,
|
|
||||||
maxHistoryMsg,
|
|
||||||
chathistory,
|
|
||||||
memory,
|
|
||||||
context,
|
|
||||||
llmFormatName
|
|
||||||
)
|
|
||||||
systemmsg =
|
|
||||||
"""
|
|
||||||
# store_policy
|
|
||||||
- Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
|
|
||||||
- If you found wines in the store's database, they are in stock.
|
|
||||||
- You can only recommend wines that are currently in our inventory
|
|
||||||
- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
|
|
||||||
- Ask the user one question at a time.
|
|
||||||
- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
|
|
||||||
- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
|
|
||||||
- Spicy foods should be paired only with light red wines.
|
|
||||||
- We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such.
|
|
||||||
- Gift box, gift card, and custom messages are available. Inform the user to contact our sales team.
|
|
||||||
|
|
||||||
# store_guidelines
|
|
||||||
- Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting.
|
|
||||||
- Customer may provide images for you to look up.
|
|
||||||
- Encourage the customer to explore different options and try new things.
|
|
||||||
- If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
|
|
||||||
- Your store carries only wine.
|
|
||||||
- Vintage 0 means non-vintage.
|
|
||||||
- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.
|
|
||||||
- User usually ask for something similar. This means you should use the search term based on the profile they like.
|
|
||||||
|
|
||||||
# situation
|
|
||||||
You are having conversation with a customer.
|
|
||||||
|
|
||||||
# your role
|
|
||||||
Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store.
|
|
||||||
|
|
||||||
# objective
|
|
||||||
- Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences.
|
|
||||||
- Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences.
|
|
||||||
|
|
||||||
# your responsibility includes
|
|
||||||
- According to the store's policy and guidelines, and make an informed decision about what available_actions you need to use to achieve the objective.
|
|
||||||
- Keep the conversation with the customer going smoothly
|
|
||||||
|
|
||||||
# your responsibility does NOT includes
|
|
||||||
- Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
|
|
||||||
- Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
|
|
||||||
- Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
|
|
||||||
|
|
||||||
# you should then respond to the user with interleaving plan, action_name, action_input in JSON format
|
|
||||||
1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
|
|
||||||
2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name
|
|
||||||
3) "action_input", The input to the action you are about to perform according to your plan.
|
|
||||||
After the action is executed you gets "action_result". It is the output from the action you selected.
|
|
||||||
|
|
||||||
# available actions
|
|
||||||
"CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan.
|
|
||||||
"SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity.
|
|
||||||
Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD."
|
|
||||||
Example query 2: "Red or white wine, medium tannin, price under 700 USD"
|
|
||||||
Example query 3: "white wine from Tuscany, Italy or Bordeaux, France
|
|
||||||
"WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
|
|
||||||
"END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
system_msg = Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => systemmsg),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
push!(newAgent.chathistory, system_msg)
|
|
||||||
|
|
||||||
return newAgent
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
mutable struct virtualcustomer <: agent
|
|
||||||
name::String # agent name
|
|
||||||
id::String # agent id
|
|
||||||
systemmsg::String # system message
|
|
||||||
tools::Dict
|
|
||||||
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
|
|
||||||
chathistory::Vector{Dict{String, Any}}
|
|
||||||
memory::Dict{String, Any}
|
|
||||||
context # NamedTuple of functions
|
|
||||||
llmFormatName::String
|
|
||||||
end
|
|
||||||
|
|
||||||
function virtualcustomer(
|
|
||||||
context, # NamedTuple of functions
|
|
||||||
;
|
|
||||||
name::String= "Assistant",
|
|
||||||
id::String= string(uuid4()),
|
|
||||||
maxHistoryMsg::Integer= 20,
|
|
||||||
chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(),
|
|
||||||
llmFormatName::String= "granite3",
|
|
||||||
systemmsg::String=
|
|
||||||
"""
|
|
||||||
Your name: $name
|
|
||||||
Your sex: Female
|
|
||||||
Your role: You are a helpful assistant.
|
|
||||||
You should follow the following guidelines:
|
|
||||||
- Focus on the latest conversation.
|
|
||||||
- Your like to be short and concise.
|
|
||||||
|
|
||||||
Let's begin!
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = Dict( # update input format
|
|
||||||
"chatbox"=> Dict(
|
|
||||||
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
|
|
||||||
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
|
|
||||||
"output" => "" ,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Memory
|
|
||||||
Ref: Chat prompt format is openai
|
|
||||||
chathistory = [
|
|
||||||
Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => system_msg),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"role" => "user",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."),
|
|
||||||
Dict(
|
|
||||||
"type" => "image_url",
|
|
||||||
"image_url" => Dict("url" => data1_uri)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
memory = Dict{String, Any}(
|
|
||||||
"shortmem"=> OrderedDict{String, Any}(
|
|
||||||
),
|
|
||||||
"scratchpad"=> "",
|
|
||||||
"events"=> Vector{Dict{String, Any}}(),
|
|
||||||
"state"=> Dict{String, Any}(
|
|
||||||
),
|
|
||||||
"recap"=> OrderedDict{String, Any}(),
|
|
||||||
)
|
|
||||||
|
|
||||||
newAgent = virtualcustomer(
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
systemmsg,
|
|
||||||
tools,
|
|
||||||
maxHistoryMsg,
|
|
||||||
chathistory,
|
|
||||||
memory,
|
|
||||||
context,
|
|
||||||
llmFormatName
|
|
||||||
)
|
|
||||||
|
|
||||||
return newAgent
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # module type
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,367 +0,0 @@
|
|||||||
using Test
|
|
||||||
using Dates
|
|
||||||
using YiemAgent
|
|
||||||
using YiemAgent.toolRegistry
|
|
||||||
using YiemAgent.type
|
|
||||||
using YiemAgent.agentCore
|
|
||||||
|
|
||||||
@testset "register_all_tools with toolStore" begin
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 1. register_all_tools registers all static tools + listTools #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
store = toolStore(name="test1")
|
|
||||||
loaded = register_all_tools(store)
|
|
||||||
@test !isempty(loaded)
|
|
||||||
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
|
|
||||||
|
|
||||||
names = [k for k in keys(loaded)]
|
|
||||||
@test "getTime" in names
|
|
||||||
@test "getWeather" in names
|
|
||||||
@test "writeTool" in names
|
|
||||||
@test "listTools" in names
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 2. register_all_tools returns tools in registration order #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
@test collect(keys(loaded))[1] == "getWeather"
|
|
||||||
@test collect(keys(loaded))[2] == "getTime"
|
|
||||||
@test collect(keys(loaded))[3] == "writeTool"
|
|
||||||
@test collect(keys(loaded))[4] == "listTools"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 3. Verify loaded tool fields are correct #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# getTime
|
|
||||||
time_tool = loaded["getTime"]
|
|
||||||
@test time_tool.name == "getTime"
|
|
||||||
@test time_tool.label == "Time Lookup"
|
|
||||||
@test time_tool.validateRequiredArgs !== nothing
|
|
||||||
@test time_tool.parallelToolExecute == false
|
|
||||||
@test time_tool.inputSchema["required"] == Any[]
|
|
||||||
|
|
||||||
# getWeather
|
|
||||||
weather = loaded["getWeather"]
|
|
||||||
@test weather.name == "getWeather"
|
|
||||||
@test weather.label == "Weather Lookup"
|
|
||||||
@test weather.execute !== nothing
|
|
||||||
@test weather.parallelToolExecute == false
|
|
||||||
@test weather.inputSchema["required"] == ["city"]
|
|
||||||
|
|
||||||
# writeTool
|
|
||||||
wt = loaded["writeTool"]
|
|
||||||
@test wt.name == "writeTool"
|
|
||||||
@test wt.label == "Create Tool"
|
|
||||||
@test wt.execute !== nothing
|
|
||||||
@test "name" in wt.inputSchema["required"]
|
|
||||||
@test "executeCode" in wt.inputSchema["required"]
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 4. Tool execution returns valid results #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
sig = nothing
|
|
||||||
op = x -> x # no-op partial result callback
|
|
||||||
|
|
||||||
# execute getTime
|
|
||||||
result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op)
|
|
||||||
@test result_t isa agentToolResult
|
|
||||||
@test result_t.content[1] isa textContent
|
|
||||||
@test occursin("Tokyo", result_t.content[1].text)
|
|
||||||
|
|
||||||
# execute getTime with timezone
|
|
||||||
result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op)
|
|
||||||
@test result_tz isa agentToolResult
|
|
||||||
@test occursin("America/New_York", result_tz.content[1].text)
|
|
||||||
|
|
||||||
# execute getWeather
|
|
||||||
result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op)
|
|
||||||
@test result_w isa agentToolResult
|
|
||||||
@test result_w.content[1] isa textContent
|
|
||||||
@test occursin("Bangkok", result_w.content[1].text)
|
|
||||||
|
|
||||||
# execute getWeather with units
|
|
||||||
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
|
||||||
@test occursin("72\u00b0F", result_w2.content[1].text)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 5. getTools / registerTool / clearTools (per-store isolation) #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
store3 = toolStore(name="test3")
|
|
||||||
registry_tools = getTools(store3)
|
|
||||||
@test isempty(registry_tools)
|
|
||||||
|
|
||||||
# Register tools manually
|
|
||||||
registerTool(store3, loaded["getTime"])
|
|
||||||
registerTool(store3, loaded["getWeather"])
|
|
||||||
registerTool(store3, loaded["writeTool"])
|
|
||||||
|
|
||||||
reg = getTools(store3)
|
|
||||||
@test !isempty(reg)
|
|
||||||
@test "getTime" in keys(reg)
|
|
||||||
@test "getWeather" in keys(reg)
|
|
||||||
@test "writeTool" in keys(reg)
|
|
||||||
@test collect(keys(reg))[1] == "getTime"
|
|
||||||
@test collect(keys(reg))[2] == "getWeather"
|
|
||||||
@test collect(keys(reg))[3] == "writeTool"
|
|
||||||
|
|
||||||
clearTools(store3)
|
|
||||||
@test isempty(getTools(store3))
|
|
||||||
|
|
||||||
test_tool = agentTool(
|
|
||||||
name = "manualTool",
|
|
||||||
label = "Manual Tool",
|
|
||||||
description = "Registered manually",
|
|
||||||
inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]),
|
|
||||||
execute = (toolCallId, args, signal, onPartialResult) ->
|
|
||||||
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = nothing,
|
|
||||||
parallelToolExecute = true
|
|
||||||
)
|
|
||||||
registerTool(store3, test_tool)
|
|
||||||
reg = getTools(store3)
|
|
||||||
@test haskey(reg, "manualTool")
|
|
||||||
@test length(reg) == 1
|
|
||||||
@test reg["manualTool"].parallelToolExecute == true
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 6. getTools returns direct reference (mutations affect registry) #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
copy1 = getTools(store3)
|
|
||||||
copy2 = getTools(store3)
|
|
||||||
@test copy1 === copy2 # same reference, not a deep copy
|
|
||||||
empty!(copy1)
|
|
||||||
@test isempty(getTools(store3)) # mutation propagates
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 7. Per-store isolation — two stores don't share tools #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
storeA = toolStore(name="isolationA")
|
|
||||||
storeB = toolStore(name="isolationB")
|
|
||||||
|
|
||||||
registerTool(storeA, loaded["getTime"])
|
|
||||||
registerTool(storeB, loaded["getWeather"])
|
|
||||||
|
|
||||||
regA = getTools(storeA)
|
|
||||||
regB = getTools(storeB)
|
|
||||||
|
|
||||||
@test "getTime" in keys(regA)
|
|
||||||
@test "getWeather" ∉ keys(regA)
|
|
||||||
@test "getWeather" in keys(regB)
|
|
||||||
@test "getTime" ∉ keys(regB)
|
|
||||||
|
|
||||||
clearTools(storeA)
|
|
||||||
@test isempty(getTools(storeA))
|
|
||||||
@test !isempty(getTools(storeB)) # storeB unaffected
|
|
||||||
end
|
|
||||||
|
|
||||||
@testset "listTool" begin
|
|
||||||
store = toolStore(name="test_list")
|
|
||||||
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
|
|
||||||
|
|
||||||
# register_all_tools auto-registers listTool
|
|
||||||
@test "listTools" in keys(store.tools)
|
|
||||||
|
|
||||||
# listTool returns an agentTool, not a string or array
|
|
||||||
list_t = listTool(store)
|
|
||||||
@test list_t isa agentTool
|
|
||||||
@test list_t.name == "listTools"
|
|
||||||
@test list_t.label == "List Tools"
|
|
||||||
@test isempty(list_t.inputSchema["required"])
|
|
||||||
|
|
||||||
# Verify all tools appear (3 loaded + listTools = 4)
|
|
||||||
result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x)
|
|
||||||
@test result isa agentToolResult
|
|
||||||
@test result.content[1] isa textContent
|
|
||||||
@test occursin("listTools", result.content[1].text)
|
|
||||||
@test occursin("getWeather", result.content[1].text)
|
|
||||||
@test occursin("getTime", result.content[1].text)
|
|
||||||
@test occursin("writeTool", result.content[1].text)
|
|
||||||
@test result.details["count"] == 4
|
|
||||||
|
|
||||||
# Each listTool call creates an independent closure
|
|
||||||
storeB = toolStore(name="test_listB")
|
|
||||||
registerTool(storeB, store.tools["getWeather"])
|
|
||||||
list_tB = listTool(storeB)
|
|
||||||
|
|
||||||
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
|
||||||
resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x)
|
|
||||||
|
|
||||||
@test occursin("getWeather", resultA.content[1].text)
|
|
||||||
@test occursin("getWeather", resultB.content[1].text)
|
|
||||||
@test occursin("getTime", resultA.content[1].text)
|
|
||||||
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
|
|
||||||
end
|
|
||||||
|
|
||||||
@testset "executePreparedToolCall with static tools" begin
|
|
||||||
# Tests executePreparedToolCall with statically loaded tools.
|
|
||||||
# The world-age issue is resolved because tool.execute comes from
|
|
||||||
# a statically included module, not a dynamically created one.
|
|
||||||
|
|
||||||
store = toolStore(name="test_static")
|
|
||||||
register_all_tools(store)
|
|
||||||
|
|
||||||
weather_tool = store.tools["getWeather"]
|
|
||||||
|
|
||||||
# Create a preparedToolCall that mimics what prepareToolCall() returns
|
|
||||||
tool_call = agentToolCall(
|
|
||||||
"function", "call-static-1", "getWeather",
|
|
||||||
Dict{String,Any}("city" => "San Francisco")
|
|
||||||
)
|
|
||||||
prep = preparedToolCall(
|
|
||||||
weather_tool, tool_call, Dict{String,Any}("city" => "San Francisco")
|
|
||||||
)
|
|
||||||
sig = abortSignal(false)
|
|
||||||
|
|
||||||
# This call goes through: executePreparedToolCall -> prep.tool.execute(...)
|
|
||||||
result = executePreparedToolCall(
|
|
||||||
prep, sig, x -> nothing
|
|
||||||
)
|
|
||||||
|
|
||||||
@test result isa executedOutcome
|
|
||||||
@test result.isError == false
|
|
||||||
@test result.result.content[1] isa textContent
|
|
||||||
@test occursin("San Francisco", result.result.content[1].text)
|
|
||||||
end
|
|
||||||
|
|
||||||
@testset "executePreparedToolCall with validation (static tools)" begin
|
|
||||||
# Tests executePreparedToolCall with a tool that has custom validation hooks.
|
|
||||||
# This exercises the full tool execution path including validation.
|
|
||||||
|
|
||||||
store = toolStore(name="test_static_validate")
|
|
||||||
register_all_tools(store)
|
|
||||||
|
|
||||||
time_tool = store.tools["getTime"]
|
|
||||||
|
|
||||||
tool_call = agentToolCall(
|
|
||||||
"function", "call-static-2", "getTime",
|
|
||||||
Dict{String,Any}("timezone" => "America/New_York")
|
|
||||||
)
|
|
||||||
prep = preparedToolCall(
|
|
||||||
time_tool, tool_call, Dict{String,Any}("timezone" => "America/New_York")
|
|
||||||
)
|
|
||||||
sig = abortSignal(false)
|
|
||||||
|
|
||||||
result = executePreparedToolCall(
|
|
||||||
prep, sig, x -> nothing
|
|
||||||
)
|
|
||||||
|
|
||||||
@test result isa executedOutcome
|
|
||||||
@test result.isError == false
|
|
||||||
@test result.result.content[1] isa textContent
|
|
||||||
@test occursin("America/New_York", result.result.content[1].text)
|
|
||||||
end
|
|
||||||
|
|
||||||
@testset "executeToolCallsSequential with static tools (full pipeline)" begin
|
|
||||||
# Tests the full tool execution pipeline: executeToolCallsSequential
|
|
||||||
# which calls prepareToolCall -> executePreparedToolCall -> finalizeExecutedToolCall
|
|
||||||
# with statically loaded tools.
|
|
||||||
|
|
||||||
store = toolStore(name="test_full_pipeline")
|
|
||||||
register_all_tools(store)
|
|
||||||
|
|
||||||
# Build agentContext from the store's tools
|
|
||||||
tools = getTools(store)
|
|
||||||
ctx = agentContext(
|
|
||||||
"test system prompt",
|
|
||||||
agentMessage[],
|
|
||||||
tools
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create an assistant message containing tool calls
|
|
||||||
assistant_msg = assistantMessage(
|
|
||||||
role="assistant",
|
|
||||||
content=Vector{messageContent}(),
|
|
||||||
api="openai",
|
|
||||||
provider="test",
|
|
||||||
model="test-model",
|
|
||||||
usage=llmUsage(0, 0),
|
|
||||||
stopReason="tool_calls",
|
|
||||||
errorMessage=nothing,
|
|
||||||
timestamp=now()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create tool calls for multiple statically loaded tools
|
|
||||||
tool_calls = [
|
|
||||||
agentToolCall(
|
|
||||||
"function", "call-seq-1", "getWeather",
|
|
||||||
Dict{String,Any}("city" => "Tokyo")
|
|
||||||
),
|
|
||||||
agentToolCall(
|
|
||||||
"function", "call-seq-2", "getTime",
|
|
||||||
Dict{String,Any}("timezone" => "Europe/London")
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
config = agentLoopConfig(
|
|
||||||
nothing, nothing, "sequential"
|
|
||||||
)
|
|
||||||
sig = abortSignal(false)
|
|
||||||
|
|
||||||
# Execute the full pipeline
|
|
||||||
batch = executeToolCallsSequential(
|
|
||||||
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
|
|
||||||
)
|
|
||||||
|
|
||||||
@test batch.messages isa Vector{toolResultMessage}
|
|
||||||
@test length(batch.messages) == 2
|
|
||||||
@test batch.messages[1].toolName == "getWeather"
|
|
||||||
@test batch.messages[1].isError == false
|
|
||||||
@test occursin("Tokyo", batch.messages[1].content[1].text)
|
|
||||||
@test batch.messages[2].toolName == "getTime"
|
|
||||||
@test batch.messages[2].isError == false
|
|
||||||
@test occursin("Europe/London", batch.messages[2].content[1].text)
|
|
||||||
end
|
|
||||||
|
|
||||||
@testset "executeToolCallsParallel with static tools (full pipeline)" begin
|
|
||||||
# Same as above but tests parallel execution path.
|
|
||||||
|
|
||||||
store = toolStore(name="test_parallel")
|
|
||||||
register_all_tools(store)
|
|
||||||
|
|
||||||
tools = getTools(store)
|
|
||||||
ctx = agentContext(
|
|
||||||
"test system prompt",
|
|
||||||
agentMessage[],
|
|
||||||
tools
|
|
||||||
)
|
|
||||||
|
|
||||||
assistant_msg = assistantMessage(
|
|
||||||
role="assistant",
|
|
||||||
content=Vector{messageContent}(),
|
|
||||||
api="openai",
|
|
||||||
provider="test",
|
|
||||||
model="test-model",
|
|
||||||
usage=llmUsage(0, 0),
|
|
||||||
stopReason="tool_calls",
|
|
||||||
errorMessage=nothing,
|
|
||||||
timestamp=now()
|
|
||||||
)
|
|
||||||
|
|
||||||
tool_calls = [
|
|
||||||
agentToolCall(
|
|
||||||
"function", "call-par-1", "getWeather",
|
|
||||||
Dict{String,Any}("city" => "Paris")
|
|
||||||
),
|
|
||||||
agentToolCall(
|
|
||||||
"function", "call-par-2", "getTime",
|
|
||||||
Dict{String,Any}("city" => "Sydney")
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
config = agentLoopConfig(
|
|
||||||
nothing, nothing, "parallel"
|
|
||||||
)
|
|
||||||
sig = abortSignal(false)
|
|
||||||
|
|
||||||
batch = executeToolCallsParallel(
|
|
||||||
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
|
|
||||||
)
|
|
||||||
|
|
||||||
@test batch.messages isa Vector{toolResultMessage}
|
|
||||||
@test length(batch.messages) == 2
|
|
||||||
@test batch.messages[1].toolName == "getWeather"
|
|
||||||
@test batch.messages[1].isError == false
|
|
||||||
@test batch.messages[2].toolName == "getTime"
|
|
||||||
@test batch.messages[2].isError == false
|
|
||||||
end
|
|
||||||
+5
-5
@@ -32,7 +32,7 @@ catch e
|
|||||||
println(io)
|
println(io)
|
||||||
end
|
end
|
||||||
|
|
||||||
agentEventSink(err_msg)
|
eventSink(err_msg)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -66,13 +66,13 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct agentEventSink
|
struct eventSink
|
||||||
natsConn::NATS.Connection
|
natsConn::NATS.Connection
|
||||||
topic::String
|
topic::String
|
||||||
senderID::String
|
senderID::String
|
||||||
end
|
end
|
||||||
|
|
||||||
function (aes::agentEventSink)(msg::String)
|
function (aes::eventSink)(msg::String)
|
||||||
NATS.publish(aes.natsConn, aes.topic, msg)
|
NATS.publish(aes.natsConn, aes.topic, msg)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -88,11 +88,11 @@ text2text_llm = text2textInstructLLM(agent_conn,
|
|||||||
"sender",
|
"sender",
|
||||||
config["externalservice"]["fileserver"]["url"])
|
config["externalservice"]["fileserver"]["url"])
|
||||||
|
|
||||||
debugNats = agentEventSink(agent_conn, "sommanion.debug", "sender")
|
debugNats = eventSink(agent_conn, "sommanion.debug", "sender")
|
||||||
|
|
||||||
agent = YiemAgent.yiemAgent(
|
agent = YiemAgent.yiemAgent(
|
||||||
text2text_llm;
|
text2text_llm;
|
||||||
agentEventSink=debugNats
|
eventSink=debugNats
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = Dict(
|
msg = Dict(
|
||||||
|
|||||||
Reference in New Issue
Block a user