Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e592173a6 | |||
| 0cacb5c94a | |||
| 6c96409969 | |||
| 06d51c1ee9 | |||
| 2ad3d1df38 | |||
| 83c7770877 | |||
| bad14fbe7f | |||
| 578e8f55bd | |||
| ae3e432b02 | |||
| 7c14390400 | |||
| 89885c1583 | |||
| 5a27630ccf | |||
| ed91260468 |
@@ -6,7 +6,7 @@ Julia framework for building agents with tool use.
|
|||||||
|
|
||||||
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
|
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
|
||||||
2. Create a `yiemAgent` with `loadTools("src/tools")`
|
2. Create a `yiemAgent` with `loadTools("src/tools")`
|
||||||
3. Call `run_agent(agent, "message")` then `take_response(agent)`
|
3. Call `runAgent(agent, "message")` then `takeResponse(agent)`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ src/
|
|||||||
├── type.jl # Core types (messages, tools, agent state)
|
├── type.jl # Core types (messages, tools, agent state)
|
||||||
├── utils.jl # Message formatting, validation
|
├── utils.jl # Message formatting, validation
|
||||||
├── agentCore.jl # Agent loop, tool execution pipeline
|
├── agentCore.jl # Agent loop, tool execution pipeline
|
||||||
├── api.jl # Public API (run_agent, take_response, etc.)
|
├── api.jl # Public API (runAgent, takeResponse, etc.)
|
||||||
└── tools/
|
└── tools/
|
||||||
├── registry.jl # Tool registry (loadTools, registerTool, listTools)
|
├── registry.jl # Tool registry (loadTools, registerTool, listTools)
|
||||||
├── getWeather.jl # Weather lookup tool
|
├── getWeather.jl # Weather lookup tool
|
||||||
|
|||||||
@@ -52,20 +52,21 @@ result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x)
|
|||||||
|
|
||||||
### Step 2: Load — `loadTools()`
|
### Step 2: Load — `loadTools()`
|
||||||
|
|
||||||
Load all tool modules from a directory into a `ToolStore`. Each `.jl` file must define `getTool()::agentTool`.
|
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.
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
using YiemAgent, YiemAgent.toolRegistry
|
using YiemAgent, YiemAgent.toolRegistry
|
||||||
|
|
||||||
store = ToolStore(name="myAgent")
|
store = toolStore(name="myAgent")
|
||||||
tools = loadTools(store, "src/tools")
|
tools = loadTools(store, "src/tools")
|
||||||
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools
|
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools
|
||||||
|
# Also auto-registers listTools for runtime discovery
|
||||||
```
|
```
|
||||||
|
|
||||||
**Result extraction:**
|
**Result extraction:**
|
||||||
```julia
|
```julia
|
||||||
all_tools = getTools(store) # OrderedDict{String, agentTool}
|
all_tools = getTools(store) # OrderedDict{String, agentTool}
|
||||||
# Keys: "getTime", "getWeather", "writeTool"
|
# Keys: "getTime", "getWeather", "writeTool", "listTools"
|
||||||
getTime_tool = all_tools["getTime"]
|
getTime_tool = all_tools["getTime"]
|
||||||
|
|
||||||
# Manual registration (alternative to loadTools)
|
# Manual registration (alternative to loadTools)
|
||||||
@@ -73,7 +74,7 @@ registerTool(store, my_tool)
|
|||||||
clearTools(store) # Clear all tools from store
|
clearTools(store) # Clear all tools from store
|
||||||
```
|
```
|
||||||
|
|
||||||
**Source:** `toolRegistry.jl:113-172`
|
**Source:** `toolRegistry.jl:126-178`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -84,8 +85,8 @@ Wire the loaded tools into a new `yiemAgent` instance. The `tools` parameter is
|
|||||||
```julia
|
```julia
|
||||||
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
||||||
|
|
||||||
# 1. Set up ToolStore and load tools
|
# 1. Set up toolStore and load tools (auto-registers listTools)
|
||||||
store = ToolStore(name="myAgent")
|
store = toolStore(name="myAgent")
|
||||||
loadTools(store, "src/tools")
|
loadTools(store, "src/tools")
|
||||||
|
|
||||||
# 2. Create agent — pass tools + _tool_store
|
# 2. Create agent — pass tools + _tool_store
|
||||||
@@ -102,9 +103,10 @@ agent = yiemAgent(
|
|||||||
**Manual registration** (without `loadTools`):
|
**Manual registration** (without `loadTools`):
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
store = ToolStore(name="myAgent")
|
store = toolStore(name="myAgent")
|
||||||
registerTool(store, getTime_tool)
|
registerTool(store, getTime_tool)
|
||||||
registerTool(store, getWeather_tool)
|
registerTool(store, getWeather_tool)
|
||||||
|
registerTool(store, listTool(store)) # needed for manual registration
|
||||||
|
|
||||||
agent = yiemAgent(
|
agent = yiemAgent(
|
||||||
tools = getTools(store),
|
tools = getTools(store),
|
||||||
@@ -124,7 +126,7 @@ agent = yiemAgent(
|
|||||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
||||||
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
||||||
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
||||||
| `_tool_store` | `ToolStore` | No | Runtime tool registry for `registerTool()` |
|
| `_tool_store` | `toolStore` | No | Runtime tool registry for `registerTool()` |
|
||||||
|
|
||||||
Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`.
|
Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`.
|
||||||
|
|
||||||
@@ -149,7 +151,7 @@ result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
|||||||
|
|
||||||
**Via agent loop (production):**
|
**Via agent loop (production):**
|
||||||
```
|
```
|
||||||
user message → run_agent(agent, Dict("role"=>"user", "content"=>...))
|
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||||
→ _agent_loop detects message → @spawn _process_message(agent)
|
→ _agent_loop detects message → @spawn _process_message(agent)
|
||||||
→ prepareContext → formatMsgForLLM → llmCall
|
→ prepareContext → formatMsgForLLM → llmCall
|
||||||
→ LLM returns tool_calls
|
→ LLM returns tool_calls
|
||||||
@@ -278,12 +280,12 @@ The `terminate` flag is checked at the batch level. See [Section 10](#10-tool-ca
|
|||||||
|
|
||||||
**Source:** `toolRegistry.jl`
|
**Source:** `toolRegistry.jl`
|
||||||
|
|
||||||
### How `ToolStore` Works
|
### How `toolStore` Works
|
||||||
|
|
||||||
The registry uses **per-agent isolated storage** via the `ToolStore` struct. Each agent gets its own store, so tool registration is independent — `registerTool(store, tool)` only affects that agent's tool set.
|
The registry uses **per-agent isolated storage** via the `toolStore` struct. Each agent gets its own store, so tool registration is independent — `registerTool(store, tool)` only affects that agent's tool set.
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct ToolStore
|
struct toolStore
|
||||||
tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration
|
tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration
|
||||||
name::String # identifier for debugging/logs
|
name::String # identifier for debugging/logs
|
||||||
end
|
end
|
||||||
@@ -294,10 +296,10 @@ end
|
|||||||
### How `loadTools(store, dir)` Works
|
### How `loadTools(store, dir)` Works
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool}
|
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Source:** `toolRegistry.jl:113-172`
|
**Source:** `toolRegistry.jl:126-178`
|
||||||
|
|
||||||
1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
|
1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
|
||||||
2. **Sorts** filenames alphabetically for deterministic registration order
|
2. **Sorts** filenames alphabetically for deterministic registration order
|
||||||
@@ -312,7 +314,8 @@ function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool
|
|||||||
```
|
```
|
||||||
4. **Evaluates** `getTool()` within the submodule scope using `Core.eval(mod, :(getTool()))` — this avoids world-age issues
|
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
|
5. **Validates** the return value is an `agentTool` instance
|
||||||
6. **Registers** the tool in `store.tools` and returns an `OrderedDict{String, agentTool}`
|
6. **Registers** the tool in `store.tools`
|
||||||
|
7. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime
|
||||||
|
|
||||||
### Why Submodules?
|
### Why Submodules?
|
||||||
|
|
||||||
@@ -325,10 +328,10 @@ Each tool file is loaded into its own **namespaced submodule**. This means:
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Create per-agent stores
|
# Create per-agent stores
|
||||||
store1 = ToolStore(name="agent1")
|
store1 = toolStore(name="agent1")
|
||||||
store2 = ToolStore(name="agent2")
|
store2 = toolStore(name="agent2")
|
||||||
|
|
||||||
# Load tools into specific stores
|
# Load tools into specific stores (auto-registers listTools)
|
||||||
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
|
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
|
||||||
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
|
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
|
||||||
|
|
||||||
@@ -350,11 +353,11 @@ clearTools(store1) # only clears store1
|
|||||||
|
|
||||||
### Per-Agent Isolation
|
### Per-Agent Isolation
|
||||||
|
|
||||||
Each `ToolStore` is completely independent — tools registered in one store do not appear in another:
|
Each `toolStore` is completely independent — tools registered in one store do not appear in another:
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
storeA = ToolStore(name="A")
|
storeA = toolStore(name="A")
|
||||||
storeB = ToolStore(name="B")
|
storeB = toolStore(name="B")
|
||||||
|
|
||||||
registerTool(storeA, getTime_tool)
|
registerTool(storeA, getTime_tool)
|
||||||
registerTool(storeB, getWeather_tool)
|
registerTool(storeB, getWeather_tool)
|
||||||
@@ -379,10 +382,10 @@ The `_agent_loop()` function runs as a background `@spawn` task, created when `y
|
|||||||
|
|
||||||
```
|
```
|
||||||
yiemAgent struct contains:
|
yiemAgent struct contains:
|
||||||
- inputChannel (Channel, capacity 16) ← user sends messages here via run_agent()
|
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
|
||||||
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via follow_up()
|
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
||||||
- outputChannel (Channel, capacity 16) ← agent sends responses here via take_response()
|
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
||||||
- _tool_store (ToolStore) ← per-agent isolated tool registry
|
- _tool_store (toolStore) ← per-agent isolated tool registry
|
||||||
```
|
```
|
||||||
|
|
||||||
### Loop States
|
### Loop States
|
||||||
@@ -630,7 +633,7 @@ function prepareToolCall(
|
|||||||
- On failure: throws `ArgumentError(error_string)`, caught by the try-catch below
|
- On failure: throws `ArgumentError(error_string)`, caught by the try-catch below
|
||||||
|
|
||||||
4. **Run `beforeToolCall` hook** — if `config.beforeToolCall !== nothing`
|
4. **Run `beforeToolCall` hook** — if `config.beforeToolCall !== nothing`
|
||||||
- Passes `assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context)` and `signal`
|
- Passes `beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context)` and `signal`
|
||||||
- Hook can return `nothing` (proceed), or `Dict(:block => true, :reason => "...")` (reject)
|
- Hook can return `nothing` (proceed), or `Dict(:block => true, :reason => "...")` (reject)
|
||||||
- If `signal.aborted == true` → `immediateOutcome(createErrorToolResult("Operation aborted"), true)`
|
- If `signal.aborted == true` → `immediateOutcome(createErrorToolResult("Operation aborted"), true)`
|
||||||
- If `before.block == true` → `immediateOutcome(createErrorToolResult(get(before, :reason, "blocked")), true)`
|
- If `before.block == true` → `immediateOutcome(createErrorToolResult(get(before, :reason, "blocked")), true)`
|
||||||
@@ -719,10 +722,10 @@ function finalizeExecutedToolCall(
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
|
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
|
||||||
- Passes `afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
||||||
- Hook can mutate the result:
|
- Hook can mutate the result:
|
||||||
```julia
|
```julia
|
||||||
after = config.afterToolCall(afterCtx(...))
|
after = config.afterToolCall(afterToolCallContext(...))
|
||||||
if after !== nothing
|
if after !== nothing
|
||||||
result = merge(result, dict(
|
result = merge(result, dict(
|
||||||
:content => get(after, :content, result.content),
|
:content => get(after, :content, result.content),
|
||||||
@@ -1143,8 +1146,8 @@ The `agentEventSink` function is a user-provided callback that receives all even
|
|||||||
| `prepareContext` | `(state::agentState) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
| `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 |
|
| `formatMsgForLLM` | `(ctx::agentContext) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||||
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
||||||
| `beforeToolCall` | `(msgCtx::assistantMsgCtx, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
|
| `beforeToolCall` | `(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
|
||||||
| `afterToolCall` | `(afterCtx::afterCtx, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
|
| `afterToolCall` | `(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
|
||||||
| `agentEventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
|
| `agentEventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
|
||||||
|
|
||||||
### `beforeToolCall` Hook
|
### `beforeToolCall` Hook
|
||||||
@@ -1154,7 +1157,7 @@ The `agentEventSink` function is a user-provided callback that receives all even
|
|||||||
```julia
|
```julia
|
||||||
if config.beforeToolCall !== nothing
|
if config.beforeToolCall !== nothing
|
||||||
before = config.beforeToolCall(
|
before = config.beforeToolCall(
|
||||||
assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal
|
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context), signal
|
||||||
)
|
)
|
||||||
if signal !== nothing && signal.aborted
|
if signal !== nothing && signal.aborted
|
||||||
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
||||||
@@ -1179,7 +1182,7 @@ end
|
|||||||
if config.afterToolCall !== nothing
|
if config.afterToolCall !== nothing
|
||||||
try
|
try
|
||||||
after = config.afterToolCall(
|
after = config.afterToolCall(
|
||||||
afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
||||||
)
|
)
|
||||||
if after !== nothing
|
if after !== nothing
|
||||||
result = merge(result, dict(
|
result = merge(result, dict(
|
||||||
@@ -1282,9 +1285,9 @@ The framework supports tools that modify the tool system itself at runtime.
|
|||||||
|
|
||||||
### `listTool` — Discover Available Tools
|
### `listTool` — Discover Available Tools
|
||||||
|
|
||||||
**Source:** `toolRegistry.jl:54-82`
|
**Source:** `toolRegistry.jl:55-82`
|
||||||
|
|
||||||
Each `ToolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. Primarily 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. `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`.
|
||||||
|
|
||||||
### Self-Tooling Workflow
|
### Self-Tooling Workflow
|
||||||
|
|
||||||
@@ -1324,7 +1327,7 @@ Each `ToolStore` gets its own `listTool` instance bound to that store via `listT
|
|||||||
|
|
||||||
```
|
```
|
||||||
USER SENDS MESSAGE
|
USER SENDS MESSAGE
|
||||||
└─> run_agent(agent, "What's the weather in Tokyo?")
|
└─> runAgent(agent, "What's the weather in Tokyo?")
|
||||||
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
||||||
|
|
||||||
|
|
||||||
@@ -1427,7 +1430,7 @@ LOOP ITERATION 2 — LLM RETURNS FINAL TEXT RESPONSE
|
|||||||
|
|
||||||
AGENT LOOP: SEND RESPONSE TO USER
|
AGENT LOOP: SEND RESPONSE TO USER
|
||||||
└─> put!(agent.outputChannel, final_response)
|
└─> put!(agent.outputChannel, final_response)
|
||||||
└─> take_response(agent) → assistantMessage("The weather in Tokyo is sunny, 22°C.")
|
└─> takeResponse(agent) → assistantMessage("The weather in Tokyo is sunny, 22°C.")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1603,8 +1606,8 @@ All functions in the file are scoped under `_tool_myTool`, preventing name colli
|
|||||||
| `agentState` | `type.jl:310` | Mutable runtime state (systemPrompt, model, tools, messages, pendingToolCalls, activeRun, errorMessage) |
|
| `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 (tools, beforeToolCall, afterToolCall, toolExecution) |
|
||||||
| `abortSignal` | `type.jl:416` | Abort flag (`aborted::Bool`) |
|
| `abortSignal` | `type.jl:416` | Abort flag (`aborted::Bool`) |
|
||||||
| `assistantMsgCtx` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
|
| `beforeToolCallContext` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
|
||||||
| `afterCtx` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
|
| `afterToolCallContext` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
|
||||||
|
|
||||||
### Event Types
|
### Event Types
|
||||||
|
|
||||||
@@ -1,2 +1,77 @@
|
|||||||
# ── executeToolCalls() Julia pseudo code ──────────────────────────
|
i am not sure that's the case. see my NATS message log:
|
||||||
# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit
|
<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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+203
-94
@@ -1,14 +1,142 @@
|
|||||||
module agentCore
|
module agentCore
|
||||||
|
|
||||||
export _agent_loop, OpenAiToUserMessage
|
export yiemAgent, _agent_loop, OpenAiToUserMessage
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Base.Threads
|
DataFrames, Base.Threads
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils
|
using ..type, ..utils, ..toolRegistry
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
docstring
|
||||||
|
"""
|
||||||
|
mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||||
|
_state::agentState # Current state (prompt, model, messages, tools, etc.)
|
||||||
|
|
||||||
|
# user sends prompt message to agent. if agent is idle, it process user message right away.
|
||||||
|
# if agent is running, it process user message after the current tool call finished.
|
||||||
|
inputChannel::Channel
|
||||||
|
|
||||||
|
# Buffers messages the user sends while the agent is busy. Processed after all inputChannel
|
||||||
|
# messages are handled and the agent is idle (not using a tool call).
|
||||||
|
followUpChannel::Channel
|
||||||
|
|
||||||
|
# agent sends response message to user after processing all user messages in inputChannel
|
||||||
|
# and all followUp messages.
|
||||||
|
outputChannel::Channel
|
||||||
|
|
||||||
|
_agent_loop::Union{Task, Nothing} # agent loop running in the background
|
||||||
|
|
||||||
|
# Preprocess/transform messages and context (modify, filter, prune, inject context from memory,
|
||||||
|
# reorder, ...) for a single LLM call in _process_message()'s loop.
|
||||||
|
# returns new Vector{agentMessage}
|
||||||
|
prepareContext::Union{Function, Nothing}
|
||||||
|
|
||||||
|
# Convert prepareContext()'s new Vector{agentMessage} to LLM message format
|
||||||
|
formatMsgForLLM::Function
|
||||||
|
|
||||||
|
# A callable struct. Actually invoke the LLM to get a completion response.
|
||||||
|
# The LLM response comes back as an assistantMessage whose content is an array of content blocks.
|
||||||
|
# Each block has a type — "text", "thinking", or "toolCall".
|
||||||
|
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
||||||
|
llmCall
|
||||||
|
|
||||||
|
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
||||||
|
beforeToolCall::Union{Function, Nothing}
|
||||||
|
|
||||||
|
# Callback invoked after executing a tool call to sanitize tools output so the output is ready
|
||||||
|
# to be converted into toolResults message
|
||||||
|
afterToolCall::Union{Function, Nothing}
|
||||||
|
# prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn
|
||||||
|
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
|
||||||
|
sessionId::Union{String, Nothing} # Optional session identifier
|
||||||
|
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||||
|
parallelToolExecute::Bool # Default: false
|
||||||
|
agentEventSink # agent emits its status via this function
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create a new yiemAgent instance with a background loop task.
|
||||||
|
|
||||||
|
Spawns a background `@spawn` task that runs the agent loop, listening
|
||||||
|
on `inputChannel` and `followUpChannel` channels concurrently.
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `systemPrompt::String`: System prompt for the agent
|
||||||
|
- `model`: LLM model to use
|
||||||
|
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
|
||||||
|
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
|
||||||
|
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
|
||||||
|
- `llmCall::Function`: Function to invoke the LLM (required)
|
||||||
|
- `prepareContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`)
|
||||||
|
- `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`)
|
||||||
|
- `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`)
|
||||||
|
- `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`)
|
||||||
|
- `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`)
|
||||||
|
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
|
||||||
|
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||||
|
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
||||||
|
- `agentEventSink::Function`: Callback to receive agent events
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A new `yiemAgent` instance with an active background task
|
||||||
|
"""
|
||||||
|
function yiemAgent(
|
||||||
|
toolsFolderPath::String,
|
||||||
|
llmCall,
|
||||||
|
;
|
||||||
|
systemPrompt::String="You are helpful assistant.",
|
||||||
|
model=nothing,
|
||||||
|
messages::Vector{agentMessage}=agentMessage[],
|
||||||
|
prepareContext::Function=prepareContext,
|
||||||
|
formatMsgForLLM::Function=formatMsgForLLM,
|
||||||
|
beforeToolCall::Function=beforeToolCall,
|
||||||
|
afterToolCall::Function=afterToolCall,
|
||||||
|
# prepareNextTurn::Union{Function, Nothing}=nothing,
|
||||||
|
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
|
||||||
|
sessionId::Union{String, Nothing}=nothing,
|
||||||
|
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||||
|
parallelToolExecute::Bool=false,
|
||||||
|
agentEventSink=agentEventSink,
|
||||||
|
)
|
||||||
|
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||||
|
inputChannel = Channel(16)
|
||||||
|
followUp = Channel(32)
|
||||||
|
outputChannel = Channel(16)
|
||||||
|
|
||||||
|
# load tools from toolsFolderPath
|
||||||
|
toolStore1 = toolStore(name="myagent")
|
||||||
|
loadTools(toolStore1, toolsFolderPath)
|
||||||
|
|
||||||
|
# Create struct with a placeholder task, then spawn and replace it
|
||||||
|
agent = yiemAgent(
|
||||||
|
agentState(systemPrompt, model, getTools(toolStore1), messages),
|
||||||
|
inputChannel,
|
||||||
|
followUp,
|
||||||
|
outputChannel,
|
||||||
|
nothing, # placeholder — replaced below
|
||||||
|
prepareContext,
|
||||||
|
formatMsgForLLM,
|
||||||
|
llmCall,
|
||||||
|
beforeToolCall,
|
||||||
|
afterToolCall,
|
||||||
|
# prepareNextTurn,
|
||||||
|
# prepareNextTurnWithContext,
|
||||||
|
sessionId,
|
||||||
|
maxRetryDelayMs,
|
||||||
|
parallelToolExecute,
|
||||||
|
agentEventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spawn the background loop and attach it
|
||||||
|
agent._agent_loop = @spawn _agent_loop(agent)
|
||||||
|
|
||||||
|
return agent
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Private agent loop. Runs in a background `@spawn` task.
|
Private agent loop. Runs in a background `@spawn` task.
|
||||||
|
|
||||||
@@ -81,7 +209,8 @@ function _agent_loop(agent::yiemAgent)
|
|||||||
if isready(agent.inputChannel)
|
if isready(agent.inputChannel)
|
||||||
|
|
||||||
# message will be taken in _process_message()
|
# message will be taken in _process_message()
|
||||||
msg = fetch!(agent.inputChannel)
|
msg = fetch(agent.inputChannel)
|
||||||
|
agent.agentEventSink("new user msg")
|
||||||
else
|
else
|
||||||
yield()
|
yield()
|
||||||
end
|
end
|
||||||
@@ -107,9 +236,11 @@ function _agent_loop(agent::yiemAgent)
|
|||||||
|
|
||||||
# start _process_message loop
|
# start _process_message loop
|
||||||
if agent._state.activeRun == false
|
if agent._state.activeRun == false
|
||||||
|
agent.agentEventSink("_agent_loop 2")
|
||||||
# Dispatch message through the processing pipeline
|
# Dispatch message through the processing pipeline
|
||||||
processingTask = Threads.@spawn _process_message(agent)
|
processingTask = Threads.@spawn _process_message(agent)
|
||||||
agent._state.activeRun = true
|
agent._state.activeRun = true
|
||||||
|
agent.agentEventSink("_agent_loop 3")
|
||||||
end
|
end
|
||||||
|
|
||||||
# during agent runs, check followUp message after _process_message() is done
|
# during agent runs, check followUp message after _process_message() is done
|
||||||
@@ -173,6 +304,7 @@ julia> # Currently returns a placeholder echo response
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function _process_message(agent::yiemAgent)::assistantMessage
|
function _process_message(agent::yiemAgent)::assistantMessage
|
||||||
|
agent.agentEventSink("_process_message 1")
|
||||||
# loop until llmCall() response didn't use tool calls
|
# loop until llmCall() response didn't use tool calls
|
||||||
final_response = nothing
|
final_response = nothing
|
||||||
while true
|
while true
|
||||||
@@ -185,21 +317,26 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
|||||||
Dict(
|
Dict(
|
||||||
"type" => "image_url",
|
"type" => "image_url",
|
||||||
"image_url" => Dict("url" => "data:mime_type;base64,image2_base64_string")
|
"image_url" => Dict("url" => "data:mime_type;base64,image2_base64_string")
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
),
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||||
while isready(agent.inputChannel)
|
while isready(agent.inputChannel)
|
||||||
|
agent.agentEventSink("_process_message 2")
|
||||||
raw_msg = take!(agent.inputChannel)
|
raw_msg = take!(agent.inputChannel)
|
||||||
|
agent.agentEventSink("_process_message 3")
|
||||||
if raw_msg === :shutdown
|
if raw_msg === :shutdown
|
||||||
|
agent.agentEventSink("_process_message 4")
|
||||||
# Re-emit shutdown signal for the loop to handle
|
# Re-emit shutdown signal for the loop to handle
|
||||||
put!(agent.inputChannel, :shutdown)
|
put!(agent.inputChannel, :shutdown)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
agent.agentEventSink("_process_message 5")
|
||||||
user_msg = OpenAiToUserMessage(raw_msg)
|
user_msg = OpenAiToUserMessage(raw_msg)
|
||||||
push!(agent._state.messages, user_msg)
|
push!(agent._state.messages, user_msg)
|
||||||
|
agent.agentEventSink("_process_message 6")
|
||||||
end
|
end
|
||||||
|
|
||||||
# call agent.prepareContext()
|
# call agent.prepareContext()
|
||||||
@@ -208,10 +345,11 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
|||||||
# Call agent.formatMsgForLLM(agent._state) to format for LLM
|
# Call agent.formatMsgForLLM(agent._state) to format for LLM
|
||||||
formatted_messages = agent.formatMsgForLLM(preparedContext)
|
formatted_messages = agent.formatMsgForLLM(preparedContext)
|
||||||
|
|
||||||
|
agent.agentEventSink("_process_message 7")
|
||||||
# Call llmCall() (blocking — the task waits here)
|
# Call llmCall() (blocking — the task waits here)
|
||||||
|
error("debug marker")
|
||||||
response = agent.llmCall(formatted_messages)
|
response = agent.llmCall(formatted_messages)
|
||||||
|
agent.agentEventSink("_process_message 8")
|
||||||
error(5555555)
|
|
||||||
|
|
||||||
#WORKING Check if LLM used tool calls (inspect content for tool_call blocks)
|
#WORKING Check if LLM used tool calls (inspect content for tool_call blocks)
|
||||||
has_tool_calls = false
|
has_tool_calls = false
|
||||||
@@ -509,41 +647,42 @@ prepareToolCall(context, msg, tc, config, abortedSignal)
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function prepareToolCall(
|
function prepareToolCall(
|
||||||
context::agentContext,
|
context::agentContext,
|
||||||
assistantMsg::assistantMessage,
|
assistantMsg::assistantMessage,
|
||||||
toolCall::agentToolCall,
|
toolCall::agentToolCall,
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::Union{Nothing, abortSignal},
|
signal::Union{Nothing, abortSignal},
|
||||||
)::Union{preparedToolCall,immediateOutcome}
|
)::Union{preparedToolCall,immediateOutcome}
|
||||||
|
|
||||||
tool = get(context.tools, toolCall.name, nothing)
|
tool = get(context.tools, toolCall.name, nothing)
|
||||||
if tool === nothing
|
if tool === nothing
|
||||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
try
|
||||||
|
# 1. prepare arguments (tool-specific transform)
|
||||||
|
prepared = prepareToolCallArguments(tool, toolCall)
|
||||||
|
validatedArgs = validateToolArguments(tool, prepared)
|
||||||
|
|
||||||
|
# 2. beforeToolCall hook — can block
|
||||||
|
if config.beforeToolCall !== nothing
|
||||||
|
before = config.beforeToolCall(
|
||||||
|
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
|
||||||
|
signal
|
||||||
|
)
|
||||||
|
if signal !== nothing && signal.aborted
|
||||||
|
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
||||||
|
end
|
||||||
|
if before !== nothing && before.block
|
||||||
|
return immediateOutcome(
|
||||||
|
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
try
|
return preparedToolCall(tool, toolCall, validatedArgs)
|
||||||
# 1. prepare arguments (tool-specific transform)
|
catch err
|
||||||
prepared = prepareToolCallArguments(tool, toolCall)
|
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||||
validatedArgs = validateToolArguments(tool, prepared)
|
end
|
||||||
|
|
||||||
# 2. beforeToolCall hook — can block
|
|
||||||
if config.beforeToolCall !== nothing
|
|
||||||
before = config.beforeToolCall(
|
|
||||||
assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal
|
|
||||||
)
|
|
||||||
if signal !== nothing && signal.aborted
|
|
||||||
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
|
||||||
end
|
|
||||||
if before !== nothing && before.block
|
|
||||||
return immediateOutcome(
|
|
||||||
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return preparedToolCall(tool, toolCall, validatedArgs)
|
|
||||||
catch err
|
|
||||||
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── per-call execution ──────────────────────────────────────────
|
# ── per-call execution ──────────────────────────────────────────
|
||||||
@@ -681,61 +820,28 @@ function finalizeExecutedToolCall(
|
|||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
)::finalizedOutcome
|
)::finalizedOutcome
|
||||||
|
|
||||||
result = executed.result
|
result = executed.result
|
||||||
isError = executed.isError
|
isError = executed.isError
|
||||||
|
|
||||||
if config.afterToolCall !== nothing
|
if config.afterToolCall !== nothing
|
||||||
try
|
try
|
||||||
after = config.afterToolCall(
|
after = config.afterToolCall(
|
||||||
afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
||||||
)
|
)
|
||||||
if after !== nothing
|
if after !== nothing
|
||||||
result = merge(result, dict(:content=>get(after,:content,result.content),
|
result = merge(result, dict(:content=>get(after,:content,result.content),
|
||||||
:details=>get(after,:details,result.details),
|
:details=>get(after,:details,result.details),
|
||||||
:usage=>get(after,:usage,result.usage),
|
:usage=>get(after,:usage,result.usage),
|
||||||
:terminate=>get(after,:terminate,result.terminate)))
|
:terminate=>get(after,:terminate,result.terminate)))
|
||||||
isError = get(after, :isError, isError)
|
isError = get(after, :isError, isError)
|
||||||
end
|
end
|
||||||
catch err
|
catch err
|
||||||
result = createErrorToolResult(sprint(showerror, err))
|
result = createErrorToolResult(sprint(showerror, err))
|
||||||
isError = true
|
isError = true
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return finalizedOutcome(prep.toolCall, result, isError)
|
return finalizedOutcome(prep.toolCall, result, isError)
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
emitToolExecutionEnd(finalized, emit)
|
|
||||||
|
|
||||||
Emits the `toolExecutionEnd` event with the finalized outcome,
|
|
||||||
signalling to listeners that the tool call has completed.
|
|
||||||
|
|
||||||
This event is part of the tool execution lifecycle:
|
|
||||||
`toolExecutionStart` → (zero or more `toolExecutionUpdate` events) →
|
|
||||||
`toolExecutionEnd`. Listeners (such as the TUI or logging systems)
|
|
||||||
use this lifecycle to track individual tool calls. The event carries
|
|
||||||
the final result so listeners have all the data they need without
|
|
||||||
requiring external state lookups.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `finalized::finalizedOutcome`: The finalized outcome to report
|
|
||||||
- `emit::Function`: Event emitter
|
|
||||||
|
|
||||||
# Notes
|
|
||||||
- Part of a three-event lifecycle per tool call
|
|
||||||
- Carries the complete result so listeners need no external lookups
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
# Emits a single event; returns nothing
|
|
||||||
emitToolExecutionEnd(finalized, emit)
|
|
||||||
# (emit receives toolExecEndEvent("call_1", "search_wine", result, false))
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
|
|
||||||
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
|
||||||
finalized.result, finalized.isError))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── sequential execution ────────────────────────────────────────
|
# ── sequential execution ────────────────────────────────────────
|
||||||
@@ -816,7 +922,8 @@ function executeToolCallsSequential(
|
|||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
end
|
end
|
||||||
|
|
||||||
emitToolExecutionEnd(finalized, emit)
|
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||||
|
finalized.result, finalized.isError))
|
||||||
push!(messages, createToolResultMessage(finalized))
|
push!(messages, createToolResultMessage(finalized))
|
||||||
push!(finalizedCalls, finalized)
|
push!(finalizedCalls, finalized)
|
||||||
|
|
||||||
@@ -903,13 +1010,15 @@ function executeToolCallsParallel(
|
|||||||
|
|
||||||
if prep isa immediateOutcome
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
emitToolExecutionEnd(finalized, emit)
|
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||||
|
finalized.result, finalized.isError))
|
||||||
push!(entries, finalized)
|
push!(entries, finalized)
|
||||||
else
|
else
|
||||||
task = task() do
|
task = task() do
|
||||||
executed = executePreparedToolCall(prep, signal, emit)
|
executed = executePreparedToolCall(prep, signal, emit)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
emitToolExecutionEnd(finalized, emit)
|
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||||
|
finalized.result, finalized.isError))
|
||||||
return finalized
|
return finalized
|
||||||
end
|
end
|
||||||
schedule(task)
|
schedule(task)
|
||||||
|
|||||||
+14
-15
@@ -5,12 +5,11 @@ export prompt
|
|||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames
|
DataFrames
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils
|
using ..type, ..utils, ..agentCore, ..toolRegistry
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Send a message to the agent's input channel.
|
Send a message to the agent's input channel.
|
||||||
|
|
||||||
@@ -25,16 +24,16 @@ The agent processes messages from `inputChannel` in the background task.
|
|||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- Use `take_response(agent)` to receive the agent's response after sending a message.
|
- Use `takeResponse(agent)` to receive the agent's response after sending a message.
|
||||||
- Use `follow_up(agent, msg)` to send messages while the agent is still processing.
|
- Use `followUp(agent, msg)` to send messages while the agent is still processing.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> run_agent(agent, "Hello!")
|
julia> runAgent(agent, "Hello!")
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function run_agent(agent::yiemAgent, msg)
|
function runAgent(agent::yiemAgent, msg)
|
||||||
put!(agent.inputChannel, msg)
|
put!(agent.inputChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
@@ -51,15 +50,15 @@ Blocks until the agent sends a response.
|
|||||||
- An `assistantMessage` instance representing the agent's response
|
- An `assistantMessage` instance representing the agent's response
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- Use `run_agent(agent, msg)` to send a message before calling this function.
|
- Use `runAgent(agent, msg)` to send a message before calling this function.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> response = take_response(agent)
|
julia> response = takeResponse(agent)
|
||||||
assistantMessage(...)
|
assistantMessage(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function take_response(agent::yiemAgent)
|
function takeResponse(agent::yiemAgent)
|
||||||
return take!(agent.outputChannel)
|
return take!(agent.outputChannel)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -77,17 +76,17 @@ and before any tool call results are sent.
|
|||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- Use `run_agent(agent, msg)` for the primary message and `follow_up(agent, msg)` for additional
|
- Use `runAgent(agent, msg)` for the primary message and `followUp(agent, msg)` for additional
|
||||||
messages while the agent is processing.
|
messages while the agent is processing.
|
||||||
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> follow_up(agent, "Also consider red wines")
|
julia> followUp(agent, "Also consider red wines")
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function follow_up(agent::yiemAgent, msg)
|
function followUp(agent::yiemAgent, msg)
|
||||||
put!(agent.followUpChannel, msg)
|
put!(agent.followUpChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
@@ -105,16 +104,16 @@ then closes all channels (`inputChannel`, `outputChannel`, `followUpChannel`).
|
|||||||
- `nothing`
|
- `nothing`
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- After calling `stop_agent`, the agent is no longer usable. A new agent must be created
|
- After calling `stopAgent`, the agent is no longer usable. A new agent must be created
|
||||||
for further interaction.
|
for further interaction.
|
||||||
- If the background task throws a `TaskFailedException`, it is rethrown.
|
- If the background task throws a `TaskFailedException`, it is rethrown.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> stop_agent(agent)
|
julia> stopAgent(agent)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function stop_agent(agent::yiemAgent)
|
function stopAgent(agent::yiemAgent)
|
||||||
put!(agent.inputChannel, :shutdown)
|
put!(agent.inputChannel, :shutdown)
|
||||||
try
|
try
|
||||||
fetch(agent._agent_loop)
|
fetch(agent._agent_loop)
|
||||||
|
|||||||
+84
-56
@@ -1,6 +1,6 @@
|
|||||||
module toolRegistry
|
module toolRegistry
|
||||||
|
|
||||||
export ToolStore, loadTools, registerTool, getTools, clearTools, listTool
|
export toolStore, loadTools, registerTool, getTools, clearTools, listTool
|
||||||
|
|
||||||
using Dates
|
using Dates
|
||||||
using JSON, DataStructures
|
using JSON, DataStructures
|
||||||
@@ -9,47 +9,65 @@ using ..type
|
|||||||
"""
|
"""
|
||||||
Per-agent isolated tool storage.
|
Per-agent isolated tool storage.
|
||||||
|
|
||||||
Each agent gets its own `ToolStore` so tool registration is independent —
|
Each agent gets its own `toolStore` so tool registration is independent —
|
||||||
`registerTool(store, tool)` only affects that agent's tool set.
|
`registerTool(store, tool)` only affects that agent's tool set.
|
||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
||||||
- `name::String` — identifier for debugging/logs
|
- `name::String` — identifier for debugging/logs
|
||||||
"""
|
"""
|
||||||
struct ToolStore
|
struct toolStore
|
||||||
tools::OrderedDict{String, agentTool}
|
tools::OrderedDict{String, agentTool}
|
||||||
name::String
|
name::String
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create a new isolated tool store.
|
toolStore(; name="default") -> toolStore
|
||||||
|
|
||||||
|
Create a new empty tool store.
|
||||||
|
|
||||||
# Keyword Arguments
|
# Keyword Arguments
|
||||||
- `name::String`: Identifier for this store (default: "default")
|
- `name::String`: Display name for logging (default: `"default"`)
|
||||||
|
|
||||||
# Examples
|
# Example
|
||||||
```julia
|
```julia
|
||||||
store = ToolStore(name="agent1")
|
julia> store = toolStore(name="agent1")
|
||||||
tools = loadTools(store, "src/tools")
|
toolStore(OrderedDict{String, agentTool}(), "agent1")
|
||||||
registerTool(store, my_tool)
|
|
||||||
agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store)
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function ToolStore(; name::String="default")::ToolStore
|
function toolStore(; name::String="default")::toolStore
|
||||||
ToolStore(OrderedDict{String, agentTool}(), name)
|
toolStore(OrderedDict{String, agentTool}(), name)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
List tool definition — lets the agent query available tools for collision detection
|
listTool(store::toolStore) -> agentTool
|
||||||
when creating new tools via writeTool.
|
|
||||||
|
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
|
||||||
|
at runtime.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store::ToolStore`: The tool store to list from
|
- `store`: The tool store whose tools will be listed when the tool runs
|
||||||
|
|
||||||
Each `ToolStore` gets its own `listTool` instance bound to that store,
|
# Example
|
||||||
so each agent sees only its own tools.
|
```julia
|
||||||
|
julia> store = toolStore(name="agent1");
|
||||||
|
|
||||||
|
julia> loadTools(store, "src/tools") # auto-registers listTools
|
||||||
|
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||||
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
|
|
||||||
|
julia> tools = getTools(store)
|
||||||
|
OrderedDict{String, agentTool} with 4 entries:
|
||||||
|
"getWeather" => agentTool(...)
|
||||||
|
"getTime" => agentTool(...)
|
||||||
|
"writeTool" => agentTool(...)
|
||||||
|
"listTools" => agentTool(...)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function listTool(store::ToolStore)::agentTool
|
function listTool(store::toolStore)::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "listTools",
|
name = "listTools",
|
||||||
label = "List Tools",
|
label = "List Tools",
|
||||||
@@ -80,37 +98,38 @@ function listTool(store::ToolStore)::agentTool
|
|||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Load all tool modules from a directory into a specific ToolStore.
|
Load `.jl` tool files from `dir` into `store`, then auto-register
|
||||||
|
`listTool` so the LLM can discover available tools at runtime.
|
||||||
|
|
||||||
Scans `dir` for `.jl` files. Each file must define a function named
|
Each `.jl` file must define `function getTool()::agentTool ... end`.
|
||||||
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
Files are sorted alphabetically for deterministic registration order.
|
||||||
registration order is deterministic.
|
Each file is loaded into its own Julia submodule to avoid name collisions.
|
||||||
|
|
||||||
Each `.jl` file is loaded into its own **submodule** so that all functions
|
|
||||||
defined in the file (`validateRequiredArgs`, `prepareArguments`, `executeTool`,
|
|
||||||
and any helper functions) are namespaced and never collide with other tools.
|
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store::ToolStore`: The tool store to register tools into
|
- `store`: Tool store to populate
|
||||||
- `dir::String`: Directory path to scan for `.jl` tool files
|
- `dir`: Directory containing `.jl` tool files
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `OrderedDict{String, agentTool}`: All loaded tools keyed by name
|
- The same `store.tools` dict (modified in place)
|
||||||
|
|
||||||
# Errors
|
# Errors
|
||||||
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
- Throws `ArgumentError` if `dir` does not exist or a file lacks `getTool()`
|
||||||
|
|
||||||
# Examples
|
# Example
|
||||||
```julia
|
```julia
|
||||||
julia> store = ToolStore(name="agent1")
|
julia> store = toolStore(name="agent1");
|
||||||
julia> tools = loadTools(store, "src/tools")
|
|
||||||
|
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:
|
OrderedDict{String, agentTool} with 3 entries:
|
||||||
"getWeather" => agentTool(...)
|
"getWeather" => agentTool(...)
|
||||||
"getTime" => agentTool(...)
|
"getTime" => agentTool(...)
|
||||||
"listTools" => agentTool(...)
|
"listTools" => agentTool(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool}
|
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
||||||
if !isdir(dir)
|
if !isdir(dir)
|
||||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||||
end
|
end
|
||||||
@@ -168,73 +187,82 @@ function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
registerTool(store, listTool(store))
|
||||||
return store.tools
|
return store.tools
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Register a single agentTool into a specific ToolStore.
|
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||||
|
|
||||||
|
Add `tool` to `store`, overwriting any existing tool with the same name.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store::ToolStore`: The tool store to register into
|
- `store`: Tool store to modify
|
||||||
- `tool::agentTool`: The tool to register
|
- `tool`: The `agentTool` to register
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `OrderedDict{String, agentTool}`: Updated tool dict for this store
|
- The same `store.tools` dict (modified in place)
|
||||||
|
|
||||||
# Examples
|
# Example
|
||||||
```julia
|
```julia
|
||||||
julia> store = ToolStore(name="agent1")
|
julia> store = toolStore(name="agent1");
|
||||||
julia> registerTool(store, my_tool)
|
|
||||||
[toolRegistry:agent1] Registered tool: my_tool
|
julia> registerTool(store, listTool(store))
|
||||||
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
|
OrderedDict{String, agentTool} with 1 entry:
|
||||||
|
"listTools" => agentTool(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function registerTool(store::ToolStore, tool::agentTool)::OrderedDict{String, agentTool}
|
function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
|
||||||
store.tools[tool.name] = tool
|
store.tools[tool.name] = tool
|
||||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||||
return store.tools
|
return store.tools
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Get the registered tools from a specific ToolStore.
|
Return the tools registered in `store`.
|
||||||
|
|
||||||
Returns the internal `OrderedDict` directly — O(1) lookup by name,
|
The returned dict is the **same object** stored inside `store` — mutations
|
||||||
ordered iteration preserving registration order.
|
to it (e.g. via `registerTool`) are visible through subsequent calls.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store::ToolStore`: The tool store to query
|
- `store`: Tool store to query
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
||||||
|
|
||||||
# Examples
|
# Example
|
||||||
```julia
|
```julia
|
||||||
julia> getTools(store)
|
julia> tools = getTools(store)
|
||||||
OrderedDict{String, agentTool} with 3 entries:
|
OrderedDict{String, agentTool} with 2 entries:
|
||||||
"listTools" => agentTool(...)
|
|
||||||
"getWeather" => agentTool(...)
|
"getWeather" => agentTool(...)
|
||||||
"getTime" => agentTool(...)
|
"getTime" => agentTool(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function getTools(store::ToolStore)::OrderedDict{String, agentTool}
|
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
||||||
return store.tools
|
return store.tools
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Clear all registered tools from a specific ToolStore.
|
Remove all tools from `store`.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store::ToolStore`: The tool store to clear
|
- `store`: Tool store to clear
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `nothing`
|
- `nothing`
|
||||||
|
|
||||||
# Examples
|
# Example
|
||||||
```julia
|
```julia
|
||||||
julia> clearTools(store)
|
julia> clearTools(store)
|
||||||
[toolRegistry:agent1] Registry cleared
|
[toolRegistry:agent1] Registry cleared
|
||||||
|
nothing
|
||||||
|
|
||||||
|
julia> getTools(store)
|
||||||
|
OrderedDict{String, agentTool} with 0 entries
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function clearTools(store::ToolStore)::Nothing
|
function clearTools(store::toolStore)::Nothing
|
||||||
empty!(store.tools)
|
empty!(store.tools)
|
||||||
println("[$(store.name)] Registry cleared")
|
println("[$(store.name)] Registry cleared")
|
||||||
return nothing
|
return nothing
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ Execute the getTime tool.
|
|||||||
|
|
||||||
Returns mock time data for the given timezone or city.
|
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 executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
|
onPartialResult::Function)::agentToolResult
|
||||||
tz = get(args, "timezone", nothing)
|
tz = get(args, "timezone", nothing)
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
if tz !== nothing
|
if tz !== nothing
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Execute the getWeather tool.
|
|||||||
|
|
||||||
Returns mock weather data for the given city and temperature units.
|
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
|
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
|
onPartialResult::Function)::agentToolResult
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
units = get(args, "units", "celsius")
|
units = get(args, "units", "celsius")
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
temp = units == "fahrenheit" ? "72" : "22"
|
||||||
|
|||||||
+26
-158
@@ -13,8 +13,8 @@
|
|||||||
# Context types
|
# Context types
|
||||||
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
||||||
# Loop & execution types
|
# Loop & execution types
|
||||||
agentLoopConfig, abortSignal, agentToolResult,
|
agentLoopConfig, abortSignal, agentToolResult,beforeToolCallContext,
|
||||||
assistantMsgCtx, afterCtx,
|
beforeToolCallResult, afterToolCallContext,
|
||||||
# Event types
|
# Event types
|
||||||
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
|
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
|
||||||
# Agent
|
# Agent
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
||||||
agentToolCallBatch,
|
agentToolCallBatch,
|
||||||
# Functions (defined elsewhere)
|
# Functions (defined elsewhere)
|
||||||
run_agent, take_response, follow_up, stop_agent
|
runAgent, takeResponse, followUp, stopAgent
|
||||||
|
|
||||||
|
|
||||||
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
||||||
@@ -144,7 +144,7 @@ assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "en
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function assistantMessage(; role="assistant", content=Vector{messageContent}(),
|
function assistantMessage(; role="assistant", content=Vector{messageContent}(),
|
||||||
api="", provider="", model="", usage=llmUsage(0, 0), stopReason="end_turn",
|
api="", provider="", model=nothing, usage=llmUsage(0, 0), stopReason="end_turn",
|
||||||
errorMessage=nothing, timestamp=now())
|
errorMessage=nothing, timestamp=now())
|
||||||
return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp)
|
return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp)
|
||||||
end
|
end
|
||||||
@@ -309,7 +309,7 @@ end
|
|||||||
|
|
||||||
mutable struct agentState # Mutable runtime state of an agent
|
mutable struct agentState # Mutable runtime state of an agent
|
||||||
systemPrompt::String # System prompt for the agent
|
systemPrompt::String # System prompt for the agent
|
||||||
model::llmModel # LLM model to use
|
model::Union{llmModel, Nothing} # LLM model to use
|
||||||
tools::OrderedDict{String, agentTool} # Available tools keyed by name, insertion-ordered
|
tools::OrderedDict{String, agentTool} # Available tools keyed by name, insertion-ordered
|
||||||
|
|
||||||
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
|
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
|
||||||
@@ -341,21 +341,21 @@ julia> state = agentState(systemPrompt="You are a helpful assistant")
|
|||||||
agentState("You are a helpful assistant", OrderedDict{String, agentTool}(), agentMessage[], String[], nothing)
|
agentState("You are a helpful assistant", OrderedDict{String, agentTool}(), agentMessage[], String[], nothing)
|
||||||
"""
|
"""
|
||||||
function agentState(
|
function agentState(
|
||||||
systemPrompt::String="",
|
systemPrompt::String="",
|
||||||
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[],
|
model=llmModel("model_1", "unknown", "unknown", "", false, String[],
|
||||||
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||||
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||||
messages::Vector{agentMessage}=agentMessage[],
|
messages::Vector{agentMessage}=agentMessage[],
|
||||||
)
|
)
|
||||||
agentState(
|
agentState(
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
model,
|
model,
|
||||||
deepcopy(tools),
|
deepcopy(tools),
|
||||||
deepcopy(messages),
|
deepcopy(messages),
|
||||||
Vector{String}(),
|
Vector{String}(),
|
||||||
false,
|
false,
|
||||||
nothing,
|
nothing,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -442,13 +442,18 @@ Context passed to the `beforeToolCall` hook.
|
|||||||
- `args::Dict{String,Any}`: Validated tool arguments
|
- `args::Dict{String,Any}`: Validated tool arguments
|
||||||
- `context::agentContext`: Current conversation context
|
- `context::agentContext`: Current conversation context
|
||||||
"""
|
"""
|
||||||
struct assistantMsgCtx
|
struct beforeToolCallContext
|
||||||
message::assistantMessage
|
message::assistantMessage
|
||||||
toolCall::agentToolCall
|
toolCall::agentToolCall
|
||||||
args::Dict{String,Any}
|
args::Dict{String,Any}
|
||||||
context::agentContext
|
context::agentContext
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct beforeToolCallResult
|
||||||
|
block::Bool
|
||||||
|
reason::String
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Context passed to the `afterToolCall` hook.
|
Context passed to the `afterToolCall` hook.
|
||||||
|
|
||||||
@@ -460,7 +465,7 @@ Context passed to the `afterToolCall` hook.
|
|||||||
- `isError::Bool`: Whether execution resulted in an error
|
- `isError::Bool`: Whether execution resulted in an error
|
||||||
- `context::agentContext`: Current conversation context
|
- `context::agentContext`: Current conversation context
|
||||||
"""
|
"""
|
||||||
struct afterCtx
|
struct afterToolCallContext
|
||||||
message::assistantMessage
|
message::assistantMessage
|
||||||
toolCall::agentToolCall
|
toolCall::agentToolCall
|
||||||
args::Dict{String,Any}
|
args::Dict{String,Any}
|
||||||
@@ -521,143 +526,6 @@ end
|
|||||||
|
|
||||||
abstract type agent end
|
abstract type agent end
|
||||||
|
|
||||||
"""
|
|
||||||
docstring
|
|
||||||
"""
|
|
||||||
mutable struct yiemAgent <: agent # High-level agent wrapper
|
|
||||||
_state::agentState # Current state (prompt, model, messages, tools, etc.)
|
|
||||||
|
|
||||||
# user sends prompt message to agent. if agent is idle, it process user message right away.
|
|
||||||
# if agent is running, it process user message after the current tool call finished.
|
|
||||||
inputChannel::Channel
|
|
||||||
|
|
||||||
# Buffers messages the user sends while the agent is busy. Processed after all inputChannel
|
|
||||||
# messages are handled and the agent is idle (not using a tool call).
|
|
||||||
followUpChannel::Channel
|
|
||||||
|
|
||||||
# agent sends response message to user after processing all user messages in inputChannel
|
|
||||||
# and all followUp messages.
|
|
||||||
outputChannel::Channel
|
|
||||||
|
|
||||||
_agent_loop::Union{Task, Nothing} # agent loop running in the background
|
|
||||||
|
|
||||||
# Preprocess/transform messages and context (modify, filter, prune, inject context from memory,
|
|
||||||
# reorder, ...) for a single LLM call in _process_message()'s loop.
|
|
||||||
# returns new Vector{agentMessage}
|
|
||||||
prepareContext ::Union{Function, Nothing}
|
|
||||||
|
|
||||||
# Convert prepareContext()'s new Vector{agentMessage} to LLM message format
|
|
||||||
formatMsgForLLM::Function
|
|
||||||
|
|
||||||
# Actually invoke the LLM to get a completion response. The LLM response comes back as an
|
|
||||||
# assistantMessage whose content is an array of content blocks.
|
|
||||||
# Each block has a type — "text", "thinking", or "toolCall".
|
|
||||||
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
|
||||||
llmCall::Function
|
|
||||||
|
|
||||||
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
|
||||||
beforeToolCall::Union{Function, Nothing}
|
|
||||||
|
|
||||||
executeToolCalls::Function # execute tool calls ()
|
|
||||||
|
|
||||||
# Callback invoked after executing a tool call to sanitize tools output so the output is ready
|
|
||||||
# to be converted into toolResults message
|
|
||||||
afterToolCall::Union{Function, Nothing}
|
|
||||||
# prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn
|
|
||||||
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
|
|
||||||
sessionId::Union{String, Nothing} # Optional session identifier
|
|
||||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
|
||||||
parallelToolExecute::Bool # Default: false
|
|
||||||
agentEventSink::Function # agent emits its status via this function
|
|
||||||
_tool_store::Any # Reference to the ToolStore for runtime registration
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Create a new yiemAgent instance with a background loop task.
|
|
||||||
|
|
||||||
Spawns a background `@spawn` task that runs the agent loop, listening
|
|
||||||
on `inputChannel` and `followUpChannel` channels concurrently.
|
|
||||||
|
|
||||||
# Keyword Arguments
|
|
||||||
- `systemPrompt::String`: System prompt for the agent
|
|
||||||
- `model`: LLM model to use
|
|
||||||
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
|
|
||||||
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
|
|
||||||
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
|
|
||||||
- `llmCall::Function`: Function to invoke the LLM (required)
|
|
||||||
- `prepareContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`)
|
|
||||||
- `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`)
|
|
||||||
- `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`)
|
|
||||||
- `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`)
|
|
||||||
- `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`)
|
|
||||||
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
|
|
||||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
|
||||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
|
||||||
- `agentEventSink::Function`: Callback to receive agent events
|
|
||||||
- `tool_store::Union{Any, Nothing}`: ToolStore for runtime tool registration (default: `nothing`)
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- A new `yiemAgent` instance with an active background task
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
julia> store = ToolStore(name="agent1")
|
|
||||||
julia> tools = loadTools(store, "src/tools")
|
|
||||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
|
|
||||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
|
|
||||||
"""
|
|
||||||
function yiemAgent(
|
|
||||||
; systemPrompt::String="You are helpful assistant.",
|
|
||||||
model=nothing,
|
|
||||||
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
|
||||||
messages::Vector{agentMessage}=agentMessage[],
|
|
||||||
prepareContext::Union{Function, Nothing}=nothing,
|
|
||||||
formatMsgForLLM::Function=defaultformatMsgForLLM,
|
|
||||||
llmCall::Function,
|
|
||||||
beforeToolCall::Union{Function, Nothing}=nothing,
|
|
||||||
afterToolCall::Union{Function, Nothing}=nothing,
|
|
||||||
# prepareNextTurn::Union{Function, Nothing}=nothing,
|
|
||||||
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
|
|
||||||
sessionId::Union{String, Nothing}=nothing,
|
|
||||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
|
||||||
parallelToolExecute::Bool=false,
|
|
||||||
agentEventSink::Function,
|
|
||||||
tool_store::Union{Any, Nothing}=nothing,
|
|
||||||
)
|
|
||||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
|
||||||
inputChannel = Channel(16)
|
|
||||||
followUp = Channel(32)
|
|
||||||
outputChannel = Channel(16)
|
|
||||||
|
|
||||||
# Create struct with a placeholder task, then spawn and replace it
|
|
||||||
agent = yiemAgent(
|
|
||||||
agentState(systemPrompt, model, tools, messages),
|
|
||||||
inputChannel,
|
|
||||||
followUp,
|
|
||||||
outputChannel,
|
|
||||||
nothing, # placeholder — replaced below
|
|
||||||
prepareContext,
|
|
||||||
formatMsgForLLM,
|
|
||||||
llmCall,
|
|
||||||
beforeToolCall,
|
|
||||||
afterToolCall,
|
|
||||||
# prepareNextTurn,
|
|
||||||
# prepareNextTurnWithContext,
|
|
||||||
sessionId,
|
|
||||||
maxRetryDelayMs,
|
|
||||||
parallelToolExecute,
|
|
||||||
agentEventSink,
|
|
||||||
tool_store,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Spawn the background loop and attach it
|
|
||||||
agent._agent_loop = @spawn _agent_loop(agent)
|
|
||||||
|
|
||||||
return agent
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
preparedToolCall(tool, toolCall, args)
|
preparedToolCall(tool, toolCall, args)
|
||||||
|
|
||||||
|
|||||||
+32
-2
@@ -1,7 +1,9 @@
|
|||||||
module utils
|
module utils
|
||||||
|
|
||||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI,
|
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
|
validateToolArguments, _userMessageToOpenAI,
|
||||||
|
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks,
|
||||||
|
beforeToolCall, afterToolCall, agentEventSink
|
||||||
|
|
||||||
using UUIDs, Dates, DataStructures, HTTP, JSON
|
using UUIDs, Dates, DataStructures, HTTP, JSON
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
@@ -218,6 +220,34 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
|||||||
return Dict("messages" => messages)
|
return Dict("messages" => messages)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||||
|
)::beforeToolCallResult
|
||||||
|
|
||||||
|
# final context check
|
||||||
|
|
||||||
|
# seek user approval via UI
|
||||||
|
|
||||||
|
# other check
|
||||||
|
|
||||||
|
return beforeToolCallResult(false, "N/A")
|
||||||
|
end
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
function afterToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||||
|
)::Union{agentToolResult, Nothing}
|
||||||
|
|
||||||
|
# modify context.result if needed and return agentToolResult
|
||||||
|
|
||||||
|
return nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
function agentEventSink(x)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Convert a userMessage to OpenAI message format.
|
Convert a userMessage to OpenAI message format.
|
||||||
|
|||||||
-401
@@ -1,401 +0,0 @@
|
|||||||
using JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64,
|
|
||||||
NATS, Base.Threads
|
|
||||||
using YiemAgent, GeneralUtils, msghandler
|
|
||||||
|
|
||||||
function text2text_instruct_llm(sender_id::String, openai_msg::Dict{String, Any})
|
|
||||||
payloads = [("msg", openai_msg, "dictionary")] # List of tuples
|
|
||||||
_, msg_envelope_json_str = msghandler.smartpack(
|
|
||||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
|
||||||
payloads;
|
|
||||||
sender_id=sender_id,
|
|
||||||
msg_purpose="text2text",
|
|
||||||
broker_url=config["nats_server_info"]["url"],
|
|
||||||
fileserver_url=config["externalservice"]["fileserver"]["url"])
|
|
||||||
|
|
||||||
reply = NATS.request(agent_conn,
|
|
||||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
|
||||||
msg_envelope_json_str, timeout=120)
|
|
||||||
|
|
||||||
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]["message"]["content"]
|
|
||||||
return llm_response
|
|
||||||
end
|
|
||||||
|
|
||||||
""" get a single text embedding from a LLM service
|
|
||||||
Example
|
|
||||||
text = ["hello"]
|
|
||||||
embedding = get_embedding(text)
|
|
||||||
"""
|
|
||||||
function get_embedding(text::AbstractArray{String})
|
|
||||||
documents_dict = Dict("documents" => text)
|
|
||||||
payloads = [("documents", documents_dict, "dictionary")]
|
|
||||||
_, msg_envelope_json_str = msghandler.smartpack(
|
|
||||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
|
||||||
payloads;
|
|
||||||
msg_purpose="embedding",
|
|
||||||
broker_url=config["nats_server_info"]["url"],
|
|
||||||
fileserver_url=config["externalservice"]["fileserver"]["url"])
|
|
||||||
|
|
||||||
reply = NATS.request(agent_conn,
|
|
||||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
|
||||||
msg_envelope_json_str, timeout=120)
|
|
||||||
incoming_env_json_str = String(reply.payload)
|
|
||||||
incoming_env = msghandler.smartunpack(incoming_env_json_str)
|
|
||||||
embedding_response = incoming_env["payloads"][1][2]
|
|
||||||
|
|
||||||
return embedding_response
|
|
||||||
end
|
|
||||||
|
|
||||||
""" sql = "SELECT * FROM wine;"
|
|
||||||
result = execute_sql_winedb(sql)
|
|
||||||
"""
|
|
||||||
function execute_sql_winedb(sql::T) where {T<:AbstractString}
|
|
||||||
host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':')
|
|
||||||
port = parse(Int, _port)
|
|
||||||
dbname = "winedb"
|
|
||||||
user = config["externalservice"]["sommpanion_db"]["user"]
|
|
||||||
password = config["externalservice"]["sommpanion_db"]["password"]
|
|
||||||
db_connection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password")
|
|
||||||
result = nothing
|
|
||||||
try
|
|
||||||
result = LibPQ.execute(db_connection, sql)
|
|
||||||
catch e
|
|
||||||
LibPQ.close(db_connection)
|
|
||||||
end
|
|
||||||
|
|
||||||
LibPQ.close(db_connection)
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
""" find similar sql from vector database
|
|
||||||
sql = "SELECT * FROM wine;"
|
|
||||||
result, distance = similar_sql_vectordb(sql)
|
|
||||||
"""
|
|
||||||
function similar_sql_vectordb(sql::T; maxdistance::Number=0.2) where {T<:AbstractString}
|
|
||||||
tablename = "sqlllm_decision_repository"
|
|
||||||
# get embedding of the query
|
|
||||||
df = find_similar_text_from_vectordb(sql, tablename,
|
|
||||||
"function_input_embedding", execute_sql_vectordb)
|
|
||||||
# println(df[1, [:id, :function_output]])
|
|
||||||
row, col = size(df)
|
|
||||||
distance = row == 0 ? Inf : df[1, :distance]
|
|
||||||
if row != 0 && distance < maxdistance
|
|
||||||
# if there is usable SQL, return it.
|
|
||||||
output_b64 = df[1, :function_output_base64] # pick the closest match
|
|
||||||
output_str = String(base64decode(output_b64))
|
|
||||||
rowid = df[1, :id]
|
|
||||||
println("\n--| similar sql found. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
pprintln(output_str)
|
|
||||||
return (result=output_str, distance=distance)
|
|
||||||
else
|
|
||||||
println("\n--| similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
return (result=nothing, distance=nothing)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
""" insert query and sql into vector database
|
|
||||||
query = "get all wines from wine table"
|
|
||||||
sql = "SELECT * FROM wine;"
|
|
||||||
insert_sql_vectordb(query, sql)
|
|
||||||
"""
|
|
||||||
function insert_sql_vectordb(query::T1, SQL::T2; maxdistance::Number=3
|
|
||||||
) where {T1<:AbstractString, T2<:AbstractString}
|
|
||||||
|
|
||||||
tablename = "sqlllm_decision_repository"
|
|
||||||
# get embedding of the query
|
|
||||||
# query = state[:thoughtHistory][:question]
|
|
||||||
df = find_similar_text_from_vectordb(query, tablename,
|
|
||||||
"function_input_embedding", execute_sql_vectordb)
|
|
||||||
row, col = size(df)
|
|
||||||
distance = row == 0 ? Inf : df[1, :distance]
|
|
||||||
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
|
|
||||||
_query_embedding = get_embedding([query])
|
|
||||||
_query_embedding = GeneralUtils.dictify(_query_embedding)
|
|
||||||
# println("\n--- _query_embedding() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
# println(_query_embedding)
|
|
||||||
# println("---\n")
|
|
||||||
query_embedding = _query_embedding["data"][1]["embedding"]
|
|
||||||
query = replace(query, "'" => "")
|
|
||||||
sql_base64 = base64encode(SQL)
|
|
||||||
sql_ = replace(SQL, "'" => "")
|
|
||||||
|
|
||||||
sql =
|
|
||||||
"""
|
|
||||||
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
|
|
||||||
"""
|
|
||||||
# println("\n--| added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
# println(sql)
|
|
||||||
_ = execute_sql_vectordb(sql)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
""" execute sql against vectordb
|
|
||||||
sql = "SELECT * FROM wine;"
|
|
||||||
result = execute_sql_vectordb(sql)
|
|
||||||
"""
|
|
||||||
function execute_sql_vectordb(sql::T) where {T<:AbstractString}
|
|
||||||
host_url, _port = split(config["externalservice"]["sommpanion_vectordb"]["url"], ':')
|
|
||||||
port = parse(Int, _port)
|
|
||||||
dbname = config["externalservice"]["sommpanion_vectordb"]["dbname"]
|
|
||||||
user = config["externalservice"]["sommpanion_vectordb"]["user"]
|
|
||||||
password = config["externalservice"]["sommpanion_vectordb"]["password"]
|
|
||||||
DBconnection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password")
|
|
||||||
result = LibPQ.execute(DBconnection, sql)
|
|
||||||
close(DBconnection)
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
""" search similar decision llm made from vectordb
|
|
||||||
"""
|
|
||||||
function similar_sommelier_decision(recentevents::T1; maxdistance::Integer=3
|
|
||||||
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
|
|
||||||
|
|
||||||
tablename = "sommelier_decision_repository"
|
|
||||||
# find similar
|
|
||||||
df = find_similar_text_from_vectordb(recentevents, tablename,
|
|
||||||
"function_input_embedding", execute_sql_vectordb)
|
|
||||||
row, col = size(df)
|
|
||||||
distance = row == 0 ? Inf : df[1, :distance]
|
|
||||||
if row != 0 && distance < maxdistance
|
|
||||||
# if there is usable decision, return it.
|
|
||||||
rowid = df[1, :id]
|
|
||||||
println("\n--| found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
|
|
||||||
output_b64 = df[1, :function_output_base64] # pick the closest match
|
|
||||||
_output_str = String(base64decode(output_b64))
|
|
||||||
output = copy(JSON.read(_output_str))
|
|
||||||
return output
|
|
||||||
else
|
|
||||||
println("\n--| similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
|
|
||||||
return nothing
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
""" search similar text from vectordb
|
|
||||||
"""
|
|
||||||
function find_similar_text_from_vectordb(text::T1, tablename::T2, embeddingColumnName::T3,
|
|
||||||
vectorDB::Function; limit::Integer=1
|
|
||||||
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
|
|
||||||
# get embedding from LLM service
|
|
||||||
_embedding = get_embedding([text])
|
|
||||||
_embedding = _embedding["data"][1]["embedding"]
|
|
||||||
_embedding = "$_embedding"
|
|
||||||
|
|
||||||
embedding = _embedding[4:end]
|
|
||||||
|
|
||||||
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
|
|
||||||
sql = """
|
|
||||||
SELECT *, $embeddingColumnName <-> '$embedding' as distance
|
|
||||||
FROM $tablename
|
|
||||||
ORDER BY distance LIMIT $limit;
|
|
||||||
"""
|
|
||||||
response = vectorDB(sql)
|
|
||||||
df = DataFrame(response)
|
|
||||||
|
|
||||||
return df
|
|
||||||
end
|
|
||||||
|
|
||||||
""" insert decision llm made to vectordb
|
|
||||||
"""
|
|
||||||
function insert_sommelier_decision(recentevents::T1, decision::T2; maxdistance::Integer=5
|
|
||||||
) where {T1<:AbstractString, T2<:AbstractDict}
|
|
||||||
tablename = "sommelier_decision_repository"
|
|
||||||
# find similar
|
|
||||||
df = find_similar_text_from_vectordb(recentevents, tablename,
|
|
||||||
"function_input_embedding", execute_sql_vectordb)
|
|
||||||
row, col = size(df)
|
|
||||||
distance = row == 0 ? Inf : df[1, :distance]
|
|
||||||
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
|
|
||||||
_embedding = get_embedding([recentevents])[1]
|
|
||||||
recentevents_embedding = _embedding["data"][1]["embedding"]
|
|
||||||
recentevents = replace(recentevents, "'" => "")
|
|
||||||
decision_json = JSON.json(decision)
|
|
||||||
decision_base64 = base64encode(decision_json)
|
|
||||||
decision = replace(decision_json, "'" => "")
|
|
||||||
|
|
||||||
sql =
|
|
||||||
"""
|
|
||||||
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
|
|
||||||
"""
|
|
||||||
println("\n--| added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
|
|
||||||
println(sql)
|
|
||||||
_ = execute_sql_vectordb(sql)
|
|
||||||
else
|
|
||||||
println("--| similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
config = JSON.parsefile("./appconfig.json")
|
|
||||||
sessionId = "0"
|
|
||||||
backend_session_topic = "sommpanion.testsubject"
|
|
||||||
agent_ch = Channel(8)
|
|
||||||
agent_conn = NATS.connect(config["nats_server_info"]["url"])
|
|
||||||
|
|
||||||
sub2 = NATS.subscribe(agent_conn, backend_session_topic) do msg
|
|
||||||
put!(agent_ch, msg)
|
|
||||||
end
|
|
||||||
|
|
||||||
agent_context = YiemAgent.agentcontext(
|
|
||||||
text2text_instruct_llm,
|
|
||||||
get_embedding,
|
|
||||||
execute_sql_winedb,
|
|
||||||
similar_sql_vectordb,
|
|
||||||
insert_sql_vectordb,
|
|
||||||
similar_sommelier_decision,
|
|
||||||
insert_sommelier_decision
|
|
||||||
)
|
|
||||||
|
|
||||||
# can't instantiate
|
|
||||||
agent = YiemAgent.sommelier(
|
|
||||||
agent_context;
|
|
||||||
name="Janie",
|
|
||||||
id=sessionId, # agent instance id
|
|
||||||
retailername="Yiem Wine Ltd.",
|
|
||||||
llmFormatName=""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
image1_path = "test/large_image.png"
|
|
||||||
image1_bytes = read(image1_path)
|
|
||||||
image1_base64_string = base64encode(image1_bytes)
|
|
||||||
mime_type = "image/png"
|
|
||||||
data1_uri = "data:$(mime_type);base64,$(image1_base64_string)"
|
|
||||||
|
|
||||||
# 1. Read local file and encode to base64 string
|
|
||||||
image2_path = "test/small_image.png"
|
|
||||||
image2_bytes = read(image2_path)
|
|
||||||
image2_base64_string = base64encode(image2_bytes)
|
|
||||||
mime_type = "image/png"
|
|
||||||
data2_uri = "data:$(mime_type);base64,$(image2_base64_string)"
|
|
||||||
|
|
||||||
# 3. Construct payload with the Data URI
|
|
||||||
message = Dict(
|
|
||||||
"role" => "user",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "Do you know type of wine in the image?"),
|
|
||||||
Dict(
|
|
||||||
"type" => "image_url",
|
|
||||||
"image_url" => Dict("url" => data1_uri)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
result = YiemAgent.conversation(agent; userinput=message)
|
|
||||||
println("\n$result")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# message = Dict(
|
|
||||||
# "role" => "user",
|
|
||||||
# "content" => [
|
|
||||||
# Dict("type" => "text", "text" =>
|
|
||||||
# "
|
|
||||||
# เป็นงานเลี้ยงทั่วไป
|
|
||||||
# "),
|
|
||||||
# ]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# result = YiemAgent.conversation(agent; userinput=message)
|
|
||||||
# println("\n$result")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# message = Dict(
|
|
||||||
# "role" => "user",
|
|
||||||
# "content" => [
|
|
||||||
# Dict("type" => "text", "text" => "no thanks. that's all"),
|
|
||||||
# ]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# result = YiemAgent.conversation(agent; userinput=message)
|
|
||||||
# println("\n$result")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# message = Dict(
|
|
||||||
# "role" => "user",
|
|
||||||
# "content" => [
|
|
||||||
# Dict("type" => "text", "text" => "What about this wine?"),
|
|
||||||
# Dict(
|
|
||||||
# "type" => "image_url",
|
|
||||||
# "image_url" => Dict("url" => data2_uri)
|
|
||||||
# )
|
|
||||||
# ]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# result = YiemAgent.conversation(agent; userinput=message)
|
|
||||||
# println("\n$result")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+48
-9
@@ -6,12 +6,12 @@ using YiemAgent.type
|
|||||||
# Path to the real tools directory
|
# Path to the real tools directory
|
||||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||||
|
|
||||||
@testset "loadTools with ToolStore" begin
|
@testset "loadTools with toolStore" begin
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 1. loadTools throws on non-existent directory #
|
# 1. loadTools throws on non-existent directory #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
store = ToolStore(name="test1")
|
store = toolStore(name="test1")
|
||||||
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -26,24 +26,25 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 3. loadTools loads actual tool files from src/tools/ #
|
# 3. loadTools loads actual tool files from src/tools/ #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
store2 = ToolStore(name="test2")
|
store2 = toolStore(name="test2")
|
||||||
loaded = loadTools(store2, TOOLS_DIR)
|
loaded = loadTools(store2, TOOLS_DIR)
|
||||||
@test !isempty(loaded)
|
@test !isempty(loaded)
|
||||||
@test length(loaded) == 3
|
@test length(loaded) == 4 # 3 files + auto-registered listTools
|
||||||
|
|
||||||
names = [k for k in keys(loaded)]
|
names = [k for k in keys(loaded)]
|
||||||
@test "getTime" in names
|
@test "getTime" in names
|
||||||
@test "getWeather" in names
|
@test "getWeather" in names
|
||||||
@test "writeTool" in names
|
@test "writeTool" in names
|
||||||
|
@test "listTools" in names
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 4. loadTools returns tools sorted alphabetically by filename #
|
# 4. loadTools returns tools sorted alphabetically by filename #
|
||||||
# (getTime.jl < getWeather.jl < writeTool.jl) #
|
# (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
|
||||||
# because 'T' < 'W' in ASCII #
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@test collect(keys(loaded))[1] == "getTime"
|
@test collect(keys(loaded))[1] == "getTime"
|
||||||
@test collect(keys(loaded))[2] == "getWeather"
|
@test collect(keys(loaded))[2] == "getWeather"
|
||||||
@test collect(keys(loaded))[3] == "writeTool"
|
@test collect(keys(loaded))[3] == "writeTool"
|
||||||
|
@test collect(keys(loaded))[4] == "listTools"
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 5. Verify loaded tool fields are correct #
|
# 5. Verify loaded tool fields are correct #
|
||||||
@@ -102,7 +103,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
store3 = ToolStore(name="test3")
|
store3 = toolStore(name="test3")
|
||||||
registry_tools = getTools(store3)
|
registry_tools = getTools(store3)
|
||||||
@test isempty(registry_tools)
|
@test isempty(registry_tools)
|
||||||
|
|
||||||
@@ -153,8 +154,8 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 9. Per-store isolation — two stores don't share tools #
|
# 9. Per-store isolation — two stores don't share tools #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
storeA = ToolStore(name="isolationA")
|
storeA = toolStore(name="isolationA")
|
||||||
storeB = ToolStore(name="isolationB")
|
storeB = toolStore(name="isolationB")
|
||||||
|
|
||||||
registerTool(storeA, loaded["getTime"])
|
registerTool(storeA, loaded["getTime"])
|
||||||
registerTool(storeB, loaded["getWeather"])
|
registerTool(storeB, loaded["getWeather"])
|
||||||
@@ -171,3 +172,41 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
@test isempty(getTools(storeA))
|
@test isempty(getTools(storeA))
|
||||||
@test !isempty(getTools(storeB)) # storeB unaffected
|
@test !isempty(getTools(storeB)) # storeB unaffected
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@testset "listTool" begin
|
||||||
|
store = toolStore(name="test_list")
|
||||||
|
loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools
|
||||||
|
|
||||||
|
# loadTools auto-registers listTool
|
||||||
|
@test "listTools" in keys(loaded)
|
||||||
|
|
||||||
|
# listTool returns an agentTool, not a string or array
|
||||||
|
list_t = listTool(store)
|
||||||
|
@test list_t isa agentTool
|
||||||
|
@test list_t.name == "listTools"
|
||||||
|
@test list_t.label == "List Tools"
|
||||||
|
@test isempty(list_t.inputSchema["required"])
|
||||||
|
|
||||||
|
# Verify all tools appear (3 loaded + listTools = 4)
|
||||||
|
result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x)
|
||||||
|
@test result isa agentToolResult
|
||||||
|
@test result.content[1] isa textContent
|
||||||
|
@test occursin("listTools", result.content[1].text)
|
||||||
|
@test occursin("getWeather", result.content[1].text)
|
||||||
|
@test occursin("getTime", result.content[1].text)
|
||||||
|
@test occursin("writeTool", result.content[1].text)
|
||||||
|
@test result.details["count"] == 4
|
||||||
|
|
||||||
|
# Each listTool call creates an independent closure
|
||||||
|
storeB = toolStore(name="test_listB")
|
||||||
|
registerTool(storeB, loaded["getWeather"])
|
||||||
|
list_tB = listTool(storeB)
|
||||||
|
|
||||||
|
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
||||||
|
resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x)
|
||||||
|
|
||||||
|
@test occursin("getWeather", resultA.content[1].text)
|
||||||
|
@test occursin("getWeather", resultB.content[1].text)
|
||||||
|
@test occursin("getTime", resultA.content[1].text)
|
||||||
|
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user