Files
YiemAgent/src/util.jl
T
2026-07-04 13:23:46 +07:00

457 lines
12 KiB
Julia

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.
# Arguments
- `a::agent`
an agent
# Return
- nothing
# Example
```jldoctest
julia> using YiemAgent, MQTTClient, GeneralUtils
julia> client, connection = MakeConnection("test.mosquitto.org", 1883)
julia> connect(client, connection)
julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic")
julia> agentConfig = Dict(
"receiveprompt"=>Dict(
"mqtttopic"=> "testtopic/receive",
),
"receiveinternal"=>Dict(
"mqtttopic"=> "testtopic/internal",
),
"text2text"=>Dict(
"mqtttopic"=> "testtopic/text2text",
),
)
julia> a = YiemAgent.sommelier(
client,
msgMeta,
agentConfig,
)
julia> YiemAgent.addNewMessage(a, "user", "hello")
julia> YiemAgent.clearhistory(a)
```
# TODO
- [PENDING] clear memory
# Signature
"""
function clearhistory(a::T) where {T<:agent}
empty!(a.chathistory)
empty!(a.memory["shortmem"])
empty!(a.memory["events"])
a.memory["chatbox"] = ""
end
""" Add new message to agent.
messages => Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Describe this image for me"),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data_uri)
)
]
)
Arguments\n
-----
a::agent
an agent
role::String
message sender role i.e. system, user or assistant
text::String
message text
Return\n
-----
nothing
Example\n
-----
```jldoctest
```
Signature\n
-----
"""
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
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.
# 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
- `outcome::Union{String, Nothing}`
The result or consequence of the event after action execution
- `note::Union{String, Nothing}`
Additional notes or comments
# Returns
A dictionary with event details as symbol-keyed key-value pairs
"""
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 containing subject, action_input and optional outcome fields
Each event dictionary should have the following keys:
- :subject - The subject or entity performing the action
- :action_input - The action or input performed by the subject
- :observation - (Optional) The result or outcome of the action
# Returns
- `timeline::String`
A formatted string representing the events with their subjects, actions, and optional outcomes
Format: "{index}) {subject}> {action_input} {outcome}\n" for each event
# Example
events = [
Dict("subject" => "User", "action_input" => "Hello", "observation" => nothing),
Dict("subject" => "Assistant", "action_input" => "Hi there!", "observation" => "with a smile")
]
timeline = createTimeline(events)
# 1) User> Hello
# 2) Assistant> Hi there! with a smile
"""
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"] == "CHECK_WINE"
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
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
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
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