This commit is contained in:
narawat lamaiin
2025-04-04 15:04:02 +07:00
parent c21f943b12
commit c0edf7dadf
3 changed files with 247 additions and 204 deletions

View File

@@ -4,7 +4,7 @@ export addNewMessage, conversation, decisionMaker, reflector, generatechat,
generalconversation, detectWineryName, generateSituationReport generalconversation, detectWineryName, generateSituationReport
using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames DataFrames, CSV
using GeneralUtils using GeneralUtils
using ..type, ..util, ..llmfunction using ..type, ..util, ..llmfunction
@@ -97,7 +97,8 @@ julia> output_thoughtDict = Dict(
# Signature # Signature
""" """
function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:agent} function decisionMaker(a::T; recent::Integer=10
) where {T<:agent}
# lessonDict = copy(JSON3.read("lesson.json")) # lessonDict = copy(JSON3.read("lesson.json"))
@@ -126,9 +127,9 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent)
recentevents = a.memory[:events][recent_ind] recentevents = a.memory[:events][recent_ind]
timeline = createTimeline(recentevents) timeline = createTimeline(recentevents; eventindex=recent_ind)
#[TESTING] recap as caching # recap as caching
# query similar result from vectorDB # query similar result from vectorDB
recapkeys = keys(a.memory[:recap]) recapkeys = keys(a.memory[:recap])
_recapkeys_vec = [i for i in recapkeys] _recapkeys_vec = [i for i in recapkeys]
@@ -174,13 +175,14 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
2) Processing sales orders or engaging in any other sales-related activities. 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. 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 current situation: At each round of conversation, you will be given the following information:
Your recent events: latest 5 events of the situation Your recent events: latest 5 events of the situation
Your Q&A: the question and answer you have asked yourself Your Q&A: the question and answer you have asked yourself
You must follow the following guidelines: You must follow the following guidelines:
- Focus on the latest event
- 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. - 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. - All wines in your inventory are always in stock
- Approach each customer with open-ended questions to understand their preferences, budget, and occasion. This will help you guide the conversation naturally while gathering essential insights. Once you have this information, you can efficiently check your inventory for the best match. - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. This will help you guide the conversation naturally while gathering essential insights. Once you have this information, you can efficiently check your inventory for the best match.
- 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. - 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. - 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.
@@ -196,13 +198,13 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
- Vintage 0 means non-vintage. - Vintage 0 means non-vintage.
You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input: You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input:
1) Thought: Articulate your current understanding and consider the present situation. 1) Thought: Articulate your current understanding and consider the current situation.
2) Plan: Based on the current situation, state a complete action plan to complete the task. Be specific. 2) Plan: Based on the current situation, state a complete action plan to complete the task. Be specific.
3) Action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the following function names: 3) Action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the following function names:
- CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific.
- CHECKINVENTORY which you can use to check info about wine you want in your inventory. The input is a search term is verbal english and it should includes - winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, wine price, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. - CHECKINVENTORY which you can use to check info about wine you want in your inventory. The input is a search term is verbal english and it should includes - winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, wine price, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity.
Bad query example: red wine that pair well with spicy food. Invalid query example: red wine that pair well with spicy food.
- PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present.
- ENDCONVERSATION which you can use when the user has finished their conversation with you, so that you can properly end the conversation. Input is "NA". - ENDCONVERSATION which you can use when the user has finished their conversation with you, so that you can properly end the conversation. Input is "NA".
4) Action_input: input of the action 4) Action_input: input of the action
@@ -220,32 +222,6 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
chathistory = chatHistoryToText(a.chathistory) chathistory = chatHistoryToText(a.chathistory)
# check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX
if length(a.memory[:shortmem][:found_wine]) != 0
# check if wine name mentioned in recentevents, only check first wine name is enough
# because agent will recommend every wines it found each time.
winenames = []
for wine in a.memory[:shortmem][:found_wine]
push!(winenames, wine["wine_name"])
end
for winename in winenames
if !occursin(winename, chathistory)
println("\nYiem decisionMaker() found wines from DB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
d = Dict(
:thought=> "I understand that the customer is looking for a wine that matches their intention and budget. I checked the inventory and found wines that match the customer's criteria. I will present the wines to the customer.",
:plan=> "1) Provide detailed introductions of the wines you just found to the customer.
2) Explain how the wine could match the customer's intention and what its effects might mean for the customer's experience.
3) If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the customer's intention and what the potential effects of each option could mean for the customer's experience.
4) Provide your personal recommendation based on your understanding of the customer's preferences.",
:action_name=> "PRESENTBOX",
:action_input=> "")
a.memory[:shortmem][:found_wine] = [] # clear because PRESENTBOX command is issued. This is to prevent decisionMaker() keep presenting the same wines
return d
end
end
end
context = # may b add wine name instead of the hold wine data is better context = # may b add wine name instead of the hold wine data is better
if length(a.memory[:shortmem][:available_wine]) != 0 if length(a.memory[:shortmem][:available_wine]) != 0
winenames = [] winenames = []
@@ -277,14 +253,41 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
$errornote $errornote
""" """
_prompt = unformatPrompt =
[ [
Dict(:name => "system", :text => systemmsg), Dict(:name => "system", :text => systemmsg),
Dict(:name => "user", :text => usermsg) Dict(:name => "user", :text => usermsg)
] ]
#BUG found wine is "count 0" invalid return from CHECKINVENTORY()
# check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX
# if length(a.memory[:shortmem][:found_wine]) != 0
# # check if wine name mentioned in recentevents, only check first wine name is enough
# # because agent will recommend every wines it found each time.
# winenames = []
# for wine in a.memory[:shortmem][:found_wine]
# push!(winenames, wine["wine_name"])
# end
# for winename in winenames
# if !occursin(winename, chathistory)
# println("\nYiem decisionMaker() found wines from DB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# d = Dict(
# :thought=> "The user is looking for a wine that matches their intention and budget. I've checked the inventory and found wines that match the customer's criteria. I will present the wines to the customer.",
# :plan=> "1) I'll provide detailed introductions of the wines I just found to the user. 2) I'll explain how the wine could match the user's intention and what its effects might mean for the user's experience. 3) If multiple wines are available, I'll 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. 4) I'll provide my personal recommendation.",
# :action_name=> "PRESENTBOX",
# :action_input=> "I need to present to the user the following wines: $winenames")
# a.memory[:shortmem][:found_wine] = [] # clear because PRESENTBOX command is issued. This is to prevent decisionMaker() keep presenting the same wines
# result = (systemmsg=systemmsg, usermsg=usermsg, unformatPrompt=unformatPrompt, result=d)
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(Dict(d))
# return result
# end
# end
# end
# change qwen format put in model format # change qwen format put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") prompt = GeneralUtils.formatLLMtext(unformatPrompt; formatname="qwen")
response = a.func[:text2textInstructLLM](prompt) response = a.func[:text2textInstructLLM](prompt)
response = GeneralUtils.remove_french_accents(response) response = GeneralUtils.remove_french_accents(response)
@@ -294,7 +297,7 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
# check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"]
count = 0 count = 0
for i ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] for i ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"]
if occursin(i, response) if occursin(i, response)
count += 1 count += 1
end end
@@ -323,8 +326,8 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
responsedict = GeneralUtils.textToDict(response, header; responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true) dictKey=dictkey, symbolkey=true)
if responsedict[:action_name] ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] if responsedict[:action_name] ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"]
errornote = "You must use the given functions" errornote = "Your previous attempt didn't use the given functions"
println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
@@ -345,7 +348,7 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
for i Symbol.(dictkey) for i Symbol.(dictkey)
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
if length(matchkeys) > 1 if length(matchkeys) > 1
errornote = "DecisionMaker has more than one key per categories" errornote = "Your previous attempt has more than one key per categories"
println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
checkFlag = true checkFlag = true
break break
@@ -353,6 +356,15 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
end end
checkFlag == true ? continue : nothing checkFlag == true ? continue : nothing
# check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or
# "pair well" in it because it is not a valid query.
detected_kw = GeneralUtils.detect_keyword(["pair", "pairs", "pairing", "well"], responsedict[:action_input])
if responsedict[:action_name] == "CHECKINVENTORY" && sum(values(detected_kw)) != 0
errornote = "Your previous attempt has invalid query"
println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(Dict(responsedict)) pprintln(Dict(responsedict))
@@ -386,6 +398,20 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
end end
delete!(responsedict, :mentioned_winery) delete!(responsedict, :mentioned_winery)
responsedict[:systemmsg] = systemmsg
responsedict[:usermsg] = usermsg
responsedict[:unformatPrompt] = unformatPrompt
responsedict[:QandA] = QandA
# store responsedict in decisionlog.csv. if it is the first time, create the file
if !isfile("/appfolder/app/decisionlog.csv")
CSV.write(decisionlog, responsedict)
else
CSV.write(decisionlog, responsedict, append=true)
end
return responsedict return responsedict
end end
@@ -813,9 +839,8 @@ julia>
# Signature # Signature
""" """
function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent}
a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0)
thoughtDict = decisionMaker(a; recent=3) thoughtDict = decisionMaker(a; recent=5)
actionname = thoughtDict[:action_name] actionname = thoughtDict[:action_name]
actioninput = thoughtDict[:action_input] actioninput = thoughtDict[:action_input]
@@ -844,22 +869,9 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
errormsg::Union{AbstractString,Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing errormsg::Union{AbstractString,Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing
success::Bool = haskey(response, :success) ? response[:success] : false success::Bool = haskey(response, :success) ? response[:success] : false
# manage memory (pass msg to generatechat) # # manage memory (pass msg to generatechat)
if actionname ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"] # if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"]
chatresponse = generatechat(a, thoughtDict) # chatresponse = generatechat(a, thoughtDict)
push!(a.memory[:events],
eventdict(;
event_description="the assistant talks to the user.",
timestamp=Dates.now(),
subject="assistant",
thought=thoughtDict,
actionname=actionname,
actioninput=actioninput,
)
)
result = chatresponse
# if actionname ∈ ["CHATBOX", "ENDCONVERSATION"]
# # chatresponse = generatechat(a, thoughtDict)
# push!(a.memory[:events], # push!(a.memory[:events],
# eventdict(; # eventdict(;
# event_description="the assistant talks to the user.", # event_description="the assistant talks to the user.",
@@ -870,20 +882,33 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
# actioninput=actioninput, # actioninput=actioninput,
# ) # )
# ) # )
# result = actioninput
# elseif actionname ∈ ["PRESENTBOX"]
# chatresponse = generatechat(a, thoughtDict)
# push!(a.memory[:events],
# eventdict(;
# event_description="the assistant talks to the user.",
# timestamp=Dates.now(),
# subject="assistant",
# thought=thoughtDict,
# actionname=actionname,
# actioninput=chatresponse,
# )
# )
# result = chatresponse # result = chatresponse
if actionname ["CHATBOX", "ENDCONVERSATION"]
# chatresponse = generatechat(a, thoughtDict)
push!(a.memory[:events],
eventdict(;
event_description="the assistant talks to the user.",
timestamp=Dates.now(),
subject="assistant",
thought=thoughtDict,
actionname=actionname,
actioninput=actioninput,
)
)
result = actioninput
elseif actionname ["PRESENTBOX"]
chatresponse = presentbox(a, thoughtDict)
push!(a.memory[:events],
eventdict(;
event_description="the assistant talks to the user.",
timestamp=Dates.now(),
subject="assistant",
thought=thoughtDict,
actionname=actionname,
actioninput=chatresponse,
)
)
result = chatresponse
elseif actionname == "CHECKINVENTORY" elseif actionname == "CHECKINVENTORY"
if rawresponse !== nothing if rawresponse !== nothing
@@ -926,41 +951,34 @@ function presentbox(a::sommelier, thoughtDict)
Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store.
</Your profile> </Your profile>
<Situation> <Situation>
You have checked the inventory and found wines that match the customer's criteria. You have checked the inventory and found wines.
</Situation> </Situation>
<Your mission> <Your mission>
Present the wines to the customer in a way that keep the conversation smooth and engaging. Present the wines to the customer in a way that keep the conversation smooth and engaging.
</Your mission> </Your mission>
<At each round of conversation, you will be given the following> <At each round of conversation, you will be given the following information>
Your ongoing conversation with the user: ... Additional info: additional information
Inventory check result: ... Chat history: your ongoing conversation with the user
Your thoughts: Your current thoughts in your mind Wine name: name if wines you found.
</At each round of conversation, you will be given the following> </At each round of conversation, you will be given the following>
<You must follow the following guidelines>
- Do not offer additional services you didn't think.
- Focus on plan.
</You must follow the following guidelines>
<You should follow the following guidelines> <You should follow the following guidelines>
- Focus on the latest conversation. - Provide detailed introductions of the wines you've found to the user.
- If the user interrupts, prioritize the user - Explain how the wine could match the user's intention and what its effects might mean for the user's experience.
- Be honest - 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.
- Medium and full-bodied red wines should not be paired with spicy foods. - Provide your personal recommendation and provide a brief explanation of why you recommend it.
</You should follow the following guidelines> </You should follow the following guidelines>
<You should then respond to the user with> <You should then respond to the user with>
Chat: ... Dialogue: your wine presentation to the user
</You should then respond to the user with> </You should then respond to the user with>
<Here are some examples> <You should only respond in format as described below>
Your ongoing conversation with the user: "user> hello, I need a new car\n" Dialogue: ...
Additional info: "Car previously found in your inventory: 1) Toyota Camry 2020 2) Honda Civic 2021 3) Ford Mustang 2022" </You should only respond in format as described below>
Your thoughts: "I should recommend the car we have to the user."
Chat: "We have a variety of cars available, including the Toyota Camry 2020, the Honda Civic 2021, and the Ford Mustang 2022. Which one would you like to see?"
</Here are some examples>
Let's begin! Let's begin!
""" """
header = ["Chat:"] header = ["Dialogue:"]
dictkey = ["chat"] dictkey = ["dialogue"]
# a.memory[:shortmem][:available_wine] is a vector of dictionary # a.memory[:shortmem][:available_wine] is a vector of dictionary
context = context =
@@ -974,28 +992,25 @@ function presentbox(a::sommelier, thoughtDict)
errornote = "" errornote = ""
response = nothing # placeholder for show when error msg show up response = nothing # placeholder for show when error msg show up
yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])"
yourthought1 = nothing # yourthought1 = nothing
for attempt in 1:10 for attempt in 1:10
if attempt > 1 # use to prevent LLM generate the same respond over and over if attempt > 1 # use to prevent LLM generate the same respond over and over
yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) println("\nYiemAgent presentbox() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought)
# llmkwargs[:temperature] = 0.1 * attempt
else else
yourthought1 = yourthought # yourthought1 = yourthought
end end
usermsg = """ usermsg =
<Your ongoing conversation with the user> """
$chathistory
</Your ongoing conversation with the user>
<Additional info>
$context
</Additional info>
<Your thoughts>
$yourthought1
</Your thoughts>
$errornote $errornote
Additional info: $context
Chat history: $chathistory
Wine name: $(thoughtDict[:action_input])
""" """
_prompt = _prompt =
@@ -1010,8 +1025,9 @@ function presentbox(a::sommelier, thoughtDict)
response = a.func[:text2textInstructLLM](prompt) response = a.func[:text2textInstructLLM](prompt)
# sometime the model response like this "here's how I would respond: ..." # sometime the model response like this "here's how I would respond: ..."
if occursin("respond:", response) if occursin("respond:", response)
errornote = "You don't need to intro your response" errornote = "Your previous response contains 'response:' which is not allowed"
error("generatechat() response contain : ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end end
response = GeneralUtils.remove_french_accents(response) response = GeneralUtils.remove_french_accents(response)
response = replace(response, '*'=>"") response = replace(response, '*'=>"")
@@ -1023,28 +1039,32 @@ function presentbox(a::sommelier, thoughtDict)
# check whether response has all header # check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response) detected_kw = GeneralUtils.detect_keyword(header, response)
if 0 values(detected_kw) if 0 values(detected_kw)
errornote = "\nYiemAgent generatechat() response does not have all header" errornote = "$missingkeys are missing from your previous response"
println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
elseif sum(values(detected_kw)) > length(header) elseif sum(values(detected_kw)) > length(header)
errornote = "\nnYiemAgent generatechat() response has duplicated header" errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
responsedict = GeneralUtils.textToDict(response, header; responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true) dictKey=dictkey, symbolkey=true)
# check if Context: is in chat # check if Context: is in dialogue
if occursin("Context:", responsedict[:chat]) if occursin("Context:", responsedict[:dialogue])
error("Context: is in text. This is not allowed") errornote = "Your previous response contains 'Context:' which is not allowed"
println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end end
println("\ngeneratechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nYiemAgent presentbox() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(Dict(responsedict)) pprintln(Dict(responsedict))
# check whether an agent recommend wines before checking inventory or recommend wines # check whether an agent recommend wines before checking inventory or recommend wines
# outside its inventory # outside its inventory
# ask LLM whether there are any winery mentioned in the response # ask LLM whether there are any winery mentioned in the response
mentioned_winery = detectWineryName(a, responsedict[:chat]) mentioned_winery = detectWineryName(a, responsedict[:dialogue])
if mentioned_winery != "None" if mentioned_winery != "None"
mentioned_winery = String.(strip.(split(mentioned_winery, ","))) mentioned_winery = String.(strip.(split(mentioned_winery, ",")))
@@ -1062,13 +1082,13 @@ function presentbox(a::sommelier, thoughtDict)
# if wine is mentioned but not in timeline or shortmem, # if wine is mentioned but not in timeline or shortmem,
# then the agent is not supposed to recommend the wine # then the agent is not supposed to recommend the wine
if isWineInEvent == false if isWineInEvent == false
errornote = "Your previous response recommends wines that is not in your inventory which is not allowed"
errornote = "Previously, You recommend wines that is not in your inventory which is not allowed." println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
error("Previously, You recommend wines that is not in your inventory which is not allowed.") continue
end end
end end
result = responsedict[:chat] result = responsedict[:dialogue]
return result return result
end end
@@ -1111,8 +1131,8 @@ function generatechat(a::sommelier, thoughtDict)
- 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. - 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 following: At each round of conversation, you will be given the following:
Additional info: ... Additional info: ...
Your thoughts: Your current thoughts in your mind
Your ongoing conversation with the user: ... Your ongoing conversation with the user: ...
Your thoughts: Your current thoughts in your mind
You must follow the following guidelines: You must follow the following guidelines:
- Do not offer additional services you didn't think - Do not offer additional services you didn't think
You should follow the following guidelines: You should follow the following guidelines:
@@ -1150,11 +1170,16 @@ function generatechat(a::sommelier, thoughtDict)
yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])"
yourthought1 = nothing yourthought1 = nothing
for attempt in 1:10 llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.1,
)
for attempt in 1:10
if attempt > 1 # use to prevent LLM generate the same respond over and over if attempt > 1 # use to prevent LLM generate the same respond over and over
println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought)
llmkwargs[:temperature] = 0.1 * attempt
else else
yourthought1 = yourthought yourthought1 = yourthought
end end
@@ -1163,8 +1188,8 @@ function generatechat(a::sommelier, thoughtDict)
""" """
$errornote $errornote
Additional info: $context Additional info: $context
Your thoughts: $yourthought1
Your ongoing conversation with the user: $chathistory Your ongoing conversation with the user: $chathistory
Your thoughts: $yourthought1
""" """
_prompt = _prompt =
@@ -1175,15 +1200,15 @@ function generatechat(a::sommelier, thoughtDict)
# put in model format # put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen")
response = a.func[:text2textInstructLLM](prompt) response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs)
# sometime the model response like this "here's how I would respond: ..." # sometime the model response like this "here's how I would respond: ..."
if occursin("respond:", response) if occursin("respond:", response)
errornote = "You don't need to put 'response:' in your response" errornote = "Your previous response contains 'response:' which is not allowed"
println("ERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
elseif occursin("Your thoughts:", response) || occursin("your thoughts:", response) elseif occursin("Your thoughts:", response) || occursin("your thoughts:", response)
errornote = "You don't need to put 'Your thoughts:' in your response" errornote = "You don't need to put 'Your thoughts:' in your response"
println("ERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end end
response = GeneralUtils.remove_french_accents(response) response = GeneralUtils.remove_french_accents(response)
response = replace(response, '*'=>"") response = replace(response, '*'=>"")
@@ -1199,20 +1224,21 @@ function generatechat(a::sommelier, thoughtDict)
missingkeys = [header[i] for i in zeroind] missingkeys = [header[i] for i in zeroind]
if 0 values(detected_kw) if 0 values(detected_kw)
errornote = "$missingkeys are missing from your previous response" errornote = "$missingkeys are missing from your previous response"
println("\nYiemAgent generatechat() $errornote:\n $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
elseif sum(values(detected_kw)) > length(header) elseif sum(values(detected_kw)) > length(header)
errornote = "Your response has duplicated points" errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\n$errornote: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
responsedict = GeneralUtils.textToDict(response, header; responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true) dictKey=dictkey, symbolkey=true)
# check if Context: is in chat # check if Context: is in dialogue
if occursin("Context:", responsedict[:dialogue]) if occursin("Context:", responsedict[:dialogue])
println("\nYiemAgent generatechat() context is in response. This is not allowed", @__FILE__, ":", @__LINE__, " $(Dates.now())") errornote = "Your previous response contains 'Context:' which is not allowed"
println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
@@ -1473,17 +1499,14 @@ function generatequestion(a, text2textInstructLLM::Function;
2) Thanks the user when they don't need any further assistance and invite them to comeback next time 2) Thanks the user when they don't need any further assistance and invite them to comeback next time
Your responsibility includes: Your responsibility includes:
1) Ask yourself: 1) From your point of view as a sommelier helping the user, ask yourself multiple questions based on the current situation
- what do you know
- what you do not know
- what could you do
Your responsibility does NOT includes: 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. 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. 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. 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 current situation: At each round of conversation, you will be given the info:
Recap: recap of what has happened so far Recap: recap of what has happened so far
Additional info: ... Additional info: ...
Your recent events: latest 5 events of the situation Your recent events: latest 5 events of the situation
@@ -1523,10 +1546,14 @@ function generatequestion(a, text2textInstructLLM::Function;
... ...
Here are some examples: Here are some examples:
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: What the user is looking for? Q: What the user is looking for?
A: The user is asking for a MPV car with 7-seat 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 I do not know?
A: I don't know about the user budget, car's color, powertrain and other user's preferences.
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? Q: Why the user saying this?
A: The user does not want an SUV because it does not have sliding doors 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? Q: The user is asking for a cappuccino. Do I have it at my cafe?
@@ -1539,18 +1566,18 @@ function generatequestion(a, text2textInstructLLM::Function;
A: No. I need more information from the user including ... A: No. I need more information from the user including ...
Q: What else do I need to know? Q: What else do I need to know?
A: ... A: ...
Q: Should I check the inventory now? Q: Should I check our inventory now?
A: ... A: ...
Q: What the user intend to do with the car? Q: What the user intend to do with the car?
A: I don't know yet. I will need to ask the user. A: I don't know yet. Let's ask the user.
Q: What do I have in the inventory? Q: What do I have in our inventory?
A: ... A: ...
Q: Which items are within the user price range? And which items are out of the user price rance? Q: Which items are within the user price range? And which items are out of the user price rance?
A: ... A: ...
Q: Do I have them in stock? Q: Do I have what the user is looking for in our stock?
A: ... A: ...
Q: Did I introduce them to the user already? Q: Did I introduce what I found in our inventory to the user already?
A: Not yet. A: According to my conversation with the user, not yet.
Q: Am I certain about the information I'm going to share with the user, or should I verify the information first? Q: Am I certain about the information I'm going to share with the user, or should I verify the information first?
A: ... A: ...
Q: What should I do? Q: What should I do?
@@ -1575,7 +1602,7 @@ function generatequestion(a, text2textInstructLLM::Function;
recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent)
recentevents = a.memory[:events][recent_ind] recentevents = a.memory[:events][recent_ind]
timeline = createTimeline(recentevents) timeline = createTimeline(recentevents; eventindex=recent_ind)
errornote = "" errornote = ""
response = nothing # store for show when error msg show up response = nothing # store for show when error msg show up
@@ -1596,9 +1623,15 @@ function generatequestion(a, text2textInstructLLM::Function;
GeneralUtils.dictToString(tempmem) GeneralUtils.dictToString(tempmem)
end end
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.2,
)
for attempt in 1:10 for attempt in 1:10
if attempt > 1 if attempt > 1
println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
llmkwargs[:temperature] = 0.1 * attempt
end end
usermsg = usermsg =
@@ -1618,7 +1651,7 @@ function generatequestion(a, text2textInstructLLM::Function;
# put in model format # put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen")
response = text2textInstructLLM(prompt, modelsize="medium") response = text2textInstructLLM(prompt, modelsize="medium", llmkwargs=llmkwargs)
# make sure generatequestion() don't have wine name that is not from retailer inventory # 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 # check whether an agent recommend wines before checking inventory or recommend wines
# outside its inventory # outside its inventory
@@ -1649,24 +1682,26 @@ function generatequestion(a, text2textInstructLLM::Function;
q_number = count("Q", response) q_number = count("Q", response)
# check for valid response # check for valid response
if q_number < 2 if q_number < 1
errornote = "too few questions only $q_number questions are generated previously." errornote = "Your previous response has too few questions."
println("too few questions only $q_number questions are generated ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
# check whether "A1" is in the response, if not error. # check whether "A1" is in the response, if not error.
elseif !occursin("A1:", response) elseif !occursin("A1:", response)
errornote = "previous response does not have A1" errornote = "Your previous response does not have A1:"
println("\nprevious response does not have A1 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
# check whether response has all header # check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response) detected_kw = GeneralUtils.detect_keyword(header, response)
if 0 values(detected_kw) if 0 values(detected_kw)
errornote = "\nresponse does not have all header" 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 continue
elseif sum(values(detected_kw)) > length(header) elseif sum(values(detected_kw)) > length(header)
errornote = "\nYiemAgent generatequestion() response has duplicated header" errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end

View File

@@ -281,18 +281,27 @@ timeline = createTimeline(events)
# 2) Assistant> Hi there! with a smile # 2) Assistant> Hi there! with a smile
""" """
function createTimeline(events::T1) where {T1<:AbstractVector} function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty timeline string # Initialize empty timeline string
timeline = "" timeline = ""
# Iterate through events with index # Determine which indices to use - either provided range or full length
for (i, event) in enumerate(events) ind =
if eventindex !== nothing
[eventindex...]
else
1:length(events)
end
# Iterate through events and format each one
for (i, event) in zip(ind, events)
# If no outcome exists, format without outcome # If no outcome exists, format without outcome
if event[:outcome] === nothing if event[:outcome] === nothing
timeline *= "Event_$i) $(event[:subject])> $(event[:actioninput])\n" timeline *= "Event_$i $(event[:subject])> $(event[:actioninput])\n"
# If outcome exists, include it in formatting # If outcome exists, include it in formatting
else else
timeline *= "Event_$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
end end
end end
@@ -302,7 +311,6 @@ end
# """ Convert a single chat dictionary into LLM model instruct format. # """ Convert a single chat dictionary into LLM model instruct format.
# # Llama 3 instruct format example # # Llama 3 instruct format example

View File

@@ -83,7 +83,7 @@ function getEmbedding(text::T) where {T<:AbstractString}
msgPurpose="embedding", msgPurpose="embedding",
senderName="yiemagent", senderName="yiemagent",
senderId=sessionId, senderId=sessionId,
receiverName="text2textinstruct_small", receiverName="textembedding",
mqttBrokerAddress=config[:mqttServerInfo][:broker], mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port], mqttBrokerPort=config[:mqttServerInfo][:port],
) )