Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7a98f1710 | |||
| c7abf844ea | |||
| 00447e4dde | |||
| 25f8468696 | |||
| a29a82b74c | |||
| fd616409dd | |||
| 2543e6cbf1 | |||
| b8067c2d33 | |||
| 510cf6126c | |||
| 90fb97a4e7 | |||
| 77adeb3a6b |
+50
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
julia_version = "1.12.6"
|
||||
manifest_format = "2.0"
|
||||
project_hash = "0db36d4fb31037ba05065476e6aebaf4cd0e1e8c"
|
||||
project_hash = "aa163e2bf572632825162936e107be18384fd40f"
|
||||
|
||||
[[deps.Accessors]]
|
||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||
@@ -44,6 +44,12 @@ git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6"
|
||||
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
|
||||
version = "0.4.0"
|
||||
|
||||
[[deps.Arrow]]
|
||||
deps = ["ArrowTypes", "BitIntegers", "CodecLz4", "CodecZstd", "ConcurrentUtilities", "DataAPI", "Dates", "EnumX", "Mmap", "PooledArrays", "SentinelArrays", "StringViews", "Tables", "TimeZones", "TranscodingStreams", "UUIDs"]
|
||||
git-tree-sha1 = "4a69a3eadc1f7da78d950d1ef270c3a62c1f7e01"
|
||||
uuid = "69666777-d1a9-59fb-9406-91d4454c9d45"
|
||||
version = "2.8.1"
|
||||
|
||||
[[deps.ArrowTypes]]
|
||||
deps = ["Sockets", "UUIDs"]
|
||||
git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101"
|
||||
@@ -58,6 +64,12 @@ version = "1.11.0"
|
||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.BitIntegers]]
|
||||
deps = ["Random"]
|
||||
git-tree-sha1 = "091d591a060e43df1dd35faab3ca284925c48e46"
|
||||
uuid = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1"
|
||||
version = "0.3.7"
|
||||
|
||||
[[deps.BufferedStreams]]
|
||||
git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
|
||||
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
|
||||
@@ -90,12 +102,24 @@ git-tree-sha1 = "40956acdbef3d8c7cc38cba42b56034af8f8581a"
|
||||
uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
||||
version = "0.3.4"
|
||||
|
||||
[[deps.CodecLz4]]
|
||||
deps = ["Lz4_jll", "TranscodingStreams"]
|
||||
git-tree-sha1 = "d58afcd2833601636b48ee8cbeb2edcb086522c2"
|
||||
uuid = "5ba52731-8f18-5e0d-9241-30f10d1ec561"
|
||||
version = "0.4.6"
|
||||
|
||||
[[deps.CodecZlib]]
|
||||
deps = ["TranscodingStreams", "Zlib_jll"]
|
||||
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
|
||||
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||
version = "0.7.8"
|
||||
|
||||
[[deps.CodecZstd]]
|
||||
deps = ["TranscodingStreams", "Zstd_jll"]
|
||||
git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56"
|
||||
uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2"
|
||||
version = "0.8.7"
|
||||
|
||||
[[deps.CommonSolve]]
|
||||
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
|
||||
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
||||
@@ -130,6 +154,12 @@ weakdeps = ["InverseFunctions"]
|
||||
[deps.CompositionsBase.extensions]
|
||||
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
|
||||
|
||||
[[deps.ConcurrentUtilities]]
|
||||
deps = ["Serialization", "Sockets"]
|
||||
git-tree-sha1 = "3c9be947934c38475bafe822c6d61aaed17f0738"
|
||||
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
|
||||
version = "2.6.0"
|
||||
|
||||
[[deps.ConstructionBase]]
|
||||
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
|
||||
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
|
||||
@@ -531,6 +561,12 @@ git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
|
||||
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
|
||||
version = "3.8.0"
|
||||
|
||||
[[deps.Lz4_jll]]
|
||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||
git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33"
|
||||
uuid = "5ced341a-0733-55b8-9ab6-a4889d929147"
|
||||
version = "1.10.1+0"
|
||||
|
||||
[[deps.MacroTools]]
|
||||
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
||||
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
|
||||
@@ -938,6 +974,11 @@ git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484"
|
||||
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
|
||||
version = "0.4.7"
|
||||
|
||||
[[deps.StringViews]]
|
||||
git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8"
|
||||
uuid = "354b36f9-a18e-4713-926e-db85100087ba"
|
||||
version = "1.3.7"
|
||||
|
||||
[[deps.StructTypes]]
|
||||
deps = ["Dates", "UUIDs"]
|
||||
git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8"
|
||||
@@ -1080,6 +1121,14 @@ git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026"
|
||||
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
|
||||
version = "1.0.21+0"
|
||||
|
||||
[[deps.msghandler]]
|
||||
deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"]
|
||||
git-tree-sha1 = "e82a79cf6602541ea25409aded57b2ace4a7c29f"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/msghandler"
|
||||
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||
version = "1.2.1"
|
||||
|
||||
[[deps.nghttp2_jll]]
|
||||
deps = ["Artifacts", "Libdl"]
|
||||
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
||||
|
||||
@@ -22,6 +22,7 @@ SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||
|
||||
[compat]
|
||||
Base64 = "1.11.0"
|
||||
@@ -33,3 +34,4 @@ JSON = "1.6.1"
|
||||
LLMMCTS = "0.1.5"
|
||||
NATS = "0.1.0"
|
||||
SQLLLM = "0.2.8"
|
||||
msghandler = "1.2.1"
|
||||
|
||||
+262
-288
@@ -9,7 +9,7 @@ This document describes the complete tool lifecycle in the YiemAgent framework,
|
||||
1. [Quick Start: Tool Lifecycle](#1-quick-start-tool-lifecycle)
|
||||
2. [Overview](#2-overview)
|
||||
3. [Tool Definition — The `agentTool` Struct](#3-tool-definition--the-agenttool-struct)
|
||||
4. [Tool Registration — Per-Agent Tool Stores](#4-tool-registration--per-agent-tool-stores)
|
||||
4. [Tool Registration — Static Registration](#4-tool-registration--static-registration)
|
||||
5. [The Agent Loop — High-Level Flow](#5-the-agent-loop--high-level-flow)
|
||||
6. [Message Processing Pipeline](#6-message-processing-pipeline)
|
||||
7. [Tool Call Extraction from LLM Response](#7-tool-call-extraction-from-llm-response)
|
||||
@@ -23,7 +23,8 @@ This document describes the complete tool lifecycle in the YiemAgent framework,
|
||||
15. [Self-Modifying Tools](#15-self-modifying-tools)
|
||||
16. [Complete End-to-End Example](#16-complete-end-to-end-example)
|
||||
17. [Tool File Contract](#17-tool-file-contract)
|
||||
18. [Appendix: Type Reference](#18-appendix-type-reference)
|
||||
18. [Adding New Tools](#18-adding-new-tools)
|
||||
19. [Appendix: Type Reference](#19-appendix-type-reference)
|
||||
|
||||
---
|
||||
|
||||
@@ -43,76 +44,52 @@ tool = listTool(store) # Returns an agentTool that, when executed, lists all to
|
||||
**Result extraction:**
|
||||
```julia
|
||||
result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x)
|
||||
# result.content[1].text => "Available tools:\n- getTime: Time Lookup — Get current local time...\n- getWeather: Weather Lookup — Fetch current weather..."
|
||||
# result.content[1].text => "Available tools:\n- getWeather: Weather Lookup — Fetch current weather...\n- getTime: Time Lookup — Get current local time..."
|
||||
```
|
||||
|
||||
**Source:** `toolRegistry.jl:54-82`
|
||||
**Source:** `toolRegistry.jl:43-98`
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Load — `loadTools()`
|
||||
### Step 2: Register — `register_all_tools()`
|
||||
|
||||
Load all tool modules from a directory into a `toolStore`. Each `.jl` file must define `getTool()::agentTool`. `listTool` is auto-registered so the LLM can discover available tools.
|
||||
Tools are statically defined in `src/tools/` and registered at module initialization via `register_all_tools()`. Each tool function (e.g., `getWeatherTool()`, `getTimeTool()`, `writeToolTool()`) is called to create the `agentTool` struct. `listTool` is auto-registered so the LLM can discover available tools.
|
||||
|
||||
```julia
|
||||
using YiemAgent, YiemAgent.toolRegistry
|
||||
|
||||
store = toolStore(name="myAgent")
|
||||
tools = loadTools(store, "src/tools")
|
||||
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools
|
||||
tools = register_all_tools(store)
|
||||
# Calls getWeatherTool(), getTimeTool(), writeToolTool() to create agentTool structs
|
||||
# Also auto-registers listTools for runtime discovery
|
||||
```
|
||||
|
||||
**Result extraction:**
|
||||
```julia
|
||||
all_tools = getTools(store) # OrderedDict{String, agentTool}
|
||||
# Keys: "getTime", "getWeather", "writeTool", "listTools"
|
||||
getTime_tool = all_tools["getTime"]
|
||||
# Keys: "getWeather", "getTime", "writeTool", "listTools"
|
||||
getWeather_tool = all_tools["getWeather"]
|
||||
|
||||
# Manual registration (alternative to loadTools)
|
||||
# Manual registration (alternative to register_all_tools)
|
||||
registerTool(store, my_tool)
|
||||
clearTools(store) # Clear all tools from store
|
||||
```
|
||||
|
||||
**Source:** `toolRegistry.jl:126-178`
|
||||
**Source:** `toolRegistry.jl:38-40, 127-143`
|
||||
|
||||
---
|
||||
|
||||
### Step 2.5: Create Agent with Tools
|
||||
|
||||
Wire the loaded tools into a new `yiemAgent` instance. The `tools` parameter is deep-copied into `agent._state.tools`; `_tool_store` is kept for runtime registration.
|
||||
Wire the registered tools into a new `yiemAgent` instance. The `yiemAgent` constructor calls `register_all_tools()` automatically.
|
||||
|
||||
```julia
|
||||
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
||||
|
||||
# 1. Set up toolStore and load tools (auto-registers listTools)
|
||||
store = toolStore(name="myAgent")
|
||||
loadTools(store, "src/tools")
|
||||
|
||||
# 2. Create agent — pass tools + _tool_store
|
||||
# Create agent — tools are registered automatically via register_all_tools()
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant.",
|
||||
model = my_model,
|
||||
tools = getTools(store), # OrderedDict{String, agentTool}
|
||||
llmCall = my_llm_call, # Function that calls the LLM API
|
||||
agentEventSink = my_event_sink, # Function for TUI/logging
|
||||
_tool_store = store, # For runtime registerTool() calls
|
||||
)
|
||||
```
|
||||
|
||||
**Manual registration** (without `loadTools`):
|
||||
|
||||
```julia
|
||||
store = toolStore(name="myAgent")
|
||||
registerTool(store, getTime_tool)
|
||||
registerTool(store, getWeather_tool)
|
||||
registerTool(store, listTool(store)) # needed for manual registration
|
||||
|
||||
agent = yiemAgent(
|
||||
tools = getTools(store),
|
||||
llmCall = my_llm_call,
|
||||
agentEventSink = my_event_sink,
|
||||
_tool_store = store,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -120,17 +97,15 @@ agent = yiemAgent(
|
||||
|
||||
| Parameter | Type | Required | Purpose |
|
||||
|-----------|------|----------|---------|
|
||||
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
|
||||
| `model` | `llmModel` | No | LLM model config |
|
||||
| `tools` | `OrderedDict{String, agentTool}` | No | Available tools (deep-copied) |
|
||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
||||
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
||||
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
||||
| `_tool_store` | `toolStore` | No | Runtime tool registry for `registerTool()` |
|
||||
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
|
||||
| `model` | `llmModel` | No | LLM model config |
|
||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
||||
|
||||
Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`.
|
||||
|
||||
**Source:** `type.jl:609-657`, `toolRegistry.jl:38-40, 191-195`
|
||||
**Source:** `type.jl:609-657`, `toolRegistry.jl:127-143`
|
||||
|
||||
---
|
||||
|
||||
@@ -146,13 +121,13 @@ sig = nothing
|
||||
op = x -> x # no-op partial result callback
|
||||
|
||||
# Execute a loaded tool directly
|
||||
result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
||||
result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
||||
```
|
||||
|
||||
**Via agent loop (production):**
|
||||
```
|
||||
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||
→ _agent_loop detects message → @spawn _process_message(agent)
|
||||
→ _agentLoop detects message → @spawn _processMessage(agent)
|
||||
→ prepareContext → formatMsgForLLM → llmCall
|
||||
→ LLM returns tool_calls
|
||||
→ executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
@@ -168,10 +143,10 @@ user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||
|
||||
**`agentToolResult`** (raw tool output, `type.jl:429-434`):
|
||||
```julia
|
||||
result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), nothing, x->x)
|
||||
result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), nothing, x->x)
|
||||
|
||||
result.content[1] # textContent("Current time in Tokyo: ...")
|
||||
result.content[1].text # "Current time in Tokyo: 2026-08-10T..."
|
||||
result.content[1] # textContent("Weather in Tokyo: Sunny, 22°C")
|
||||
result.content[1].text # "Weather in Tokyo: Sunny, 22°C"
|
||||
result.details # Dict{Any,Any}() — tool-specific metadata
|
||||
result.usage # nothing — llmUsage tracking (optional)
|
||||
result.terminate # false — signals loop termination
|
||||
@@ -182,7 +157,7 @@ result.terminate # false — signals loop termination
|
||||
msg = batch.messages[1] # toolResultMessage
|
||||
|
||||
msg.toolCallId # "call-1"
|
||||
msg.toolName # "getTime"
|
||||
msg.toolName # "getWeather"
|
||||
msg.content # Vector{messageContent}
|
||||
msg.isError # false
|
||||
msg.details # tool-specific metadata
|
||||
@@ -204,7 +179,7 @@ Each phase has a single responsibility and produces an intermediate result:
|
||||
| Phase | Function | Input | Output | Purpose |
|
||||
|-------|----------|-------|--------|---------|
|
||||
| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook |
|
||||
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `emit` | `executedOutcome` | Call `tool.execute()`, stream partial results |
|
||||
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `agentEventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
|
||||
| Finalize | `finalizeExecutedToolCall()` | `agentContext`, `assistantMessage`, `preparedToolCall`, `executedOutcome`, `agentLoopConfig`, `abortSignal` | `finalizedOutcome` | Run post-hook, emit end event |
|
||||
|
||||
The pipeline ensures that **every tool call produces a result**, even on failure. Errors are captured as `immediateOutcome`, `executedOutcome`, or `finalizedOutcome` with `isError=true`, then converted to `toolResultMessage` objects that are fed back to the LLM conversation history.
|
||||
@@ -276,9 +251,9 @@ The `terminate` flag is checked at the batch level. See [Section 10](#10-tool-ca
|
||||
|
||||
---
|
||||
|
||||
## 4. Tool Registration — Per-Agent Tool Stores
|
||||
## 4. Tool Registration — Static Registration
|
||||
|
||||
**Source:** `toolRegistry.jl`
|
||||
**Source:** `toolRegistry.jl`, `YiemAgent.jl`
|
||||
|
||||
### How `toolStore` Works
|
||||
|
||||
@@ -293,36 +268,25 @@ end
|
||||
|
||||
`store.tools` is an `OrderedDict` — it provides O(1) lookup by tool name and preserves insertion order for iteration. `getTools(store)` returns this `OrderedDict` directly (not a copy), so mutations on the returned value affect the store.
|
||||
|
||||
### How `loadTools(store, dir)` Works
|
||||
### Static Registration — `register_all_tools()`
|
||||
|
||||
```julia
|
||||
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
||||
function register_all_tools(store::toolStore)::OrderedDict{String, agentTool}
|
||||
```
|
||||
|
||||
**Source:** `toolRegistry.jl:126-178`
|
||||
**Source:** `YiemAgent.jl:20-29`
|
||||
|
||||
1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
|
||||
2. **Sorts** filenames alphabetically for deterministic registration order
|
||||
3. **Wraps** each file in a dynamically created submodule:
|
||||
```julia
|
||||
# For "getWeather.jl" → module _tool_getWeather
|
||||
module _tool_getWeather
|
||||
using ..type
|
||||
using Dates, UUIDs, DataStructures, JSON
|
||||
# (file contents here)
|
||||
end
|
||||
```
|
||||
4. **Evaluates** `getTool()` within the submodule scope using `Core.eval(mod, :(getTool()))` — this avoids world-age issues
|
||||
5. **Validates** the return value is an `agentTool` instance
|
||||
6. **Registers** the tool in `store.tools`
|
||||
7. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime
|
||||
1. **Calls each tool's definition function** — `getWeatherTool()`, `getTimeTool()`, `writeToolTool()` — which return `agentTool` structs
|
||||
2. **Registers each tool** via `registerTool(store, tool)`
|
||||
3. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime
|
||||
|
||||
### Why Submodules?
|
||||
### Why Static?
|
||||
|
||||
Each tool file is loaded into its own **namespaced submodule**. This means:
|
||||
- `validateRequiredArgs`, `prepareArguments`, `executeTool`, and helper functions defined in `getTime.jl` are scoped under `_tool_getTime`
|
||||
- No name collisions between tools — `getTime.validateRequiredArgs` is distinct from `getWeather.validateRequiredArgs`
|
||||
- The module reference is kept alive by the functions stored in `agentTool` (closures in `execute`, `validateRequiredArgs`, `prepareArguments`) so they don't get garbage collected
|
||||
Tools are **statically included** in `YiemAgent.jl` via `include()`. This means:
|
||||
- Tool functions live in the `YiemAgent` module, not in dynamically created submodules
|
||||
- No world-age issues when calling `tool.execute()` (Julia compiles dispatch in the same world)
|
||||
- Simpler tool definition — no need to wrap in a `module ... end` block
|
||||
- Better compiler optimization (inlining, type inference)
|
||||
|
||||
### Registration API
|
||||
|
||||
@@ -331,9 +295,9 @@ Each tool file is loaded into its own **namespaced submodule**. This means:
|
||||
store1 = toolStore(name="agent1")
|
||||
store2 = toolStore(name="agent2")
|
||||
|
||||
# Load tools into specific stores (auto-registers listTools)
|
||||
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
|
||||
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
|
||||
# Load all tools (auto-registers listTools)
|
||||
tools1 = register_all_tools(store1) # all agents get the same tools
|
||||
tools2 = register_all_tools(store2)
|
||||
|
||||
# Manual registration (per-store)
|
||||
registerTool(store1, my_tool)
|
||||
@@ -359,8 +323,8 @@ Each `toolStore` is completely independent — tools registered in one store do
|
||||
storeA = toolStore(name="A")
|
||||
storeB = toolStore(name="B")
|
||||
|
||||
registerTool(storeA, getTime_tool)
|
||||
registerTool(storeB, getWeather_tool)
|
||||
registerTool(storeA, getTimeTool())
|
||||
registerTool(storeB, getWeatherTool())
|
||||
|
||||
getTools(storeA) # only contains getTime
|
||||
getTools(storeB) # only contains getWeather
|
||||
@@ -376,7 +340,7 @@ This ensures that `yiemAgent` instances with different `tool_store` references o
|
||||
|
||||
**Source:** `agentCore.jl:35-145`
|
||||
|
||||
The `_agent_loop()` function runs as a background `@spawn` task, created when `yiemAgent` is constructed.
|
||||
The `_agentLoop()` function runs as a background `@spawn` task, created when `yiemAgent` is constructed.
|
||||
|
||||
### Channel Architecture
|
||||
|
||||
@@ -385,7 +349,6 @@ yiemAgent struct contains:
|
||||
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
|
||||
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
||||
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
||||
- _tool_store (toolStore) ← per-agent isolated tool registry
|
||||
```
|
||||
|
||||
### Loop States
|
||||
@@ -395,7 +358,7 @@ The loop tracks 6 states (documented at `agentCore.jl:39-75`):
|
||||
| State | `processingTask` | `activeRun` | `inputChannel` | `followUpChannel` | Behavior |
|
||||
|-------|-----------------|-------------|----------------|-------------------|----------|
|
||||
| 1 | `nothing` | `false` | empty | empty | Idle, waiting |
|
||||
| 2 | `nothing` | `false` | has msg | empty | New message → spawn `_process_message` |
|
||||
| 2 | `nothing` | `false` | has msg | empty | New message → spawn `_processMessage` |
|
||||
| 3 | running | `true` | empty | empty | Processing, no new input |
|
||||
| 4 | running | `true` | has msg | empty | New message while busy → queued |
|
||||
| 5 | running | `true` | empty | has msg | Follow-up while busy → queued |
|
||||
@@ -404,7 +367,7 @@ The loop tracks 6 states (documented at `agentCore.jl:39-75`):
|
||||
### Loop Logic (simplified)
|
||||
|
||||
```julia
|
||||
function _agent_loop(agent::yiemAgent)
|
||||
function _agentLoop(agent::yiemAgent)
|
||||
while true
|
||||
# 1. Wait for message from inputChannel (blocking poll)
|
||||
msg = fetch!(agent.inputChannel) # agentCore.jl:84
|
||||
@@ -414,9 +377,9 @@ function _agent_loop(agent::yiemAgent)
|
||||
drain both channels, break loop
|
||||
end
|
||||
|
||||
# 3. If agent is idle, spawn _process_message
|
||||
# 3. If agent is idle, spawn _processMessage
|
||||
if agent._state.activeRun == false
|
||||
processingTask = Threads.@spawn _process_message(agent)
|
||||
processingTask = Threads.@spawn _processMessage(agent)
|
||||
agent._state.activeRun = true
|
||||
end
|
||||
|
||||
@@ -445,12 +408,12 @@ end
|
||||
|
||||
**Source:** `agentCore.jl:175-311`
|
||||
|
||||
`_process_message(agent)` is the core function that processes a batch of user messages through the LLM pipeline.
|
||||
`_processMessage(agent)` is the core function that processes a batch of user messages through the LLM pipeline.
|
||||
|
||||
### Pipeline Steps
|
||||
|
||||
```julia
|
||||
function _process_message(agent::yiemAgent)::assistantMessage
|
||||
function _processMessage(agent::yiemAgent)::assistantMessage
|
||||
final_response = nothing
|
||||
|
||||
while true # Loop until LLM returns response without tool calls
|
||||
@@ -466,38 +429,38 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
end
|
||||
|
||||
# ── Step 2: Prepare context ─────────────────────────────────
|
||||
preparedContext = agent.prepareContext(agent._state)
|
||||
state = agentState(systemPrompt, nothing, tools, messages)
|
||||
preparedContext = prepareContext(state, agentEventSink)
|
||||
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
|
||||
# Override point: filter tools, inject context, modify system prompt
|
||||
|
||||
# ── Step 3: Format for LLM ──────────────────────────────────
|
||||
formatted_messages = agent.formatMsgForLLM(preparedContext)
|
||||
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
|
||||
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
||||
# Wraps systemPrompt as system role, converts each messageContent block
|
||||
|
||||
# ── Step 4: Call LLM ────────────────────────────────────────
|
||||
response = agent.llmCall(formatted_messages)
|
||||
response = llmCall(formattedMessages)
|
||||
# Returns assistantMessage with content::Vector{messageContent}
|
||||
# Each content block has a type: "text", "thinking", or "tool_call"
|
||||
|
||||
# ── Step 5: Extract tool calls ──────────────────────────────
|
||||
has_tool_calls, tool_call_list = extract_tool_calls(response.content)
|
||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||
# Inspects content blocks for "tool_calls" or "tool_call" Dict entries
|
||||
|
||||
# ── Step 6: Execute tool calls or return ────────────────────
|
||||
if has_tool_calls && !isempty(tool_call_list)
|
||||
if hasToolCalls && !isempty(toolCallList)
|
||||
# Build context and config
|
||||
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
||||
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, ...)
|
||||
signal = nothing
|
||||
emit = agent.agentEventSink
|
||||
context = agentContext(systemPrompt, messages, tools)
|
||||
config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||
signal = abortSignal(false)
|
||||
|
||||
# Execute tool calls (sequential or parallel)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, agentEventSink)
|
||||
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
push!(agent._state.messages, tool_result)
|
||||
push!(messages, tool_result)
|
||||
end
|
||||
|
||||
# Check termination
|
||||
@@ -517,10 +480,6 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
end
|
||||
```
|
||||
|
||||
### Debug Note
|
||||
|
||||
There is a deliberate `error(5555555)` at `agentCore.jl:214` that halts execution after the LLM call. This appears to be a debugging/staging marker. Remove or replace it before production use.
|
||||
|
||||
---
|
||||
|
||||
## 7. Tool Call Extraction from LLM Response
|
||||
@@ -652,51 +611,29 @@ function prepareToolCall(
|
||||
function executePreparedToolCall(
|
||||
prep::preparedToolCall,
|
||||
signal::Union{Nothing, abortSignal},
|
||||
emit::Function,
|
||||
agentEventSink,
|
||||
)::executedOutcome
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. **Initialize streaming state:**
|
||||
```julia
|
||||
updateEvents = promise[] # vector to collect update event handles
|
||||
accepting = true # guard to prevent duplicate emissions
|
||||
```
|
||||
1. **Call `tool.execute()`:**
|
||||
```julia
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id,
|
||||
prep.args,
|
||||
signal,
|
||||
agentEventSink # serves as onPartialResult callback
|
||||
)
|
||||
return executedOutcome(result, false)
|
||||
```
|
||||
|
||||
2. **Call `tool.execute()`:**
|
||||
```julia
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id,
|
||||
prep.args,
|
||||
signal,
|
||||
partialResult -> begin
|
||||
if accepting
|
||||
push!(updateEvents,
|
||||
emit(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name,
|
||||
prep.toolCall.arguments, partialResult)))
|
||||
end
|
||||
end
|
||||
)
|
||||
```
|
||||
|
||||
3. **Wait for streaming to settle:**
|
||||
```julia
|
||||
accepting = false
|
||||
wait.(updateEvents) # wait for all pending update event handlers
|
||||
return executedOutcome(result, false)
|
||||
```
|
||||
|
||||
4. **On error:**
|
||||
```julia
|
||||
catch err
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
```
|
||||
|
||||
**Streaming design:** The `accepting` guard prevents emitting updates after the call completes. If the tool's `execute` function yields after emitting updates but before returning, no duplicate or stale updates are emitted.
|
||||
2. **On error:**
|
||||
```julia
|
||||
catch err
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
```
|
||||
|
||||
### 8.3 Phase 3: Finalize — `finalizeExecutedToolCall()`
|
||||
|
||||
@@ -716,49 +653,36 @@ function finalizeExecutedToolCall(
|
||||
**Steps:**
|
||||
|
||||
1. **Extract execution result:**
|
||||
```julia
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
```
|
||||
```julia
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
```
|
||||
|
||||
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
|
||||
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
||||
- Hook can mutate the result:
|
||||
```julia
|
||||
after = config.afterToolCall(afterToolCallContext(...))
|
||||
if after !== nothing
|
||||
result = merge(result, dict(
|
||||
:content => get(after, :content, result.content),
|
||||
:details => get(after, :details, result.details),
|
||||
:usage => get(after, :usage, result.usage),
|
||||
:terminate => get(after, :terminate, result.terminate)
|
||||
))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
```
|
||||
- Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic
|
||||
- On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true`
|
||||
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
||||
- Hook can mutate the result:
|
||||
```julia
|
||||
after = config.afterToolCall(afterToolCallContext(...))
|
||||
if after !== nothing
|
||||
result = merge(result, dict(
|
||||
:content => get(after, :content, result.content),
|
||||
:details => get(after, :details, result.details),
|
||||
:usage => get(after, :usage, result.usage),
|
||||
:terminate => get(after, :terminate, result.terminate)
|
||||
))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
```
|
||||
- Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic
|
||||
- On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true`
|
||||
|
||||
3. **Return:**
|
||||
```julia
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
```
|
||||
```julia
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
```
|
||||
|
||||
**Source:** `type.jl:787-791` — `finalizedOutcome` holds the original tool call reference, final result (post-hook), and error status.
|
||||
|
||||
### 8.4 Emission — `emitToolExecutionEnd()`
|
||||
|
||||
**Source:** `agentCore.jl:736-739`
|
||||
|
||||
```julia
|
||||
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
|
||||
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
finalized.result, finalized.isError))
|
||||
end
|
||||
```
|
||||
|
||||
This is called immediately after finalization, before building the `toolResultMessage`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Execution Modes — Sequential vs Parallel
|
||||
@@ -774,7 +698,7 @@ function executeToolCalls(
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::Union{Nothing, abortSignal},
|
||||
emit::Function,
|
||||
agentEventSink,
|
||||
)::agentToolCallBatch
|
||||
```
|
||||
|
||||
@@ -809,18 +733,15 @@ function executeToolCallsSequential(...)::agentToolCallBatch
|
||||
messages = toolResultMessage[]
|
||||
|
||||
for tc in toolCalls
|
||||
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
else
|
||||
executed = executePreparedToolCall(prep, signal, emit)
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
end
|
||||
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
|
||||
@@ -842,19 +763,15 @@ function executeToolCallsParallel(...)::agentToolCallBatch
|
||||
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
||||
|
||||
for tc in toolCalls
|
||||
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
push!(entries, finalized) # immediate outcome — no task
|
||||
else
|
||||
task = task() do
|
||||
executed = executePreparedToolCall(prep, signal, emit)
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
return finalized
|
||||
end
|
||||
schedule(task)
|
||||
@@ -923,12 +840,12 @@ From the type documentation (`type.jl:803-815`):
|
||||
| Unrecoverable error | A tool hits a fatal condition (auth token expired, database connection lost) |
|
||||
| Async handoff | A tool triggers a long-running external operation; the external system will later resume via `continue()` |
|
||||
|
||||
### Batch Processing in `_process_message()`
|
||||
### Batch Processing in `_processMessage()`
|
||||
|
||||
**Source:** `agentCore.jl:266-307`
|
||||
|
||||
```julia
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
@@ -985,9 +902,9 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
||||
f.result.content, # content (Vector{messageContent})
|
||||
f.result.details, # details
|
||||
f.result.usage, # usage
|
||||
get(f.result, :addedToolNames, string[]), # addedToolNames (for dynamic tools)
|
||||
nothing, # addedToolNames (for dynamic tools)
|
||||
f.isError, # isError
|
||||
nowMillis(), # timestamp
|
||||
now(), # timestamp
|
||||
)
|
||||
end
|
||||
```
|
||||
@@ -1048,9 +965,6 @@ Tool call fails at any phase
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
│
|
||||
▼
|
||||
createToolResultMessage(finalized)
|
||||
│
|
||||
▼
|
||||
@@ -1119,14 +1033,14 @@ end
|
||||
|-------|-------------|------|
|
||||
| `toolExecStartEvent` | `executeToolCalls*()` loop | Before `prepareToolCall()` for each tool call |
|
||||
| `toolExecUpdateEvent` | `executePreparedToolCall()` | Inside `onPartialResult` callback during `tool.execute()` |
|
||||
| `toolExecEndEvent` | `emitToolExecutionEnd()` | After `finalizeExecutedToolCall()` for each tool call |
|
||||
| `toolExecEndEvent` | `finalizeExecutedToolCall()` | After finalization for each tool call |
|
||||
|
||||
### Event Sink
|
||||
|
||||
The `emit` function is passed through the entire call chain:
|
||||
The `agentEventSink` function is passed through the entire call chain:
|
||||
|
||||
```julia
|
||||
emit = agent.agentEventSink # set during yiemAgent construction
|
||||
agentEventSink = agent.agentEventSink # set during yiemAgent construction
|
||||
```
|
||||
|
||||
The `agentEventSink` function is a user-provided callback that receives all events. This is typically used by:
|
||||
@@ -1143,8 +1057,8 @@ The `agentEventSink` function is a user-provided callback that receives all even
|
||||
|
||||
| Hook | Signature | Called | Purpose |
|
||||
|------|-----------|--------|---------|
|
||||
| `prepareContext` | `(state::agentState) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
||||
| `beforeToolCall` | `(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
|
||||
| `afterToolCall` | `(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
|
||||
@@ -1211,7 +1125,7 @@ end
|
||||
**Source:** `utils.jl:111-125`
|
||||
|
||||
```julia
|
||||
function prepareContext(state::agentState)::agentContext
|
||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
||||
# TODO: filter tools from state.tools based on user intent
|
||||
filteredTools = state.tools
|
||||
|
||||
@@ -1238,7 +1152,7 @@ end
|
||||
Default implementation converts `agentContext` to OpenAI-compatible format:
|
||||
|
||||
```julia
|
||||
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
messages = Vector{Dict{String, Any}}()
|
||||
|
||||
# System prompt as system message
|
||||
@@ -1280,14 +1194,14 @@ The framework supports tools that modify the tool system itself at runtime.
|
||||
1. Converts `inputSchema` Dict into `Dict{String,Any}(...)` string literal
|
||||
2. Indents `executeCode` with 4 spaces
|
||||
3. Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
||||
4. Appends `getTool()` returning an `agentTool` struct
|
||||
4. Appends `writeToolTool()` returning an `agentTool` struct
|
||||
5. Writes the combined string to `src/tools/<name>.jl`
|
||||
|
||||
### `listTool` — Discover Available Tools
|
||||
|
||||
**Source:** `toolRegistry.jl:55-82`
|
||||
**Source:** `toolRegistry.jl:43-98`
|
||||
|
||||
Each `toolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. `loadTools` auto-registers one, so the LLM can discover available tools at runtime. Also useful for **collision detection** before creating a new tool via `writeTool`.
|
||||
Each `toolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. `register_all_tools` auto-registers one, so the LLM can discover available tools at runtime. Also useful for **collision detection** before creating a new tool via `writeTool`.
|
||||
|
||||
### Self-Tooling Workflow
|
||||
|
||||
@@ -1301,9 +1215,11 @@ Each `toolStore` gets its own `listTool` instance bound to that store via `listT
|
||||
- executeCode: "query = args[\"query\"]\nresult = search(query)\n..."
|
||||
- (optional) validateCode, prepareCode
|
||||
3. writeTool generates src/tools/searchWine.jl
|
||||
4. Agent restarts (or hot-reloads) → loadTools(agent._tool_store, "src/tools") picks up the new file
|
||||
5. Agent calls searchWine(query="cabernet")
|
||||
6. Result: "Found 5 cabernet wines..."
|
||||
4. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
||||
5. Developer adds `registerTool(store, searchWineTool())` to register_all_tools() in YiemAgent.jl
|
||||
6. Developer restarts Julia — new tool is loaded
|
||||
7. Agent calls searchWine(query="cabernet")
|
||||
8. Result: "Found 5 cabernet wines..."
|
||||
```
|
||||
|
||||
### `writeTool` Input Schema
|
||||
@@ -1328,14 +1244,14 @@ Each `toolStore` gets its own `listTool` instance bound to that store via `listT
|
||||
```
|
||||
USER SENDS MESSAGE
|
||||
└─> runAgent(agent, "What's the weather in Tokyo?")
|
||||
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
||||
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
||||
|
||||
|
||||
LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
└─> _agent_loop: detects msg in inputChannel
|
||||
└─> Threads.@spawn _process_message(agent)
|
||||
└─> _agentLoop: detects msg in inputChannel
|
||||
└─> @spawn _processMessage(agent)
|
||||
|
||||
── _process_message ──────────────────────────────────────────────
|
||||
── _processMessage ──────────────────────────────────────────────
|
||||
│
|
||||
│ Step 1: Drain inputChannel
|
||||
│ raw_msg = Dict("role" => "user", "content" => [...])
|
||||
@@ -1367,36 +1283,34 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
│
|
||||
│ Step 6: Execute tool calls
|
||||
│ context = agentContext(systemPrompt, messages, tools)
|
||||
│ config = agentLoopConfig(tools, beforeToolCall, afterToolCall, "sequential")
|
||||
│ batch = executeToolCalls(context, response, tool_call_list, config, nothing, emit)
|
||||
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||
|
||||
|
||||
LOOP ITERATION 1 — executeToolCallsSequential
|
||||
│
|
||||
│ ── executeToolCallsSequential ──────────────────────────────
|
||||
│ │
|
||||
│ │ For tc = agentToolCall("call_1", "getWeather", ...):
|
||||
│ │
|
||||
│ │ emit(toolExecStartEvent("call_1", "getWeather", {"city": "Tokyo"}))
|
||||
│ │
|
||||
│ │ PREPARE:
|
||||
│ │ tool = context.tools["getWeather"] → found!
|
||||
│ │ validatedArgs = validateToolArguments(tool, tc)
|
||||
│ │ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
|
||||
│ │ beforeToolCall_hook(...) → nothing (skipped)
|
||||
│ │ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||
│ │
|
||||
│ │ EXECUTE:
|
||||
│ │ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, onPartialResult)
|
||||
│ │ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||
│ │ → executedOutcome(result, false)
|
||||
│ │
|
||||
│ │ FINALIZE:
|
||||
│ │ afterToolCall_hook(...) → nothing (skipped)
|
||||
│ │ → finalizedOutcome(tc, result, false)
|
||||
│ │
|
||||
│ │ emit(toolExecEndEvent("call_1", "getWeather", result, false))
|
||||
│ │ msg = createToolResultMessage(finalized)
|
||||
│ │ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
|
||||
│ │
|
||||
│ └─> agentToolCallBatch([msg], false)
|
||||
│ For tc = agentToolCall("call_1", "getWeather", ...):
|
||||
│
|
||||
│ PREPARE:
|
||||
│ tool = context.tools["getWeather"] → found!
|
||||
│ validatedArgs = validateToolArguments(tool, tc)
|
||||
│ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
|
||||
│ beforeToolCall_hook(...) → nothing (skipped)
|
||||
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||
│
|
||||
│ EXECUTE:
|
||||
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, agentEventSink)
|
||||
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||
│ → executedOutcome(result, false)
|
||||
│
|
||||
│ FINALIZE:
|
||||
│ afterToolCall_hook(...) → nothing (skipped)
|
||||
│ → finalizedOutcome(tc, result, false)
|
||||
│
|
||||
│ msg = createToolResultMessage(finalized)
|
||||
│ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
|
||||
│
|
||||
└─> agentToolCallBatch([msg], false)
|
||||
│
|
||||
│ Save results:
|
||||
│ for tool_result in batch.messages
|
||||
@@ -1408,7 +1322,7 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
|
||||
|
||||
LOOP ITERATION 2 — LLM RETURNS FINAL TEXT RESPONSE
|
||||
── _process_message (second iteration) ───────────────────────────
|
||||
── _processMessage (second iteration) ───────────────────────────
|
||||
│
|
||||
│ Step 1: Drain inputChannel → empty
|
||||
│
|
||||
@@ -1437,12 +1351,12 @@ AGENT LOOP: SEND RESPONSE TO USER
|
||||
|
||||
## 17. Tool File Contract
|
||||
|
||||
Each `.jl` file in `src/tools/` must conform to the following contract:
|
||||
Each `.jl` file in `src/tools/` follows a flat, static structure:
|
||||
|
||||
### Required Function
|
||||
|
||||
```julia
|
||||
function getTool()::agentTool
|
||||
function <name>Tool()::agentTool
|
||||
# Must return an agentTool instance
|
||||
end
|
||||
```
|
||||
@@ -1451,22 +1365,22 @@ end
|
||||
|
||||
```julia
|
||||
# Argument preparation (before validation)
|
||||
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
function <name>PrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
# Return modified args, or args unchanged
|
||||
return args
|
||||
end
|
||||
|
||||
# Custom validation (before execution)
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function <name>ValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
# Return nothing to pass, or error string to fail
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Core execution
|
||||
function executeTool(toolCallId::String,
|
||||
args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
function <name>Execute(toolCallId::String,
|
||||
args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
# Return agentToolResult with content, details, usage, terminate
|
||||
return agentToolResult([textContent("result")], Dict{Any,Any}(), nothing, false)
|
||||
end
|
||||
@@ -1477,7 +1391,8 @@ end
|
||||
```julia
|
||||
# src/tools/myTool.jl
|
||||
|
||||
using Dates # ← tool declares its own dependencies (registry injects only `using ..type`)
|
||||
using .type # ← provides agentTool, textContent, agentToolResult, etc.
|
||||
using Dates # ← tool's own dependencies
|
||||
|
||||
# Optional: helper functions
|
||||
function helper_function(...)
|
||||
@@ -1485,24 +1400,24 @@ function helper_function(...)
|
||||
end
|
||||
|
||||
# Optional: prepareArguments
|
||||
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
return args
|
||||
end
|
||||
|
||||
# Optional: validateRequiredArgs
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Required: executeTool
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
# Required: execute function
|
||||
function myToolExecute(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
...
|
||||
end
|
||||
|
||||
# Required: getTool
|
||||
function getTool()::agentTool
|
||||
# Required: getTool function
|
||||
function myToolTool()::agentTool
|
||||
return agentTool(
|
||||
name = "myTool",
|
||||
label = "My Tool",
|
||||
@@ -1512,9 +1427,9 @@ function getTool()::agentTool
|
||||
"properties" => Dict(...),
|
||||
"required" => [...]
|
||||
),
|
||||
execute = executeTool,
|
||||
prepareArguments = prepareArguments,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
execute = myToolExecute,
|
||||
prepareArguments = myToolPrepareArguments,
|
||||
validateRequiredArgs = myToolValidateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
@@ -1522,51 +1437,110 @@ end
|
||||
|
||||
### Dependencies
|
||||
|
||||
Each tool file **declares its own dependencies** via `using` statements at the top of the file. The registry does **not** inject any standard library packages — if a tool needs `Dates`, `JSON`, `HTTP`, `CSV`, or any other package, it must include its own `using` statements.
|
||||
Each tool file declares its own dependencies via `using` statements:
|
||||
|
||||
```julia
|
||||
# src/tools/getTime.jl
|
||||
using .type
|
||||
using Dates
|
||||
|
||||
function executeTool(...)
|
||||
function getTimeExecute(...)
|
||||
now() # Dates.now requires `using Dates`
|
||||
end
|
||||
```
|
||||
|
||||
```julia
|
||||
# src/tools/myApiTool.jl
|
||||
using .type
|
||||
using HTTP, JSON
|
||||
|
||||
function executeTool(...)
|
||||
function myApiToolExecute(...)
|
||||
response = HTTP.get("https://api.example.com")
|
||||
data = JSON.parse(String(response.body))
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
### Module Isolation
|
||||
### Why Flat Modules?
|
||||
|
||||
When `loadTools()` loads a file, it wraps it in a dynamically created submodule. The registry injects **only** `using ..type` to make core types (`agentTool`, `textContent`, `agentToolResult`, `abortSignal`, etc.) available:
|
||||
|
||||
```julia
|
||||
# User writes in src/tools/myTool.jl:
|
||||
using Dates, HTTP, JSON # ← tool's own dependencies
|
||||
|
||||
function getTool()::agentTool ... end
|
||||
|
||||
# loadTools() creates:
|
||||
module _tool_myTool
|
||||
using ..type # ← injected by registry (core types only)
|
||||
using Dates, HTTP, JSON # ← from tool file
|
||||
# (user's code here)
|
||||
end
|
||||
```
|
||||
|
||||
All functions in the file are scoped under `_tool_myTool`, preventing name collisions with other tools. The module reference is kept alive by the function objects stored in `agentTool`, preventing garbage collection of closures.
|
||||
All tool files are **statically included** in `YiemAgent.jl` via `include()`. This means:
|
||||
- All functions live in the `YiemAgent` module, avoiding world-age issues
|
||||
- `using .type` makes core types (`agentTool`, `textContent`, `agentToolResult`, `abortSignal`) available
|
||||
- Functions are named with a `<toolName>` prefix to avoid name collisions (e.g., `getWeatherExecute`, `getTimeExecute`)
|
||||
- The `...Tool()` function (e.g., `getWeatherTool()`) returns the `agentTool` struct for registration
|
||||
|
||||
---
|
||||
|
||||
## 18. Appendix: Type Reference
|
||||
## 18. Adding New Tools
|
||||
|
||||
To add a new tool (e.g., `searchWine.jl`):
|
||||
|
||||
### Step 1: Create `src/tools/searchWine.jl`
|
||||
|
||||
```julia
|
||||
using .type
|
||||
# using AdditionalPkg # add if needed
|
||||
|
||||
function searchWineExecute(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal}, onPartialResult)
|
||||
query = get(args, "query", "")
|
||||
result = search_wine_db(query)
|
||||
return agentToolResult(
|
||||
[textContent("Found $(length(result)) wines")],
|
||||
Dict{Any,Any}("count" => length(result)),
|
||||
nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
function searchWineTool()::agentTool
|
||||
return agentTool(
|
||||
name = "searchWine",
|
||||
label = "Search Wine",
|
||||
description = "Search wine database...",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"query" => Dict("type" => "string", "description" => "Search query")
|
||||
),
|
||||
"required" => ["query"]
|
||||
),
|
||||
execute = searchWineExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Step 2: Include in `src/YiemAgent.jl` (before `toolRegistry.jl`)
|
||||
|
||||
```julia
|
||||
include("tools/getWeather.jl")
|
||||
include("tools/getTime.jl")
|
||||
include("tools/searchWine.jl") # ← add here
|
||||
include("tools/writeTool.jl")
|
||||
```
|
||||
|
||||
### Step 3: Register in `register_all_tools()` in `YiemAgent.jl`
|
||||
|
||||
```julia
|
||||
function register_all_tools(store::toolRegistry.toolStore)
|
||||
registerTool(store, getWeatherTool())
|
||||
registerTool(store, getTimeTool())
|
||||
registerTool(store, searchWineTool()) # ← add here
|
||||
registerTool(store, writeToolTool())
|
||||
registerTool(store, listTool(store))
|
||||
return store.tools
|
||||
end
|
||||
```
|
||||
|
||||
### Step 4: Restart Julia
|
||||
|
||||
The module recompiles on next load. The new tool is available immediately.
|
||||
|
||||
---
|
||||
|
||||
## 19. Appendix: Type Reference
|
||||
|
||||
### Message Types
|
||||
|
||||
@@ -1604,7 +1578,7 @@ All functions in the file are scoped under `_tool_myTool`, preventing name colli
|
||||
|------|--------|-------------|
|
||||
| `agentContext` | `type.jl:299` | Conversation snapshot (systemPrompt, messages, tools) |
|
||||
| `agentState` | `type.jl:310` | Mutable runtime state (systemPrompt, model, tools, messages, pendingToolCalls, activeRun, errorMessage) |
|
||||
| `agentLoopConfig` | `type.jl:403` | Loop config (tools, beforeToolCall, afterToolCall, toolExecution) |
|
||||
| `agentLoopConfig` | `type.jl:403` | Loop config (beforeToolCall, afterToolCall, toolExecution) |
|
||||
| `abortSignal` | `type.jl:416` | Abort flag (`aborted::Bool`) |
|
||||
| `beforeToolCallContext` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
|
||||
| `afterToolCallContext` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
|
||||
|
||||
@@ -1,77 +1,5 @@
|
||||
i am not sure that's the case. see my NATS message log:
|
||||
<NATS debug message>
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 3"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 5"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 6"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 7"
|
||||
</NATS debug message>
|
||||
|
||||
my NATS receiver report the following for a long time
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
|
||||
untill I Ctrl + d so shutdown the process then i got the following report
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 3"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 5"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 6"
|
||||
┌ Info: debug
|
||||
└ payload = "_process_message 7"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
my point is if _process_message() actually run then this code in _process_message()
|
||||
"raw_msg = take!(agent.inputChannel)"
|
||||
should take the new msg message out of agent.inputChannel and there should be only one debug message showing
|
||||
┌ Info: debug
|
||||
└ payload = "new user msg"
|
||||
|
||||
before reaching error("debug marker")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,8 @@
|
||||
check my understanding
|
||||
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, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is false then _processMessage() loop continue
|
||||
3) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is true then final_response message get pushed into agent._state.messages. _processMessage() loop exit. then _agentLoop() can pick it as the output to outputChannel
|
||||
|
||||
Is my understanding correct?
|
||||
|
||||
|
||||
+13
-2
@@ -1,7 +1,6 @@
|
||||
module YiemAgent
|
||||
|
||||
# export agent
|
||||
|
||||
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.
|
||||
@@ -13,9 +12,21 @@ module YiemAgent
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("tools/getWeather.jl")
|
||||
include("tools/getTime.jl")
|
||||
include("tools/writeTool.jl")
|
||||
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
function register_all_tools(store::toolRegistry.toolStore)
|
||||
registerTool(store, getWeatherTool())
|
||||
registerTool(store, getTimeTool())
|
||||
registerTool(store, writeToolTool())
|
||||
registerTool(store, listTool(store))
|
||||
return store.tools
|
||||
end
|
||||
|
||||
# include("llmfunction.jl")
|
||||
# using .llmfunction
|
||||
|
||||
|
||||
+575
-285
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -116,7 +116,7 @@ julia> stopAgent(agent)
|
||||
function stopAgent(agent::yiemAgent)
|
||||
put!(agent.inputChannel, :shutdown)
|
||||
try
|
||||
fetch(agent._agent_loop)
|
||||
fetch(agent._agentLoop)
|
||||
catch e
|
||||
if e isa TaskFailedException
|
||||
rethrow(e)
|
||||
|
||||
+4
-96
@@ -1,6 +1,6 @@
|
||||
module toolRegistry
|
||||
|
||||
export toolStore, loadTools, registerTool, getTools, clearTools, listTool
|
||||
export toolStore, registerTool, getTools, clearTools, listTool
|
||||
|
||||
using Dates
|
||||
using JSON, DataStructures
|
||||
@@ -45,7 +45,7 @@ end
|
||||
Return an `agentTool` definition for listing registered tools.
|
||||
|
||||
Each call produces a **new** tool object that captures (closes over)
|
||||
`store`. `loadTools` auto-registers one so the LLM can discover tools
|
||||
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
||||
at runtime.
|
||||
|
||||
# Arguments
|
||||
@@ -55,7 +55,7 @@ at runtime.
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1");
|
||||
|
||||
julia> loadTools(store, "src/tools") # auto-registers listTools
|
||||
julia> register_all_tools(store) # auto-registers listTools
|
||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||
[toolRegistry:agent1] Registered tool: listTools
|
||||
|
||||
@@ -97,99 +97,7 @@ function listTool(store::toolStore)::agentTool
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
Load `.jl` tool files from `dir` into `store`, then auto-register
|
||||
`listTool` so the LLM can discover available tools at runtime.
|
||||
|
||||
Each `.jl` file must define `function getTool()::agentTool ... end`.
|
||||
Files are sorted alphabetically for deterministic registration order.
|
||||
Each file is loaded into its own Julia submodule to avoid name collisions.
|
||||
|
||||
# Arguments
|
||||
- `store`: Tool store to populate
|
||||
- `dir`: Directory containing `.jl` tool files
|
||||
|
||||
# Returns
|
||||
- The same `store.tools` dict (modified in place)
|
||||
|
||||
# Errors
|
||||
- Throws `ArgumentError` if `dir` does not exist or a file lacks `getTool()`
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1");
|
||||
|
||||
julia> loadTools(store, "src/tools")
|
||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||
[toolRegistry:agent1] Loaded tool: getTime (Time Lookup)
|
||||
[toolRegistry:agent1] Registered tool: listTools
|
||||
OrderedDict{String, agentTool} with 3 entries:
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
"listTools" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
||||
if !isdir(dir)
|
||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||
end
|
||||
|
||||
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
|
||||
sort!(jl_files)
|
||||
|
||||
for filename in jl_files
|
||||
filepath = joinpath(dir, filename)
|
||||
|
||||
# Derive a unique module name from the filename only (not full path).
|
||||
# e.g. "getWeather.jl" -> "_tool_getWeather"
|
||||
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
|
||||
|
||||
# Build the complete module as a string and eval the parsed code.
|
||||
# Julia does not allow `module ... end` inside eval(quote ...),
|
||||
# and constructing the module AST by hand is fragile.
|
||||
# Instead, we generate the full module source as a string,
|
||||
# parse it, and eval the resulting expression.
|
||||
# Each tool file declares its own dependencies via `using` statements
|
||||
# at the top of the file — the registry only injects `using ..type`
|
||||
# to make core types (agentTool, textContent, etc.) available.
|
||||
file_content = read(filepath, String)
|
||||
module_code = """
|
||||
module $(mod_name)
|
||||
using ..type
|
||||
$(file_content)
|
||||
end
|
||||
"""
|
||||
mod = eval(Meta.parse(module_code))
|
||||
|
||||
# Call getTool() via Core.eval in the submodule's scope.
|
||||
# This evaluates getTool() entirely within the new module's world,
|
||||
# completely avoiding world-age issues — no invokelatest needed.
|
||||
# Note: all uses of `tool` must be inside the `try` block because
|
||||
# Julia 1.12's SSA form doesn't track `tool` as definitely assigned
|
||||
# after a `try-catch` where it's only assigned inside `try`.
|
||||
try
|
||||
tool = Core.eval(mod, :(getTool()))
|
||||
if !(tool isa agentTool)
|
||||
throw(ArgumentError(
|
||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
||||
))
|
||||
end
|
||||
store.tools[tool.name] = tool
|
||||
println("[$(store.name)] Loaded tool: $(tool.name) ($(tool.label))")
|
||||
catch e
|
||||
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
|
||||
throw(ArgumentError(
|
||||
"Tool file $(filepath) does not define a `getTool()` function in module $(mod_name). " *
|
||||
"Each tool file must define: function getTool()::agentTool ... end"
|
||||
))
|
||||
end
|
||||
rethrow(e)
|
||||
end
|
||||
end
|
||||
|
||||
registerTool(store, listTool(store))
|
||||
return store.tools
|
||||
end
|
||||
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
|
||||
|
||||
"""
|
||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using .type
|
||||
using Dates
|
||||
|
||||
"""
|
||||
@@ -15,7 +16,7 @@ Demonstrates custom validation beyond simple required-field checking:
|
||||
- `nothing` if validation passes
|
||||
- `String` error message if validation fails
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function getTimeValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
@@ -43,8 +44,8 @@ Execute the getTime tool.
|
||||
|
||||
Returns mock time data for the given timezone or city.
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
onPartialResult)
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
if tz !== nothing
|
||||
@@ -61,7 +62,7 @@ end
|
||||
"""
|
||||
Define and return the getTime agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function getTimeTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getTime",
|
||||
label = "Time Lookup",
|
||||
@@ -74,9 +75,9 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => []
|
||||
),
|
||||
execute = executeTool,
|
||||
execute = getTimeExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
validateRequiredArgs = getTimeValidateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
|
||||
+21
-12
@@ -1,24 +1,33 @@
|
||||
using msghandler
|
||||
using .type
|
||||
|
||||
"""
|
||||
Execute the getWeather tool.
|
||||
|
||||
Returns mock weather data for the given city and temperature units.
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
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
|
||||
)
|
||||
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
agentEventSink)
|
||||
|
||||
agentEventSink("Getting weather...")
|
||||
|
||||
city = get(args, "city", "")
|
||||
units = get(args, "units", "celsius")
|
||||
temp = units == "fahrenheit" ? "72" : "22"
|
||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
||||
Dict{Any,Any}(),
|
||||
nothing,
|
||||
false
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
Define and return the getWeather agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function getWeatherTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
label = "Weather Lookup",
|
||||
@@ -31,7 +40,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = executeTool,
|
||||
execute = getWeatherExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
|
||||
+12
-9
@@ -1,3 +1,4 @@
|
||||
using .type
|
||||
using JSON
|
||||
|
||||
"""
|
||||
@@ -7,15 +8,17 @@ 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, restart the agent so `loadTools(agent._tool_store, "src/tools")` picks
|
||||
up the new file. The new tool is immediately available.
|
||||
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. Restart agent — loadTools() picks up the new file
|
||||
4. Agent calls searchWine with args
|
||||
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
|
||||
|
||||
@@ -24,7 +27,7 @@ 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 `getTool()` returning an `agentTool` struct
|
||||
- Appends `writeToolTool()` returning an `agentTool` struct
|
||||
- Writes the combined string to `src/tools/<name>.jl`
|
||||
|
||||
# Important Notes
|
||||
@@ -105,7 +108,7 @@ end
|
||||
"""
|
||||
Define and return the writeTool agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function writeToolTool()::agentTool
|
||||
return agentTool(
|
||||
name = "writeTool",
|
||||
label = "Create Tool",
|
||||
@@ -127,7 +130,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
||||
),
|
||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin
|
||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
|
||||
tool_name = get(args, "name", "")::String
|
||||
tool_label = get(args, "label", tool_name)::String
|
||||
tool_description = get(args, "description", "")::String
|
||||
@@ -250,13 +253,13 @@ function getTool()::agentTool
|
||||
|
||||
tool_code = join(parts)
|
||||
|
||||
# Write the file — tool is loaded on next agent restart via loadTools(store, "src/tools")
|
||||
# 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. Restart the agent so loadTools(agent._tool_store, \"src/tools\") picks it up, then call listTools to verify.")],
|
||||
[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,
|
||||
|
||||
+71
-25
@@ -4,10 +4,10 @@
|
||||
messageContent, agentMessage, agent,
|
||||
# Model types
|
||||
modelCost, llmModel, llmUsage,
|
||||
# Message content types
|
||||
textContent, imageContent,
|
||||
# Message types
|
||||
userMessage, assistantMessage, toolResultMessage,
|
||||
# Message content types
|
||||
textContent, imageContent, reasoningContent,
|
||||
# Message types
|
||||
userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage,
|
||||
# Tool types
|
||||
agentTool, validateRequiredArgs,
|
||||
# Context types
|
||||
@@ -31,6 +31,13 @@ using GeneralUtils
|
||||
|
||||
const Timestamp = DateTime
|
||||
|
||||
struct agentToolCall # A tool invocation from the LLM
|
||||
type::String # Always "function"
|
||||
id::String # Unique tool call identifier
|
||||
name::String # Tool name
|
||||
arguments::Dict{String, Any} # Parsed tool arguments
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
# LLM model info #
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
@@ -75,6 +82,10 @@ struct imageContent <: messageContent # Image message content
|
||||
mimeType::String # MIME type (e.g., "image/png")
|
||||
end
|
||||
|
||||
struct reasoningContent <: messageContent # LLM reasoning/thinking content
|
||||
text::String # The reasoning text
|
||||
end
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
# Message types #
|
||||
@@ -108,6 +119,52 @@ function userMessage(; role="user", content=Vector{messageContent}(), timestamp=
|
||||
return userMessage(role, content, timestamp)
|
||||
end
|
||||
|
||||
struct assistantMessageToolCall <: agentMessage # Assistant message containing tool calls
|
||||
role::String # Always "assistant"
|
||||
toolCalls::Vector{agentToolCall} # Tool calls to execute
|
||||
content::Vector{messageContent} # Reasoning/thinking content blocks
|
||||
api::String # API name used (e.g., "openai")
|
||||
provider::String # Provider name (e.g., "anthropic")
|
||||
model::String # Model identifier
|
||||
usage::llmUsage # Token usage for this message
|
||||
stopReason::String # Why generation stopped (e.g., "tool_calls")
|
||||
errorMessage::Union{String, Nothing} # Error if generation failed
|
||||
timestamp::Timestamp # When the message was received
|
||||
end
|
||||
|
||||
"""
|
||||
Create a new assistant message containing tool calls.
|
||||
|
||||
# Arguments
|
||||
- `role::String`: Always "assistant"
|
||||
- `toolCalls::Vector{agentToolCall}`: Tool calls to execute
|
||||
- `content::Vector{messageContent}`: Reasoning/thinking content blocks
|
||||
- `api::String`: API name used
|
||||
- `provider::String`: Provider name
|
||||
- `model::String`: Model identifier
|
||||
- `usage::llmUsage`: Token usage
|
||||
- `stopReason::String`: Why generation stopped
|
||||
- `errorMessage::Union{String, Nothing}`: Error if generation failed
|
||||
- `timestamp::Timestamp`: When the message was received
|
||||
|
||||
# Returns
|
||||
- A new `assistantMessageToolCall` instance
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> tc = agentToolCall("function", "call_1", "getWeather", Dict("city" => "Tokyo"))
|
||||
julia> msg = assistantMessageToolCall(toolCalls=[tc], stopReason="tool_calls")
|
||||
assistantMessageToolCall("assistant", [agentToolCall(...)], messageContent[], "", "", "", llmUsage(0, 0), "tool_calls", nothing, DateTime(...))
|
||||
```
|
||||
"""
|
||||
function assistantMessageToolCall(; role="assistant", toolCalls=agentToolCall[],
|
||||
content=Vector{messageContent}(), api="", provider="", model=nothing, usage=llmUsage(0, 0),
|
||||
stopReason="tool_calls", errorMessage=nothing, timestamp=now())
|
||||
model_str = model isa AbstractString ? String(model) : ""
|
||||
return assistantMessageToolCall(role, toolCalls, content, api, provider, model_str,
|
||||
usage, stopReason, errorMessage, timestamp)
|
||||
end
|
||||
|
||||
struct assistantMessage <: agentMessage # Message from the AI assistant
|
||||
role::String # Always "assistant"
|
||||
content::Vector{messageContent} # Text and/or image content
|
||||
@@ -146,7 +203,8 @@ assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "en
|
||||
function assistantMessage(; role="assistant", content=Vector{messageContent}(),
|
||||
api="", provider="", model=nothing, usage=llmUsage(0, 0), stopReason="end_turn",
|
||||
errorMessage=nothing, timestamp=now())
|
||||
return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp)
|
||||
model_str = model isa AbstractString ? String(model) : ""
|
||||
return assistantMessage(role, content, api, provider, model_str, usage, stopReason, errorMessage, timestamp)
|
||||
end
|
||||
|
||||
struct toolResultMessage <: agentMessage # Result returned from a tool execution
|
||||
@@ -263,7 +321,7 @@ struct agentTool # A tool available to the agent
|
||||
label::String # Human-readable tool name
|
||||
description::String # What the tool does
|
||||
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||
execute::Function # Tool execution function
|
||||
execute # Tool execution function
|
||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||
@@ -273,7 +331,7 @@ end
|
||||
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
|
||||
"""
|
||||
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
||||
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
|
||||
execute, prepareArguments::Union{Function, Nothing}=nothing,
|
||||
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false)
|
||||
return agentTool(name, label, description, inputSchema, execute,
|
||||
@@ -291,7 +349,7 @@ Snapshot of the agent's conversation context.
|
||||
# Arguments
|
||||
- `systemPrompt::String`: System prompt for the agent
|
||||
- `messages::Vector{agentMessage}`: Conversation messages
|
||||
- `tools::Union{Dict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
|
||||
- `tools::Union{OrderedDict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
|
||||
|
||||
# Returns
|
||||
- A new `agentContext` instance
|
||||
@@ -299,7 +357,7 @@ Snapshot of the agent's conversation context.
|
||||
struct agentContext # Snapshot of the agent's conversation context
|
||||
systemPrompt::String # System prompt for the agent
|
||||
messages::Vector{agentMessage} # Conversation messages
|
||||
tools::Union{Dict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||
end
|
||||
|
||||
|
||||
@@ -316,7 +374,6 @@ mutable struct agentState # Mutable runtime state of an agen
|
||||
messages::Vector{agentMessage}
|
||||
|
||||
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
|
||||
activeRun::Bool # is agent processing user message?
|
||||
errorMessage::Union{String, Nothing} # Last error message
|
||||
end
|
||||
|
||||
@@ -353,20 +410,11 @@ function agentState(
|
||||
deepcopy(tools),
|
||||
deepcopy(messages),
|
||||
Vector{String}(),
|
||||
false,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
struct agentToolCall # A tool invocation from the LLM
|
||||
type::String # Always "function"
|
||||
id::String # Unique tool call identifier
|
||||
name::String # Tool name
|
||||
arguments::Dict{String, Any} # Parsed tool arguments
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
Context for preparing the next conversation turn.
|
||||
|
||||
@@ -395,13 +443,11 @@ end
|
||||
Configuration for the agent tool execution loop.
|
||||
|
||||
# Arguments
|
||||
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name
|
||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||
"""
|
||||
struct agentLoopConfig
|
||||
tools::OrderedDict{String, agentTool}
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
toolExecution::String
|
||||
@@ -437,13 +483,13 @@ end
|
||||
Context passed to the `beforeToolCall` hook.
|
||||
|
||||
# Arguments
|
||||
- `message::assistantMessage`: The assistant message containing the tool call
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call being prepared
|
||||
- `args::Dict{String,Any}`: Validated tool arguments
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct beforeToolCallContext
|
||||
message::assistantMessage
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
context::agentContext
|
||||
@@ -458,7 +504,7 @@ end
|
||||
Context passed to the `afterToolCall` hook.
|
||||
|
||||
# Arguments
|
||||
- `message::assistantMessage`: The assistant message containing the tool call
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call that was executed
|
||||
- `args::Dict{String,Any}`: Tool arguments
|
||||
- `result::agentToolResult`: The raw tool result
|
||||
@@ -466,7 +512,7 @@ Context passed to the `afterToolCall` hook.
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct afterToolCallContext
|
||||
message::assistantMessage
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
result::agentToolResult
|
||||
|
||||
+165
-16
@@ -2,10 +2,10 @@ module utils
|
||||
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||
validateToolArguments, _userMessageToOpenAI,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI,
|
||||
beforeToolCall, afterToolCall, agentEventSink
|
||||
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
|
||||
using GeneralUtils
|
||||
using ..type
|
||||
|
||||
@@ -75,7 +75,6 @@ function availableWineToText(vecd::Vector)::String
|
||||
end
|
||||
|
||||
|
||||
|
||||
"""
|
||||
prepareContext(state::agentState) -> agentContext
|
||||
|
||||
@@ -110,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages)
|
||||
# end
|
||||
```
|
||||
"""
|
||||
function prepareContext(state::agentState)::agentContext
|
||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
||||
|
||||
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||
filteredTools = state.tools
|
||||
@@ -157,7 +156,7 @@ formatMsgForLLm(ctx) == Dict("messages" => [
|
||||
])
|
||||
```
|
||||
"""
|
||||
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
|
||||
""" openai message format example
|
||||
msg = Dict(
|
||||
@@ -185,19 +184,31 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
Dict("type" => "text", "text" => "let me check."),
|
||||
]
|
||||
),
|
||||
],
|
||||
"tools"=> [
|
||||
Dict(
|
||||
"role" => "toolResult",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "name: Chateau Montelena ..."),
|
||||
]
|
||||
),
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => "getWeather",
|
||||
"description" => "Get current weather",
|
||||
"parameters" => Dict(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string")
|
||||
),
|
||||
"required" => ["city"]
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
"temperature" => 0.7
|
||||
)
|
||||
"""
|
||||
|
||||
openaiReadyMsg = Dict{String, Any}()
|
||||
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
|
||||
messages = Vector{Dict{String, Any}}()
|
||||
|
||||
agentEventSink("formatMsgForLLM 1")
|
||||
# System prompt as system message
|
||||
if !isempty(ctx.systemPrompt)
|
||||
push!(messages, Dict(
|
||||
@@ -205,22 +216,65 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||
))
|
||||
end
|
||||
|
||||
agentEventSink("formatMsgForLLM 2")
|
||||
# Conversation messages
|
||||
for msg in ctx.messages
|
||||
if msg isa userMessage
|
||||
push!(messages, _userMessageToOpenAI(msg))
|
||||
elseif msg isa assistantMessageToolCall
|
||||
push!(messages, _assistantMessageToolCallToOpenAI(msg))
|
||||
elseif msg isa assistantMessage
|
||||
push!(messages, _assistantMessageToOpenAI(msg))
|
||||
elseif msg isa toolResultMessage
|
||||
push!(messages, _toolResultMessageToOpenAI(msg))
|
||||
end
|
||||
end
|
||||
agentEventSink("formatMsgForLLM 3")
|
||||
# Convert ctx.tools into OpenAI tools format
|
||||
tools_array = _toolsToOpenAI(ctx.tools, agentEventSink)
|
||||
agentEventSink("formatMsgForLLM 4")
|
||||
openaiReadyMsg["messages"] = messages
|
||||
openaiReadyMsg["temperature"] = 0.7
|
||||
|
||||
return Dict("messages" => messages)
|
||||
if !isempty(tools_array)
|
||||
openaiReadyMsg["tools"] = tools_array
|
||||
end
|
||||
|
||||
return openaiReadyMsg
|
||||
end
|
||||
|
||||
#TODO
|
||||
"""
|
||||
beforeToolCall(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult
|
||||
|
||||
Callback invoked before executing a tool call. Use this hook to inspect
|
||||
the tool call and decide whether to allow, block, or modify it.
|
||||
|
||||
Common use cases:
|
||||
- Request user approval via UI before running destructive tools.
|
||||
- Validate business rules that cannot be expressed in the JSON schema.
|
||||
- Check final context (e.g. session state, rate limits, permissions).
|
||||
|
||||
# Arguments
|
||||
- `context::beforeToolCallContext`: Contains the assistant message, tool call,
|
||||
validated arguments, and current conversation context.
|
||||
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||
|
||||
# Returns
|
||||
- `beforeToolCallResult(false, "N/A")` to allow the call to proceed.
|
||||
- `beforeToolCallResult(true, "Reason")` to block the call with a reason.
|
||||
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
|
||||
|
||||
# Example
|
||||
```julia
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
|
||||
if context.toolCall.name == "deleteFile"
|
||||
# Block file deletion unless explicitly approved
|
||||
return beforeToolCallResult(true, "User must approve file deletion")
|
||||
end
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
```
|
||||
"""
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
)::beforeToolCallResult
|
||||
|
||||
@@ -233,8 +287,31 @@ function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
|
||||
#TODO
|
||||
function afterToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
"""
|
||||
afterToolCall(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}
|
||||
|
||||
Callback invoked after a tool call finishes executing (before and after errors).
|
||||
Use this hook to post-process the tool result before it is fed back to the LLM.
|
||||
|
||||
Common use cases:
|
||||
- Mask sensitive data (API keys, tokens) from result content.
|
||||
- Normalize usage tracking data into a consistent format.
|
||||
- Inspect the result and set `terminate: true` based on business logic
|
||||
(e.g. "if deployment failed, stop the agent rather than retrying").
|
||||
- Wrap error results in friendlier messages for the LLM to understand.
|
||||
|
||||
# Arguments
|
||||
- `context::afterToolCallContext`: Contains the assistant message, tool call,
|
||||
arguments, raw result, error status, and current conversation context.
|
||||
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||
|
||||
# Returns
|
||||
- `nothing` to pass the result through unchanged.
|
||||
- `agentToolResult(...)` to return a modified result (content, details, usage,
|
||||
terminate flag can all be overridden).
|
||||
|
||||
"""
|
||||
function afterToolCall(context::afterToolCallContext, signal::abortSignal
|
||||
)::Union{agentToolResult, Nothing}
|
||||
|
||||
# modify context.result if needed and return agentToolResult
|
||||
@@ -261,6 +338,44 @@ end
|
||||
|
||||
|
||||
"""
|
||||
Convert an assistantMessageToolCall to OpenAI message format.
|
||||
|
||||
Produces a message with role="assistant", content=null, and a tool_calls array:
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"location\": \"San Francisco, CA\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{String, Any}
|
||||
tool_calls = Dict{String, Any}[]
|
||||
for tc in msg.toolCalls
|
||||
push!(tool_calls, Dict(
|
||||
"id" => tc.id,
|
||||
"type" => tc.type,
|
||||
"function" => Dict(
|
||||
"name" => tc.name,
|
||||
"arguments" => JSON.json(tc.arguments)
|
||||
)
|
||||
))
|
||||
end
|
||||
return Dict(
|
||||
"role" => "assistant",
|
||||
"content" => nothing,
|
||||
"tool_calls" => tool_calls
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
Convert an assistantMessage to OpenAI message format.
|
||||
"""
|
||||
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
|
||||
@@ -309,6 +424,40 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
_toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}) -> Vector{Dict{String, Any}}
|
||||
|
||||
Convert an OrderedDict of agentTool definitions into OpenAI function tool format.
|
||||
|
||||
Returns an empty vector when `tools` is `nothing` or empty.
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
_toolsToOpenAI(nothing) # => Dict{String, Any}[]
|
||||
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
|
||||
```
|
||||
"""
|
||||
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, agentEventSink)::Vector{Dict{String, Any}}
|
||||
tools_array = Vector{Dict{String, Any}}()
|
||||
agentEventSink("_toolsToOpenAI 1")
|
||||
agentEventSink(string(typeof(tools)))
|
||||
if tools !== nothing
|
||||
for (_, tool) in tools
|
||||
push!(tools_array, Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => tool.name,
|
||||
"description" => tool.description,
|
||||
"parameters" => tool.inputSchema
|
||||
)
|
||||
))
|
||||
end
|
||||
end
|
||||
agentEventSink("_toolsToOpenAI 2")
|
||||
return tools_array
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String}
|
||||
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
using Test
|
||||
using YiemAgent
|
||||
using YiemAgent.agentCore
|
||||
using YiemAgent.type
|
||||
using JSON
|
||||
|
||||
# Import the function from the private module scope
|
||||
import YiemAgent.agentCore: _extractToolCalls
|
||||
|
||||
@testset "_extractToolCalls" begin
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Format 1: response["message"]["tool_calls"] (LMStudio.jl style) #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "single tool call via message format" begin
|
||||
response = Dict{String,Any}(
|
||||
"finish_reason" => "tool_calls",
|
||||
"index" => 0,
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"content" => "",
|
||||
"reasoning_content" => "Let me check the weather.",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Bangkok, Thailand\"}",
|
||||
),
|
||||
"id" => "tc_001",
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].id == "tc_001"
|
||||
@test tc_list[1].type == "function"
|
||||
@test tc_list[1].arguments["city"] == "Bangkok, Thailand"
|
||||
@test assistant_msg isa assistantMessage
|
||||
@test assistant_msg.role == "assistant"
|
||||
@test assistant_msg.stopReason == "tool_calls"
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1] isa reasoningContent
|
||||
@test assistant_msg.content[1].text == "Let me check the weather."
|
||||
end
|
||||
|
||||
@testset "multiple tool calls via message format" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"content" => "",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Tokyo, Japan\"}",
|
||||
),
|
||||
"id" => "tc_001",
|
||||
),
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{\"timezone\":\"Asia/Tokyo\"}",
|
||||
),
|
||||
"id" => "tc_002",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 2
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].arguments["city"] == "Tokyo, Japan"
|
||||
@test tc_list[2].name == "getTime"
|
||||
@test tc_list[2].arguments["timezone"] == "Asia/Tokyo"
|
||||
@test assistant_msg.role == "assistant"
|
||||
end
|
||||
|
||||
@testset "tool call with empty arguments string" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "listTools",
|
||||
"arguments" => "{}",
|
||||
),
|
||||
"id" => "tc_empty",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "listTools"
|
||||
@test tc_list[1].arguments == Dict{String,Any}()
|
||||
@test assistant_msg.stopReason == "end_turn"
|
||||
end
|
||||
|
||||
@testset "tool call with missing id falls back to uuid" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{\"city\":\"NYC\"}",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test !isempty(tc_list[1].id)
|
||||
@test tc_list[1].name == "getTime"
|
||||
end
|
||||
|
||||
@testset "tool call with non-string arguments (pre-parsed dict)" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => Dict{String,Any}("city" => "London", "units" => "fahrenheit"),
|
||||
),
|
||||
"id" => "tc_parsed",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].arguments["city"] == "London"
|
||||
@test tc_list[1].arguments["units"] == "fahrenheit"
|
||||
end
|
||||
|
||||
@testset "tool call with api/provider/model/usage metadata" begin
|
||||
response = Dict{String,Any}(
|
||||
"api" => "openai",
|
||||
"provider" => "anthropic",
|
||||
"model" => "claude-3-opus",
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{}",
|
||||
),
|
||||
"id" => "tc_meta",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test assistant_msg.api == "openai"
|
||||
@test assistant_msg.provider == "anthropic"
|
||||
@test assistant_msg.model == "claude-3-opus"
|
||||
end
|
||||
|
||||
# --------------------------------------------------------------- #
|
||||
# Format 2: response.content blocks (OpenAI API style) #
|
||||
# --------------------------------------------------------------- #
|
||||
|
||||
@testset "content blocks with tool_calls" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "Let me check."),
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Paris\"}",
|
||||
),
|
||||
"id" => "tc_block_1",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].arguments["city"] == "Paris"
|
||||
# text block before tool_calls should be included in content
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1].text == "Let me check."
|
||||
end
|
||||
|
||||
@testset "content blocks with tool_call (single-call format)" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_single",
|
||||
"name" => "getTime",
|
||||
"arguments" => Dict{String,Any}("timezone" => "Europe/London"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getTime"
|
||||
@test tc_list[1].id == "tc_single"
|
||||
@test tc_list[1].arguments["timezone"] == "Europe/London"
|
||||
end
|
||||
|
||||
@testset "content blocks with reasoning and text" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "reasoning", "text" => "Thinking..."),
|
||||
Dict{String,Any}("type" => "text", "text" => "Here's the answer."),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test length(assistant_msg.content) == 2
|
||||
@test assistant_msg.content[1] isa reasoningContent
|
||||
@test assistant_msg.content[1].text == "Thinking..."
|
||||
@test assistant_msg.content[2] isa textContent
|
||||
@test assistant_msg.content[2].text == "Here's the answer."
|
||||
end
|
||||
|
||||
@testset "content blocks with text and tool_call (tool_call not in content)" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "Sure, I'll check."),
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_mix",
|
||||
"name" => "getWeather",
|
||||
"arguments" => Dict{String,Any}("city" => "London"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
# text block included, tool_call block excluded from content
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1].text == "Sure, I'll check."
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# assistantMessage construction #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "assistantMessage with error_message and errorMessage fallback" begin
|
||||
response = Dict{String,Any}(
|
||||
"error_message" => "rate limit",
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fail")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test assistant_msg.errorMessage == "rate limit"
|
||||
end
|
||||
|
||||
@testset "assistantMessage with usage tracking" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
|
||||
"usage" => llmUsage(100, 50),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.usage.inputTokens == 100
|
||||
@test assistant_msg.usage.outputTokens == 50
|
||||
end
|
||||
|
||||
@testset "assistantMessage with invalid usage defaults to zero" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
|
||||
"usage" => "invalid",
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.usage.inputTokens == 0
|
||||
@test assistant_msg.usage.outputTokens == 0
|
||||
end
|
||||
|
||||
@testset "reasoning_content as textContent" begin
|
||||
response = Dict{String,Any}(
|
||||
"reasoning_content" => textContent("internal thought"),
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "output")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test length(assistant_msg.content) == 2
|
||||
@test assistant_msg.content[1] isa reasoningContent
|
||||
@test assistant_msg.content[1].text == "internal thought"
|
||||
@test assistant_msg.content[2] isa textContent
|
||||
@test assistant_msg.content[2].text == "output"
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# No tool call cases #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "no tool calls found" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "Hello world."),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test assistant_msg.stopReason == "end_turn"
|
||||
end
|
||||
|
||||
@testset "empty message" begin
|
||||
response = Dict{String,Any}()
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test assistant_msg.role == "assistant"
|
||||
@test assistant_msg.stopReason == "end_turn"
|
||||
@test length(assistant_msg.content) == 0
|
||||
end
|
||||
|
||||
@testset "message with empty tool_calls array" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test assistant_msg.role == "assistant"
|
||||
end
|
||||
|
||||
@testset "message field is not a Dict" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => "not a dict",
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fallback")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test length(assistant_msg.content) == 1
|
||||
end
|
||||
|
||||
@testset "Format 1 takes priority over Format 2" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Format1\"}",
|
||||
),
|
||||
"id" => "tc_fmt1",
|
||||
),
|
||||
],
|
||||
),
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_fmt2",
|
||||
"name" => "getTime",
|
||||
"arguments" => Dict{String,Any}("city" => "Format2"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Edge cases #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "tool call with null arguments" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => nothing,
|
||||
),
|
||||
"id" => "tc_null",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getTime"
|
||||
end
|
||||
|
||||
@testset "tool call with missing function key" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"id" => "tc_nofunc",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == ""
|
||||
end
|
||||
|
||||
@testset "tool call with missing name in function block" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}("arguments" => "{}"),
|
||||
"id" => "tc_noname",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == ""
|
||||
end
|
||||
|
||||
@testset "message format with JSON.Object (JSON.parse result)" begin
|
||||
json_str = JSON.json(Dict(
|
||||
"message" => Dict(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => [
|
||||
Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict("name" => "getWeather", "arguments" => "{\"city\":\"Test\"}"),
|
||||
"id" => "tc_jsonobj",
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
parsed = JSON.parse(json_str)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(parsed)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].arguments["city"] == "Test"
|
||||
end
|
||||
|
||||
@testset "tool_calls block with mixed content types (text + tool_calls)" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "I'll check both."),
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"London\"}",
|
||||
),
|
||||
"id" => "tc_mix1",
|
||||
),
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{\"timezone\":\"UTC\"}",
|
||||
),
|
||||
"id" => "tc_mix2",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 2
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[2].name == "getTime"
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1].text == "I'll check both."
|
||||
end
|
||||
|
||||
@testset "tool_call block without arguments field" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_noargs",
|
||||
"name" => "getTime",
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].arguments == Dict{String,Any}()
|
||||
end
|
||||
|
||||
@testset "tool_calls block with empty tool_calls array" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any[],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
end
|
||||
|
||||
@testset "tool_calls block with non-AbstractDict elements" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any["not a dict", 42, nothing],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
end
|
||||
|
||||
@testset "content field is not a Vector" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => "not a vector",
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test length(assistant_msg.content) == 0
|
||||
end
|
||||
|
||||
@testset "Dict-based response with all metadata fields" begin
|
||||
response = Dict{String,Any}(
|
||||
"api" => "openai",
|
||||
"provider" => "anthropic",
|
||||
"model" => "claude-3-sonnet",
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_meta",
|
||||
"name" => "getTime",
|
||||
"arguments" => Dict{String,Any}("city" => "Seoul"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getTime"
|
||||
@test tc_list[1].arguments["city"] == "Seoul"
|
||||
@test assistant_msg.api == "openai"
|
||||
@test assistant_msg.provider == "anthropic"
|
||||
@test assistant_msg.model == "claude-3-sonnet"
|
||||
end
|
||||
|
||||
@testset "default role is assistant" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "no role specified")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.role == "assistant"
|
||||
end
|
||||
|
||||
@testset "tool call with custom role in message format" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "custom_role",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{}",
|
||||
),
|
||||
"id" => "tc_role",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.role == "custom_role"
|
||||
end
|
||||
|
||||
@testset "image content block handling" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "image_url",
|
||||
"image_url" => Dict("url" => "data:image/png;base64,abc123"),
|
||||
),
|
||||
Dict{String,Any}("type" => "text", "text" => "What is this?"),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(assistant_msg.content) == 2
|
||||
@test assistant_msg.content[1] isa textContent
|
||||
@test assistant_msg.content[1].text == ""
|
||||
@test assistant_msg.content[2].text == "What is this?"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
using Test
|
||||
using YiemAgent
|
||||
|
||||
include("toolTest.jl")
|
||||
include("_extractToolCalls.jl")
|
||||
|
||||
+192
-37
@@ -1,35 +1,19 @@
|
||||
using Test
|
||||
using Dates
|
||||
using YiemAgent
|
||||
using YiemAgent.toolRegistry
|
||||
using YiemAgent.type
|
||||
using YiemAgent.agentCore
|
||||
|
||||
# Path to the real tools directory
|
||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
|
||||
@testset "loadTools with toolStore" begin
|
||||
@testset "register_all_tools with toolStore" begin
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. loadTools throws on non-existent directory #
|
||||
# 1. register_all_tools registers all static tools + listTools #
|
||||
# ------------------------------------------------------------------ #
|
||||
store = toolStore(name="test1")
|
||||
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 2. loadTools throws if a .jl file does not define getTool() #
|
||||
# Must run BEFORE any other loadTools call (getTool binding #
|
||||
# persists in module scope after include()). #
|
||||
# ------------------------------------------------------------------ #
|
||||
bad_dir = mktempdir()
|
||||
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
||||
@test_throws ArgumentError loadTools(store, bad_dir)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. loadTools loads actual tool files from src/tools/ #
|
||||
# ------------------------------------------------------------------ #
|
||||
store2 = toolStore(name="test2")
|
||||
loaded = loadTools(store2, TOOLS_DIR)
|
||||
loaded = register_all_tools(store)
|
||||
@test !isempty(loaded)
|
||||
@test length(loaded) == 4 # 3 files + auto-registered listTools
|
||||
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
|
||||
|
||||
names = [k for k in keys(loaded)]
|
||||
@test "getTime" in names
|
||||
@@ -38,16 +22,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test "listTools" in names
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. loadTools returns tools sorted alphabetically by filename #
|
||||
# (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
|
||||
# 2. register_all_tools returns tools in registration order #
|
||||
# ------------------------------------------------------------------ #
|
||||
@test collect(keys(loaded))[1] == "getTime"
|
||||
@test collect(keys(loaded))[2] == "getWeather"
|
||||
@test collect(keys(loaded))[1] == "getWeather"
|
||||
@test collect(keys(loaded))[2] == "getTime"
|
||||
@test collect(keys(loaded))[3] == "writeTool"
|
||||
@test collect(keys(loaded))[4] == "listTools"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Verify loaded tool fields are correct #
|
||||
# 3. Verify loaded tool fields are correct #
|
||||
# ------------------------------------------------------------------ #
|
||||
# getTime
|
||||
time_tool = loaded["getTime"]
|
||||
@@ -74,7 +57,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test "executeCode" in wt.inputSchema["required"]
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 6. Tool execution returns valid results #
|
||||
# 4. Tool execution returns valid results #
|
||||
# ------------------------------------------------------------------ #
|
||||
sig = nothing
|
||||
op = x -> x # no-op partial result callback
|
||||
@@ -98,16 +81,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
|
||||
# execute getWeather with units
|
||||
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
||||
@test occursin("72°F", result_w2.content[1].text)
|
||||
@test occursin("72\u00b0F", result_w2.content[1].text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
||||
# 5. getTools / registerTool / clearTools (per-store isolation) #
|
||||
# ------------------------------------------------------------------ #
|
||||
store3 = toolStore(name="test3")
|
||||
registry_tools = getTools(store3)
|
||||
@test isempty(registry_tools)
|
||||
|
||||
# listTool is not auto-registered anymore — each store starts empty
|
||||
# Register tools manually
|
||||
registerTool(store3, loaded["getTime"])
|
||||
registerTool(store3, loaded["getWeather"])
|
||||
@@ -143,7 +125,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test reg["manualTool"].parallelToolExecute == true
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 8. getTools returns direct reference (mutations affect registry) #
|
||||
# 6. getTools returns direct reference (mutations affect registry) #
|
||||
# ------------------------------------------------------------------ #
|
||||
copy1 = getTools(store3)
|
||||
copy2 = getTools(store3)
|
||||
@@ -152,7 +134,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||
@test isempty(getTools(store3)) # mutation propagates
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 9. Per-store isolation — two stores don't share tools #
|
||||
# 7. Per-store isolation — two stores don't share tools #
|
||||
# ------------------------------------------------------------------ #
|
||||
storeA = toolStore(name="isolationA")
|
||||
storeB = toolStore(name="isolationB")
|
||||
@@ -175,10 +157,10 @@ end
|
||||
|
||||
@testset "listTool" begin
|
||||
store = toolStore(name="test_list")
|
||||
loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools
|
||||
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
|
||||
|
||||
# loadTools auto-registers listTool
|
||||
@test "listTools" in keys(loaded)
|
||||
# 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)
|
||||
@@ -199,7 +181,7 @@ end
|
||||
|
||||
# Each listTool call creates an independent closure
|
||||
storeB = toolStore(name="test_listB")
|
||||
registerTool(storeB, loaded["getWeather"])
|
||||
registerTool(storeB, store.tools["getWeather"])
|
||||
list_tB = listTool(storeB)
|
||||
|
||||
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
||||
@@ -210,3 +192,176 @@ end
|
||||
@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
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
using Revise, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64,
|
||||
NATS, Base.Threads
|
||||
using YiemAgent, GeneralUtils, msghandler
|
||||
|
||||
|
||||
""" Debug
|
||||
|
||||
using JSON, NATS, msghandler
|
||||
using NATS
|
||||
|
||||
conn = NATS.connect("nats.yiem.cc")
|
||||
|
||||
sub = NATS.subscribe(conn, "sommanion.debug") do msg
|
||||
payload = NATS.payload(msg)
|
||||
@info "debug" payload
|
||||
|
||||
open("./log/error.log", "a") do io
|
||||
println(io, payload)
|
||||
end
|
||||
end
|
||||
|
||||
NATS.publish(conn, "sommanion.debug", "order-123")
|
||||
|
||||
|
||||
# ---------------------------- inject this code into codebase to debug --------------------------- #
|
||||
try
|
||||
batch = someFunction(x, y, z)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
err_msg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
|
||||
agentEventSink(err_msg)
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
struct text2textInstructLLM
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
fileserver_url::String
|
||||
end
|
||||
|
||||
function (t::text2textInstructLLM)(openai_msg::Dict{String, Any})
|
||||
|
||||
payloads = [("msg", openai_msg, "dictionary")] # List of tuples
|
||||
_, msg_envelope_json_str = msghandler.smartpack(
|
||||
t.topic,
|
||||
payloads;
|
||||
sender_id=t.senderID,
|
||||
msg_purpose="text2text",
|
||||
fileserver_url=t.fileserver_url)
|
||||
|
||||
reply = NATS.request(t.natsConn, t.topic, msg_envelope_json_str, timeout=180)
|
||||
|
||||
incoming_env_json_str = String(reply.payload)
|
||||
incoming_env = msghandler.smartunpack(incoming_env_json_str)
|
||||
_llm_response = incoming_env["payloads"][1][2]
|
||||
llm_response = _llm_response["choices"][1]
|
||||
|
||||
return llm_response
|
||||
end
|
||||
|
||||
|
||||
|
||||
struct agentEventSink
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
function (aes::agentEventSink)(msg::String)
|
||||
NATS.publish(aes.natsConn, aes.topic, msg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
config = JSON.parsefile("./appconfig.json")
|
||||
agent_conn = NATS.connect(config["nats_server_info"]["url"])
|
||||
|
||||
|
||||
#WORKING load tools
|
||||
text2text_llm = text2textInstructLLM(agent_conn,
|
||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||
"sender",
|
||||
config["externalservice"]["fileserver"]["url"])
|
||||
|
||||
debugNats = agentEventSink(agent_conn, "sommanion.debug", "sender")
|
||||
|
||||
agent = YiemAgent.yiemAgent(
|
||||
text2text_llm;
|
||||
agentEventSink=debugNats
|
||||
)
|
||||
|
||||
msg = Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "What's the weather in Bangkok?"),
|
||||
# Dict(
|
||||
# "type" => "image_url",
|
||||
# "image_url" => Dict("url" => "data:mime_type;base64,image2_base64_string")
|
||||
# ),
|
||||
]
|
||||
)
|
||||
|
||||
push!(agent.inputChannel, msg)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user