update
This commit is contained in:
+33
-32
@@ -509,41 +509,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)
|
||||||
|
|
||||||
|
#WORKING 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 ──────────────────────────────────────────
|
||||||
@@ -687,7 +688,7 @@ function finalizeExecutedToolCall(
|
|||||||
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),
|
||||||
|
|||||||
+142
-1
@@ -5,11 +5,152 @@ 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, ..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
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
toolsFolderPath::String,
|
||||||
|
llmCall::Function,
|
||||||
|
;
|
||||||
|
systemPrompt::String="You are helpful assistant.",
|
||||||
|
model=nothing,
|
||||||
|
messages::Vector{agentMessage}=agentMessage[],
|
||||||
|
prepareContext::Function=prepareContext,
|
||||||
|
formatMsgForLLM::Function=formatMsgForLLM,
|
||||||
|
beforeToolCall::Function,
|
||||||
|
afterToolCall::Function,
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# load tools from toolsFolderPath
|
||||||
|
toolStore = YiemAgent.toolStore(name="myagent")
|
||||||
|
loadTools(toolStore, toolsFolderPath)
|
||||||
|
|
||||||
|
# Create struct with a placeholder task, then spawn and replace it
|
||||||
|
agent = yiemAgent(
|
||||||
|
agentState(systemPrompt, model, getTools(toolStore), 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Send a message to the agent's input channel.
|
Send a message to the agent's input channel.
|
||||||
|
|||||||
+9
-9
@@ -633,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)`
|
||||||
@@ -722,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),
|
||||||
@@ -1146,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
|
||||||
@@ -1157,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)
|
||||||
@@ -1182,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(
|
||||||
@@ -1606,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
|
||||||
|
|
||||||
|
|||||||
+9
-141
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -1,7 +1,8 @@
|
|||||||
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
|
||||||
|
|
||||||
using UUIDs, Dates, DataStructures, HTTP, JSON
|
using UUIDs, Dates, DataStructures, HTTP, JSON
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
@@ -219,6 +220,16 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)::beforeToolCallResult
|
||||||
|
# final context check
|
||||||
|
|
||||||
|
# seek user approval via UI
|
||||||
|
|
||||||
|
# other check
|
||||||
|
|
||||||
|
return beforeToolCallResult(false, "N/A")
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Convert a userMessage to OpenAI message format.
|
Convert a userMessage to OpenAI message format.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user