diff --git a/src/interface.jl b/src/interface.jl index b404e08..429d4f8 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -97,7 +97,7 @@ julia> output_thoughtDict = Dict( # Signature """ -function decisionMaker(a::T; recent::Integer=10, maxattempt=10 +function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 ) where {T<:agent} # lessonDict = copy(JSON3.read("lesson.json")) @@ -125,35 +125,42 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 # """ # end - recent_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recent; includelatest=true) - recentevents = a.memory[:events][recent_ind] - recentchat = createChatLog(a.chathistory[recent_ind]; index=recent_ind) + recentevents_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentevents; + includelatest=true) + timeline = createTimeline(a.memory[:events]; eventindex=recentevents_ind) + + recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; + includelatest=true) + recentchat = createChatLog(a.chathistory; index=recentchat_ind) # recentEventsDict = createEventsLog(recentevents; index=recent_ind) + #BUG timeline only cover event 1-9 out of 10 events while recentchat cover 1-9 because + # recent_ind is based on chathistory. i should ind based on events. The reason is events always + # have more than chathistory due to CHECKINVENTORY() function which store in events BUT NOT in + # chathistory + - timeline = createTimeline(recentevents; eventindex=recent_ind) + # # recap as caching + # # query similar result from vectorDB + # recapkeys = keys(a.memory[:recap]) + # _recapkeys_vec = [i for i in recapkeys] - # recap as caching - # query similar result from vectorDB - recapkeys = keys(a.memory[:recap]) - _recapkeys_vec = [i for i in recapkeys] + # # select recent keys + # _recentRecapKeys = + # if length(a.memory[:recap]) <= 3 # 1st message is a user's hello msg + # _recapkeys_vec + # elseif length(a.memory[:recap]) > 3 + # l = length(a.memory[:recap]) + # _recapkeys_vec[l-2:l] + # end - # select recent keys - _recentRecapKeys = - if length(a.memory[:recap]) <= 3 # 1st message is a user's hello msg - _recapkeys_vec - elseif length(a.memory[:recap]) > 3 - l = length(a.memory[:recap]) - _recapkeys_vec[l-2:l] - end - - # get recent recap - _recentrecap = OrderedDict() - for (k, v) in a.memory[:recap] - if k ∈ _recentRecapKeys - _recentrecap[k] = v - end - end + # # get recent recap + # _recentrecap = OrderedDict() + # for (k, v) in a.memory[:recap] + # if k ∈ _recentRecapKeys + # _recentrecap[k] = v + # end + # end # recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) # similarDecision = a.func[:similarSommelierDecision](recentrecap) @@ -415,7 +422,7 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 #[WORKING] evaluationdict = evaluator(a, timeline, responsedict, context) - if evaluationdict[:good_decision] == "no" + if evaluationdict[:approved] == "no" mentor_comment = evaluationdict[:suggestion] errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" println("\nERROR YiemAgent decisionMaker() - $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -851,7 +858,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext - 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) good_decision: Decide whether the decision make sense under the circumstances. Can be "yes" or "no" + 3) approved: Decide whether to let the trainee proceed or not. Can be "yes" or "no" 4) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. @@ -859,14 +866,14 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext { "trajectory_evaluation": "..." "decision_evaluation": "..." - "good_decision": "..." + "approved": "..." "suggestion": "..." } Let's begin! """ - requiredKeys = [:trajectory_evaluation, :decision_evaluation, :good_decision, :suggestion] + requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approved, :suggestion] errornote = "N/A" for attempt in 1:10 @@ -1116,7 +1123,7 @@ julia> """ 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=5) + thoughtDict = decisionMaker(a; recentevents=5) actionname = thoughtDict[:action_name] actioninput = thoughtDict[:action_input] @@ -1230,30 +1237,39 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh end -function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) +function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentevents::Integer=10) + recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; + includelatest=true) + recentchat = createChatLog(a.chathistory; index=recentchat_ind) systemmsg = """ - Your profile: - 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. - Situation: - You have checked the inventory and found wines. - Your mission: - Present the wines to the customer in a way that keep the conversation smooth and engaging. - At each round of conversation, you will be given the following information: + + 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. + + + You have checked the inventory and found wines that may match what the user wants + + + Present the wines to the user in a way that keep the conversation smooth and engaging. + + + Name of the wines that needs to be introduced: name of wines you are going to introduce to the user Database search result: the result of a database search using SQL commands you have found so far - Chat history: your ongoing conversation with the user - Wine name: name of wines you are going to introduce. - You should follow the following guidelines: + + - 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 then respond to the user with: + + dialogue: Your presentation to the user - You should only respond in format as described below: + + { "dialogue": "..." } + Let's begin! """ @@ -1265,7 +1281,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) "N/A" end - chathistory = chatHistoryToText(a.chathistory) + # chathistory = chatHistoryToText(a.chathistory) errornote = "N/A" response = nothing # placeholder for show when error msg show up @@ -1279,9 +1295,8 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) context = """ + Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) Database search result: $database_search_result - Chat history: $chathistory - Wine name: $(thoughtDict[:action_input]) P.S. $errornote """ @@ -1291,6 +1306,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) Dict(:name => "system", :text => systemmsg), ] + unformatPrompt = vcat(unformatPrompt, recentchat) # put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) # add info @@ -1311,7 +1327,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) try responsedict = copy(JSON3.read(response)) catch - println("\nERROR YiemAgent presentbox() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent presentbox() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1400,6 +1416,176 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) end error("presentbox() failed to generate a response") end +# function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) +# systemmsg = +# """ +# Your profile: +# 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. +# Situation: +# You have checked the inventory and found wines. +# Your mission: +# Present the wines to the customer in a way that keep the conversation smooth and engaging. +# At each round of conversation, you will be given the following information: +# Database search result: the result of a database search using SQL commands you have found so far +# Chat history: your ongoing conversation with the user +# Name of the wines that needs to be introduced: name of wines you are going to introduce to the user. +# You should follow the following guidelines: +# - 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 then respond to the user with: +# dialogue: Your presentation to the user +# You should only respond in format as described below: +# { +# "dialogue": "..." +# } + +# Let's begin! +# """ +# requiredKeys = [:dialogue] +# database_search_result = +# if length(a.memory[:shortmem][:db_search_result]) != 0 +# availableWineToText(a.memory[:shortmem][:db_search_result]) +# else +# "N/A" +# end + +# chathistory = chatHistoryToText(a.chathistory) +# errornote = "N/A" +# response = nothing # placeholder for show when error msg show up + + + +# # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" +# # yourthought1 = nothing + +# for attempt in 1:maxtattempt + +# context = +# """ +# +# Database search result: $database_search_result +# Chat history: $chathistory +# Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) +# P.S. $errornote +# +# """ + +# unformatPrompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) +# # add info +# prompt = prompt * context + +# response = a.func[: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) + +# response = replace(response, '*'=>"") +# response = replace(response, '$' => "USD") +# response = replace(response, '`' => "") +# response = replace(response, "<|eot_id|>"=>"") + +# responsedict = nothing +# try +# responsedict = copy(JSON3.read(response)) +# catch +# println("\nERROR YiemAgent presentbox() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # check whether all answer's key points are in responsedict +# _responsedictKey = keys(responsedict) +# responsedictKey = [i for i in _responsedictKey] # convert into a list +# is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + +# if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) +# errornote = "Your previous attempt has more key points than answer's required key points." +# println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# elseif !all(is_requiredKeys_in_responsedictKey) +# zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) +# missingkeys = [requiredKeys[i] for i in zeroind] +# errornote = "$missingkeys are missing from your previous response" +# println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # # check whether response has not-allowed words +# # notallowed = ["respond:", "user>", "user:"] +# # detected_kw = GeneralUtils.detect_keyword(notallowed, response) +# # # list all keys that have 1 value in detected_kw dictionary +# # k = [key for (key, value) in detected_kw if value == 1] +# # if length(k) > 0 +# # errornote = "In your previous attempt, you have $k in your response which is not allowed." +# # println("\nERROR YiemAgent presentbox() $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 = "$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 = "\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 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("\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[:dialogue]) +# 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[:outcome] !== nothing && occursin(winename, event[:outcome]) +# 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 response recommended 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[:dialogue] +# return result +# end +# error("presentbox() failed to generate a response") +# end @@ -1547,7 +1733,7 @@ function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) response = replace(response, '$' => "USD") response = replace(response, '`' => "") response = replace(response, "<|eot_id|>"=>"") - response = GeneralUtils.remove_french_accents(response) + @@ -1612,35 +1798,39 @@ function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) end -function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) +function generatechat(a::companion; recentevents::Integer=10, + converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) + + recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; + includelatest=true); + recentchat = createChatLog(a.chathistory; index=recentchat_ind) + response = nothing # placeholder for show when error msg show up errornote = "N/A" - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) + for attempt in 1:maxattempt if attempt > 1 println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - systemmsg = a.systemmsg * "\nP.S. $errornote\n" - _prompt = - [ - Dict(:name => "system", :text => systemmsg), - ] - for i in a.chathistory - tempdict = Dict{Symbol, String}() - for j in keys(i) - if j ∉ [:timestamp] - tempdict[j] = i[j] - end - end - _prompt = vcat(_prompt, tempdict) - end + context = + """ + + P.S. $errornote + + """ + unformatPrompt = + [ + Dict(:name => "system", :text => a.systemmsg), + ] + + unformatPrompt = vcat(unformatPrompt, recentchat) # put in model format - _prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context + # replace user and assistant with partner name prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") @@ -1664,6 +1854,58 @@ function generatechat(a::companion; converPartnerName::Union{String, Nothing}=no end error("generatechat failed to generate a response") end +# function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) +# response = nothing # placeholder for show when error msg show up +# errornote = "N/A" +# llmkwargs=Dict( +# :num_ctx => 32768, +# :temperature => 0.5, +# ) +# for attempt in 1:maxattempt +# if attempt > 1 +# println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# end + +# systemmsg = a.systemmsg * "\nP.S. $errornote\n" +# _prompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# ] +# for i in a.chathistory +# tempdict = Dict{Symbol, String}() +# for j in keys(i) +# if j ∉ [:timestamp] +# tempdict[j] = i[j] +# end +# end +# _prompt = vcat(_prompt, tempdict) +# end + +# # put in model format +# _prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + +# # replace user and assistant with partner name +# prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") +# prompt = replace(prompt, "|>assistant"=>"|>$(a.name)") + +# response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) +# response = replace(response, "<|im_start|>"=> "") +# response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) +# think, response = GeneralUtils.extractthink(response) + +# # check whether LLM just repeat the previous dialogue +# for msg in a.chathistory +# if msg[:text] == response +# errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." +# println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end +# end + +# return response +# end +# error("generatechat failed to generate a response") +# end # modify it to work with customer object diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 78a522f..0b10ad9 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -307,7 +307,7 @@ function checkinventory(a::T1, input::T2 println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println(textresult) - + #[PENDING] if there is no wine id, it is not a valid result return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) end diff --git a/src/util.jl b/src/util.jl index 2f18772..c5ef652 100644 --- a/src/util.jl +++ b/src/util.jl @@ -297,20 +297,53 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin 1:length(events) end - # Iterate through events and format each one - for (i, event) in zip(ind, events) + #[WORKING] Iterate through events and format each one + for i in ind + event = events[i] # If no outcome exists, format without outcome - if event[:outcome] === nothing - timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" + # if event[:actionname] == "CHATBOX" + # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n" + # elseif event[:actionname] == "CHECKINVENTORY" && event[:outcome] === nothing + # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" # If outcome exists, include it in formatting - else + if event[:actionname] == "CHECKINVENTORY" timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" + else + timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n" end end # Return formatted timeline string return timeline end +# function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing +# ) where {T1<:AbstractVector} +# # Initialize empty timeline string +# timeline = "" + +# # Determine which indices to use - either provided range or full length +# ind = +# if eventindex !== nothing +# [eventindex...] +# else +# 1:length(events) +# end + +# # Iterate through events and format each one +# for i in ind +# event = events[i] +# # If no outcome exists, format without outcome +# if event[:outcome] === nothing +# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" +# # If outcome exists, include it in formatting +# else +# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" +# end +# end + +# # Return formatted timeline string +# return timeline +# end function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing @@ -327,7 +360,8 @@ function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing end # Iterate through events and format each one - for (i, event) in zip(ind, events) + for i in ind + event = events[i] # If no outcome exists, format without outcome if event[:outcome] === nothing subject = event[:subject] @@ -362,7 +396,8 @@ function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing end # Iterate through events and format each one - for (i, event) in zip(ind, chatdict) + for i in ind + event = chatdict[i] subject = event[:name] text = event[:text] d = Dict{Symbol, String}(:name=>subject, :text=>text)