Files
YiemAgent/src/interface.jl
T
2026-07-17 07:01:55 +07:00

1302 lines
50 KiB
Julia

module interface
export addNewMessage, conversation, decisionMaker, reflector, generatechat,
generalconversation, detectWineryName, generateSituationReport
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, CSV
using GeneralUtils
using ..type, ..util, ..llmfunction
# ------------------------------------------------------------------------------------------------ #
# pythoncall setting #
# ------------------------------------------------------------------------------------------------ #
# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252
# by setting the following variables, PythonCall.jl will use:
# 1. system's python and packages installed by system (via apt install)
# or 2. conda python and packages installed by conda
# if these setting are not set (comment out), PythonCall will use its own python and packages that
# installed by CondaPkg.jl (from env_preparation.jl)
# ENV["JULIA_CONDAPKG_BACKEND"] = "Null" # set condapkg backend = none
# systemPython = split(read(`which python`, String), "\n")[1] # system's python path
# ENV["JULIA_PYTHONCALL_EXE"] = systemPython # find python location with $> which python ex. raw"/root/conda/bin/python"
# using PythonCall
# const py_agents = PythonCall.pynew()
# const py_llms = PythonCall.pynew()
# function __init__()
# # PythonCall.pycopy!(py_cv2, pyimport("cv2"))
# # equivalent to from urllib.request import urlopen in python
# PythonCall.pycopy!(py_agents, pyimport("langchain.agents"))
# PythonCall.pycopy!(py_llms, pyimport("langchain.llms"))
# end
# ---------------------------------------------- 100 --------------------------------------------- #
macro executeStringFunction(functionStr, args...)
# Parse the function string into an expression
func_expr = Meta.parse(functionStr)
# Create a new function with the parsed expression
function_to_call = eval(Expr(:function,
Expr(:call, func_expr, args...), func_expr.args[2:end]...))
# Call the newly created function with the provided arguments
function_to_call(args...)
end
""" Think and choose action
# Arguments
- `config::T1`
config
- `state::T2`
a game state
# Keyword Arguments
# Return
- `thoughtdict::Dict`
# Example
```jldoctest
julia> result = decisionMaker(agent)
OrderedDict{String, Any} with 4 entries:
"plan" => "The user provided an image of a sparkling white wine (Asolo Prosecco Bella Principessa from Italy) and requested a search for similar wines in the inventory. According to store guidelines, I must st…
"action_name" => "SEARCH_WINE_DATABASE"
"action_input" => "Sparkling white wine from Italy"
"action_result" => "1) winery: Terrazze dell Etna, wine_name: Rose Brut.
```
"""
function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
) where {T<:agent}
@info "YiemAgent decisionMaker() start " @__LINE__
# lessonDict = copy(JSON.parsefile("lesson.json"))
# lesson =
# if isempty(lessonDict)
# ""
# else
# lessons = Dict{String, Any}()
# for (k, v) in lessonDict
# lessons[k] = lessonDict[k][:lesson]
# end
# """
# You have attempted to help the user before and failed, either because your reasoning for the
# recommendation was incorrect or your response did not exactly match the user expectation.
# The following lesson(s) give a plan to avoid failing to help the user in the same way you
# did previously. Use them to improve your strategy to help the user.
# Here are some lessons in JSON format:
# $(JSON.json(lessons))
# When providing the thought and action for the current trial, that into account these failed
# trajectories and make sure not to repeat the same mistakes and incorrect answers.
# """
# end
# recentevents_ind = GeneralUtils.recentElementsIndex(
# length(a.memory["events"]), recentevents; includelatest=true)
requiredKeys = ["plan", "action_name", "action_input"]
context =
"""
<internal_context_for_assistant>
</internal_context_for_assistant>
"""
# add context to text of the latest message (in the front).
# use for loop because in openai format, each msg may contain both text and image.
for d in a.chathistory[end]["content"]
if d["type"] == "text"
d["text"] = context * d["text"]
break
end
end
errornote = "N/A"
response = nothing # placeholder for show when error msg show up
for attempt in 1:maxattempt
msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
"messages" => a.chathistory,
"temperature" => 0.7
)
response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response)
response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response)
response = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to
response = strip(response)
# dollar sign in Julia means string interpolation
while occursin('$', response)
response = replace(response, '$' => "USD")
end
responsedict = nothing
if occursin(requiredKeys[2], response)
try
_responsedict = JSON.parse(response)
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
catch
println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
else
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
# check whether all answer's key points are in responsedict
println("\n---")
println(responsedict)
println("---\n")
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass
errornote = errormsg
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
if responsedict["action_input"] == "CHAT_BOX" &&
occursin("similar", responsedict["action_input"])
continue
end
# if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"]
# errornote = "Your previous attempt didn't use the given functions"
# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# end
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict)
@info "YiemAgent decisionMaker() end " @__LINE__
return responsedict
end
error("DecisionMaker failed to generate a thought ", response)
end
""" Assigns a scalar value to each new child node to be used for selec-
tion and backpropagation. This value effectively quantifies the agent's progress in task completion,
serving as a heuristic to steer the search algorithm towards the most promising regions of the tree.
# Arguments
- `state<:AbstractDict`
one of Yiem's agent
- `text2textInstructLLM::Function`
A function that handles communication to LLM service
# Return
- `score::Integer`
# Example
```jldoctest
julia>
```
# Signature
"""
function evaluator(a::T1, timeline, decisiondict, evaluateecontext
) where {T1<:agent}
systemmsg =
"""
<Your role>
- You are a master sommelier of an online wine store.
</Your role>
<Situation>
- Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding.
</Situation>
<Your mission>
- Improve a trainee sommelier decision based on the store policy and guidelines while ensuring seamless interactions between the trainee and customers.
</Your mission>
<At each round of conversation, you will be given the following information>
- trajectory: A conversation between your trainee and the customer that have occurred up until now
- evaluatee_context: The context that evaluatee use to make a decision
- evaluatee_decision: The decision made by the evaluatee, consists of the following elements:
"plan" is the trainee's plan
"action_name" is the name of the action taken, which can be one of the available tool name.
"action_input" is the input to the action.
</At each round of conversation, you will be given the following information>
<You must follow the following policy>
- Use only infomation provided by the store policy and guidelines as a bedrocks for your response.
</You must follow the following policy>
<You should follow the following guidelines>
- The trainee's plan, action_name, and action_input must be logically consistent
- The trainee's action_input should be in a proper format as specified by the tools.
- The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the END_CONVER_GUIDELINE tool.
</You should follow the following guidelines>
<You should then respond to the user with>
1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question.
- Evaluate the correctness of each section and the overall trajectory based on the given question.
- Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation.
- Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached.
- Do not generate additional thoughts or actions.
2) decision_evaluation:
- Examine how the trainee's decisions align with the store's policies and guidelines before proceeding.
3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only.
4) approval: Can be "yes" or "no". "no" if the suggestion contradict the trainee's decision; otherwise, it is "yes".
</You should then respond to the user with>
<You should only respond in JSON format as described below>
{
"trajectory_evaluation": "...",
"decision_evaluation": "...",
"suggestion": "...",
"approval": "...",
}
</You should only respond in format as described below>
Let's begin!
"""
requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approval, :suggestion]
errornote = "N/A"
for attempt in 1:10
evaluateecontext = replace(evaluateecontext, "<context>" => "")
evaluateecontext = replace(evaluateecontext, "</context>" => "")
context =
"""
<context>
<assistant_trajectories>
$timeline
</assistant_trajectories>
<evaluatee_context>
$evaluateecontext
</evaluatee_context>
<evaluatee_decision>
{plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])}
</evaluatee_decision>
P.S. $errornote
</context>
"""
unformatPrompt =
[
Dict("name" => "system", "text" => systemmsg),
]
# put in model format
prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
# add info
prompt = prompt * context
response = a.context.text2textInstructLLM(prompt; senderId=a.id)
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
response = GeneralUtils.remove_french_accents(response)
# response = replace(response, '$'=>"USD")
think, response = GeneralUtils.extractthink(response)
responsedict = nothing
try
responsedict = copy(JSON.parsefile(response))
catch
println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
# check whether all answer's key points are in responsedict
ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass
errornote = errormsg
println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
# if accepted_as_answer ∉ ["yes", "no"] # [PENDING] add errornote into the prompt
# error("generated accepted_as_answer has wrong format")
# end
println("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(Dict(responsedict))
return responsedict
end
error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>")
end
""" Chat with llm.
# Example userinput
image_path = "test/large_image.png"
image_bytes = read(image_path)
base64_string = base64encode(image_bytes)
# 2. Match the MIME type according to your file extension (e.g., png, jpeg)
mime_type = "image/png"
data1_uri = "data:<mime_type>;base64,<image1_base64_string>"
# 3. Construct payload with the Data URI
message => Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Describe this image for me"),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data_uri)
)
]
)
"""
function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}},
maximumMsg=50, max_think_loop::Integer=3)
@info "YiemAgent conversation() start " @__LINE__
userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"])
# find text in usermsg
usertext = nothing
for (i, d) in enumerate(userinput["content"])
if d["type"] == "text"
d["text"] = GeneralUtils.remove_french_accents(d["text"])
usertext = d["text"]
end
end
if usertext == "newtopic"
clearhistory(a)
return "Okay. What shall we talk about?"
else
# add usermsg to a.chathistory but how do I handle images?
addNewMessage(a, "user", userinput; maximumMsg=maximumMsg)
# thinking loop until AI wants to communicate with the user
loopcount = 0
while true
loopcount += 1
if loopcount > max_think_loop
thoughtdict, result_raw = generatechat!(a)
assistant_response = Dict{String, Any}(
"role" => "assistant",
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
)
addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)
items_info = []
if haskey(a.memory["shortmem"], "items_info")
for (i, item) in enumerate(a.memory["shortmem"]["items_info"])
if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"])
push!(items_info, item)
deleteat!(a.memory["shortmem"]["items_info"], i)
end
end
end
response_to_frontend = Dict{String, Any}(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => thoughtdict["action_input"]),
Dict(
"type" => "items_info",
"items_info" => items_info
),
]
)
return response_to_frontend
end
thoughtdict, result_raw = think(a)
if thoughtdict["action_name"] ["CHAT_BOX"]
assistant_response = Dict{String, Any}(
"role" => "assistant",
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
)
addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)
items_info = []
if haskey(a.memory["shortmem"], "items_info")
for (i, item) in enumerate(a.memory["shortmem"]["items_info"])
if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"])
push!(items_info, item)
deleteat!(a.memory["shortmem"]["items_info"], i)
end
end
end
response_to_frontend = Dict{String, Any}(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => thoughtdict["action_input"]),
Dict(
"type" => "items_info",
"items_info" => items_info
),
]
)
""" intended message to send to frontend should have the following format.
response_to_frontend = Dict{String, Any}(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => "assistant_text_response"),
Dict(
"type" => "items_info",
"items_info" => [
Dict(
"wine_name"=> "wine name 1",
"wine_id"=> "...",
"image"=> base64 encoded image,
...
),
Dict(
"wine_name"=> "wine name 2",
"wine_id"=> "...",
"image"=> base64 encoded image,
...
),
]
),
]
)
"""
return response_to_frontend
else # still in action
action_name = thoughtdict["action_name"]
action_input = thoughtdict["action_input"]
action_call = Dict{String, Any}(
"role" => "action_call",
"content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),]
)
addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg)
action_result = thoughtdict["action_result"]
actionresult = Dict{String, Any}(
"role" => "action_result",
"content" => [Dict("type" => "text", "text" => "$action_result"),]
)
addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg)
@info "YiemAgent conversation() end think count $loopcount " @__LINE__
end
end
end
end
"""
# Arguments
# Return
# Example
```jldoctest
julia>
```
"""
function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
# a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0)
@info "YiemAgent think() start " @__LINE__
thoughtdict = decisionMaker(a)
@info "YiemAgent think() 1 " @__LINE__
@show thoughtdict
println("---\n")
result_raw = nothing
if thoughtdict["action_name"] ["CHAT_BOX"]
thoughtdict, result_raw = generatechat!(a)
elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE"
thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict)
elseif thoughtdict["action_name"] ["WINE_PRESENTATION_GUIDELINE"]
thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict)
elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE"
thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false)
if result_raw !== nothing && result_raw isa Vector
if haskey(a.memory["shortmem"], "items_info")
append!(a.memory["shortmem"]["items_info"], result_raw)
else
a.memory["shortmem"]["items_info"] = result_raw
end
end
else
error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
@info "YiemAgent think() end " @__LINE__
@show thoughtdict
println("---\n")
return (thoughtdict=thoughtdict, result_raw=result_raw)
end
function chatbox!(a::T, thoughtdict::AbstractDict
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
thoughtdict["action_result"] = "Action result is the next user dialogue."
return (thoughtdict=thoughtdict, result_raw=nothing)
end
function end_conversation_guideline!(a::T, thoughtdict::AbstractDict
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
guideline =
"""
<end_conversation_guideline>
- Provide customer with store contact info and business hours
- Invite customer to comeback
<store_info>
Business Hours: everyday 9.00-20.00
Tel. 0863055790
</store_info>
</end_conversation_guideline>
"""
thoughtdict["action_result"] = guideline
return (thoughtdict=thoughtdict, result_raw=nothing)
end
function wine_presentation_guideline!(a::T, thoughtdict::AbstractDict
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
guideline =
"""
<wine_presentation_guideline>
- Provide detailed introductions of the wines you've found to the user.
- Explain how the wine could match the user's intention and what its effects might mean for the user's experience.
- If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience.
- Provide your personal recommendation and provide a brief explanation of why you recommend it.
- People don't describe wine quality level in numbers so use convertion_table if neccessary
<conversion_table>
Intensity level:
1 to 2: May correspond to "light-bodied" or a similar description.
2 to 3: May correspond to "med light bodied", "medium light" or a similar description.
3 to 4: May correspond to "medium bodied" or a similar description.
4 to 5: May correspond to "med full bodied", "medium full" or a similar description.
4 to 5: May correspond to "full bodied" or a similar description.
Sweetness level:
1 to 2: May correspond to "dry", "no sweet" or a similar description.
2 to 3: May correspond to "off dry", "less sweet" or a similar description.
3 to 4: May correspond to "semi sweet" or a similar description.
4 to 5: May correspond to "sweet" or a similar description.
4 to 5: May correspond to "very sweet" or a similar description.
Tannin level:
1 to 2: May correspond to "low tannin" or a similar description.
2 to 3: May correspond to "semi low tannin" or a similar description.
3 to 4: May correspond to "medium tannin" or a similar description.
4 to 5: May correspond to "semi high tannin" or a similar description.
4 to 5: May correspond to "high tannin" or a similar description.
Acidity level:
1 to 2: May correspond to "low acidity" or a similar description.
2 to 3: May correspond to "semi low acidity" or a similar description.
3 to 4: May correspond to "medium acidity" or a similar description.
4 to 5: May correspond to "semi high acidity" or a similar description.
4 to 5: May correspond to "high acidity" or a similar description.
</conversion_table>
</wine_presentation_guideline>
"""
thoughtdict["action_result"] = guideline
return (thoughtdict=thoughtdict, result_raw=nothing)
end
#PENDING
function generatechat!(a::T; maxattempt::Integer=10
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
@info "YiemAgent generatechat!() start " @__LINE__
# lessonDict = copy(JSON.parsefile("lesson.json"))
# lesson =
# if isempty(lessonDict)
# ""
# else
# lessons = Dict{String, Any}()
# for (k, v) in lessonDict
# lessons[k] = lessonDict[k][:lesson]
# end
# """
# You have attempted to help the user before and failed, either because your reasoning for the
# recommendation was incorrect or your response did not exactly match the user expectation.
# The following lesson(s) give a plan to avoid failing to help the user in the same way you
# did previously. Use them to improve your strategy to help the user.
# Here are some lessons in JSON format:
# $(JSON.json(lessons))
# When providing the thought and action for the current trial, that into account these failed
# trajectories and make sure not to repeat the same mistakes and incorrect answers.
# """
# end
# recentevents_ind = GeneralUtils.recentElementsIndex(
# length(a.memory["events"]), recentevents; includelatest=true)
systemmsg =
"""
# store_policy
- Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
- If you found wines in the store's database, they are in stock.
- You can only recommend wines that are currently in our inventory
- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
- Ask the user one question at a time.
- Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database.
- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
- Spicy foods should be paired only with light red wines.
- We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user immediately if they are looking for these types of wines. Do not sell our wines as such.
- Gift box, gift card, and custom messages are available. Inform the user to contact our sales team.
# store_guidelines
- Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting.
- Customer may provide images for you to look up.
- Encourage the customer to explore different options and try new things.
- If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
- Your store carries only wine.
- Vintage 0 means non-vintage.
- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.
# situation
You are continuing the conversation with the user.
# your role
Your name is $(a.name). You are a helpful sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision.
# objective
- Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences.
- Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences.
# your responsibility includes
- According to the store's policy and guidelines, continuing conversation with the customer using CHAT_BOX action.
- Keep the conversation with the customer going smoothly
# your responsibility does NOT includes
- Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
- Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
- Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
# you should then respond to the user with interleaving plan, action_name, action_input
1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
2) "action_name", Must be "CHAT_BOX
3) "action_input", Dialogue you want to chat with the user according to your plan.
After the action is executed you gets "action_result". It is the output from the action you selected.
# you should only respond in JSON format as described below
"plan": "...",
"action_name": "...",
"action_input": "..."
"""
system_msg = Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => systemmsg),
]
)
chathistory = deepcopy(a.chathistory[2:end]) # use deep copy because I want to replace system msg
pushfirst!(chathistory, system_msg)
requiredKeys = ["plan", "action_name", "action_input"]
errornote = "N/A"
response = nothing # placeholder for show when error msg show up
for attempt in 1:maxattempt
if attempt > 1
println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
"messages" => chathistory,
"temperature" => 0.7
)
response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response)
response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response)
response = strip(response)
responsedict = nothing
if occursin(requiredKeys[2], response)
try
_responsedict = JSON.parse(response)
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
catch
println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
else
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
# check whether all answer's key points are in responsedict
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass
errornote = errormsg
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
# if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"]
# errornote = "Your previous attempt didn't use the given functions"
# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# end
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict)
responsedict["action_result"] = "Action result is the next user dialogue."
@info "YiemAgent generatechat!() end " @__LINE__
return (thoughtdict=responsedict, result_raw=responsedict["action_input"])
end
error("YiemAgent generatechat() failed to generate a thought ", response)
end
function generatequestion(a, text2textInstructLLM::Function, timeline)::String
systemmsg =
"""
Your role:
Your name is $(a.name). You are a helpful English-speaking, website-based sommelier for $(a.retailername)'s online store currently talking with the user.
Your goal includes:
1) Help the user select the best wines from your inventory that align with the user's preferences
2) Thanks the user when they don't need any further assistance and invite them to comeback next time
Your responsibility includes:
1) From your point of view as a sommelier helping the user, ask yourself multiple questions based on the current situation
Your responsibility does NOT includes:
1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
At each round of conversation, you will be given the info:
Additional info: ...
Your recent events: latest 5 events of the situation
You must follow the following guidelines:
- Your question should be specific, self-contained and not require any additional context.
- Once the user has chose their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time.
You should follow the following guidelines:
- Focus on the latest conversation
- If the user interrupts, prioritize the user
- If you don't already know, find out the user's budget
- If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified
- If you don't already know, find out the occasion for which the user is buying wine
- If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity
- If you don't already know, find out what food will be served with wine
- If you haven't already, introduce the wines you found in the database to the user first
- Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
- All wines in your inventory are always in stock.
- Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory.
- Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database.
- Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time.
- Medium and full-bodied red wines are bad with spicy foods.
- If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store.
You should then respond to the user with:
1) Thought: State your thought about the current situation
2) Q: "Ask yourself" at least three, but no more than five, questions about the situation from your perspective.
3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions
You must only respond in format as described below:
Q1: ...
A1: ...
Q2: ...
A2: ...
...
Here are some examples:
Q: What the user is looking for?
A: The user is asking for a MPV car with 7-seat
Q: What do I know?
A: The user is looking for a car with 7-seat. Our dealer sell these kind of cars
Q: What brands the user prefer?
A: I don't know. The user didn't mentioned that. Let's find out.
Q: What else do I need to know before proceeding?
A: I don't know about the user budget, car's color, and other user's preferences yet. Let's find out more about the user's preferences.
Q: I'm still lacking information regarding the user's preferences for the powertrain. I've asked the user twice already, but perhaps they're not familiar with this. What should I do.
A: I'll proceed without asking the user about the powertrain.
Q: The user is buying for her husband, should I dig in to get more information?
A: Yes, I should. So that I have better idea about the user's preferences.
Q: Why the user saying this?
A: The user does not want an SUV because it does not have sliding doors
Q: The user is asking for a cappuccino. Do I have it at my cafe?
A: No I don't have.
Q: Since I don't have a cappuccino but I have a Late, should I ask if they are okay with that?
A: Yes, I should.
Q: Are they allergic to milk?
A: Since they mentioned a cappuccino before, it seems they are not allergic to milk.
Q: Have I checked the inventory yet?
A: No. I need more information from the user including ...
Q: What else do I need to know?
A: ...
Q: Should I present my item to the user?
A: Not yet, I will need to check my inventory first.
Q: Should I check our inventory now?
A: ...
Q: What the user intend to do with the car?
A: I don't know yet. Let's ask the user.
Q: What do I have in our inventory?
A: ...
Q: Which items are within the user price range? And which items are out of the user price rance?
A: ...
Q: Do I have what the user is looking for in our stock?
A: ...
Q: Am I certain about the information I'm going to share with the user, or should I verify the information first?
A: ...
Q: What should I do?
A: ...
Q: What shouldn't I do?
A: ...
Q: what kind of car suitable for off-road trip?
A: A four-wheel drive SUV is a good choice for off-road trips.
Q: What car specification would satisfy the user's needs?
A: The user is seeking an eco-friendly vehicle that accommodates seven passengers, including seniors and children, with prioritized accessibility and efficient refueling. While electric vehicles (EVs) offer eco-friendly benefits, their long charging times make hybrid models more practical for fast refueling. Additionally, a lower ground level is essential for ease of entry/exit for seniors and children. A hybrid multi-purpose vehicle (MPV) emerges as the optimal solution, balancing sustainability, seating capacity, accessibility, and refueling efficiency.
Let's begin!
"""
header = ["Q1:"]
dictkey = ["q1"]
# context =
# if length(a.memory["shortmem"]["available_wine"]) != 0
# "Available wines you've found in your inventory so far: $(availableWineToText(a.memory["shortmem"]["available_wine"]))"
# else
# "N/A"
# end
database_search_result = a.memory["shortmem"]["db_search_result"]
# recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent)
# recentevents = a.memory[:events][recent_ind]
# timeline = createTimeline(recentevents; eventindex=recent_ind)
errornote = "N/A"
response = nothing # store for show when error msg show up
# recap =
# if length(a.memory[:recap]) <= recent
# "N/A"
# else
# recapkeys = keys(a.memory[:recap])
# recapkeys_vec = [i for i in recapkeys]
# recapkeys_vec = recapkeys_vec[1:end-recent]
# tempmem = OrderedDict()
# for (k, v) in a.memory[:recap]
# if k ∈ recapkeys_vec
# tempmem[k] = v
# end
# end
# GeneralUtils.dictToString(tempmem)
# end
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.5,
)
for attempt in 1:10
if attempt > 1
println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
usermsg =
"""
Additional info: $database_search_result
Your recent events: $timeline
P.S. $errornote
"""
_prompt =
[
Dict("name" => "system", "text" => systemmsg),
Dict("name" => "user", "text" => usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)
response = text2textInstructLLM(prompt;
modelsize="medium", llmkwargs=llmkwargs, senderId=a.id)
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
think, response = GeneralUtils.extractthink(response)
# make sure generatequestion() don't have wine name that is not from retailer inventory
# check whether an agent recommend wines before checking inventory or recommend wines
# outside its inventory
# ask LLM whether there are any winery mentioned in the response
mentioned_winery = detectWineryName(a, response)
if mentioned_winery != "None"
mentioned_winery = String.(strip.(split(mentioned_winery, ",")))
# check whether the wine is in event
isWineInEvent = false
for winename in mentioned_winery
for event in a.memory["events"]
if event["observation"] !== nothing && occursin(winename, event["observation"])
isWineInEvent = true
break
end
end
end
# if wine is mentioned but not in timeline or shortmem,
# then the agent is not supposed to recommend the wine
if isWineInEvent == false
errornote = "Your previous attempt mentioned wines that are not in your inventory which is not allowed."
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
end
q_number = count("Q", response)
# check for valid response
if q_number < 1
errornote = "Your previous attempt has too few questions."
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
# check whether "A1" is in the response, if not error.
elseif !occursin("A1:", response)
errornote = "Your previous attempt does not have A1:"
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
# check whether response has all header
detected_kw = GeneralUtils.detectKeywordVariation(header, response)
if 0 values(detected_kw)
errornote = "\nYour previous attempt did not have all points according to the required response format"
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
response = "Q1: " * responsedict["q1"]
println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
try pprintln(response) catch e println(response) end
return response
end
error("YiemAgent generatequestion() failed to generate a response ", response)
end
function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0
)::OrderedDict
systemmsg =
"""
You are an assistant being in the given events.
Your task is to writes a summary for each event seperately into an ongoing, interleaving series.
At each round of conversation, you will be given the situation:
Total events: number of events you need to summarize.
Events timeline: ...
Context: ...
You should follow the following guidelines:
- Use the word "user" and "assistant" instead of their name in the report
You should then respond to the user with the following:
Event: a detailed summary for each event without exaggerated details.
You must only respond in format as described below:
Event_1: ...
Event_2: ...
...
Here are some examples:
Event_1: The user ask me about where to buy a toy.
Event_2: I told the user to go to the store at 2nd floor.
Event_1: The user greets the assistant by saying 'hello'.
Event_2: The assistant respond warmly and inquire about how he can assist the user.
Let's begin!
"""
header = ["Event_$i:" for i in eachindex(a.memory["events"])]
dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory["events"])])
ind = GeneralUtils.nonRecentElementsIndex(length(a.memory["events"]), skiprecent)
events = a.memory["events"][ind]
timeline = createTimeline(events)
errornote = "N/A"
response = nothing # store for show when error msg show up
for attempt in 1:10
if attempt > 1 # use to prevent LLM generate the same respond over and over
println("\nYiemAgent generateSituationReport() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
usermsg = """
Total events: $(length(events))
Events timeline: $timeline
P.S. $errornote
"""
_prompt =
[
Dict("name" => "system", "text" => systemmsg),
Dict("name" => "user", "text" => usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3")
response = text2textInstructLLM(prompt; senderId=a.id)
response = GeneralUtils.deFormatLLMtext(response, "qwen3")
# check whether response has all header
detected_kw = GeneralUtils.detectKeywordVariation(header, response)
kwvalue = [i for i in values(detected_kw)]
zeroind = findall(x -> x == 0, kwvalue)
missingkeys = [header[i] for i in zeroind]
if 0 values(detected_kw)
errornote = "$missingkeys are missing in your previous attempt"
println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "Your previous response has duplicated events"
println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
println("\ngenerateSituationReport() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
try pprintln(response) catch e println(response) end
return responsedict
end
error("generateSituationReport failed to generate a response ", response)
end
function detectWineryName(a, text)
systemmsg =
"""
You are a sommelier of a wine store.
Your task is to identify and list any winery names mentioned in the provided text.
At each round of conversation, you will be given the situation:
Text: a text describing the situation.
Tips:
- Winery usually contains Château, Chateau, Domaine, Côte, Cotes, St. de, or a combination of these words.
You should then respond to the user with:
Winery_names: A list of winery names mentioned in the text or "None" if no winery name is mentioned.
You must only respond in format as described below:
Winery_names: ...
Here are some examples:
Winery_names: Domaine Courbis, Chateau Lafite Rothschild, Matarromera Domaine Roulot, Château, Cotes
Let's begin!
"""
header = ["Winery_names:"]
dictkey = ["winery_names"]
response = nothing # placeholder for show when error msg show up
for attempt in 1:10
usermsg = """
Text: $text
"""
_prompt =
[
Dict("name" => "system", "text" => systemmsg),
Dict("name" => "user", "text" => usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)
response = a.context.text2textInstructLLM(prompt; senderId=a.id)
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
think, response = GeneralUtils.extractthink(response)
println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
try pprintln(response) catch e println(response) end
# check whether response has all header
detected_kw = GeneralUtils.detectKeywordVariation(header, response)
missingkeys = [k for (k, v) in detected_kw if v === nothing]
if !isempty(missingkeys)
errornote = "$missingkeys are missing from your previous response"
println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum([length(i) for i in values(detected_kw)]) > length(header)
errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
result = responsedict["winery_names"]
return result
end
error("detectWineryName failed to generate a response")
end
end # module interface