V0.8.0 async think loop #40

Merged
ton merged 50 commits from v0.8.0-async_think_loop into v0.8.0 2026-08-08 04:13:05 +00:00
6 changed files with 458 additions and 630 deletions
Showing only changes of commit 8a2da0f5c3 - Show all commits
+2 -2
View File
@@ -10,8 +10,8 @@ module YiemAgent
include("type.jl")
using .type
include("util.jl")
using .util
include("utils.jl")
using .utils
include("llmfunction.jl")
using .llmfunction
+102 -84
View File
@@ -1,11 +1,11 @@
module agentCore
# export prompt
export _agent_loop
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde
using GeneralUtils
using ..type, ..util, ..llmfunction
using ..type, ..utils, ..llmfunction
# ---------------------------------------------- 100 --------------------------------------------- #
@@ -74,7 +74,6 @@ function _agent_loop(agent::yiemAgent)
agent.followUpChannel -> nothing
"""
while true
result = nothing
msg = nothing
@@ -106,7 +105,7 @@ function _agent_loop(agent::yiemAgent)
break
end
# make active
# start _process_message loop
if agent._state.activeRun == false
# Dispatch message through the processing pipeline
processing_task = @spawn _process_message(agent)
@@ -122,8 +121,8 @@ function _agent_loop(agent::yiemAgent)
put!(agent.inputChannel, followMsg)
end
end
continue # continue to process user message in the next loop
elseif typeof(processing_task) == Task && istaskdone(processing_task) == true
# if agent runs is done but followUpChannel has messages, discard all message in it.
# when agent work is done it should not accept follow up msg.
@@ -135,8 +134,8 @@ function _agent_loop(agent::yiemAgent)
end
result = fetch(processing_task)
put!(agent.outputChannel, result)
agent._state.activeRun = false
processing_task = nothing
agent._state.activeRun = false # reset
processing_task = nothing # reset
end
end
catch e
@@ -163,7 +162,7 @@ should be implemented. Currently a placeholder that echoes back the received mes
- Implement the full processing pipeline:
1. Add `msg` to `agent._state.messages`
2. Call `agent.formatMsgForLLM(agent._state)` 1 to format for LLM
3. If `agent.preprocessContext` is set, call it on the formatted messages
3. If `agent.prepareContext` is set, call it on the formatted messages
4. Call the LLM (blocking — the task waits here)
5. If agent has tools, handle tool calls in a loop
6. Build `assistantMessage` and return it
@@ -174,93 +173,116 @@ julia> # Currently returns a placeholder echo response
```
"""
function _process_message(agent::yiemAgent)::assistantMessage
# WORKING
# loop until llmCall() response didn't use tool calls
while
# take every messages from agent.inputChannel, convert them into userMessage
# and add them to agent._state.messages
final_response = nothing
while true
# call agent.prepareContext()
preparedContext = agent.prepareContext(agent._state)
# call agent.preprocessContext()
# Call agent.formatMsgForLLM(agent._state) to format for LLM
#WORKING Call agent.formatMsgForLLM(agent._state) to format for LLM
formatted_messages = agent.formatMsgForLLM(preparedContext)
# Call llmCall() (blocking — the task waits here)
response = agent.llmCall(formatted_messages)
# if (LLM use tool calls)
# Check if LLM used tool calls (inspect content for tool_call blocks)
has_tool_calls = false
tool_call_list = agentToolCall[]
for content_block in response.content
if content_block isa Dict
if get(content_block, :type, "") == "tool_calls"
has_tool_calls = true
for tc_data in get(content_block, :tool_calls, [])
tc = agentToolCall(
type="function",
id=get(tc_data, :id, string(uuid4())),
name=get(tc_data, :function, Dict{String,Any}())[:name],
arguments=get(tc_data, :function, Dict{String,Any}())[:arguments],
)
push!(tool_call_list, tc)
end
elseif get(content_block, :type, "") == "tool_call"
has_tool_calls = true
tc_data = content_block
tc = agentToolCall(
type="function",
id=get(tc_data, :id, string(uuid4())),
name=get(tc_data, :name, ""),
arguments=get(tc_data, :arguments, Dict{String,Any}()),
)
push!(tool_call_list, tc)
end
end
end
if has_tool_calls && length(tool_call_list) > 0
# Build context and config for executeToolCalls
context = agentContext(
agent._state.systemPrompt,
agent._state.messages,
agent._state.tools,
)
config = agentLoopConfig(
agent._state.tools,
agent.beforeToolCall,
agent.afterToolCall,
agent.parallelToolExecute ? "parallel" : "sequential",
)
signal = nothing
emit = agent.agentEventSink
# call executeToolCalls()
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
# save toolResults to agent._state.messages
for tool_result in batch.messages
push!(agent._state.messages, tool_result)
end
# else (LLM not use tool calls)
# break out of while loop
if batch.terminate
# If batch requested termination, build a final response
final_content = [textContent("Tool execution completed.")]
for tool_result in batch.messages
for content_block in tool_result.content
if content_block isa textContent
append!(final_content, [content_block])
elseif content_block isa Dict
if haskey(content_block, :text)
push!(final_content, textContent(content_block[:text]))
end
end
end
end
final_response = assistantMessage(
role="assistant",
content=final_content,
api=response.api,
model=response.model,
usage=response.usage,
stopReason="tool_use_terminated",
errorMessage=if any(x -> x.isError, batch.messages)
"One or more tool calls failed"
else
nothing
end,
timestamp=now(),
)
break
end
else
# LLM did not use tool calls — this is the final response
final_response = response
break
end
end
# Build assistantMessage and return it
# Placeholder: echo back the message as a simple response
@warn "TODO: implement _process_message"
return assistantMessage(
role="assistant",
content=[textContent("Received: $(msg)")],
api="", model="", usage=nothing,
stopReason="end_turn",
errorMessage=nothing,
timestamp=now(),
)
return final_response
end
"""
executeToolCalls(context, assistantMsg, toolCalls, config, signal, emit)
Dispatches to sequential or parallel execution. Uses sequential mode
when `config.toolExecution == "sequential"` or when any of the
tool calls reference a tool with `executionMode: "sequential"`.
Otherwise uses parallel execution. This is the entry point called
from `streamAssistantResponse` in the agent loop.
The sequential mode takes priority over parallel because it is the
safe default. If even one tool in a batch is marked sequential, all
tools execute sequentially — this prevents a single dependent tool
from racing with an otherwise independent one. The per-tool
`executionMode` allows fine-grained control (e.g. most tools are
parallel but a specific write tool is sequential), while the config-level
`toolExecution` provides a global override.
"""
function executeToolCalls(
context::agentContext,
assistantMsg::assistantMessage,
toolCalls::vector{agentToolCall},
config::agentLoopConfig,
signal::union{nothing,abortSignal},
emit::Function,
)::agentToolCallBatch
hasSequential = false
for tc in toolCalls
for t in context.tools
if t.name == tc.name && get(t.executionMode, "parallel") == "sequential"
hasSequential = true
break
end
end
if hasSequential
break
end
end
if config.toolExecution == "sequential" || hasSequential
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit)
else
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit)
end
end
"""
createErrorToolResult(msg::String) -> agentToolResult
@@ -1040,10 +1062,6 @@ end
+175 -39
View File
@@ -1,15 +1,11 @@
module type
export agent, sommelier, companion, virtualcustomer, agentContext, yiemAgent,
export agentContext, yiemAgent,
run_agent, take_response, follow_up, stop_agent
using Dates, UUIDs, DataStructures, JSON, NATS
using GeneralUtils
# ============================================================================
# Simple type aliases / definitions
# ============================================================================
const Timestamp = DateTime
struct Usage
@@ -17,12 +13,10 @@ struct Usage
outputTokens::Int64
end
# ---------------------------------------------- 100 --------------------------------------------- #
# ============================================================================
# Message types
# ============================================================================
# ------------------------------------------------------------------------------------------------ #
# Message types #
# ------------------------------------------------------------------------------------------------ #
abstract type agentMessage end # Base type for all agent messages
struct userMessage <: agentMessage # Message from the user
@@ -135,9 +129,9 @@ function toolResultMessage(; role="tool", toolCallId="", toolName="",
end
# ============================================================================
# Message content types
# ============================================================================
# ------------------------------------------------------------------------------------------------ #
# Message content types #
# ------------------------------------------------------------------------------------------------ #
abstract type messageContent end # Base type for message content
@@ -190,9 +184,9 @@ function imageContent(; data="", mimeType="")
end
# ============================================================================
# Tool types
# ============================================================================
# ------------------------------------------------------------------------------------------------ #
# Tool types #
# ------------------------------------------------------------------------------------------------ #
"""
A tool available to the agent.
@@ -220,9 +214,9 @@ struct agentTool{TParameters, TDetails} # A tool available to the agent
end
# ============================================================================
# Agent context
# ============================================================================
# ------------------------------------------------------------------------------------------------ #
# Agent context #
# ------------------------------------------------------------------------------------------------ #
"""
Snapshot of the agent's conversation context.
@@ -242,15 +236,18 @@ struct agentContext # Snapshot of the agent's conversa
end
# ============================================================================
# Agent state
# ============================================================================
# ------------------------------------------------------------------------------------------------ #
# Agent state #
# ------------------------------------------------------------------------------------------------ #
mutable struct agentState # Mutable runtime state of an agent
systemPrompt::String # System prompt text
model::llmModel # LLM model to use
tools::Vector{agentTool} # Available tools
messages::Vector{agentMessage} # Conversation messages
# messages history includes userMessage, assistantMessage, toolResultMessage
messages::Vector{agentMessage}
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
activeRun::Bool # is agent processing user message?
errorMessage::Union{String, Nothing} # Last error message
@@ -343,9 +340,148 @@ struct llmModel{Api} # LLM model configuration
maxTokens::Int64 # Maximum output tokens per completion
end
# ============================================================================
# Agent struct
# ============================================================================
# ------------------------------------------------------------------------------------------------ #
# Agent loop configuration & tool execution types #
# ------------------------------------------------------------------------------------------------ #
"""
Configuration for the agent tool execution loop.
# Arguments
- `tools::Vector{agentTool}`: Available tools
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
"""
struct agentLoopConfig
tools::Vector{agentTool}
beforeToolCall::Union{Function, Nothing}
afterToolCall::Union{Function, Nothing}
toolExecution::String
end
"""
Signal for aborting ongoing operations.
# Arguments
- `aborted::Bool`: Whether the operation has been aborted
"""
struct abortSignal
aborted::Bool
end
"""
Result returned by tool execution before the `afterToolCall` hook.
# Arguments
- `content::Vector{messageContent}`: Tool output content
- `details::Dict{Any,Any}`: Tool-specific details
- `usage::Union{Usage, Nothing}`: Token usage if applicable
- `terminate::Bool`: Whether tool requests termination of the agent loop
"""
struct agentToolResult
content::Vector{messageContent}
details::Dict{Any,Any}
usage::Union{Usage, Nothing}
terminate::Bool
end
"""
Function type for parallel execution override on a tool.
# Arguments
- Context for parallel execution
# Returns
- `agentToolCallBatch`: The result batch from parallel execution
"""
const toolparallelExecute = Function
"""
Context passed to the `beforeToolCall` hook.
# Arguments
- `message::assistantMessage`: 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 assistantMsgCtx
message::assistantMessage
toolCall::agentToolCall
args::Dict{String,Any}
context::agentContext
end
"""
Context passed to the `afterToolCall` hook.
# Arguments
- `message::assistantMessage`: 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
- `isError::Bool`: Whether execution resulted in an error
- `context::agentContext`: Current conversation context
"""
struct afterCtx
message::assistantMessage
toolCall::agentToolCall
args::Dict{String,Any}
result::agentToolResult
isError::Bool
context::agentContext
end
"""
Event emitted when a tool call execution starts.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `arguments::Dict{String,Any}`: Tool arguments
"""
struct toolExecStartEvent
toolCallId::String
toolName::String
arguments::Dict{String,Any}
end
"""
Event emitted with partial results during tool execution.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `arguments::Dict{String,Any}`: Tool arguments
- `partialResult::Any`: The partial result data
"""
struct toolExecUpdateEvent
toolCallId::String
toolName::String
arguments::Dict{String,Any}
partialResult::Any
end
"""
Event emitted when a tool call execution ends.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `result::agentToolResult`: The final tool result
- `isError::Bool`: Whether execution resulted in an error
"""
struct toolExecEndEvent
toolCallId::String
toolName::String
result::agentToolResult
isError::Bool
end
# ------------------------------------------------------------------------------------------------ #
# Agent struct #
# ------------------------------------------------------------------------------------------------ #
abstract type agent end
@@ -367,14 +503,14 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
# and all followUp messages.
outputChannel::Channel
_task::Union{Task, Nothing} # Background task running the agent loop
_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}
preprocessContext ::Union{Function, Nothing}
prepareContext ::Union{Function, Nothing}
# Convert preprocessContext()'s new Vector{agentMessage} to LLM message format
# 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
@@ -391,8 +527,8 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
# 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
# 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
@@ -412,7 +548,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
- `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)
- `preprocessContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`)
- `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`)
@@ -436,13 +572,13 @@ function yiemAgent(
model=nothing,
tools::Vector{agentTool}=agentTool[],
messages::Vector{agentMessage}=agentMessage[],
preprocessContext::Union{Function, Nothing}=nothing,
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,
# prepareNextTurn::Union{Function, Nothing}=nothing,
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
sessionId::Union{String, Nothing}=nothing,
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false,
@@ -460,13 +596,13 @@ function yiemAgent(
followUp,
outputChannel,
nothing, # placeholder — replaced below
preprocessContext,
prepareContext,
formatMsgForLLM,
llmCall,
beforeToolCall,
afterToolCall,
prepareNextTurn,
prepareNextTurnWithContext,
# prepareNextTurn,
# prepareNextTurnWithContext,
sessionId,
maxRetryDelayMs,
parallelToolExecute,
@@ -474,7 +610,7 @@ function yiemAgent(
)
# Spawn the background loop and attach it
agent._task = @spawn _agent_loop(agent)
agent._agent_loop = @spawn _agent_loop(agent)
return agent
end
-505
View File
@@ -1,505 +0,0 @@
module util
export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText, createEventsLog, createChatLog, checkAgentResponse_JSON,
checkAgentResponse_text
using UUIDs, Dates, DataStructures, HTTP, JSON
using GeneralUtils
using ..type
# ---------------------------------------------- 100 --------------------------------------------- #
"""
Clear agent chat history.
Empties the conversation history, short-term memory, events log, and chatbox.
# Arguments
- `a::T`: An agent instance (subtype of `agent`)
# Returns
- `nothing`
# Notes
- Does not clear long-term memory; use `[PENDING] clear memory` when implemented.
# Examples
```jldoctest
julia> YiemAgent.clearhistory(agent)
```
"""
function clearhistory(a::T) where {T<:agent}
empty!(a.chathistory)
empty!(a.memory["shortmem"])
empty!(a.memory["events"])
a.memory["chatbox"] = ""
end
"""
Add a new message to the agent's conversation history.
Automatically summarizes the oldest messages if the history exceeds `maximumMsg`.
# Arguments
- `a::T1`: An agent instance (subtype of `agent`)
- `name::String`: Message sender role (e.g. "system", "user", "assistant")
- `userinput::T2`: Message dictionary to append (must contain "name" and "text" keys)
# Keyword Arguments
- `maximumMsg::Integer=30`: Maximum number of messages before summarization kicks in
# Returns
- `nothing`
# Notes
- When history length exceeds `maximumMsg`, the oldest messages are summarized automatically.
# Examples
```jldoctest
julia> YiemAgent.addNewMessage(agent, "user", Dict("name" => "user", "text" => "hello"))
```
"""
function addNewMessage(a::T1, name::String, userinput::T2;
maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict}
# if name ∉ ["system", "user", "assistant"] # guard against typo
# error("name is not in agent.availableRole $(@__LINE__)")
# end
#TODO summarize the oldest 10 message
if length(a.chathistory) > maximumMsg
summarize(a.chathistory)
else
# userinput["timestamp"] = Dates.now()
push!(a.chathistory, userinput)
end
end
""" Converts a vector of dictionaries to a formatted string.
This function takes in a vector of dictionaries and outputs a single string where each dictionary's keys are prefixed by their values.
# Arguments
- `vecd::Vector`
A vector of dictionaries containing chat messages
- `withkey::Bool`
Whether to include the name as a prefix in the output text. Default is true
- `range::Union{Nothing,UnitRange,Int}`
Optional range of messages to include. If nothing, includes all messages
# Returns
A formatted string where each line contains either:
- If withkey=true: "name> message\n"
- If withkey=false: "message\n"
# Example
julia> using Revise
julia> using GeneralUtils
julia> vecd = [Dict("name" => "John", "text" => "Hello"), Dict("name" => "Jane", "text" => "Goodbye")]
julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true)
"John> Hello\nJane> Goodbye\n"
```
"""
function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String
# Initialize an empty string to hold the final text
text = ""
# Get the elements within the specified range, or all elements if no range provided
elements = isnothing(range) ? vecd : vecd[range]
# Determine whether to include the key in the output text or not
if withkey
# Loop through each dictionary in the input vector
for d in elements
# Extract the 'name' and 'text' keys from the dictionary
name = titlecase(d[:name])
_text = d[:text]
# Append the formatted string to the text variable
text *= "$name> $_text \n"
end
else
# Loop through each dictionary in the input vector
for d in elements
# Iterate over all key-value pairs in the dictionary
for (k, v) in d
# Append the formatted string to the text variable
text *= "$v \n"
end
end
end
# Return the final text
return text
end
"""
Convert a vector of wine dictionaries to a formatted text string.
# Arguments
- `vecd::Vector`: A vector of dictionaries, each representing a wine with key-value pairs
# Returns
- A formatted string where each wine is numbered and each key-value pair is comma-separated
in the format: `"1) key1:value1,key2:value2 key3:value3 2) ..."`
# Examples
```jldoctest
julia> vecd = [Dict("wine_name" => "Chateau A", "price" => "50")]
julia> YiemAgent.availableWineToText(vecd)
"1) wine_name:Chateau A,price:50 "
```
"""
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
"""
Create a dictionary representing an event with optional details.
# Keyword Arguments
- `event_description::Union{String, Nothing}`: A description of the event
- `timestamp::Union{DateTime, Nothing}`: The time when the event occurred
- `subject::Union{String, Nothing}`: The subject or entity associated with the event
- `thought::Union{AbstractDict, Nothing}`: Any associated thoughts or metadata
- `action_name::Union{String, Nothing}`: The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
- `action_input::Union{String, Nothing}`: Input or parameters for the action
- `location::Union{String, Nothing}`: Where the event took place
- `equipment_used::Union{String, Nothing}`: Equipment involved in the event
- `material_used::Union{String, Nothing}`: Materials used during the event
- `observation::Union{String, Nothing}`: Observation of the event
- `note::Union{String, Nothing}`: Additional notes or comments
# Returns
- A `Dict{String, Any}` with event details as string-keyed key-value pairs
# Examples
```jldoctest
julia> YiemAgent.eventdict(action_name="CHAT", action_input="hello")
Dict{String, Any} with 11 entries: ...
```
"""
function eventdict(;
event_description::Union{String, Nothing}=nothing,
timestamp::Union{DateTime, Nothing}=nothing,
subject::Union{String, Nothing}=nothing,
thought::Union{AbstractDict, Nothing}=nothing,
action_name::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENT_WINE_GUIDELINE", etc
action_input::Union{String, Nothing}=nothing,
location::Union{String, Nothing}=nothing,
equipment_used::Union{String, Nothing}=nothing,
material_used::Union{String, Nothing}=nothing,
observation::Union{String, Nothing}=nothing,
note::Union{String, Nothing}=nothing,
)
d = Dict{String, Any}(
"event_description"=> event_description,
"timestamp"=> timestamp,
"subject"=> subject,
"thought"=> thought,
"action_name"=> action_name,
"action_input"=> action_input,
"location"=> location,
"equipment_used"=> equipment_used,
"material_used"=> material_used,
"observation"=> observation,
"note"=> note,
)
return d
end
"""
Create a formatted timeline string from a sequence of events.
# Arguments
- `events::T1`: Vector of event dictionaries. Each must have `action_name` and `action_input` keys,
and optionally `subject` and `observation` keys.
# Keyword Arguments
- `eventindex::Union{UnitRange, Nothing}=nothing`: Optional range of event indices to include.
If `nothing`, all events are included.
# Returns
- `timeline::String`: A formatted string where each event appears on its own line in the format:
`"Event_{index} {subject}> action_name: {action_name}, action_input: {action_input}"`
If `observation` is present, it is appended.
# Examples
```jldoctest
julia> events = [
Dict("subject" => "User", "action_input" => "Hello", "action_name" => "CHAT", "observation" => nothing),
Dict("subject" => "Assistant", "action_input" => "Hi there!", "action_name" => "CHAT", "observation" => "with a smile")
];
julia> YiemAgent.createTimeline(events)
"Event_1 User> action_name: CHAT, action_input: Hello\\nEvent_2 Assistant> action_name: CHAT, action_input: Hi there!\\n"
```
"""
function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty timeline string
timeline = ""
# Determine which indices to use - either provided range or full length
ind =
if eventindex !== nothing
[eventindex...]
else
1:length(events)
end
# Iterate through events and format each one
for i in ind
event = events[i]
# If no outcome exists, format without outcome
# if event["action_name"] == "CHAT_BOX"
# timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\n"
# elseif event["action_name"] == "CHECKINVENTORY" && event["observation"] === nothing
# timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: Not done yet.\n"
if event["action_name"] == "SEARCH_WINE_DATABASE"
timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: $(event["observation"])\\n"
else
timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\\n"
end
end
# Return formatted timeline string
return timeline
end
"""
Create a formatted event log from a sequence of events.
# Arguments
- `events::T1`: Vector of event dictionaries. Each must have `subject`, `action_name`, `action_input`,
and optionally `observation` keys.
# Keyword Arguments
- `index::Union{UnitRange, Nothing}=nothing`: Optional range of event indices to include.
If `nothing`, all events are included.
# Returns
- A `Vector{Dict{String, String}}` where each dictionary has `"name"` (from event subject) and
`"text"` (formatted action description) keys.
# Examples
```jldoctest
julia> events = [Dict("subject" => "User", "action_name" => "CHAT", "action_input" => "hello", "observation" => nothing)];
julia> log = YiemAgent.createEventsLog(events);
julia> log[1]["name"]
"User"
```
"""
function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty log array
log = Dict{String, String}[]
# Determine which indices to use - either provided range or full length
ind =
if index !== nothing
[index...]
else
1:length(events)
end
# Iterate through events and format each one
for i in ind
event = events[i]
# If no outcome exists, format without outcome
if event["observation"] === nothing
subject = event["subject"]
action_name = event["action_name"]
action_input = event["action_input"]
str = "action_name: $action_name, action_input: $action_input"
d = Dict{String, String}("name"=>subject, "text"=>str)
push!(log, d)
else
subject = event["subject"]
action_name = event["action_name"]
action_input = event["action_input"]
observation = event["observation"]
str = "action_name: $action_name, action_input: $action_input, observation: $observation"
d = Dict{String, String}("name"=>subject, "text"=>str)
push!(log, d)
end
end
return log
end
"""
Create a formatted chat log from a sequence of chat entries.
# Arguments
- `chatdict::T1`: Vector of chat entry dictionaries. Each must have `"name"` and `"text"` keys.
# Keyword Arguments
- `index::Union{UnitRange, Nothing}=nothing`: Optional range of entry indices to include.
If `nothing`, all entries are included.
# Returns
- A `Vector{Dict{String, String}}` where each dictionary has `"name"` and `"text"` keys
copied from the corresponding input entry.
# Examples
```jldoctest
julia> chats = [Dict("name" => "user", "text" => "hello"), Dict("name" => "assistant", "text" => "hi")];
julia> YiemAgent.createChatLog(chats)[1]["name"]
"user"
```
"""
function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty log array
log = Dict{String, String}[]
# Determine which indices to use - either provided range or full length
ind =
if index !== nothing
[index...]
else
1:length(chatdict)
end
# Iterate through events and format each one
for i in ind
event = chatdict[i]
subject = event["name"]
text = event["text"]
d = Dict{String, String}("name"=>subject, "text"=>text)
push!(log, d)
end
return log
end
"""
Check if an agent's text response contains all required header keywords.
Validates that the response includes all required keywords without duplications.
# Arguments
- `response::String`: The agent's text response to validate
- `requiredHeader::T`: Array of required keyword strings (subtype of `Array{String}`)
# Returns
- `Tuple{Bool, Union{String, Nothing}}`: A two-element tuple where:
- First element: `true` if all required keywords are present and not duplicated, `false` otherwise
- Second element: An error description string if validation failed, or `nothing` if passed
# Notes
- Uses `GeneralUtils.detectKeywordVariation` for flexible keyword matching.
# Examples
```jldoctest
julia> ispass, err = YiemAgent.checkAgentResponse_text("hello world", ["hello"])
(true, nothing)
```
"""
function checkAgentResponse_text(response::String, requiredHeader::T
)::Tuple where {T<:Array{String}}
detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response)
missingkeys = [k for (k, v) in detected_kw if v === nothing]
ispass = false
errormsg = nothing
if !isempty(missingkeys)
errormsg = "$missingkeys are missing from your previous response"
ispass = false
elseif sum([length(i) for i in values(detected_kw)]) > length(requiredHeader)
errormsg = "Your previous attempt has duplicated points according to the required response format"
ispass = false
else
ispass = true
end
return (ispass, errormsg)
end
end # module util
+179
View File
@@ -0,0 +1,179 @@
module utils
export clearhistory, availableWineToText, prepareContext
using UUIDs, Dates, DataStructures, HTTP, JSON
using GeneralUtils
using ..type
# ---------------------------------------------- 100 --------------------------------------------- #
"""
Clear agent chat history.
Empties the conversation history, short-term memory, events log, and chatbox.
# Arguments
- `a::T`: An agent instance (subtype of `agent`)
# Returns
- `nothing`
# Notes
- Does not clear long-term memory; use `[PENDING] clear memory` when implemented.
# Examples
```jldoctest
julia> YiemAgent.clearhistory(agent)
```
"""
function clearhistory(a::T) where {T<:agent}
empty!(a.chathistory)
empty!(a.memory["shortmem"])
empty!(a.memory["events"])
a.memory["chatbox"] = ""
end
"""
Convert a vector of wine dictionaries to a formatted text string.
# Arguments
- `vecd::Vector`: A vector of dictionaries, each representing a wine with key-value pairs
# Returns
- A formatted string where each wine is numbered and each key-value pair is comma-separated
in the format: `"1) key1:value1,key2:value2 key3:value3 2) ..."`
# Examples
```jldoctest
julia> vecd = [Dict("wine_name" => "Chateau A", "price" => "50")]
julia> YiemAgent.availableWineToText(vecd)
"1) wine_name:Chateau A,price:50 "
```
"""
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
"""
prepareContext(state::agentState) -> Vector{agentMessage}
Returns a deep copy of the messages from the given `agentState`, ready
to be sent to the LLM. Override this function to inject additional
context — such as retrieved documents, current time, user preferences,
or any other relevant information — into the message list before
formatting and calling the LLM.
By default, returns an exact copy of `state.messages` without
modification.
# Arguments
- `state::agentState`: The current agent state containing conversation history
# Returns
- `Vector{agentMessage}`: A deep copy of the messages to be sent to the LLM
# Examples
```julia
# Default: returns a deep copy of messages
prepareContext(state) == deepcopy(state.messages)
# Override to inject system context:
# function Base.prepareContext(state::agentState)
# msgs = deepcopy(state.messages)
# pushfirst!(msgs, textMessage("system", "You are a helpful assistant."))
# return msgs
# end
```
"""
function prepareContext(state::agentState)::Vector{agentMessage}
messages = deepcopy(state.messages) # messages that will be send to LLM
#TODO adjust/modify and inject additional context into messages
return messages
end
function formatMsgForLLM()
end
end # module util