Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9193260539 | |||
| 5f284883b3 | |||
| d2592ab6d6 | |||
| da21790263 | |||
| c59f6bfa61 | |||
| 3c91222462 | |||
| f62b8f14e7 | |||
| 7ffb720f86 |
+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 `promptAgent(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 (promptAgent, 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 |
|
||||||
|
|----------|-------------|
|
||||||
|
| `promptAgent(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 `promptAgent()` 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 |
|
||||||
|
|||||||
+35
-35
@@ -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 |
|
||||||
@@ -126,7 +126,7 @@ result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
|||||||
|
|
||||||
**Via agent loop (production):**
|
**Via agent loop (production):**
|
||||||
```
|
```
|
||||||
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
user message → promptAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||||
→ _agentLoop detects message → @spawn _processMessage(agent)
|
→ _agentLoop detects message → @spawn _processMessage(agent)
|
||||||
→ prepareContext → formatMsgForLLM → llmCall
|
→ prepareContext → formatMsgForLLM → llmCall
|
||||||
→ LLM returns tool_calls
|
→ LLM returns tool_calls
|
||||||
@@ -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
|
||||||
```
|
```
|
||||||
@@ -346,7 +346,7 @@ The `_agentLoop()` function runs as a background `@spawn` task, created when `yi
|
|||||||
|
|
||||||
```
|
```
|
||||||
yiemAgent struct contains:
|
yiemAgent struct contains:
|
||||||
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
|
- inputChannel (Channel, capacity 16) ← user sends messages here via promptAgent()
|
||||||
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
||||||
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
||||||
```
|
```
|
||||||
@@ -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
|
||||||
@@ -1243,7 +1243,7 @@ Each `toolStore` gets its own `listTool` instance bound to that store via `listT
|
|||||||
|
|
||||||
```
|
```
|
||||||
USER SENDS MESSAGE
|
USER SENDS MESSAGE
|
||||||
└─> runAgent(agent, "What's the weather in Tokyo?")
|
└─> promptAgent(agent, "What's the weather in Tokyo?")
|
||||||
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
||||||
|
|
||||||
|
|
||||||
@@ -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,54 +0,0 @@
|
|||||||
{
|
|
||||||
"nats_server_info": {
|
|
||||||
"description": "nats server",
|
|
||||||
"url": "nats.yiem.cc"
|
|
||||||
},
|
|
||||||
"testingOrProduction": "testing",
|
|
||||||
"agentId": "2b74b87a-5413-4fe2-a4d3-405891051680",
|
|
||||||
"agentCentralConfigSubject": "/yiem/hq/agent/sommelier/backend/config/api/v1.1",
|
|
||||||
"this_service_name": "agent_backend",
|
|
||||||
"this_service_input_channel": {
|
|
||||||
"mqtt": [
|
|
||||||
"/yiem/hq/agent/sommpanion/backend/db/api_v1"
|
|
||||||
],
|
|
||||||
"nats": [
|
|
||||||
"sommpanion.backend.agentbackend.v1.inbox"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"agentRole": "sommelier",
|
|
||||||
"organization": "yiem_hq",
|
|
||||||
"externalservice": {
|
|
||||||
"servicesloadbalancer": {
|
|
||||||
"nats": "sommpanion.backend.servicesloadbalancer.v1.inbox"
|
|
||||||
},
|
|
||||||
"textembedding": {
|
|
||||||
"url": "textembedding.api.v1"
|
|
||||||
},
|
|
||||||
"textimage_to_text_llm": {
|
|
||||||
"url": "https://llmcoder.yiem.cc/v1/chat/completions",
|
|
||||||
"modelname": "Qwen3.6-35B-A3B-UD-Q4_K_M"
|
|
||||||
},
|
|
||||||
"virtualWineCustomer_1": {
|
|
||||||
"serviceSubject": "",
|
|
||||||
"modelName": "qwen3:8b"
|
|
||||||
},
|
|
||||||
"sommpanion_db" : {
|
|
||||||
"description": "A database connection info for LibPQ client",
|
|
||||||
"url": "192.168.88.106:5432",
|
|
||||||
"dbname": "winedb",
|
|
||||||
"user": "admin",
|
|
||||||
"password": "admin@Sommpanion_0.0"
|
|
||||||
},
|
|
||||||
"sommpanion_vectordb" : {
|
|
||||||
"description": "A wine database connection info for LibPQ client",
|
|
||||||
"url": "192.168.88.106:5433",
|
|
||||||
"dbname": "vectordb",
|
|
||||||
"user": "admin",
|
|
||||||
"password": "admin@Sommpanion_0.0"
|
|
||||||
},
|
|
||||||
"fileserver": {
|
|
||||||
"description": "temporary file server",
|
|
||||||
"url": "https://fileserver.yiem.cc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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?
|
||||||
+30
-66
@@ -1,66 +1,30 @@
|
|||||||
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")
|
||||||
"""
|
using .type
|
||||||
|
|
||||||
include("type.jl")
|
include("utils.jl")
|
||||||
using .type
|
using .utils
|
||||||
|
|
||||||
include("utils.jl")
|
include("toolRegistry.jl")
|
||||||
using .utils
|
using .toolRegistry
|
||||||
|
|
||||||
include("tools/getWeather.jl")
|
include("agentCore.jl")
|
||||||
include("tools/getTime.jl")
|
using .agentCore
|
||||||
include("tools/searchWine.jl")
|
|
||||||
include("tools/writeTool.jl")
|
include("api.jl")
|
||||||
|
using .api
|
||||||
include("toolRegistry.jl")
|
|
||||||
using .toolRegistry
|
export promptAgent, takeResponse, followUp, stopAgent
|
||||||
|
|
||||||
function register_all_tools(store::toolRegistry.toolStore)
|
|
||||||
registerTool(store, getWeatherTool())
|
|
||||||
registerTool(store, getTimeTool())
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
registerTool(store, searchWineTool())
|
|
||||||
registerTool(store, writeToolTool())
|
|
||||||
registerTool(store, listTool(store))
|
|
||||||
return store.tools
|
|
||||||
end
|
end # end module
|
||||||
|
|
||||||
# include("llmfunction.jl")
|
|
||||||
# using .llmfunction
|
|
||||||
|
|
||||||
include("agentCore.jl")
|
|
||||||
using .agentCore
|
|
||||||
|
|
||||||
include("api.jl")
|
|
||||||
using .api
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # module YiemAgent_v1
|
|
||||||
|
|||||||
+274
-181
@@ -9,11 +9,6 @@ using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serializ
|
|||||||
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 --------------------------------------------- #
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -49,7 +44,83 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
|||||||
# Each block has a type — "text", "thinking", or "toolCall".
|
# Each block has a type — "text", "thinking", or "toolCall".
|
||||||
# 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
|
||||||
@@ -102,23 +176,24 @@ function yiemAgent(
|
|||||||
afterToolCall::Function=afterToolCall,
|
afterToolCall::Function=afterToolCall,
|
||||||
# prepareNextTurn::Union{Function, Nothing}=nothing,
|
# prepareNextTurn::Union{Function, Nothing}=nothing,
|
||||||
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
|
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
|
||||||
sessionId::Union{String, Nothing}=nothing,
|
sessionId::Union{String, 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)
|
||||||
followUp = Channel(32)
|
followUp = Channel(32)
|
||||||
outputChannel = Channel(16)
|
outputChannel = Channel(16)
|
||||||
|
|
||||||
# load tools (statically registered at module init)
|
|
||||||
toolStore1 = toolStore(name="myagent")
|
tools = OrderedDict{String, agentTool}()
|
||||||
register_all_tools(toolStore1)
|
registerAllTools(tools, mcpServer; eventSink=eventSink, sessionId=sessionId)
|
||||||
|
|
||||||
# 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(
|
||||||
agentState(systemPrompt, model, getTools(toolStore1), messages),
|
agentState(systemPrompt, model, tools, messages),
|
||||||
inputChannel,
|
inputChannel,
|
||||||
followUp,
|
followUp,
|
||||||
outputChannel,
|
outputChannel,
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -359,113 +442,124 @@ function _processMessage(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
while true
|
while true
|
||||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
try
|
||||||
while isready(inputChannel)
|
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||||
agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
while isready(inputChannel)
|
||||||
newUserMsg_openai = take!(inputChannel)
|
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||||
agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
newUserMsg_openai = take!(inputChannel)
|
||||||
if newUserMsg_openai === :shutdown
|
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||||
agentEventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
if newUserMsg_openai === :shutdown
|
||||||
# Re-emit shutdown signal for the loop to handle
|
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
||||||
put!(inputChannel, :shutdown)
|
# Re-emit shutdown signal for the loop to handle
|
||||||
break
|
put!(inputChannel, :shutdown)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
||||||
|
push!(agentMsgHistory, newUserMsg)
|
||||||
|
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||||
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
# call prepareContext()
|
||||||
push!(agentMsgHistory, newUserMsg)
|
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||||
agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||||
end
|
preparedContext = prepareContext(state, eventSink)
|
||||||
agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||||
# call prepareContext()
|
# Call formatMessagesForLLM() to format for LLM
|
||||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
|
||||||
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
preparedContext = prepareContext(state, agentEventSink, llmCall)
|
eventSink("_processMessage 10 formattedMessages $formattedMessages")
|
||||||
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
# Call formatMessagesForLLM() to format for LLM
|
|
||||||
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
|
|
||||||
|
|
||||||
agentEventSink("_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("LLM response " * 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)
|
||||||
|
|
||||||
if hasToolCalls && length(toolCallList) > 0
|
if hasToolCalls && length(toolCallList) > 0
|
||||||
# Build context and config for executeToolCalls
|
# Build context and config for executeToolCalls
|
||||||
|
|
||||||
config = agentLoopConfig(
|
config = agentLoopConfig(
|
||||||
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
|
|
||||||
agentEventSink("toolResult " * string(toolResult))
|
|
||||||
push!(agentMsgHistory, toolResult)
|
|
||||||
end
|
|
||||||
agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
if toolResultBatch.terminate
|
|
||||||
agentEventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
# If toolResultBatch requested termination, build a final response
|
|
||||||
final_content = [textContent("Tool execution completed.")]
|
|
||||||
for toolResult in toolResultBatch.messages
|
for toolResult in toolResultBatch.messages
|
||||||
for content_block in toolResult.content
|
eventSink("toolResult " * string(toolResult))
|
||||||
if content_block isa textContent
|
push!(agentMsgHistory, toolResult)
|
||||||
append!(final_content, [content_block])
|
end
|
||||||
elseif content_block isa Dict
|
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||||
if haskey(content_block, :text)
|
if toolResultBatch.terminate
|
||||||
push!(final_content, textContent(content_block[:text]))
|
eventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
# If toolResultBatch requested termination, build a final response
|
||||||
|
final_content = [textContent("Tool execution completed.")]
|
||||||
|
for toolResult in toolResultBatch.messages
|
||||||
|
for content_block in toolResult.content
|
||||||
|
if content_block isa textContent
|
||||||
|
append!(final_content, [content_block])
|
||||||
|
elseif content_block isa Dict
|
||||||
|
if haskey(content_block, :text)
|
||||||
|
push!(final_content, textContent(content_block[:text]))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
final_response = assistantMessage(
|
||||||
|
role="assistant",
|
||||||
|
content=final_content,
|
||||||
|
api=assistant_msg.api,
|
||||||
|
model=assistant_msg.model,
|
||||||
|
usage=assistant_msg.usage,
|
||||||
|
stopReason="tool_use_terminated",
|
||||||
|
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
|
||||||
|
"One or more tool calls failed"
|
||||||
|
else
|
||||||
|
nothing
|
||||||
|
end,
|
||||||
|
timestamp=now(),
|
||||||
|
)
|
||||||
|
push!(agentMsgHistory, final_response)
|
||||||
|
break
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
else
|
||||||
final_response = assistantMessage(
|
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||||
role="assistant",
|
# LLM did not use tool calls —
|
||||||
content=final_content,
|
|
||||||
api=assistant_msg.api,
|
|
||||||
model=assistant_msg.model,
|
|
||||||
usage=assistant_msg.usage,
|
|
||||||
stopReason="tool_use_terminated",
|
|
||||||
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
|
|
||||||
"One or more tool calls failed"
|
|
||||||
else
|
|
||||||
nothing
|
|
||||||
end,
|
|
||||||
timestamp=now(),
|
|
||||||
)
|
|
||||||
push!(agentMsgHistory, final_response)
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
else
|
catch e
|
||||||
agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
bt = catch_backtrace()
|
||||||
# LLM did not use tool calls —
|
err_msg = sprint() do io
|
||||||
break
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
eventSink(err_msg)
|
||||||
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
|
||||||
|
|
||||||
@@ -903,45 +997,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()
|
||||||
@@ -950,7 +1047,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
|
||||||
@@ -1000,17 +1097,15 @@ 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, llmCall)
|
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink)
|
||||||
agentEventSink(result.content[1].text)
|
eventSink("executePreparedToolCall 2")
|
||||||
agentEventSink("executePreparedToolCall 4")
|
eventSink("tool execute raw result: " * sprint(show, result))
|
||||||
|
eventSink("executePreparedToolCall 3")
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
catch e
|
catch e
|
||||||
bt = catch_backtrace()
|
bt = catch_backtrace()
|
||||||
@@ -1018,7 +1113,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
|
||||||
@@ -1088,19 +1183,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),
|
||||||
@@ -1114,13 +1209,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
|
||||||
|
|
||||||
@@ -1184,41 +1279,41 @@ function executeToolCallsSequential(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
llmCall = config.llmCall
|
eventSink("executeToolCallsSequential 1")
|
||||||
agentEventSink("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")
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
|
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||||
agentEventSink("executeToolCallsSequential 3-1")
|
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
eventSink("executeToolCallsSequential 3-1")
|
||||||
signal, agentEventSink)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||||
agentEventSink("executeToolCallsSequential 3-2")
|
signal, eventSink)
|
||||||
|
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
|
||||||
|
|
||||||
@@ -1285,27 +1380,26 @@ 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, llmCall)
|
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||||
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
|
||||||
@@ -1388,11 +1482,10 @@ function executeToolCalls(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
llmCall = config.llmCall
|
eventSink("_executeToolCalls 1")
|
||||||
agentEventSink("_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)
|
||||||
@@ -1401,15 +1494,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
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
module api
|
module api
|
||||||
|
|
||||||
export prompt
|
export promptAgent, takeResponse, followUp, stopAgent
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames
|
DataFrames
|
||||||
@@ -18,7 +18,7 @@ The agent processes messages from `inputChannel` in the background task.
|
|||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `agent::yiemAgent`: The agent instance to send a message to
|
- `agent::yiemAgent`: The agent instance to send a message to
|
||||||
- `msg`: The message to send (any type accepted by the agent's processing pipeline)
|
- `msg`: The message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Hello!")`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
@@ -29,11 +29,11 @@ The agent processes messages from `inputChannel` in the background task.
|
|||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> runAgent(agent, "Hello!")
|
julia> promptAgent(agent, Dict("role" => "user", "content" => "Hello!"))
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function runAgent(agent::yiemAgent, msg)
|
function promptAgent(agent::yiemAgent, msg::AbstractDict{String, Any})
|
||||||
put!(agent.inputChannel, msg)
|
put!(agent.inputChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
@@ -50,7 +50,7 @@ Blocks until the agent sends a response.
|
|||||||
- An `assistantMessage` instance representing the agent's response
|
- An `assistantMessage` instance representing the agent's response
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- Use `runAgent(agent, msg)` to send a message before calling this function.
|
- Use `promptAgent(agent, msg)` to send a message before calling this function.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
@@ -70,23 +70,23 @@ and before any tool call results are sent.
|
|||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `agent::yiemAgent`: The agent instance to send a follow-up message to
|
- `agent::yiemAgent`: The agent instance to send a follow-up message to
|
||||||
- `msg`: The follow-up message to send
|
- `msg`: The follow-up message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Also consider red wines")`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- Use `runAgent(agent, msg)` for the primary message and `followUp(agent, msg)` for additional
|
- Use `promptAgent(agent, msg)` for the primary message and `followUp(agent, msg)` for additional
|
||||||
messages while the agent is processing.
|
messages while the agent is processing.
|
||||||
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> followUp(agent, "Also consider red wines")
|
julia> followUp(agent, Dict("role" => "user", "content" => "Also consider red wines"))
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function followUp(agent::yiemAgent, msg)
|
function followUp(agent::yiemAgent, msg::AbstractDict{String, Any})
|
||||||
put!(agent.followUpChannel, msg)
|
put!(agent.followUpChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
|
|||||||
+245
-133
@@ -1,179 +1,291 @@
|
|||||||
module toolRegistry
|
module toolRegistry
|
||||||
|
|
||||||
export toolStore, registerTool, getTools, clearTools, listTool
|
export registerTool, registerAllTools, clearTools, listTool
|
||||||
|
|
||||||
using Dates
|
using Dates
|
||||||
using JSON, DataStructures
|
using JSON, DataStructures
|
||||||
using ..type
|
using ..type
|
||||||
|
|
||||||
"""
|
|
||||||
Per-agent isolated tool storage.
|
|
||||||
|
|
||||||
Each agent gets its own `toolStore` so tool registration is independent —
|
|
||||||
`registerTool(store, tool)` only affects that agent's tool set.
|
|
||||||
|
|
||||||
# Fields
|
# ── MCP helper functions ────────────────────────────────────────────
|
||||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
|
||||||
- `name::String` — identifier for debugging/logs
|
|
||||||
"""
|
"""
|
||||||
struct toolStore
|
Extract text from MCP tool result content array.
|
||||||
tools::OrderedDict{String, agentTool}
|
|
||||||
name::String
|
Handles JSON-RPC 2.0 result content format:
|
||||||
|
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
||||||
|
"""
|
||||||
|
function _extract_text_content(result::AbstractDict)::String
|
||||||
|
content = get(result, "content", Any[])
|
||||||
|
if content isa Vector && !isempty(content)
|
||||||
|
lines = String[]
|
||||||
|
for block in content
|
||||||
|
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
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
toolStore(; name="default") -> toolStore
|
Wrap an MCP tool definition as an `agentTool`.
|
||||||
|
|
||||||
Create a new empty tool store.
|
The returned tool's `execute` function calls the MCP server's "tools/call"
|
||||||
|
method with the validated arguments.
|
||||||
# Keyword Arguments
|
|
||||||
- `name::String`: Display name for logging (default: `"default"`)
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> store = toolStore(name="agent1")
|
|
||||||
toolStore(OrderedDict{String, agentTool}(), "agent1")
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
function toolStore(; name::String="default")::toolStore
|
function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSink=nothing, sessionId=nothing)::agentTool
|
||||||
toolStore(OrderedDict{String, agentTool}(), name)
|
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",
|
||||||
|
"properties" => Dict{String,Any}(),
|
||||||
|
"required" => Any[],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return agentTool(
|
||||||
|
name=name,
|
||||||
|
label=title,
|
||||||
|
description=desc,
|
||||||
|
inputSchema=params,
|
||||||
|
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||||
|
signal::Union{Nothing,abortSignal},
|
||||||
|
eventSink) -> begin
|
||||||
|
try
|
||||||
|
response = mcpserver("tools/call", name, args, sessionId)
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
[textContent("MCP error: $err_msg")],
|
||||||
|
Dict{Any,Any}("error" => err_msg),
|
||||||
|
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,
|
||||||
|
prepareArguments=nothing,
|
||||||
|
validateRequiredArgs=nothing,
|
||||||
|
parallelToolExecute=false,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
listTool(store::toolStore) -> agentTool
|
Discover and register MCP tools into `store.tools`.
|
||||||
|
|
||||||
Return an `agentTool` definition for listing registered 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.
|
||||||
|
|
||||||
Each call produces a **new** tool object that captures (closes over)
|
# Returns
|
||||||
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
- `Int`: number of new tools registered
|
||||||
at runtime.
|
"""
|
||||||
|
function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::Int
|
||||||
|
if mcpserver === nothing
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
new_count = 0
|
||||||
|
try
|
||||||
|
eventSink("register_mcp_tools 1")
|
||||||
|
response = mcpserver("tools/list", sessionId)
|
||||||
|
eventSink("register_mcp_tools 2")
|
||||||
|
# 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] 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(tools, name)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
|
||||||
|
tools[name] = wrapped
|
||||||
|
new_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Paginate: fetch remaining tools if nextCursor is present
|
||||||
|
while cursor !== nothing && cursor !== ""
|
||||||
|
response = sessionId !== nothing ? mcpserver("tools/list", sessionId) : 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(tools, name)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
|
||||||
|
tools[name] = wrapped
|
||||||
|
new_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(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] MCP tools/list failed: $errMsg")
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_count
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||||
|
|
||||||
|
Add `tool` to `tools`, overwriting any existing tool with the same name.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store`: The tool store whose tools will be listed when the tool runs
|
- `tools`: Tool dict to modify
|
||||||
|
- `tool`: The `agentTool` to register
|
||||||
|
|
||||||
# Example
|
# Returns
|
||||||
```julia
|
- The same `tools` dict (modified in place)
|
||||||
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 registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
|
||||||
|
)::OrderedDict{String, agentTool}
|
||||||
|
tools[tool.name] = tool
|
||||||
|
println("[toolRegistry] Registered tool: $(tool.name)")
|
||||||
|
return tools
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Remove all tools from the dict.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `tools`: Tool dict to clear
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `nothing`
|
||||||
|
"""
|
||||||
|
function clearTools(tools::OrderedDict{String, agentTool})::Nothing
|
||||||
|
empty!(tools)
|
||||||
|
println("[toolRegistry] Registry cleared")
|
||||||
|
return nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
|
||||||
|
|
||||||
|
"""
|
||||||
|
listTool(tools::OrderedDict{String, agentTool}, 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 `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(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "listTools",
|
name="listTools",
|
||||||
label = "List Tools",
|
label="List Tools",
|
||||||
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
|
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}(
|
inputSchema=Dict{String,Any}(
|
||||||
"type" => "object",
|
"type" => "object",
|
||||||
"properties" => Dict{String,Any}(),
|
"properties" => Dict{String,Any}(),
|
||||||
"required" => Any[]
|
"required" => Any[]
|
||||||
),
|
),
|
||||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||||
tools = getTools(store)
|
signal::Union{Nothing,abortSignal},
|
||||||
if isempty(tools)
|
eventSink) -> begin
|
||||||
result_text = "No tools registered."
|
# Discover and register MCP tools (idempotent — skips already registered)
|
||||||
else
|
new_count = register_mcp_tools(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
|
||||||
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, t) in tools]
|
eventSink("tools dump: " * sprint(show, tools))
|
||||||
result_text = "Available tools:\n" * join(lines, "\n")
|
|
||||||
end
|
# Always include listTools itself in the count
|
||||||
|
total = length(tools)
|
||||||
|
|
||||||
|
lines = String[
|
||||||
|
"- $(t.name): $(t.label) — $(t.description)"
|
||||||
|
for (k, t) in tools
|
||||||
|
]
|
||||||
|
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent(result_text)],
|
[textContent(result_text)],
|
||||||
Dict{Any,Any}("count" => length(tools)),
|
Dict{Any,Any}("count" => total),
|
||||||
nothing, false
|
nothing, false
|
||||||
)
|
)
|
||||||
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
|
# ── High-level API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
"""
|
"""
|
||||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver)
|
||||||
|
|
||||||
Add `tool` to `store`, overwriting any existing tool with the same name.
|
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
|
# Arguments
|
||||||
- `store`: Tool store to modify
|
- `tools`: The tool dict to populate
|
||||||
- `tool`: The `agentTool` to register
|
- `mcpserver`: A callable struct that communicates with the MCP server
|
||||||
|
|
||||||
# Returns
|
|
||||||
- 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 registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing)
|
||||||
store.tools[tool.name] = tool
|
list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
|
||||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
registerTool(tools, list_t; eventSink=eventSink)
|
||||||
return store.tools
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Return the tools registered in `store`.
|
|
||||||
|
|
||||||
The returned dict is the **same object** stored inside `store` — mutations
|
|
||||||
to it (e.g. via `registerTool`) are visible through subsequent calls.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `store`: Tool store to query
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> tools = getTools(store)
|
|
||||||
OrderedDict{String, agentTool} with 2 entries:
|
|
||||||
"getWeather" => agentTool(...)
|
|
||||||
"getTime" => agentTool(...)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
|
||||||
return store.tools
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Remove all tools from `store`.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `store`: Tool store to clear
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- `nothing`
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> clearTools(store)
|
|
||||||
[toolRegistry:agent1] Registry cleared
|
|
||||||
nothing
|
|
||||||
|
|
||||||
julia> getTools(store)
|
|
||||||
OrderedDict{String, agentTool} with 0 entries
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function clearTools(store::toolStore)::Nothing
|
|
||||||
empty!(store.tools)
|
|
||||||
println("[$(store.name)] Registry cleared")
|
|
||||||
return nothing
|
|
||||||
end
|
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, llmCall=nothing)
|
|
||||||
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, llmCall=nothing)
|
|
||||||
|
|
||||||
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,397 +0,0 @@
|
|||||||
using .type
|
|
||||||
using LibPQ, DataFrames, JSON, DataStructures
|
|
||||||
using Dates, Random, HTTP
|
|
||||||
using GeneralUtils
|
|
||||||
|
|
||||||
# ── Database config — update for your environment ───────────────────────
|
|
||||||
const DB_CONFIG = Dict{String,Any}(
|
|
||||||
"host" => "localhost",
|
|
||||||
"port" => 5432,
|
|
||||||
"dbname" => "winedb",
|
|
||||||
"user" => "postgres",
|
|
||||||
"password" => "",
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Execute the search_wine_database! tool.
|
|
||||||
|
|
||||||
Uses the agent's LLM to generate SQL from the free-form text query,
|
|
||||||
then executes it against the wine database and returns formatted results.
|
|
||||||
"""
|
|
||||||
function searchWineExecute(
|
|
||||||
toolCallId::String,
|
|
||||||
args::Dict{String,Any},
|
|
||||||
signal::Union{Nothing,abortSignal},
|
|
||||||
agentEventSink,
|
|
||||||
llmCall,
|
|
||||||
)::agentToolResult
|
|
||||||
#WORKING
|
|
||||||
search_query = get(args, "searchQuery", "")::String
|
|
||||||
|
|
||||||
if isempty(search_query)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Please provide a search query for the wine database.")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
agentEventSink("searchWineExecute: query=$search_query")
|
|
||||||
|
|
||||||
# ── SQL generation prompt ───────────────────────────────────────────
|
|
||||||
systemmsg = """
|
|
||||||
# database_search_guidelines
|
|
||||||
- Keep SQL queries focused only on the provided information.
|
|
||||||
- Use wildcard character (%) to search more effectively.
|
|
||||||
- Do not create any table in the database.
|
|
||||||
- Text information in the database is usually stored in lower case.
|
|
||||||
If your search returns empty, try using lower case to search.
|
|
||||||
- Overly strict conditions usually yield empty results.
|
|
||||||
- Use ILIKE for case-insensitive text matching.
|
|
||||||
- Only output the SQL query — do not wrap it in backticks or add comments.
|
|
||||||
|
|
||||||
# situation
|
|
||||||
You are a wine store database assistant. You will be given a user's
|
|
||||||
natural language search query and the database table schema.
|
|
||||||
|
|
||||||
# objective
|
|
||||||
Generate a single SQL query to find wines matching the user's request.
|
|
||||||
|
|
||||||
# your responsibility includes
|
|
||||||
Fulfill the objective.
|
|
||||||
|
|
||||||
# you should respond with ONLY the SQL query string, ending with ';'
|
|
||||||
"""
|
|
||||||
|
|
||||||
table_schema = """
|
|
||||||
CREATE TABLE wine (
|
|
||||||
wine_id uuid primary key default gen_random_uuid (),
|
|
||||||
wine_name varchar(128) not null,
|
|
||||||
winery varchar(128) not null,
|
|
||||||
vintage integer not null,
|
|
||||||
region varchar(128) not null,
|
|
||||||
country varchar(128) not null,
|
|
||||||
wine_type varchar(128) not null,
|
|
||||||
grape varchar(128) not null,
|
|
||||||
serving_temperature varchar(128) not null,
|
|
||||||
intensity integer,
|
|
||||||
sweetness integer,
|
|
||||||
tannin integer,
|
|
||||||
acidity integer,
|
|
||||||
fizziness integer,
|
|
||||||
tasting_notes text,
|
|
||||||
image_url jsonb,
|
|
||||||
manufacturer_sku text,
|
|
||||||
note text,
|
|
||||||
other_attributes jsonb,
|
|
||||||
created_time timestamptz default current_timestamp,
|
|
||||||
updated_time timestamptz default current_timestamp,
|
|
||||||
description text
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE retailer (
|
|
||||||
retailer_id uuid primary key default gen_random_uuid (),
|
|
||||||
retailer_name varchar(128) not null,
|
|
||||||
retailer_username varchar(128) not null,
|
|
||||||
retailer_password varchar(128) not null,
|
|
||||||
retailer_address text not null,
|
|
||||||
country varchar(128) not null,
|
|
||||||
contact_person varchar(128) not null,
|
|
||||||
telephone varchar(128) not null,
|
|
||||||
email varchar(128) not null,
|
|
||||||
note text,
|
|
||||||
other_attributes jsonb,
|
|
||||||
created_time timestamptz default current_timestamp,
|
|
||||||
updated_time timestamptz default current_timestamp,
|
|
||||||
description text
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE retailer_wine (
|
|
||||||
retailer_id uuid references retailer(retailer_id),
|
|
||||||
wine_id uuid references wine(wine_id),
|
|
||||||
constraint retailer_wine_id primary key (retailer_id, wine_id),
|
|
||||||
price NUMERIC(10, 2),
|
|
||||||
currency varchar(3) not null,
|
|
||||||
created_time timestamptz default current_timestamp,
|
|
||||||
updated_time timestamptz default current_timestamp
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
|
|
||||||
context = "<internal_context_for_assistant>\n<database_table_schema>\n$table_schema\n</database_table_schema>\n</internal_context_for_assistant>\n\n"
|
|
||||||
input = context * "User query: $search_query\n\nGenerate the SQL query:"
|
|
||||||
|
|
||||||
# ── Call LLM for SQL generation ────────────────────────────────────
|
|
||||||
max_attempts = 5
|
|
||||||
generated_sql = nothing
|
|
||||||
|
|
||||||
for attempt in 1:max_attempts
|
|
||||||
msg = Dict(
|
|
||||||
"messages" => [
|
|
||||||
Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [Dict("type" => "text", "text" => systemmsg)],
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"role" => "user",
|
|
||||||
"content" => [Dict("type" => "text", "text" => input)],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
"temperature" => 0.7,
|
|
||||||
)
|
|
||||||
|
|
||||||
llm_response = llmCall(msg)
|
|
||||||
|
|
||||||
# Clean the response — extract SQL from potential markdown/code blocks
|
|
||||||
sql_text = _clean_sql_response(llm_response)
|
|
||||||
|
|
||||||
# Validate it looks like SQL
|
|
||||||
if _is_valid_sql(sql_text)
|
|
||||||
generated_sql = sql_text
|
|
||||||
agentEventSink("searchWine: generated SQL (attempt $attempt)\n$sql_text")
|
|
||||||
break
|
|
||||||
else
|
|
||||||
agentEventSink("searchWine: invalid SQL attempt $attempt: $sql_text")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if generated_sql === nothing
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Failed to generate a valid SQL query for your search. Please try rephrasing.")],
|
|
||||||
Dict{Any,Any}("error" => "sql_generation_failed"), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# ── Execute SQL ────────────────────────────────────────────────────
|
|
||||||
try
|
|
||||||
conn = LibPQ.Connection(DB_CONFIG)
|
|
||||||
|
|
||||||
# Ensure LIMIT to prevent large result sets
|
|
||||||
sanitized_sql = _ensure_limit(generated_sql)
|
|
||||||
agentEventSink("searchWine: executing\n$sanitized_sql")
|
|
||||||
|
|
||||||
result = LibPQ.execute(conn, sanitized_sql)
|
|
||||||
close(conn)
|
|
||||||
|
|
||||||
if !LibPQ.hasdata(result)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("No wines found matching your search. Try loosening your criteria.")],
|
|
||||||
Dict{Any,Any}("count" => 0), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
df = DataFrame(result)
|
|
||||||
num_rows, num_cols = size(df)
|
|
||||||
|
|
||||||
if num_cols > 30
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("The result has more than 30 columns. Please be more specific in your search.")],
|
|
||||||
Dict{Any,Any}("error" => "too_many_columns"), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Randomly sample up to 2 rows for display if more than 2 results
|
|
||||||
display_df = df
|
|
||||||
if num_rows > 2
|
|
||||||
idx = sample(1:num_rows, min(2, num_rows), replace=false)
|
|
||||||
display_df = df[idx, :]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convert to vector of dicts
|
|
||||||
result_vec = GeneralUtils.dfToVectorDict(display_df)
|
|
||||||
|
|
||||||
# Fetch bottle images if available
|
|
||||||
for d in result_vec
|
|
||||||
image_url_json_str = get(d, "image_url", nothing)
|
|
||||||
if image_url_json_str !== nothing && !isempty(string(image_url_json_str))
|
|
||||||
try
|
|
||||||
image_url_json_obj = JSON.parse(string(image_url_json_str))
|
|
||||||
base_url = "http://192.168.88.106:8080/"
|
|
||||||
if haskey(image_url_json_obj, "bottle")
|
|
||||||
url = base_url * string(image_url_json_obj["bottle"])
|
|
||||||
image_data = HTTP.get(url)
|
|
||||||
image_base64_string = base64encode(image_data.body)
|
|
||||||
d["image"] = image_base64_string
|
|
||||||
end
|
|
||||||
catch
|
|
||||||
# Skip image fetch on error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Format results as readable text
|
|
||||||
result_str = _format_wine_results(display_df)
|
|
||||||
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(result_str)],
|
|
||||||
Dict{Any,Any}(
|
|
||||||
"count" => num_rows,
|
|
||||||
"displayed" => size(display_df, 1),
|
|
||||||
),
|
|
||||||
nothing, false
|
|
||||||
)
|
|
||||||
|
|
||||||
catch e
|
|
||||||
errMsg = sprint(showerror, e)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Database error: $errMsg")],
|
|
||||||
Dict{Any,Any}("error" => errMsg), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Extract a SQL query string from the LLM response, handling potential
|
|
||||||
markdown code blocks, extra text, or JSON wrapping.
|
|
||||||
"""
|
|
||||||
function _clean_sql_response(response)::String
|
|
||||||
text = string(response)
|
|
||||||
|
|
||||||
# Try to extract from code block
|
|
||||||
if occursin("```", text)
|
|
||||||
extracted = GeneralUtils.extract_triple_backtick_text(text)
|
|
||||||
if !isempty(extracted)
|
|
||||||
text = extracted[1]
|
|
||||||
# Remove "sql\n" prefix if present
|
|
||||||
if startswith(text, "sql\n") || startswith(text, "SQL\n")
|
|
||||||
text = text[5:end]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove JSON wrapping if present
|
|
||||||
text = strip(text)
|
|
||||||
if startswith(text, "{") && occursin("action_input", text)
|
|
||||||
# Parse as JSON and extract action_input
|
|
||||||
try
|
|
||||||
parsed = JSON.parse(text)
|
|
||||||
if parsed isa Dict
|
|
||||||
text = get(parsed, "action_input", text)
|
|
||||||
end
|
|
||||||
catch
|
|
||||||
# Keep original
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Extract SQL keywords to find the actual query
|
|
||||||
lines = split(strip(text), '\n')
|
|
||||||
sql_lines = String[]
|
|
||||||
for line in lines
|
|
||||||
stripped = strip(line)
|
|
||||||
if occursin(r"(?i)(SELECT|FROM|WHERE|JOIN|ORDER|LIMIT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|WITH)", stripped)
|
|
||||||
# Take everything from this line to the end
|
|
||||||
push!(sql_lines, line)
|
|
||||||
elseif !isempty(sql_lines)
|
|
||||||
# Continue collecting if we already found SQL
|
|
||||||
push!(sql_lines, line)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
result = join(sql_lines, "\n")
|
|
||||||
|
|
||||||
# Ensure it ends with semicolon
|
|
||||||
result = strip(result)
|
|
||||||
if !endswith(result, ";")
|
|
||||||
result *= ";"
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Check if a string looks like a valid SQL query.
|
|
||||||
"""
|
|
||||||
function _is_valid_sql(sql::String)::Bool
|
|
||||||
sql = strip(sql)
|
|
||||||
# Must start with a SQL keyword
|
|
||||||
has_sql_keyword = occursin(r"(?i)(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|WITH)\s", sql) ||
|
|
||||||
occursin(r"(?i)(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|WITH)\s*;", sql)
|
|
||||||
# Must end with semicolon
|
|
||||||
has_semicolon = endswith(sql, ";")
|
|
||||||
# Must not be too short (reject single words)
|
|
||||||
reasonable_length = length(sql) > 10
|
|
||||||
return has_sql_keyword && has_semicolon && reasonable_length
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Ensure the SQL query has a LIMIT clause to prevent loading excessive data.
|
|
||||||
"""
|
|
||||||
function _ensure_limit(sql::String)::String
|
|
||||||
sql = strip(sql)
|
|
||||||
if !occursin(r"(?i)LIMIT", sql)
|
|
||||||
# Remove existing semicolon, add LIMIT, re-add semicolon
|
|
||||||
if endswith(sql, ";")
|
|
||||||
sql = sql[1:end-1]
|
|
||||||
end
|
|
||||||
sql *= " ORDER BY RANDOM() LIMIT 2;"
|
|
||||||
end
|
|
||||||
return sql
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Format wine database results as human-readable text.
|
|
||||||
"""
|
|
||||||
function _format_wine_results(df::DataFrame)::String
|
|
||||||
lines = String[]
|
|
||||||
num_rows = size(df, 1)
|
|
||||||
|
|
||||||
for i in 1:num_rows
|
|
||||||
row = df[i, :]
|
|
||||||
push!(lines, "$(i). $(get(row, :wine_name, "Unknown")) $(get(row, :vintage, ""))")
|
|
||||||
|
|
||||||
winery = get(row, :winery, "Unknown")
|
|
||||||
region = get(row, :region, "Unknown")
|
|
||||||
country = get(row, :country, "Unknown")
|
|
||||||
push!(lines, " Winery: $winery")
|
|
||||||
push!(lines, " Region: $region, $country")
|
|
||||||
|
|
||||||
grape = get(row, :grape, "Unknown")
|
|
||||||
wtype = get(row, :wine_type, "Unknown")
|
|
||||||
push!(lines, " Grape: $grape")
|
|
||||||
push!(lines, " Type: $wtype")
|
|
||||||
|
|
||||||
sweetness = get(row, :sweetness, "N/A")
|
|
||||||
intensity = get(row, :intensity, "N/A")
|
|
||||||
tannin_val = get(row, :tannin, "N/A")
|
|
||||||
acidity = get(row, :acidity, "N/A")
|
|
||||||
push!(lines, " Profile: Sweetness: $sweetness, Intensity: $intensity, Tannin: $tannin_val, Acidity: $acidity")
|
|
||||||
|
|
||||||
tasting = get(row, :tasting_notes, nothing)
|
|
||||||
if tasting !== nothing && !isempty(string(tasting))
|
|
||||||
tn = string(tasting)
|
|
||||||
limit = min(200, length(tn))
|
|
||||||
push!(lines, " Notes: $(tn[1:limit])$(length(tn) > limit ? "..." : "")")
|
|
||||||
end
|
|
||||||
|
|
||||||
price = get(row, :price, "N/A")
|
|
||||||
currency = get(row, :currency, "")
|
|
||||||
retailer = get(row, :retailer_name, "N/A")
|
|
||||||
push!(lines, " Price: $price $currency at $retailer")
|
|
||||||
push!(lines, "")
|
|
||||||
end
|
|
||||||
|
|
||||||
return join(lines, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the searchWine agentTool.
|
|
||||||
"""
|
|
||||||
function searchWineTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "searchWine",
|
|
||||||
label = "Search Wine Database",
|
|
||||||
description = "Search the wine database for wines matching a free-text query. Uses the LLM to generate SQL and execute it against the database. Returns wine details including name, winery, vintage, tasting notes, and price.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"searchQuery" => Dict(
|
|
||||||
"type" => "string",
|
|
||||||
"description" => "Free-text description of the wine you're looking for, e.g., 'a light-bodied red wine from France under 50 dollars'",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"required" => ["searchQuery"],
|
|
||||||
),
|
|
||||||
execute = searchWineExecute,
|
|
||||||
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, llmCall=nothing) -> 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
|
|
||||||
+11
-13
@@ -23,7 +23,7 @@
|
|||||||
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
||||||
agentToolCallBatch,
|
agentToolCallBatch,
|
||||||
# Functions (defined elsewhere)
|
# Functions (defined elsewhere)
|
||||||
runAgent, takeResponse, followUp, stopAgent
|
promptAgent, takeResponse, followUp, stopAgent
|
||||||
|
|
||||||
|
|
||||||
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
||||||
@@ -358,7 +358,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -408,7 +407,7 @@ function agentState(
|
|||||||
agentState(
|
agentState(
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
model,
|
model,
|
||||||
deepcopy(tools),
|
tools,
|
||||||
deepcopy(messages),
|
deepcopy(messages),
|
||||||
Vector{String}(),
|
Vector{String}(),
|
||||||
nothing,
|
nothing,
|
||||||
@@ -443,7 +442,7 @@ end
|
|||||||
"""
|
"""
|
||||||
Configuration for the agent tool execution loop.
|
Configuration for the agent tool execution loop.
|
||||||
|
|
||||||
# Arguments
|
# Fields
|
||||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||||
@@ -452,7 +451,6 @@ 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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -487,13 +485,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
|
||||||
|
|
||||||
@@ -508,7 +506,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
|
||||||
@@ -516,7 +514,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
|
||||||
@@ -528,12 +526,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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -542,13 +540,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
|
||||||
|
|
||||||
|
|||||||
+22
-22
@@ -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,18 +109,18 @@ prepareContext(state).messages == deepcopy(state.messages)
|
|||||||
# end
|
# end
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function prepareContext(state::agentState, agentEventSink, llmCall=nothing)::agentContext
|
function prepareContext(state::agentState, eventSink)::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
|
||||||
|
|
||||||
#TODO add filtered tools to the current system prompt / modify systemPrompt here
|
#TODO add filtered tools to the current system prompt / modify systemPrompt here
|
||||||
preparedSystemPrompt = state.systemPrompt
|
preparedSystemPrompt = state.systemPrompt
|
||||||
|
|
||||||
#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, llmCall)
|
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@ function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Convert an assistantMessage to OpenAI message format.
|
Convert an assistantMessage to OpenAI message format.
|
||||||
"""
|
"""
|
||||||
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
|
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
|
||||||
@@ -437,10 +437,10 @@ _toolsToOpenAI(nothing) # => Dict{String, Any}[]
|
|||||||
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
|
_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)
|
||||||
|
|||||||
@@ -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