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
using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames
DataFrames, CSV
using GeneralUtils
using ..type, ..util, ..llmfunction
@@ -97,7 +97,8 @@ julia> output_thoughtDict = Dict(
# 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"))
@@ -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)
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
recapkeys = keys(a.memory[:recap])
_recapkeys_vec = [i for i in recapkeys]
@@ -160,59 +161,60 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
else
systemmsg =
"""
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 goal includes:
1) Establish a connection with the customer by greeting them warmly
2) Guide them to select the best wines only from your store's inventory that align with their preferences
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 goal includes:
1) Establish a connection with the customer by greeting them warmly
2) Guide them to select the best wines only from your store's inventory that align with their preferences
Your responsibility includes:
1) Make an informed decision about what you need to do to achieve the goal
2) Thanks the user when they don't need any further assistance and invite them to comeback next time
Your responsibility includes:
1) Make an informed decision about what you need to do to achieve the goal
2) Thanks the user when they don't need any further assistance and invite them to comeback next time
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.
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 current situation:
Your recent events: latest 5 events of the situation
Your Q&A: the question and answer you have asked yourself
At each round of conversation, you will be given the following information:
Your recent events: latest 5 events of the situation
Your Q&A: the question and answer you have asked yourself
You must follow the following guidelines:
- 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.
- 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.
- 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.
- Spicy foods should not be paired with medium and full-bodied red wines.
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.
- 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.
- 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.
- Spicy foods should not be paired with medium and full-bodied red wines.
You should follow the following guidelines:
- When searching an inventory, search as broadly as possible based on the information you have gathered so far.
- Encourage the customer to explore different options and try new things.
- Sometimes, the item a user desires might not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
You should follow the following guidelines:
- When searching an inventory, search as broadly as possible based on the information you have gathered so far.
- Encourage the customer to explore different options and try new things.
- Sometimes, the item a user desires might not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
For your information:
- Your store carries only wine.
- Vintage 0 means non-vintage.
For your information:
- Your store carries only wine.
- Vintage 0 means non-vintage.
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.
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:
- 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.
Bad query example: red wine that pair well with spicy food.
You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input:
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.
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.
- 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.
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".
4) Action_input: input of the action
- 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
You should only respond in format as described below:
Thought: ...
Plan: ...
Action_name: ...
Action_input: ...
You should only respond in format as described below:
Thought: ...
Plan: ...
Action_name: ...
Action_input: ...
Let's begin!
Let's begin!
"""
header = ["Thought:", "Plan:", "Action_name:", "Action_input:"]
@@ -220,32 +222,6 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
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
if length(a.memory[:shortmem][:available_wine]) != 0
winenames = []
@@ -277,14 +253,41 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
$errornote
"""
_prompt =
unformatPrompt =
[
Dict(:name => "system", :text => systemmsg),
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
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen")
prompt = GeneralUtils.formatLLMtext(unformatPrompt; formatname="qwen")
response = a.func[:text2textInstructLLM](prompt)
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"]
count = 0
for i ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"]
for i ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"]
if occursin(i, response)
count += 1
end
@@ -323,8 +326,8 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
if responsedict[:action_name] ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"]
errornote = "You must use the given functions"
if responsedict[:action_name] ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"]
errornote = "Your previous attempt didn't use the given functions"
println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
@@ -345,7 +348,7 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
for i Symbol.(dictkey)
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
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())")
checkFlag = true
break
@@ -353,6 +356,15 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
end
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())")
pprintln(Dict(responsedict))
@@ -386,6 +398,20 @@ function decisionMaker(a::T; recent::Integer=10)::Dict{Symbol,Any} where {T<:age
end
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
end
@@ -813,9 +839,8 @@ julia>
# Signature
"""
function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent}
a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0)
thoughtDict = decisionMaker(a; recent=3)
thoughtDict = decisionMaker(a; recent=5)
actionname = thoughtDict[:action_name]
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
success::Bool = haskey(response, :success) ? response[:success] : false
# manage memory (pass msg to generatechat)
if actionname ["CHATBOX", "PRESENTBOX", "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 = chatresponse
# if actionname ∈ ["CHATBOX", "ENDCONVERSATION"]
# # chatresponse = generatechat(a, thoughtDict)
# # manage memory (pass msg to generatechat)
# if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"]
# chatresponse = generatechat(a, thoughtDict)
# push!(a.memory[:events],
# eventdict(;
# 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,
# )
# )
# 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
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"
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 profile>
<Situation>
You have checked the inventory and found wines that match the customer's criteria.
You have checked the inventory and found wines.
</Situation>
<Your mission>
Present the wines to the customer in a way that keep the conversation smooth and engaging.
</Your mission>
<At each round of conversation, you will be given the following>
Your ongoing conversation with the user: ...
Inventory check result: ...
Your thoughts: Your current thoughts in your mind
<At each round of conversation, you will be given the following information>
Additional info: additional information
Chat history: your ongoing conversation with the user
Wine name: name if wines you found.
</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>
- Focus on the latest conversation.
- If the user interrupts, prioritize the user
- Be honest
- Medium and full-bodied red wines should not be paired with spicy foods.
- 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.
</You should follow the following guidelines>
<You should then respond to the user with>
Chat: ...
Dialogue: your wine presentation to the user
</You should then respond to the user with>
<Here are some examples>
Your ongoing conversation with the user: "user> hello, I need a new car\n"
Additional info: "Car previously found in your inventory: 1) Toyota Camry 2020 2) Honda Civic 2021 3) Ford Mustang 2022"
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>
<You should only respond in format as described below>
Dialogue: ...
</You should only respond in format as described below>
Let's begin!
"""
header = ["Chat:"]
dictkey = ["chat"]
header = ["Dialogue:"]
dictkey = ["dialogue"]
# a.memory[:shortmem][:available_wine] is a vector of dictionary
context =
@@ -974,29 +992,26 @@ function presentbox(a::sommelier, thoughtDict)
errornote = ""
response = nothing # placeholder for show when error msg show up
yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])"
yourthought1 = nothing
# yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])"
# yourthought1 = nothing
for attempt in 1:10
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
yourthought1 = yourthought
# yourthought1 = yourthought
end
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
"""
usermsg =
"""
$errornote
Additional info: $context
Chat history: $chathistory
Wine name: $(thoughtDict[:action_input])
"""
_prompt =
[
@@ -1010,8 +1025,9 @@ function presentbox(a::sommelier, thoughtDict)
response = a.func[:text2textInstructLLM](prompt)
# sometime the model response like this "here's how I would respond: ..."
if occursin("respond:", response)
errornote = "You don't need to intro your response"
error("generatechat() response contain : ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
errornote = "Your previous response contains 'response:' which is not allowed"
println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
response = GeneralUtils.remove_french_accents(response)
response = replace(response, '*'=>"")
@@ -1023,28 +1039,32 @@ function presentbox(a::sommelier, thoughtDict)
# check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response)
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
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
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
# check if Context: is in chat
if occursin("Context:", responsedict[:chat])
error("Context: is in text. This is not allowed")
# check if Context: is in dialogue
if occursin("Context:", responsedict[:dialogue])
errornote = "Your previous response contains 'Context:' which is not allowed"
println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
println("\ngeneratechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
println("\nYiemAgent presentbox() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(Dict(responsedict))
# 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, responsedict[:chat])
mentioned_winery = detectWineryName(a, responsedict[:dialogue])
if mentioned_winery != "None"
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,
# then the agent is not supposed to recommend the wine
if isWineInEvent == false
errornote = "Previously, You recommend wines that is not in your inventory which is not allowed."
error("Previously, You recommend wines that is not in your inventory which is not allowed.")
errornote = "Your previous response recommends wines that is not in your inventory which is not allowed"
println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
end
result = responsedict[:chat]
result = responsedict[:dialogue]
return result
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.
At each round of conversation, you will be given the following:
Additional info: ...
Your thoughts: Your current thoughts in your mind
Your ongoing conversation with the user: ...
Your thoughts: Your current thoughts in your mind
You must follow the following guidelines:
- Do not offer additional services you didn't think
You should follow the following guidelines:
@@ -1150,11 +1170,16 @@ function generatechat(a::sommelier, thoughtDict)
yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])"
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
println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought)
llmkwargs[:temperature] = 0.1 * attempt
else
yourthought1 = yourthought
end
@@ -1163,8 +1188,8 @@ function generatechat(a::sommelier, thoughtDict)
"""
$errornote
Additional info: $context
Your thoughts: $yourthought1
Your ongoing conversation with the user: $chathistory
Your thoughts: $yourthought1
"""
_prompt =
@@ -1175,15 +1200,15 @@ function generatechat(a::sommelier, thoughtDict)
# put in model format
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: ..."
if occursin("respond:", response)
errornote = "You don't need to put 'response:' in your response"
println("ERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
errornote = "Your previous response contains 'response:' which is not allowed"
println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
elseif occursin("Your thoughts:", response) || occursin("your thoughts:", 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
response = GeneralUtils.remove_french_accents(response)
response = replace(response, '*'=>"")
@@ -1199,20 +1224,21 @@ function generatechat(a::sommelier, thoughtDict)
missingkeys = [header[i] for i in zeroind]
if 0 values(detected_kw)
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
elseif sum(values(detected_kw)) > length(header)
errornote = "Your response has duplicated points"
println("\n$errornote: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
# check if Context: is in chat
# check if Context: is in 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
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
Your responsibility includes:
1) Ask yourself:
- what do you know
- what you do not know
- what could you do
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 current situation:
At each round of conversation, you will be given the info:
Recap: recap of what has happened so far
Additional info: ...
Your recent events: latest 5 events of the situation
@@ -1523,10 +1546,14 @@ function generatequestion(a, text2textInstructLLM::Function;
...
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?
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?
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?
@@ -1539,18 +1566,18 @@ function generatequestion(a, text2textInstructLLM::Function;
A: No. I need more information from the user including ...
Q: What else do I need to know?
A: ...
Q: Should I check the inventory now?
Q: Should I check our inventory now?
A: ...
Q: What the user intend to do with the car?
A: I don't know yet. I will need to ask the user.
Q: What do I have in the inventory?
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 them in stock?
Q: Do I have what the user is looking for in our stock?
A: ...
Q: Did I introduce them to the user already?
A: Not yet.
Q: Did I introduce what I found in our inventory to the user already?
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?
A: ...
Q: What should I do?
@@ -1575,7 +1602,7 @@ function generatequestion(a, text2textInstructLLM::Function;
recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent)
recentevents = a.memory[:events][recent_ind]
timeline = createTimeline(recentevents)
timeline = createTimeline(recentevents; eventindex=recent_ind)
errornote = ""
response = nothing # store for show when error msg show up
@@ -1596,9 +1623,15 @@ function generatequestion(a, text2textInstructLLM::Function;
GeneralUtils.dictToString(tempmem)
end
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.2,
)
for attempt in 1:10
if attempt > 1
println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
llmkwargs[:temperature] = 0.1 * attempt
end
usermsg =
@@ -1618,7 +1651,7 @@ function generatequestion(a, text2textInstructLLM::Function;
# put in model format
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
# check whether an agent recommend wines before checking inventory or recommend wines
# outside its inventory
@@ -1649,24 +1682,26 @@ function generatequestion(a, text2textInstructLLM::Function;
q_number = count("Q", response)
# check for valid response
if q_number < 2
errornote = "too few questions only $q_number questions are generated previously."
println("too few questions only $q_number questions are generated ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
if q_number < 1
errornote = "Your previous response 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 = "previous response does not have A1"
println("\nprevious response does not have A1 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
errornote = "Your previous response does not have A1:"
println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
# check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response)
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
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
end

View File

@@ -281,18 +281,27 @@ timeline = createTimeline(events)
# 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
timeline = ""
# Iterate through events with index
for (i, event) in enumerate(events)
# 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, event) in zip(ind, events)
# If no outcome exists, format without outcome
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
else
timeline *= "Event_$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
end
end
@@ -302,7 +311,6 @@ end
# """ Convert a single chat dictionary into LLM model instruct format.
# # Llama 3 instruct format example

View File

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