This commit is contained in:
2026-08-06 15:32:56 +07:00
parent 4cb01c71bb
commit 8a2da0f5c3
6 changed files with 458 additions and 630 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ module YiemAgent
include("type.jl") include("type.jl")
using .type using .type
include("util.jl") include("utils.jl")
using .util using .utils
include("llmfunction.jl") include("llmfunction.jl")
using .llmfunction using .llmfunction
+102 -84
View File
@@ -1,11 +1,11 @@
module agentCore module agentCore
# export prompt export _agent_loop
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde DataFrames, Serde
using GeneralUtils using GeneralUtils
using ..type, ..util, ..llmfunction using ..type, ..utils, ..llmfunction
# ---------------------------------------------- 100 --------------------------------------------- # # ---------------------------------------------- 100 --------------------------------------------- #
@@ -74,7 +74,6 @@ function _agent_loop(agent::yiemAgent)
agent.followUpChannel -> nothing agent.followUpChannel -> nothing
""" """
while true while true
result = nothing result = nothing
msg = nothing msg = nothing
@@ -106,7 +105,7 @@ function _agent_loop(agent::yiemAgent)
break break
end end
# make active # start _process_message loop
if agent._state.activeRun == false if agent._state.activeRun == false
# Dispatch message through the processing pipeline # Dispatch message through the processing pipeline
processing_task = @spawn _process_message(agent) processing_task = @spawn _process_message(agent)
@@ -122,8 +121,8 @@ function _agent_loop(agent::yiemAgent)
put!(agent.inputChannel, followMsg) put!(agent.inputChannel, followMsg)
end end
end end
continue # continue to process user message in the next loop continue # continue to process user message in the next loop
elseif typeof(processing_task) == Task && istaskdone(processing_task) == true elseif typeof(processing_task) == Task && istaskdone(processing_task) == true
# if agent runs is done but followUpChannel has messages, discard all message in it. # 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. # when agent work is done it should not accept follow up msg.
@@ -135,8 +134,8 @@ function _agent_loop(agent::yiemAgent)
end end
result = fetch(processing_task) result = fetch(processing_task)
put!(agent.outputChannel, result) put!(agent.outputChannel, result)
agent._state.activeRun = false agent._state.activeRun = false # reset
processing_task = nothing processing_task = nothing # reset
end end
end end
catch e catch e
@@ -163,7 +162,7 @@ should be implemented. Currently a placeholder that echoes back the received mes
- Implement the full processing pipeline: - Implement the full processing pipeline:
1. Add `msg` to `agent._state.messages` 1. Add `msg` to `agent._state.messages`
2. Call `agent.formatMsgForLLM(agent._state)` 1 to format for LLM 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) 4. Call the LLM (blocking — the task waits here)
5. If agent has tools, handle tool calls in a loop 5. If agent has tools, handle tool calls in a loop
6. Build `assistantMessage` and return it 6. Build `assistantMessage` and return it
@@ -174,93 +173,116 @@ julia> # Currently returns a placeholder echo response
``` ```
""" """
function _process_message(agent::yiemAgent)::assistantMessage function _process_message(agent::yiemAgent)::assistantMessage
# WORKING
# loop until llmCall() response didn't use tool calls # loop until llmCall() response didn't use tool calls
while final_response = nothing
# take every messages from agent.inputChannel, convert them into userMessage while true
# and add them to agent._state.messages # call agent.prepareContext()
preparedContext = agent.prepareContext(agent._state)
# call agent.preprocessContext() #WORKING Call agent.formatMsgForLLM(agent._state) to format for LLM
formatted_messages = agent.formatMsgForLLM(preparedContext)
# Call agent.formatMsgForLLM(agent._state) to format for LLM
# Call llmCall() (blocking — the task waits here) # 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() # call executeToolCalls()
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
# save toolResults to agent._state.messages # 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) if batch.terminate
# break out of while loop # 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 end
# Build assistantMessage and return it return final_response
# 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(),
)
end 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 createErrorToolResult(msg::String) -> agentToolResult
@@ -1040,10 +1062,6 @@ end
+175 -39
View File
@@ -1,15 +1,11 @@
module type module type
export agent, sommelier, companion, virtualcustomer, agentContext, yiemAgent, export agentContext, yiemAgent,
run_agent, take_response, follow_up, stop_agent run_agent, take_response, follow_up, stop_agent
using Dates, UUIDs, DataStructures, JSON, NATS using Dates, UUIDs, DataStructures, JSON, NATS
using GeneralUtils using GeneralUtils
# ============================================================================
# Simple type aliases / definitions
# ============================================================================
const Timestamp = DateTime const Timestamp = DateTime
struct Usage struct Usage
@@ -17,12 +13,10 @@ struct Usage
outputTokens::Int64 outputTokens::Int64
end end
# ---------------------------------------------- 100 --------------------------------------------- #
# ------------------------------------------------------------------------------------------------ #
# ============================================================================ # Message types #
# Message types # ------------------------------------------------------------------------------------------------ #
# ============================================================================
abstract type agentMessage end # Base type for all agent messages abstract type agentMessage end # Base type for all agent messages
struct userMessage <: agentMessage # Message from the user struct userMessage <: agentMessage # Message from the user
@@ -135,9 +129,9 @@ function toolResultMessage(; role="tool", toolCallId="", toolName="",
end end
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
# Message content types # Message content types #
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
abstract type messageContent end # Base type for message content abstract type messageContent end # Base type for message content
@@ -190,9 +184,9 @@ function imageContent(; data="", mimeType="")
end end
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
# Tool types # Tool types #
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
""" """
A tool available to the agent. A tool available to the agent.
@@ -220,9 +214,9 @@ struct agentTool{TParameters, TDetails} # A tool available to the agent
end end
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
# Agent context # Agent context #
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
""" """
Snapshot of the agent's conversation context. Snapshot of the agent's conversation context.
@@ -242,15 +236,18 @@ struct agentContext # Snapshot of the agent's conversa
end end
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
# Agent state # Agent state #
# ============================================================================ # ------------------------------------------------------------------------------------------------ #
mutable struct agentState # Mutable runtime state of an agent mutable struct agentState # Mutable runtime state of an agent
systemPrompt::String # System prompt text systemPrompt::String # System prompt text
model::llmModel # LLM model to use model::llmModel # LLM model to use
tools::Vector{agentTool} # Available tools 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 pendingToolCalls::Vector{String} # Tool call IDs waiting for results
activeRun::Bool # is agent processing user message? activeRun::Bool # is agent processing user message?
errorMessage::Union{String, Nothing} # Last error 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 maxTokens::Int64 # Maximum output tokens per completion
end 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 abstract type agent end
@@ -367,14 +503,14 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
# and all followUp messages. # and all followUp messages.
outputChannel::Channel 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, # Preprocess/transform messages and context (modify, filter, prune, inject context from memory,
# reorder, ...) for a single LLM call in _process_message()'s loop. # reorder, ...) for a single LLM call in _process_message()'s loop.
# returns new Vector{agentMessage} # 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 formatMsgForLLM::Function
# Actually invoke the LLM to get a completion response. The LLM response comes back as an # 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 # Callback invoked after executing a tool call to sanitize tools output so the output is ready
# to be converted into toolResults message # to be converted into toolResults message
afterToolCall::Union{Function, Nothing} afterToolCall::Union{Function, Nothing}
prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn # prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn
prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context # prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
sessionId::Union{String, Nothing} # Optional session identifier sessionId::Union{String, Nothing} # Optional session identifier
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
parallelToolExecute::Bool # Default: false parallelToolExecute::Bool # Default: false
@@ -412,7 +548,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty) - `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`) - `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
- `llmCall::Function`: Function to invoke the LLM (required) - `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`) - `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`) - `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`) - `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`)
@@ -436,13 +572,13 @@ function yiemAgent(
model=nothing, model=nothing,
tools::Vector{agentTool}=agentTool[], tools::Vector{agentTool}=agentTool[],
messages::Vector{agentMessage}=agentMessage[], messages::Vector{agentMessage}=agentMessage[],
preprocessContext::Union{Function, Nothing}=nothing, prepareContext::Union{Function, Nothing}=nothing,
formatMsgForLLM::Function=defaultformatMsgForLLM, formatMsgForLLM::Function=defaultformatMsgForLLM,
llmCall::Function, llmCall::Function,
beforeToolCall::Union{Function, Nothing}=nothing, beforeToolCall::Union{Function, Nothing}=nothing,
afterToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing,
prepareNextTurn::Union{Function, Nothing}=nothing, # prepareNextTurn::Union{Function, Nothing}=nothing,
prepareNextTurnWithContext::Union{Function, Nothing}=nothing, # prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
sessionId::Union{String, Nothing}=nothing, sessionId::Union{String, Nothing}=nothing,
maxRetryDelayMs::Union{Int64, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false, parallelToolExecute::Bool=false,
@@ -460,13 +596,13 @@ function yiemAgent(
followUp, followUp,
outputChannel, outputChannel,
nothing, # placeholder — replaced below nothing, # placeholder — replaced below
preprocessContext, prepareContext,
formatMsgForLLM, formatMsgForLLM,
llmCall, llmCall,
beforeToolCall, beforeToolCall,
afterToolCall, afterToolCall,
prepareNextTurn, # prepareNextTurn,
prepareNextTurnWithContext, # prepareNextTurnWithContext,
sessionId, sessionId,
maxRetryDelayMs, maxRetryDelayMs,
parallelToolExecute, parallelToolExecute,
@@ -474,7 +610,7 @@ function yiemAgent(
) )
# Spawn the background loop and attach it # Spawn the background loop and attach it
agent._task = @spawn _agent_loop(agent) agent._agent_loop = @spawn _agent_loop(agent)
return agent return agent
end 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