update
This commit is contained in:
+107
-90
@@ -208,25 +208,25 @@ function _agentLoop(agent::yiemAgent)
|
||||
while true
|
||||
while newUserMsg === nothing
|
||||
if isready(agent.inputChannel)
|
||||
agent.agentEventSink("_agentLoop 1")
|
||||
agent.agentEventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))")
|
||||
# agent process new user msg immediately after the current tool call finished.
|
||||
newUserMsg = take!(agent.inputChannel)
|
||||
agent.agentEventSink("new user msg")
|
||||
else
|
||||
# check followUp message after _processMessage() is done
|
||||
if typeof(processingTask) == Task && istaskdone(processingTask) == true
|
||||
agent.agentEventSink("_agentLoop 2")
|
||||
agent.agentEventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))")
|
||||
# if agent runs is done but followUpChannel has messages,
|
||||
# put new message in inputChannel instead
|
||||
if isready(agent.followUpChannel)
|
||||
agent.agentEventSink("_agentLoop 3")
|
||||
agent.agentEventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))")
|
||||
while isready(agent.followUpChannel)
|
||||
followUpMsg = take!(agent.followUpChannel)
|
||||
put!(agent.inputChannel, followUpMsg)
|
||||
end
|
||||
else # _processMessage() done and no followUp message.
|
||||
agent.agentEventSink("_agentLoop 4")
|
||||
result = fetch(processingTask)
|
||||
agent.agentEventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
|
||||
result = agent._state.messages[end]
|
||||
put!(agent.outputChannel, result)
|
||||
agent.agentEventSink(result.content[1].text)
|
||||
processingTask = nothing # reset
|
||||
@@ -259,7 +259,7 @@ function _agentLoop(agent::yiemAgent)
|
||||
else
|
||||
# spawn new _processMessage() if it is not already running.
|
||||
if processingTask === nothing
|
||||
agent.agentEventSink("_agentLoop 2")
|
||||
agent.agentEventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))")
|
||||
# Dispatch message through the processing pipeline
|
||||
processingTask = @spawn _processMessage(
|
||||
processMessageInputCh,
|
||||
@@ -325,8 +325,8 @@ function _processMessage(
|
||||
beforeToolCall::Union{Function, Nothing},
|
||||
afterToolCall::Union{Function, Nothing},
|
||||
parallelToolExecute::Bool,
|
||||
)::assistantMessage
|
||||
agentEventSink("_processMessage 1")
|
||||
)::Nothing
|
||||
agentEventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
|
||||
# loop until llmCall() response didn't use tool calls
|
||||
final_response = nothing
|
||||
|
||||
@@ -346,41 +346,46 @@ function _processMessage(
|
||||
while true
|
||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||
while isready(inputChannel)
|
||||
agentEventSink("_processMessage 2")
|
||||
agentEventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||
newUserMsg_openai = take!(inputChannel)
|
||||
agentEventSink("_processMessage 3")
|
||||
agentEventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||
if newUserMsg_openai === :shutdown
|
||||
agentEventSink("_processMessage 4")
|
||||
agentEventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
||||
# Re-emit shutdown signal for the loop to handle
|
||||
put!(inputChannel, :shutdown)
|
||||
break
|
||||
end
|
||||
agentEventSink("_processMessage 5")
|
||||
agentEventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
||||
push!(agentMsgHistory, newUserMsg)
|
||||
agentEventSink("_processMessage 6")
|
||||
agentEventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||
end
|
||||
agentEventSink("_processMessage 7")
|
||||
agentEventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||
# call prepareContext()
|
||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||
agentEventSink("_processMessage 8")
|
||||
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||
preparedContext = prepareContext(state, agentEventSink)
|
||||
agentEventSink("_processMessage 8")
|
||||
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||
# Call formatMessagesForLLM() to format for LLM
|
||||
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
|
||||
|
||||
agentEventSink("_processMessage 10")
|
||||
|
||||
agentEventSink("_processMessage 10 formattedMessages $formattedMessages")
|
||||
|
||||
""" response example
|
||||
response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")]))
|
||||
"""
|
||||
response = llmCall(formattedMessages)
|
||||
agentEventSink(string(response))
|
||||
agentEventSink(" llmCall " * string(response))
|
||||
|
||||
agentEventSink("_processMessage 11")
|
||||
agentEventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
|
||||
# Extract tool calls from LLM response content blocks
|
||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList")
|
||||
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
||||
agentEventSink(string(assistant_msg))
|
||||
agentEventSink("_processMessage 11-1")
|
||||
|
||||
# Add assistant message (tool calls or text) to history for next LLM turn
|
||||
push!(agentMsgHistory, assistant_msg)
|
||||
|
||||
if hasToolCalls && length(toolCallList) > 0
|
||||
# Build context and config for executeToolCalls
|
||||
@@ -392,20 +397,20 @@ function _processMessage(
|
||||
)
|
||||
|
||||
signal = abortSignal(false)
|
||||
agentEventSink("_processMessage 12")
|
||||
agentEventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
||||
|
||||
# call executeToolCalls()
|
||||
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||
signal, agentEventSink)
|
||||
agentEventSink("_processMessage 13")
|
||||
agentEventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
||||
|
||||
# save toolResults to messages
|
||||
for toolResult in toolResultBatch.messages
|
||||
push!(agentMsgHistory, toolResult)
|
||||
end
|
||||
agentEventSink("_processMessage 14")
|
||||
agentEventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||
if toolResultBatch.terminate
|
||||
agentEventSink("_processMessage 15")
|
||||
agentEventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
||||
# If toolResultBatch requested termination, build a final response
|
||||
final_content = [textContent("Tool execution completed.")]
|
||||
for toolResult in toolResultBatch.messages
|
||||
@@ -419,7 +424,7 @@ function _processMessage(
|
||||
end
|
||||
end
|
||||
end
|
||||
agentEventSink("_processMessage 16")
|
||||
agentEventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||
final_response = assistantMessage(
|
||||
role="assistant",
|
||||
content=final_content,
|
||||
@@ -434,17 +439,17 @@ function _processMessage(
|
||||
end,
|
||||
timestamp=now(),
|
||||
)
|
||||
push!(agentMsgHistory, final_response)
|
||||
break
|
||||
end
|
||||
else
|
||||
agentEventSink("_processMessage 17")
|
||||
# LLM did not use tool calls — this is the final response
|
||||
final_response = assistant_msg
|
||||
agentEventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||
# LLM did not use tool calls —
|
||||
break
|
||||
end
|
||||
end
|
||||
agentEventSink("_processMessage 18")
|
||||
return final_response
|
||||
agentEventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
@@ -516,24 +521,28 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
||||
end
|
||||
|
||||
"""
|
||||
_extractToolCalls(response) -> Tuple{Bool, Vector{agentToolCall}, assistantMessage}
|
||||
_extractToolCalls(response) -> Tuple{Bool, Vector{agentToolCall}, Union{assistantMessageToolCall, assistantMessage}}
|
||||
|
||||
Extracts tool calls from the LLM response and constructs an `assistantMessage`.
|
||||
Extracts tool calls from the LLM response and constructs a message object.
|
||||
Supports two response formats:
|
||||
|
||||
1. **Message format** (e.g. from LMStudio.jl / vLLM):
|
||||
`response["message"]["tool_calls"]` — array of tool call objects with
|
||||
`"type" => "function"`, `"function" => Dict("name" => ..., "arguments" => "...")`,
|
||||
and `"id"`. The `"arguments"` value is a JSON string that gets parsed via `JSON.parse`.
|
||||
`response["message"]["tool_calls"]` — array of tool call objects with
|
||||
`"type" => "function"`, `"function" => Dict("name" => ..., "arguments" => "...")`,
|
||||
and `"id"`. The `"arguments"` value is a JSON string that gets parsed via `JSON.parse`.
|
||||
|
||||
2. **Content blocks format** (e.g. from OpenAI API):
|
||||
`response.content` — array of content blocks. Blocks with `"type" => "tool_calls"`
|
||||
contain a `"tool_calls"` array in format 1. Blocks with `"type" => "tool_call"`
|
||||
have `"name"`, `"arguments"`, `"id"` at the top level (already-parsed args dict).
|
||||
`response.content` — array of content blocks. Blocks with `"type" => "tool_calls"`
|
||||
contain a `"tool_calls"` array in format 1. Blocks with `"type" => "tool_call"`
|
||||
have `"name"`, `"arguments"`, `"id"` at the top level (already-parsed args dict).
|
||||
|
||||
The `assistantMessage` is constructed from:
|
||||
- `reasoning_content` (string) → added as a `textContent` block
|
||||
- `response.content` blocks (text/ reasoning) → added to content
|
||||
When `hasToolCalls` is true, returns an `assistantMessageToolCall` with the tool calls
|
||||
and reasoning content. When `hasToolCalls` is false, returns an `assistantMessage`
|
||||
with text/content blocks from the response.
|
||||
|
||||
The message is constructed from:
|
||||
- `reasoning_content` (string) → stored in `reasoning` field (for tool calls) or `textContent` (for text)
|
||||
- `response.content` blocks (text/reasoning) → added to content for text responses
|
||||
- Top-level `api`, `provider`, `model`, `usage` → copied to the message
|
||||
- `finish_reason` → used as `stopReason`
|
||||
|
||||
@@ -541,7 +550,9 @@ The `assistantMessage` is constructed from:
|
||||
- `response`: LLM response object (Dict/JSON.Object or struct with `.content` field)
|
||||
|
||||
# Returns
|
||||
- `Tuple{Bool, Vector{agentToolCall}, assistantMessage}`: `(hasToolCalls, toolCallList, assistantMsg)`
|
||||
- `Tuple{Bool, Vector{agentToolCall}, Union{assistantMessageToolCall, assistantMessage}}`:
|
||||
`(hasToolCalls, toolCallList, message)` where message is `assistantMessageToolCall`
|
||||
when tool calls exist, `assistantMessage` otherwise
|
||||
|
||||
# Example response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")]))
|
||||
"""
|
||||
@@ -616,20 +627,6 @@ function _extractToolCalls(response)
|
||||
end
|
||||
|
||||
# ── Construct assistantMessage from response ──────────────────────
|
||||
# Check Format 1 nested message for reasoning_content, role, etc.
|
||||
reasoning = get(response, "reasoning_content", nothing)
|
||||
if reasoning === nothing && msg !== nothing && msg isa AbstractDict
|
||||
reasoning = get(msg, "reasoning_content", nothing)
|
||||
end
|
||||
if reasoning isa String
|
||||
reasoning_text = reasoning
|
||||
elseif reasoning isa textContent
|
||||
reasoning_text = reasoning.text
|
||||
else
|
||||
reasoning_text = ""
|
||||
end
|
||||
reasoning_block = !isempty(reasoning_text) ? [textContent(reasoning_text)] : textContent[]
|
||||
|
||||
finish_reason = get(response, "finish_reason", nothing)
|
||||
stop_reason = finish_reason isa AbstractString ? String(finish_reason) : "end_turn"
|
||||
|
||||
@@ -638,6 +635,21 @@ function _extractToolCalls(response)
|
||||
model = get(response, "model", nothing)
|
||||
usage = get(response, "usage", nothing)
|
||||
|
||||
# Collect reasoning from reasoning_content field (Format 1: Anthropic-style)
|
||||
reasoning_text = get(response, "reasoning_content", nothing)
|
||||
if reasoning_text === nothing && msg !== nothing && msg isa AbstractDict
|
||||
reasoning_text = get(msg, "reasoning_content", nothing)
|
||||
end
|
||||
if reasoning_text isa String
|
||||
reasoning_text = reasoning_text
|
||||
elseif reasoning_text isa textContent
|
||||
reasoning_text = reasoning_text.text
|
||||
else
|
||||
reasoning_text = ""
|
||||
end
|
||||
reasoning_content = !isempty(reasoning_text) ? [textContent(reasoning_text)] : textContent[]
|
||||
|
||||
# Collect content blocks from response.content array (Format 2: OpenAI-style)
|
||||
content_from_response = get(response, "content", nothing)
|
||||
content_blocks = Vector{messageContent}()
|
||||
if content_from_response isa Vector
|
||||
@@ -658,15 +670,11 @@ function _extractToolCalls(response)
|
||||
end
|
||||
end
|
||||
|
||||
# Combine content blocks and reasoning
|
||||
if !isempty(content_blocks) && !isempty(reasoning_block)
|
||||
all_content = vcat(reasoning_block, content_blocks)
|
||||
elseif !isempty(content_blocks)
|
||||
all_content = content_blocks
|
||||
elseif !isempty(reasoning_block)
|
||||
all_content = reasoning_block
|
||||
# Combine reasoning_content field + content array blocks, deduplicating reasoning
|
||||
if !isempty(reasoning_content)
|
||||
all_content = vcat(reasoning_content, content_blocks)
|
||||
else
|
||||
all_content = textContent[]
|
||||
all_content = content_blocks
|
||||
end
|
||||
|
||||
error_msg = get(response, "error_message", get(response, "errorMessage", nothing))
|
||||
@@ -684,17 +692,32 @@ function _extractToolCalls(response)
|
||||
end
|
||||
end
|
||||
|
||||
assistant_msg = assistantMessage(
|
||||
role = role,
|
||||
content = all_content,
|
||||
api = api isa AbstractString ? String(api) : "",
|
||||
provider = provider isa AbstractString ? String(provider) : "",
|
||||
model = model,
|
||||
usage = usage,
|
||||
stopReason = stop_reason,
|
||||
errorMessage = error_msg,
|
||||
timestamp = now(),
|
||||
)
|
||||
if hasToolCalls
|
||||
assistant_msg = assistantMessageToolCall(
|
||||
role = role,
|
||||
toolCalls = toolCallList,
|
||||
content = all_content,
|
||||
api = api isa AbstractString ? String(api) : "",
|
||||
provider = provider isa AbstractString ? String(provider) : "",
|
||||
model = model,
|
||||
usage = usage,
|
||||
stopReason = stop_reason,
|
||||
errorMessage = error_msg,
|
||||
timestamp = now(),
|
||||
)
|
||||
else
|
||||
assistant_msg = assistantMessage(
|
||||
role = role,
|
||||
content = all_content,
|
||||
api = api isa AbstractString ? String(api) : "",
|
||||
provider = provider isa AbstractString ? String(provider) : "",
|
||||
model = model,
|
||||
usage = usage,
|
||||
stopReason = stop_reason,
|
||||
errorMessage = error_msg,
|
||||
timestamp = now(),
|
||||
)
|
||||
end
|
||||
|
||||
return hasToolCalls, toolCallList, assistant_msg
|
||||
end
|
||||
@@ -846,7 +869,7 @@ prepareToolCall(context, msg, tc, config, abortedSignal)
|
||||
"""
|
||||
function prepareToolCall(
|
||||
context::agentContext,
|
||||
assistantMsg::assistantMessage,
|
||||
assistantMsg::assistantMessageToolCall,
|
||||
toolCall::agentToolCall,
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
@@ -863,10 +886,10 @@ function prepareToolCall(
|
||||
agentEventSink("prepareToolCall 3")
|
||||
# 1. prepare arguments (tool-specific transform)
|
||||
prepared = prepareToolCallArguments(tool, toolCall)
|
||||
agentEventSink(string(prepared.arguments))
|
||||
agentEventSink("prepared " * string(prepared.arguments))
|
||||
agentEventSink("prepareToolCall 4")
|
||||
validatedArgs = validateToolArguments(tool, prepared)
|
||||
agentEventSink(string(validatedArgs))
|
||||
agentEventSink("validatedArgs " * string(validatedArgs))
|
||||
agentEventSink("prepareToolCall 5")
|
||||
# 2. beforeToolCall hook — can block
|
||||
if config.beforeToolCall !== nothing
|
||||
@@ -950,14 +973,8 @@ function executePreparedToolCall(
|
||||
agentEventSink,
|
||||
)::executedOutcome
|
||||
agentEventSink("executePreparedToolCall 1")
|
||||
agentEventSink(prep.toolCall.id)
|
||||
agentEventSink(prep.toolCall.name)
|
||||
agentEventSink("executePreparedToolCall 2")
|
||||
s = string(prep.args)
|
||||
agentEventSink(s)
|
||||
agentEventSink("executePreparedToolCall 3")
|
||||
t = string(fieldnames(typeof(prep.tool)))
|
||||
agentEventSink("executePreparedToolCall 3-1 " * t)
|
||||
|
||||
try
|
||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
|
||||
@@ -1035,7 +1052,7 @@ finalizeExecutedToolCall(context, msg, prep, execFail, config, nothing)
|
||||
"""
|
||||
function finalizeExecutedToolCall(
|
||||
context::agentContext,
|
||||
assistantMsg::assistantMessage,
|
||||
assistantMsg::assistantMessageToolCall,
|
||||
prep::preparedToolCall,
|
||||
executed::executedOutcome,
|
||||
config::agentLoopConfig,
|
||||
@@ -1132,7 +1149,7 @@ executeToolCallsSequential(ctx, msg, [deployTc], config, nothing, emit)
|
||||
"""
|
||||
function executeToolCallsSequential(
|
||||
context::agentContext,
|
||||
assistantMsg::assistantMessage,
|
||||
assistantMsg::assistantMessageToolCall,
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
@@ -1161,7 +1178,7 @@ function executeToolCallsSequential(
|
||||
agentEventSink("executeToolCallsSequential 3-2")
|
||||
end
|
||||
agentEventSink("executeToolCallsSequential 4")
|
||||
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
agentEventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
$(finalized.result), $(finalized.isError)")
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
@@ -1233,7 +1250,7 @@ executeToolCallsParallel(ctx, msg, [tc1, tc2, tc3], config, abortedSignal, emit)
|
||||
"""
|
||||
function executeToolCallsParallel(
|
||||
context::agentContext,
|
||||
assistantMsg::assistantMessage,
|
||||
assistantMsg::assistantMessageToolCall,
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
@@ -1335,7 +1352,7 @@ executeToolCalls(ctx, msg, [searchTc, fetchTc], configSequential, nothing, emit)
|
||||
"""
|
||||
function executeToolCalls(
|
||||
context::agentContext,
|
||||
assistantMsg::assistantMessage,
|
||||
assistantMsg::assistantMessageToolCall,
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::abortSignal,
|
||||
|
||||
+52
-6
@@ -6,8 +6,8 @@
|
||||
modelCost, llmModel, llmUsage,
|
||||
# Message content types
|
||||
textContent, imageContent,
|
||||
# Message types
|
||||
userMessage, assistantMessage, toolResultMessage,
|
||||
# Message types
|
||||
userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage,
|
||||
# Tool types
|
||||
agentTool, validateRequiredArgs,
|
||||
# Context types
|
||||
@@ -108,6 +108,52 @@ function userMessage(; role="user", content=Vector{messageContent}(), timestamp=
|
||||
return userMessage(role, content, timestamp)
|
||||
end
|
||||
|
||||
struct assistantMessageToolCall <: agentMessage # Assistant message containing tool calls
|
||||
role::String # Always "assistant"
|
||||
toolCalls::Vector{agentToolCall} # Tool calls to execute
|
||||
content::Vector{messageContent} # Reasoning/thinking content blocks
|
||||
api::String # API name used (e.g., "openai")
|
||||
provider::String # Provider name (e.g., "anthropic")
|
||||
model::String # Model identifier
|
||||
usage::llmUsage # Token usage for this message
|
||||
stopReason::String # Why generation stopped (e.g., "tool_calls")
|
||||
errorMessage::Union{String, Nothing} # Error if generation failed
|
||||
timestamp::Timestamp # When the message was received
|
||||
end
|
||||
|
||||
"""
|
||||
Create a new assistant message containing tool calls.
|
||||
|
||||
# Arguments
|
||||
- `role::String`: Always "assistant"
|
||||
- `toolCalls::Vector{agentToolCall}`: Tool calls to execute
|
||||
- `content::Vector{messageContent}`: Reasoning/thinking content blocks
|
||||
- `api::String`: API name used
|
||||
- `provider::String`: Provider name
|
||||
- `model::String`: Model identifier
|
||||
- `usage::llmUsage`: Token usage
|
||||
- `stopReason::String`: Why generation stopped
|
||||
- `errorMessage::Union{String, Nothing}`: Error if generation failed
|
||||
- `timestamp::Timestamp`: When the message was received
|
||||
|
||||
# Returns
|
||||
- A new `assistantMessageToolCall` instance
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> tc = agentToolCall("function", "call_1", "getWeather", Dict("city" => "Tokyo"))
|
||||
julia> msg = assistantMessageToolCall(toolCalls=[tc], stopReason="tool_calls")
|
||||
assistantMessageToolCall("assistant", [agentToolCall(...)], messageContent[], "", "", "", llmUsage(0, 0), "tool_calls", nothing, DateTime(...))
|
||||
```
|
||||
"""
|
||||
function assistantMessageToolCall(; role="assistant", toolCalls=agentToolCall[],
|
||||
content=Vector{messageContent}(), api="", provider="", model=nothing, usage=llmUsage(0, 0),
|
||||
stopReason="tool_calls", errorMessage=nothing, timestamp=now())
|
||||
model_str = model isa AbstractString ? String(model) : ""
|
||||
return assistantMessageToolCall(role, toolCalls, content, api, provider, model_str,
|
||||
usage, stopReason, errorMessage, timestamp)
|
||||
end
|
||||
|
||||
struct assistantMessage <: agentMessage # Message from the AI assistant
|
||||
role::String # Always "assistant"
|
||||
content::Vector{messageContent} # Text and/or image content
|
||||
@@ -434,13 +480,13 @@ end
|
||||
Context passed to the `beforeToolCall` hook.
|
||||
|
||||
# Arguments
|
||||
- `message::assistantMessage`: The assistant message containing the tool call
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call being prepared
|
||||
- `args::Dict{String,Any}`: Validated tool arguments
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct beforeToolCallContext
|
||||
message::assistantMessage
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
context::agentContext
|
||||
@@ -455,7 +501,7 @@ end
|
||||
Context passed to the `afterToolCall` hook.
|
||||
|
||||
# Arguments
|
||||
- `message::assistantMessage`: The assistant message containing the tool call
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call that was executed
|
||||
- `args::Dict{String,Any}`: Tool arguments
|
||||
- `result::agentToolResult`: The raw tool result
|
||||
@@ -463,7 +509,7 @@ Context passed to the `afterToolCall` hook.
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct afterToolCallContext
|
||||
message::assistantMessage
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
result::agentToolResult
|
||||
|
||||
@@ -221,6 +221,8 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
for msg in ctx.messages
|
||||
if msg isa userMessage
|
||||
push!(messages, _userMessageToOpenAI(msg))
|
||||
elseif msg isa assistantMessageToolCall
|
||||
push!(messages, _assistantMessageToolCallToOpenAI(msg))
|
||||
elseif msg isa assistantMessage
|
||||
push!(messages, _assistantMessageToOpenAI(msg))
|
||||
elseif msg isa toolResultMessage
|
||||
@@ -336,6 +338,44 @@ end
|
||||
|
||||
|
||||
"""
|
||||
Convert an assistantMessageToolCall to OpenAI message format.
|
||||
|
||||
Produces a message with role="assistant", content=null, and a tool_calls array:
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"location\": \"San Francisco, CA\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{String, Any}
|
||||
tool_calls = Dict{String, Any}[]
|
||||
for tc in msg.toolCalls
|
||||
push!(tool_calls, Dict(
|
||||
"id" => tc.id,
|
||||
"type" => tc.type,
|
||||
"function" => Dict(
|
||||
"name" => tc.name,
|
||||
"arguments" => JSON.json(tc.arguments)
|
||||
)
|
||||
))
|
||||
end
|
||||
return Dict(
|
||||
"role" => "assistant",
|
||||
"content" => nothing,
|
||||
"tool_calls" => tool_calls
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
Convert an assistantMessage to OpenAI message format.
|
||||
"""
|
||||
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
|
||||
|
||||
Reference in New Issue
Block a user