From 814a0ecc6adffb93841b6db5fec5829fd26a7c4a Mon Sep 17 00:00:00 2001 From: tonaerospace Date: Fri, 27 Dec 2024 20:53:15 +0700 Subject: [PATCH 01/16] update --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 4287c4b..2ce7794 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" authors = ["narawat lamaiin "] -version = "0.1.1" +version = "0.1.2" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" From 82167fe006db9e105e95398c862ab07d577f54cd Mon Sep 17 00:00:00 2001 From: tonaerospace Date: Sat, 4 Jan 2025 16:07:18 +0700 Subject: [PATCH 02/16] update --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2ce7794..3b08a83 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" authors = ["narawat lamaiin "] -version = "0.1.2" +version = "0.1.2-dev" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" From cff0d31ae6ed5e0dc2e8d8afcc9228f61c05cffd Mon Sep 17 00:00:00 2001 From: tonaerospace Date: Sat, 4 Jan 2025 16:10:23 +0700 Subject: [PATCH 03/16] update --- src/interface.jl | 261 ++++++++++++------------- src/llmfunction.jl | 461 ++++++++++++++++----------------------------- src/type.jl | 35 ++-- src/util.jl | 35 +++- 4 files changed, 349 insertions(+), 443 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 676656e..b779f7e 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -135,9 +135,9 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen timeline = "" for (i, event) in enumerate(a.memory[:events][ind]) if event[:outcome] === nothing - timeline *= "$i) $(event[:subject])> $(event[:action_or_dialogue])\n" + timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" else - timeline *= "$i) $(event[:subject])> $(event[:action_or_dialogue]) $(event[:outcome])\n" + timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" end end @@ -170,7 +170,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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. - - Before checking the inventory, engage in conversation to indirectly investigate the customer's intention, budget and preferences, which will significantly improve inventory search results. + - Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory. - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - Medium and full-bodied red wines should not be paired with spicy foods. @@ -218,13 +218,16 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen for winename in winenames if !occursin(winename, chathistory) println("\n~~~ Yiem decisionMaker() found wines from DB ", @__FILE__, " ", @__LINE__) - return Dict(:action_name=> "PRESENTBOX", - :action_input=> """ - 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. - """) + d = Dict( + :understanding=> "I understand that the customer is looking for a wine that matches their intention and budget.", + :reasoning=> "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=> "") + return d end end end @@ -254,87 +257,95 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen <|start_header_id|>assistant<|end_header_id|> """ - try - response = a.func[:text2textInstructLLM](prompt) - response = GeneralUtils.remove_french_accents(response) - responsedict = GeneralUtils.textToDict(response, - ["Understanding", "Reasoning", "Plan", "Action_name", "Action_input"], - rightmarker=":", symbolkey=true, lowercasekey=true) + response = a.func[:text2textInstructLLM](prompt) + response = GeneralUtils.remove_french_accents(response) - if responsedict[:action_name] ∉ ["CHATBOX", "PRESENTBOX", "CHECKINVENTORY", "ENDCONVERSATION"] - errornote = "You must use the given functions" - error("You must use the given functions ", @__FILE__, " ", @__LINE__) + # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] + count = 0 + for i ∈ ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] + if occursin(i, response) + count += 1 end + end + if count > 1 + errornote = "You must use only one function" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue + end - for i ∈ [:understanding, :plan, :action_name] - if length(responsedict[i]) == 0 - error("$i is empty ", @__FILE__, " ", @__LINE__) - end + responsedict = GeneralUtils.textToDict(response, + ["Understanding", "Reasoning", "Plan", "Action_name", "Action_input"], + rightmarker=":", symbolkey=true, lowercasekey=true) + + if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] + errornote = "You must use the given functions" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue + end + + for i ∈ [:understanding, :plan, :action_name] + if length(responsedict[i]) == 0 + error("$i is empty ", @__FILE__, " ", @__LINE__) + errornote = "$i is empty" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue end + end - # check if there are more than 1 key per categories - for i ∈ [:understanding, :plan, :action_name, :action_input] - matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) - if length(matchkeys) > 1 - error("DecisionMaker has more than one key per categories") - end + # check if there are more than 1 key per categories + for i ∈ [:understanding, :plan, :action_name, :action_input] + matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) + if length(matchkeys) > 1 + errornote = "DecisionMaker has more than one key per categories" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue end + end - println("\n~~~ Yiem decisionMaker() ", @__FILE__, " ", @__LINE__) - pprintln(Dict(responsedict)) + println("\n~~~ Yiem decisionMaker() ", @__FILE__, " ", @__LINE__) + 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, response) - if mentioned_winery != "None" - mentioned_winery = String.(strip.(split(mentioned_winery, ","))) + # check whether an agent recommend wines before checking inventory or recommend wines + # outside its inventory + # ask LLM whether there are any winery mentioned in the response + mentioned_winery = detectWineryName(a, response) + if mentioned_winery != "None" + mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - # check whether the wine is in event - isWineInEvent = false - for winename in mentioned_winery - for event in a.memory[:events] - if event[:outcome] !== nothing && occursin(winename, event[:outcome]) - isWineInEvent = true - break - end + # 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 - - # if wine is mentioned but not in timeline or shortmem, - # then the agent is not supposed to recommend the wine - if responsedict[:action_name] == "CHATBOX" && - isWineInEvent == false - - errornote = "Note: Before recommending a wine, ensure it's in your inventory. Check your stock first." - error("Before recommending a wine, ensure it's in your inventory. Check your stock first.") - end end - if occursin("--|", response) - errornote = "Note: tables are not allowed. Do not include them your response." - error("your response contain tables which is not allowed.") + # if wine is mentioned but not in timeline or shortmem, + # then the agent is not supposed to recommend the wine + if responsedict[:action_name] == "CHATBOX" && + isWineInEvent == false + + errornote = "Note: Before recommending a wine, ensure it's in your inventory. Check your stock first." + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue end - - delete!(responsedict, :mentioned_winery) - - # #CHANGE cache decision dict into vectorDB, this should be after new message is added to a.memory[:events] - # println("\n~~~ Do you want to cache decision dict? (y/n)") - # user_answer = readline() - # if user_answer == "y" - # timeline = timeline - # decisiondict = responsedict - # a.func[:insertSommelierDecision](timeline, decisiondict) - # end - - return responsedict - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\nAttempt $attempt. \nError occurred: $errorMsg\n$st \nPrompt $prompt", @__FILE__, " ", @__LINE__) end + + delete!(responsedict, :mentioned_winery) + + # #CHANGE cache decision dict into vectorDB, this should be after new message is added to a.memory[:events] + # println("\n~~~ Do you want to cache decision dict? (y/n)") + # user_answer = readline() + # if user_answer == "y" + # timeline = timeline + # decisiondict = responsedict + # a.func[:insertSommelierDecision](timeline, decisiondict) + # end + + return responsedict end error("DecisionMaker failed to generate a thought ", response) end @@ -693,7 +704,7 @@ function conversation(a::sommelier, userinput::Dict) event_description="the user talks to the assistant.", timestamp=Dates.now(), subject="user", - action_or_dialogue=userinput[:text], + actioninput=userinput[:text], ) ) @@ -729,7 +740,7 @@ function conversation(a::companion, userinput::Dict) event_description="the user talks to the assistant.", timestamp=Dates.now(), subject="user", - action_or_dialogue=userinput[:text], + actioninput=userinput[:text], ) ) chatresponse = generatechat(a) @@ -741,7 +752,7 @@ function conversation(a::companion, userinput::Dict) event_description="the assistant talks to the user.", timestamp=Dates.now(), subject="assistant", - action_or_dialogue=chatresponse, + actioninput=chatresponse, ) ) return chatresponse @@ -775,8 +786,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh # map action and input() to llm function response = if actionname == "CHATBOX" - input = thoughtDict[:action_input] - (result=input, errormsg=nothing, success=true) + (result=thoughtDict[:plan], errormsg=nothing, success=true) elseif actionname == "CHECKINVENTORY" checkinventory(a, actioninput) elseif actionname == "PRESENTBOX" @@ -797,16 +807,25 @@ 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) + #[WORKING] manage memory (pass msg to generatechat) if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"] - chatresponse = generatechat(a, result) + chatresponse = generatechat(a, thoughtDict) push!(a.memory[:events], eventdict(; event_description="the assistant talks to the user.", timestamp=Dates.now(), subject="assistant", - action_or_dialogue=chatresponse, + thought=thoughtDict, + actionname=actionname, + actioninput=chatresponse, ) + + # eventdict(; + # event_description="the assistant talks to the user.", + # timestamp=Dates.now(), + # subject="assistant", + # actioninput=chatresponse, + # ) ) result = chatresponse if actionname == "PRESENTBOX" @@ -833,8 +852,10 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh event_description= "the assistant searched the database.", timestamp= Dates.now(), subject= "assistant", - action_or_dialogue= "I searched the database with this query: $actioninput", - outcome= "This is what I found in the database, $result" + thought=thoughtDict, + actionname=actionname, + actioninput= "I searched the database with this query: $actioninput", + outcome= "This is what I've found in the database, $result" ) ) else @@ -866,7 +887,7 @@ julia> # Signature """ -function generatechat(a::sommelier, thought::T) where {T<:AbstractString} +function generatechat(a::sommelier, thoughtDict) systemmsg = """ Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for an online wine store. @@ -922,7 +943,7 @@ function generatechat(a::sommelier, thought::T) where {T<:AbstractString} usermsg = """ Your ongoing conversation with the user: $chathistory Contex: $context - Your thoughts: $thought + Your thoughts: $(thoughtDict[:understanding]) $(thoughtDict[:reasoning]) $(thoughtDict[:plan]) $errornote """ @@ -1016,28 +1037,29 @@ end function generatechat(a::companion) - systemmsg = - """ - Your name is $(a.name). You are a helpful assistant. - You are currently talking with the user. - Your goal includes: - 1) Help the user as best as you can + systemmsg = + if a.systemmsg === nothing + systemmsg = + """ + You are a helpful assistant. + You are currently talking with the user. + Your goal includes: + 1) Help the user as best as you can - Your responsibility includes: - 1) Given the situation, help the user. + At each round of conversation, you will be given the following information: + Your ongoing conversation with the user: ... - At each round of conversation, you will be given the current situation: - Your ongoing conversation with the user: ... - Context: ... + You should then respond to the user with: + 1) chat: Given the information, what would you say to the user? - You should then respond to the user with: - 1) Chat: Given the situation, what would you say to the user? + You should only respond in JSON format as described below: + {"chat": ...} - You should only respond in format as described below: - Chat: ... - - Let's begin! - """ + Let's begin! + """ + else + a.systemmsg + end chathistory = vectorOfDictToText(a.chathistory) response = nothing # placeholder for show when error msg show up @@ -1059,24 +1081,9 @@ function generatechat(a::companion) <|start_header_id|>assistant<|end_header_id|> """ - try - response = a.func[:text2textInstructLLM](prompt) - println("\n~~~ generatechat() ", @__FILE__, " ", @__LINE__) - pprintln(response) + response = a.text2textInstructLLM(prompt) - responsedict = GeneralUtils.textToDict(response, ["Chat"], - rightmarker=":", symbolkey=true, lowercasekey=true) - - result = responsedict[:chat] - - return result - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\n Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) - end + return response end error("generatechat failed to generate a response") end @@ -1185,9 +1192,9 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St timeline = "" for (i, event) in enumerate(a.memory[:events][ind]) if event[:outcome] === nothing - timeline *= "$i) $(event[:subject])> $(event[:action_or_dialogue])\n" + timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" else - timeline *= "$i) $(event[:subject])> $(event[:action_or_dialogue]) $(event[:outcome])\n" + timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" end end errornote = "" @@ -1291,9 +1298,9 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: timeline = "" for (i, event) in enumerate(events) if event[:outcome] === nothing - timeline *= "$i) $(event[:subject])> $(event[:action_or_dialogue])\n" + timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" else - timeline *= "$i) $(event[:subject])> $(event[:action_or_dialogue]) $(event[:outcome])\n" + timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" end end diff --git a/src/llmfunction.jl b/src/llmfunction.jl index c0877e9..fb8d685 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -1,7 +1,8 @@ module llmfunction export virtualWineUserChatbox, jsoncorrection, checkinventory, # recommendbox, - virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1 + virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, + extractWineAttributes_2 using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates using GeneralUtils, SQLLLM @@ -550,9 +551,8 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< attributes = ["reasoning", "winery", "wine_name", "vintage", "region", "country", "wine_type", "grape_variety", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] errornote = "" - maxattempt = 5 - for attempt in 1:maxattempt - + + for attempt in 1:5 usermsg = """ User's query: $input @@ -572,70 +572,63 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< <|start_header_id|>assistant<|end_header_id|> """ - try - response = a.func[:text2textInstructLLM](prompt) - response = GeneralUtils.remove_french_accents(response) + response = a.func[:text2textInstructLLM](prompt) + response = GeneralUtils.remove_french_accents(response) - # check wheter all attributes are in the response - for word in attributes - if !occursin(word, response) - error("$word attribute is missing") - end + # check wheter all attributes are in the response + for word in attributes + if !occursin(word, response) + errornote = "$word attribute is missing in previous attempts" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue end - - responsedict = copy(JSON3.read(response)) - - delete!(responsedict, :reasoning) - delete!(responsedict, :tasting_notes) - delete!(responsedict, :occasion) - delete!(responsedict, :food_to_be_paired_with_wine) - - # check if winery, wine_name, region, country, wine_type, grape_variety are in the query because sometime AI halucinates - for i in [:grape_variety, :winery, :wine_name, :region] - content = responsedict[i] - if occursin(",", content) - content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" - content = strip.(content) - else - content = [content] - end - - for x in content - if !occursin("NA", responsedict[i]) && !occursin(x, input) - errornote = "$x is not mentioned in the user query, you must only use the info from the query." - error(errornote) - end - end - end - - # remove (some text) - for (k, v) in responsedict - _v = replace(v, r"\(.*?\)" => "") - responsedict[k] = _v - end - - result = "" - for (k, v) in responsedict - # some time LLM generate text with "(some comment)". this line removes it - if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v) - result *= "$k: $v, " - end - end - - #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" - - result = result[1:end-2] # remove the ending ", " - - return result - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("") - println("Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) - println("") end + + responsedict = copy(JSON3.read(response)) + + delete!(responsedict, :reasoning) + delete!(responsedict, :tasting_notes) + delete!(responsedict, :occasion) + delete!(responsedict, :food_to_be_paired_with_wine) + + # check if winery, wine_name, region, country, wine_type, grape_variety are in the query because sometime AI halucinates + for i in [:grape_variety, :winery, :wine_name, :region] + content = responsedict[i] + if occursin(",", content) + content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" + content = strip.(content) + else + content = [content] + end + + for x in content + if !occursin("NA", responsedict[i]) && !occursin(x, input) + errornote = "$x is not mentioned in the user query, you must only use the info from the query." + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue + end + end + end + + # remove (some text) + for (k, v) in responsedict + _v = replace(v, r"\(.*?\)" => "") + responsedict[k] = _v + end + + result = "" + for (k, v) in responsedict + # some time LLM generate text with "(some comment)". this line removes it + if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v) + result *= "$k: $v, " + end + end + + #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" + + result = result[1:end-2] # remove the ending ", " + + return result end error("wineattributes_wordToNumber() failed to get a response") end @@ -643,6 +636,7 @@ end """ # TODO - [PENDING] "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve. + - [PENDING] French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why? """ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} @@ -675,8 +669,6 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< 4 to 5: May correspond to "high acidity" or a similar description. """ - # chathistory = vectorOfDictToText(a.chathistory) - systemmsg = """ As an helpful sommelier, your task is to fill out the user's preference form based on the corresponding words from the user's query. @@ -695,254 +687,135 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< 3) Do not generate other comments. You should then respond to the user with the following points: - - reasoning: State your understanding of the current situation + - sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine. - sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2 + - acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine. - acidity: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5 + - tannin_keyword: The exact keywords in the user's query describing the tannin level of the wine. - tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3 + - intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine. - intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4 - - notes: Anything you want to add - You should only respond in the form as described below: - reasoning: ... - sweetness: ... - acidity: ... - tannin: ... - intensity: ... - notes: ... + You should only respond in the form (JSON) as described below: + { + "sweetness_keyword": ..., + "sweetness": ..., + "acidity_keyword": ..., + "acidity": ..., + "tannin_keyword": ..., + "tannin": ..., + "intensity_keyword": ..., + "intensity": ... + } + + Here are some examples: + User's query: I want a wine with a medium-bodied, low acidity, medium tannin. + { + "sweetness_keyword": "NA", + "sweetness": "NA", + "acidity_keyword": "low acidity", + "acidity": "1-2", + "tannin_keyword": "medium tannin", + "tannin": "3-4", + "intensity_keyword": "medium-bodied", + "intensity": "3-4" + } + + + User's query: German red wine, under 100, pairs with spicy food + { + "sweetness_keyword": "NA", + "sweetness": "NA", + "acidity_keyword": "NA", + "acidity": "NA", + "tannin_keyword": "NA", + "tannin": "NA", + "intensity_keyword": "NA", + "intensity": "NA" + } + Let's begin! """ - # chathistory = vectorOfDictToText(a.chathistory) - - usermsg = - """ - $conversiontable - User's query: $input - """ - - _prompt = - [ - Dict(:name=> "system", :text=> systemmsg), - Dict(:name=> "user", :text=> usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") - prompt *= - """ - <|start_header_id|>assistant<|end_header_id|> - """ - - attributes = ["reasoning", "sweetness", "acidity", "tannin", "intensity", "notes"] + errornote = "" for attempt in 1:5 - try - response = a.func[:text2textInstructLLM](prompt) - responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) + usermsg = + """ + $conversiontable + User's query: $input + $errornote + """ - for i ∈ attributes - if length(JSON3.write(responsedict[Symbol(i)])) == 0 - error("$i is empty ", @__LINE__) - end + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + """ + + response = a.func[:text2textInstructLLM](prompt) + responsedict = copy(JSON3.read(response)) + + # check whether each describing keyword is in the input to prevent halucination + for i in ["sweetness", "acidity", "tannin", "intensity"] + keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword + value = responsedict[keyword] + if value != "NA" && !occursin(value, input) + errornote = "WARNING. Keyword $keyword: $value does not appear in the input. You must use information from the input only" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue end - delete!(responsedict, :reasoning) - delete!(responsedict, :notes) # LLM traps. so it can add useless info here like comments. - - # some time LLM think the user mentioning acidity and tannin but actually didn't - for (k, v) in responsedict - if k ∈ [:acidity, :tannin] && !occursin(string(k), input) - responsedict[k] = "NA" - end + # if value == "NA" then responsedict[i] = "NA" + # e.g. if sweetness_keyword == "NA" then sweetness = "NA" + if value == "NA" + responsedict[Symbol(i)] = "NA" end - - # remove (some text) - for (k, v) in responsedict - _v = replace(v, r"\(.*?\)" => "") - responsedict[k] = _v - end - - # some time LLM not put integer range - for (k, v) in responsedict - responsedict[k] = v - if length(v) > 5 - error("non-range is not allowed. $k $v") - end - end - - # some time LLM says NA-2. Need to convert NA to 1 - for (k, v) in responsedict - if occursin("NA", v) && occursin("-", v) - new_v = replace(v, "NA"=>"1") - responsedict[k] = new_v - end - end - - result = "" - for (k, v) in responsedict - # some time LLM generate text with "(some comment)". this line removes it - if !occursin("NA", v) - result *= "$k: $v, " - end - end - - result = result[1:end-2] # remove the ending ", " - - return result - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("") - println("Attempt $attempt. Error occurred: $errorMsg\n$st") - println("") end + + # some time LLM not put integer range + for (k, v) in responsedict + if !occursin("keyword", string(k)) + if !occursin('-', v) || length(v) > 5 + errornote = "WARNING: The non-range value for $k is not allowed. It should be specified in a range format, such as min-max." + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + continue + end + end + end + + # some time LLM says NA-2. Need to convert NA to 1 + for (k, v) in responsedict + if occursin("NA", v) && occursin("-", v) + new_v = replace(v, "NA"=>"1") + responsedict[k] = new_v + end + end + + result = "" + for (k, v) in responsedict + # some time LLM generate text with "(some comment)". this line removes it + if !occursin("NA", v) + result *= "$k: $v, " + end + end + + result = result[1:end-2] # remove the ending ", " + + return result end error("wineattributes_wordToNumber() failed to get a response") end -# function recommendbox(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} -# error("recommendbox") -# systemmsg = -# """ -# As an helpful sommelier, your task is to fill out the user's preference form based on the corresponding words from the user's query. - -# At each round of conversation, the user will give you the current situation: -# User's query: ... - -# The preference form requires the following information: -# wine_type, price, occasion, food_to_be_paired_with_wine, country, grape_variety, flavors, aromas. - -# You must follow the following guidelines: -# 1) If specific information required in the preference form is not available in the query or there isn't any, mark with 'NA' to indicate this. -# Additionally, words like 'any' or 'unlimited' mean no information is available. -# 2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. -# 3) Do not generate other comments. - -# You should then respond to the user with the following points: -# - reasoning: State your understanding of the current situation -# - wine_type: Can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" -# - price: Must be an integer representing the cost of the wine. -# - occasion: ... -# - food_to_be_paired_with_wine: food that the user will be served with wine -# - country: wine's country of origin -# - region: wine's region of origin such as Burgundy, Napa Valley -# - grape variety: a single name of grape used to make wine. -# - flavors: Names of items that the wine tastes like. -# - aromas: wine's aroma - -# You should only respond in the form as described below: -# reasoning: ... -# wine_type: ... -# price: ... -# occasion: ... -# food_to_be_paired_with_wine: ... -# country: ... -# region: ... -# grape_variety: ... -# flavors: ... -# aromas: ... - -# Let's begin! -# """ - -# attributes = ["reasoning", "wine_type", "price", "occasion", "food_to_be_paired_with_wine", "country", "region", "grape_variety", "flavors", "aromas"] -# errornote = "" -# for attempt in 1:5 - -# usermsg = -# """ -# User's query: $input -# $errornote -# """ - -# _prompt = -# [ -# Dict(:name=> "system", :text=> systemmsg), -# Dict(:name=> "user", :text=> usermsg) -# ] - -# # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") -# prompt *= -# """ -# <|start_header_id|>assistant<|end_header_id|> -# """ - -# try -# response = a.func[:text2textInstructLLM](prompt) -# responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) - -# for i ∈ attributes -# if length(JSON3.write(responsedict[Symbol(i)])) == 0 -# error("$i is empty ", @__LINE__) -# end -# end - -# #[PENDING] check if the following attributes has more than 1 name -# x = length(split(responsedict[:grape_variety], ",")) * length(split(responsedict[:grape_variety], "/")) -# if x > 1 -# errornote = "only a single name in grape_variety is allowed" -# error("only a single grape_variety name is allowed") -# end -# x = length(split(responsedict[:country], ",")) * length(split(responsedict[:country], "/")) -# if x > 1 -# errornote = "only a single name in country is allowed" -# error("only a single country name is allowed") -# end -# x = length(split(responsedict[:region], ",")) * length(split(responsedict[:region], "/")) -# if x > 1 -# errornote = "only a single name in region is allowed" -# error("only a single region name is allowed") -# end - -# # check if grape_variety is mentioned in the input -# if responsedict[:grape_variety] != "NA" && !occursin(responsedict[:grape_variety], input) -# error("$(responsedict[:grape_variety]) is not mentioned in the input") -# end - -# responsedict[:flavors] = replace(responsedict[:flavors], "notes"=>"") -# delete!(responsedict, :reasoning) -# delete!(responsedict, :tasting_notes) -# delete!(responsedict, :flavors) -# delete!(responsedict, :aromas) - -# # remove (some text) -# for (k, v) in responsedict -# _v = replace(v, r"\(.*?\)" => "") -# responsedict[k] = _v -# end - -# result = "" -# for (k, v) in responsedict -# # some time LLM generate text with "(some comment)". this line removes it -# if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v) -# result *= "$k: $v, " -# end -# end - -# #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" - -# result = result[1:end-2] # remove the ending ", " - -# return result -# catch e -# io = IOBuffer() -# showerror(io, e) -# errorMsg = String(take!(io)) -# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) -# println("") -# println("Attempt $attempt. Error occurred: $errorMsg\n$st") -# println("") -# end -# end -# error("wineattributes_wordToNumber() failed to get a response") -# end - - """ Attemp to correct LLM response's incorrect JSON response. # Arguments diff --git a/src/type.jl b/src/type.jl index 36fbc8b..939b7b9 100644 --- a/src/type.jl +++ b/src/type.jl @@ -11,8 +11,8 @@ abstract type agent end mutable struct companion <: agent - name::String # agent name id::String # agent id + systemmsg::Union{String, Nothing} maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized """ Memory @@ -34,8 +34,8 @@ end function companion( text2textInstructLLM::Function ; - name::String= "Assistant", id::String= string(uuid4()), + systemmsg::Union{String, Nothing}= nothing, maxHistoryMsg::Integer= 20, chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), ) @@ -48,13 +48,13 @@ function companion( ) newAgent = companion( - name, - id, - maxHistoryMsg, - chathistory, - memory, - text2textInstructLLM - ) + id, + systemmsg, + maxHistoryMsg, + chathistory, + memory, + text2textInstructLLM + ) return newAgent end @@ -146,7 +146,6 @@ mutable struct sommelier <: agent """ chathistory::Vector{Dict{Symbol, Any}} memory::Dict{Symbol, Any} - func # NamedTuple of functions end @@ -179,14 +178,14 @@ function sommelier( # ), ) - memory = Dict{Symbol, Any}( - :chatbox=> "", - :shortmem=> OrderedDict{Symbol, Any}(), - :events=> Vector{Dict{Symbol, Any}}(), - :state=> Dict{Symbol, Any}( - :wine_presented_to_user=> "None", - ), - ) + memory = Dict{Symbol, Any}( + :chatbox=> "", + :shortmem=> OrderedDict{Symbol, Any}(), + :events=> Vector{Dict{Symbol, Any}}(), + :state=> Dict{Symbol, Any}( + :wine_presented_to_user=> "None", + ), + ) newAgent = sommelier( name, diff --git a/src/util.jl b/src/util.jl index f390348..e449dd6 100644 --- a/src/util.jl +++ b/src/util.jl @@ -169,11 +169,38 @@ function vectorOfDictToText(vecd::Vector; withkey=true)::String end +# function eventdict(; +# event_description::Union{String, Nothing}=nothing, +# timestamp::Union{DateTime, Nothing}=nothing, +# subject::Union{String, Nothing}=nothing, +# action_or_dialogue::Union{String, Nothing}=nothing, +# location::Union{String, Nothing}=nothing, +# equipment_used::Union{String, Nothing}=nothing, +# material_used::Union{String, Nothing}=nothing, +# outcome::Union{String, Nothing}=nothing, +# note::Union{String, Nothing}=nothing, +# ) +# return Dict{Symbol, Any}( +# :event_description=> event_description, +# :timestamp=> timestamp, +# :subject=> subject, +# :action_or_dialogue=> action_or_dialogue, +# :location=> location, +# :equipment_used=> equipment_used, +# :material_used=> material_used, +# :outcome=> outcome, +# :note=> note, +# ) +# end + + function eventdict(; event_description::Union{String, Nothing}=nothing, timestamp::Union{DateTime, Nothing}=nothing, subject::Union{String, Nothing}=nothing, - action_or_dialogue::Union{String, Nothing}=nothing, + thought::Union{AbstractDict, Nothing}=nothing, + actionname::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENTBOX", etc + actioninput::Union{String, Nothing}=nothing, location::Union{String, Nothing}=nothing, equipment_used::Union{String, Nothing}=nothing, material_used::Union{String, Nothing}=nothing, @@ -184,7 +211,9 @@ function eventdict(; :event_description=> event_description, :timestamp=> timestamp, :subject=> subject, - :action_or_dialogue=> action_or_dialogue, + :thought=> thought, + :actionname=> actionname, + :actioninput=> actioninput, :location=> location, :equipment_used=> equipment_used, :material_used=> material_used, @@ -193,8 +222,6 @@ function eventdict(; ) end - - # """ Convert a single chat dictionary into LLM model instruct format. # # Llama 3 instruct format example From 022cb5caf021cbc8515f74296d84136e23d88e84 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sun, 5 Jan 2025 17:41:21 +0700 Subject: [PATCH 04/16] update --- src/interface.jl | 3 ++- src/llmfunction.jl | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index b779f7e..6b8756c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -159,9 +159,10 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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 do not include: + Your responsibility does NOT include: 1) Asking or guiding the user to make a purchase 2) Processing sales orders or engaging in any other sales-related activities + 3) Providing services other than making recommendations. At each round of conversation, you will be given the current situation: Your recent events: latest 5 events of the situation diff --git a/src/llmfunction.jl b/src/llmfunction.jl index fb8d685..5979a13 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -783,9 +783,9 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< # some time LLM not put integer range for (k, v) in responsedict - if !occursin("keyword", string(k)) - if !occursin('-', v) || length(v) > 5 - errornote = "WARNING: The non-range value for $k is not allowed. It should be specified in a range format, such as min-max." + if !occursin("keyword", string(k)) + if v !== "NA" && (!occursin('-', v) || length(v) > 5) + errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, such as min-max." println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) continue end @@ -807,7 +807,6 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< result *= "$k: $v, " end end - result = result[1:end-2] # remove the ending ", " return result From 616c1593362167faca022ed179232595a043ebb6 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Fri, 10 Jan 2025 08:06:01 +0700 Subject: [PATCH 05/16] update --- src/interface.jl | 82 +++++++++------ src/llmfunction.jl | 256 ++++++++++----------------------------------- src/util.jl | 53 +++++----- 3 files changed, 130 insertions(+), 261 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 6b8756c..808f18c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -150,7 +150,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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 online 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 goal includes: 1) Establish a connection with the customer by greeting them warmly 2) Help them select the best wines from your inventory that align with their preferences @@ -159,10 +159,10 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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 include: + Your responsibility excludes: 1) Asking or guiding the user to make a purchase 2) Processing sales orders or engaging in any other sales-related activities - 3) Providing services other than making recommendations. + 3) Answering questions and offering additional services beyond just recommendations, such as delivery, box, gift wrapping or packaging, personalized messages. Customers can reach out to our sales at the store. At each round of conversation, you will be given the current situation: Your recent events: latest 5 events of the situation @@ -194,7 +194,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen Can be one of the following functions: - 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 in verbal English. - Good query example: black car, a stereo, 200 mile range, electric motor. + Good query example: white wine, full-bodied, France, less than 2000 USD. - ENDCONVERSATION which you can use when you believe the user has concluded their interaction, to properly end the conversation with them. Input is "NA". 5) Action_input: input of the action @@ -214,7 +214,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen if haskey(a.memory[:shortmem], :available_wine) # check if wine name mentioned in timeline, only check first wine name is enough # because agent will recommend every wines it found each time. - df = a.memory[:shortmem][:available_wine] + df = a.memory[:shortmem][:available_wine] winenames = df[:, :wine_name] for winename in winenames if !occursin(winename, chathistory) @@ -260,6 +260,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen response = a.func[:text2textInstructLLM](prompt) response = GeneralUtils.remove_french_accents(response) + response = replace(response, '*'=>"") # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] count = 0 @@ -284,24 +285,30 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen continue end + checkFlag = false for i ∈ [:understanding, :plan, :action_name] if length(responsedict[i]) == 0 error("$i is empty ", @__FILE__, " ", @__LINE__) errornote = "$i is empty" println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) - continue + checkFlag = true + break end end + checkFlag == true ? continue : nothing # check if there are more than 1 key per categories + checkFlag = false for i ∈ [:understanding, :plan, :action_name, :action_input] matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) if length(matchkeys) > 1 errornote = "DecisionMaker has more than one key per categories" println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) - continue + checkFlag = true + break end end + checkFlag == true ? continue : nothing println("\n~~~ Yiem decisionMaker() ", @__FILE__, " ", @__LINE__) pprintln(Dict(responsedict)) @@ -691,6 +698,7 @@ function conversation(a::sommelier, userinput::Dict) actionname = nothing result = nothing chatresponse = nothing + userinput[:text] = GeneralUtils.remove_french_accents(userinput[:text]) if userinput[:text] == "newtopic" clearhistory(a) @@ -808,7 +816,7 @@ 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 - #[WORKING] manage memory (pass msg to generatechat) + # manage memory (pass msg to generatechat) if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"] chatresponse = generatechat(a, thoughtDict) push!(a.memory[:events], @@ -829,25 +837,28 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh # ) ) result = chatresponse - if actionname == "PRESENTBOX" - df = a.memory[:shortmem][:available_wine] - winename = join(df[:, :wine_name], ", ") - if a.memory[:state][:wine_presented_to_user] == "None" - a.memory[:state][:wine_presented_to_user] = winename - else - a.memory[:state][:wine_presented_to_user] *= ", $winename" - end - end elseif actionname == "CHECKINVENTORY" - if haskey(a.memory[:shortmem], :available_wine) # store wines in dataframe format - df = a.memory[:shortmem][:available_wine] - a.memory[:shortmem][:available_wine] = vcat(df, rawresponse) - elseif rawresponse !== nothing - a.memory[:shortmem][:available_wine] = rawresponse + if rawresponse !== nothing + if haskey(a.memory[:shortmem], :available_wine) + df = a.memory[:shortmem][:available_wine] + #[TESTING] sometime df 2 df has different column size + dfCol = names(df) + rawresponse_dfCol = names(rawresponse) + if length(dfCol) > length(rawresponse_dfCol) + a.memory[:shortmem][:available_wine] = DataFrames.outerjoin(df, rawresponse, on=rawresponse_dfCol) + elseif length(dfCol) < length(rawresponse_dfCol) + a.memory[:shortmem][:available_wine] = DataFrames.outerjoin(df, rawresponse, on=dfCol) + else + a.memory[:shortmem][:available_wine] = vcat(df, rawresponse) + end + else + a.memory[:shortmem][:available_wine] = rawresponse + end else - # skip, no result + # no result, skip end + push!(a.memory[:events], eventdict(; event_description= "the assistant searched the database.", @@ -891,7 +902,7 @@ julia> function generatechat(a::sommelier, thoughtDict) systemmsg = """ - Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for an online 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. You are currently talking with the user. Your goal includes: 1) Help the user select the best wines from your inventory that align with the user's preferences. @@ -899,9 +910,10 @@ function generatechat(a::sommelier, thoughtDict) Your responsibility includes: 1) Given the situation, convey your thoughts to the user. - Your responsibility do not include: - 1) Asking or guiding the user to make a purchase - 2) Processing sales orders or engaging in any other sales-related activities + Your responsibility excludes: + 1) Asking or guiding the user to make a purchase + 2) Processing sales orders or engaging in any other sales-related activities + 3) Answering questions and offering additional services beyond just recommendations, such as delivery, box, gift wrapping, personalized messages. Customers can reach out to our sales at the store. At each round of conversation, you will be given the current situation: Your ongoing conversation with the user: ... @@ -961,15 +973,17 @@ function generatechat(a::sommelier, thoughtDict) """ try - response_1 = a.func[:text2textInstructLLM](prompt) + response = a.func[:text2textInstructLLM](prompt) # sometime the model response like this "here's how I would respond: ..." - if occursin("respond:", response_1) + if occursin("respond:", response) errornote = "You don't need to intro your response" error("generatechat() response contain : ", @__FILE__, " ", @__LINE__) end - response_2 = replace(response_1, '*' => "") - response_3 = replace(response_2, '$' => "USD") - response = replace(response_3, '`' => "") + response = GeneralUtils.remove_french_accents(response) + response = replace(response, '*'=>"") + response = replace(response, '$' => "USD") + response = replace(response, '`' => "") + response = GeneralUtils.remove_french_accents(response) responsedict = GeneralUtils.textToDict(response, ["Chat"], rightmarker=":", symbolkey=true, lowercasekey=true) @@ -1017,8 +1031,8 @@ function generatechat(a::sommelier, thoughtDict) # then the agent is not supposed to recommend the wine if isWineInEvent == false - errornote = "Note: You are not supposed to recommend a wine that is not in your inventory." - error("Note: You are not supposed to recommend a wine that is not in your inventory.") + errornote = "Previously: You recommend a wine that is not in your inventory which is not allowed." + error("Previously: You recommend a wine that is not in your inventory which is not allowed.") end end diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 5979a13..d7b6103 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -330,183 +330,6 @@ julia> # Signature """ -# function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} - -# systemmsg = -# """ -# As a helpful sommelier, your task is to extract the user information from the user's query as much as possible to fill out user's preference form. - -# At each round of conversation, the user will give you the current situation: -# User's query: ... - -# You must follow the following guidelines: -# 1) If specific information required in the preference form is not available in the query or there isn't any, mark with "NA" to indicate this. -# Additionally, words like 'any' or 'unlimited' mean no information is available. -# 2) Do not generate other comments. - -# You should then respond to the user with the following points: -# - reasoning: state your understanding of the current situation -# - wine_name: name of the wine -# - winery: name of the winery -# - vintage: the year of the wine -# - region: a region in a country where the wine is produced, such as Burgundy, Napa Valley, etc -# - country: a country where the wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States" -# - wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" -# - grape_variety: the name of the primary grape used to make the wine -# - tasting_notes: a brief description of the wine's taste, such as "butter", "oak", "fruity", etc -# - wine_price: price of wine. For example, up to 100, less than 100, 20 to 100, 30-79.95 -# - occasion: the occasion the user is having the wine for -# - food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc - - -# You should only respond in the user's preference form as described below: -# reasoning: ... -# winery: ... -# wine_name: ... -# vintage: ... -# region: ... -# country: ... -# wine_type: ... -# grape_variety: ... -# tasting_notes: ... -# wine_price: ... -# occasion: ... -# food_to_be_paired_with_wine: ... - -# Here are some example: -# User's query: red, Chenin Blanc, Riesling, under 20 -# reasoning: ... -# winery: NA -# wine_name: NA -# vintage: NA -# region: NA -# country: NA -# wine_type: red -# grape_variety: Chenin Blanc, Riesling -# tasting_notes: NA -# wine_price: under 20 -# occasion: NA -# food_to_be_paired_with_wine: NA - -# User's query: Domaine du Collier Saumur Blanc 2019, France, white, Chenin Blanc -# reasoning: ... -# winery: Domaine du Collier -# wine_name: Saumur Blanc -# vintage: 2019 -# region: Saumur -# country: France -# wine_type: white -# grape_variety: Chenin Blanc -# tasting_notes: NA -# wine_price: 109 -# occasion: NA -# food_to_be_paired_with_wine: NA - -# Let's begin! -# """ - -# attributes = ["reasoning", "winery", "wine_name", "vintage", "region", "country", "wine_type", "grape_variety", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] -# errornote = "" -# maxattempt = 5 -# for attempt in 1:maxattempt - -# usermsg = -# """ -# User's query: $input -# $errornote -# """ - -# _prompt = -# [ -# Dict(:name=> "system", :text=> systemmsg), -# Dict(:name=> "user", :text=> usermsg) -# ] - -# # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") -# prompt *= -# """ -# <|start_header_id|>assistant<|end_header_id|> -# """ - -# try -# response = a.func[:text2textInstructLLM](prompt) -# response = GeneralUtils.remove_french_accents(response) - -# # check wheter all attributes are in the response -# for word in attributes -# if !occursin(word, response) -# error("$word attribute is missing") -# end -# end - -# responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) - -# for i ∈ attributes -# if length(JSON3.write(responsedict[Symbol(i)])) == 0 -# error("$i is empty ", @__LINE__) -# end -# end - -# #check if the following attributes has more than 1 name -# # responsedict[:grape_variety] = split(responsedict[:grape_variety], ',')[1] -# # responsedict[:grape_variety] = split(responsedict[:grape_variety], '/')[1] - -# responsedict[:country] = split(responsedict[:country], ',')[1] -# responsedict[:country] = split(responsedict[:country], '/')[1] - -# responsedict[:region] = split(responsedict[:region], ',')[1] -# responsedict[:region] = split(responsedict[:region], '/')[1] - -# delete!(responsedict, :reasoning) -# delete!(responsedict, :tasting_notes) -# delete!(responsedict, :occasion) -# delete!(responsedict, :food_to_be_paired_with_wine) - -# # check if winery, wine_name, region, country, wine_type, grape_variety are in the query because sometime AI halucinates -# for i in [:grape_variety, :winery, :wine_name, :region] -# result = check_key_in_input(input, responsedict, attempt, maxattempt, i) -# if result === nothing -# # nothing wrong -# elseif result == "NA" -# responsedict[i] = "NA" -# else -# errornote = result -# error(errornote) -# end -# end - -# # remove (some text) -# for (k, v) in responsedict -# _v = replace(v, r"\(.*?\)" => "") -# responsedict[k] = _v -# end - -# result = "" -# for (k, v) in responsedict -# # some time LLM generate text with "(some comment)". this line removes it -# if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v) -# result *= "$k: $v, " -# end -# end - -# #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" - -# result = result[1:end-2] # remove the ending ", " - -# return result -# catch e -# io = IOBuffer() -# showerror(io, e) -# errorMsg = String(take!(io)) -# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) -# println("") -# println("Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) -# println("") -# end -# end -# error("wineattributes_wordToNumber() failed to get a response") -# end function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} systemmsg = @@ -529,27 +352,27 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< - region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc - country: a country where the wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States" - wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" - - grape_variety: the name of the primary grape used to make the wine + - grape_varietal: the name of the primary grape used to make the wine - tasting_notes: a brief description of the wine's taste, such as "butter", "oak", "fruity", etc - - wine_price: price of wine. For example, up to 100, less than 100, 20 to 100, 30-79.95 + - wine_price: price range of wine. - occasion: the occasion the user is having the wine for - food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc You should only respond in the user's preference form (JSON) as described below: - {"reasoning": ..., "winery": ..., "wine_name": ..., "vintage": ..., "region": ..., "country": ..., "wine_type": ..., "grape_variety": ..., "tasting_notes": ..., "wine_price": ..., "occasion": ..., "food_to_be_paired_with_wine": ...} + {"reasoning": ..., "winery": ..., "wine_name": ..., "vintage": ..., "region": ..., "country": ..., "wine_type": ..., "grape_varietal": ..., "tasting_notes": ..., "wine_price": ..., "occasion": ..., "food_to_be_paired_with_wine": ...} Here are some example: - User's query: red, Chenin Blanc, Riesling, under 20 - {"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red", "grape_variety": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "under 20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} + User's query: red, Chenin Blanc, Riesling, 20 USD + {"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red", "grape_varietal": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "0-20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} User's query: Domaine du Collier Saumur Blanc 2019, France, white, Chenin Blanc - {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_variety": "Chenin Blanc", "tasting_notes": "NA", "wine_price": "109", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} + {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Chenin Blanc", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} Let's begin! """ - attributes = ["reasoning", "winery", "wine_name", "vintage", "region", "country", "wine_type", "grape_variety", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] + attributes = ["reasoning", "winery", "wine_name", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] errornote = "" for attempt in 1:5 @@ -576,13 +399,16 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< response = GeneralUtils.remove_french_accents(response) # check wheter all attributes are in the response + checkFlag = false for word in attributes if !occursin(word, response) errornote = "$word attribute is missing in previous attempts" println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) - continue + checkFlag = true + break end end + checkFlag == true ? continue : nothing responsedict = copy(JSON3.read(response)) @@ -591,24 +417,52 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< delete!(responsedict, :occasion) delete!(responsedict, :food_to_be_paired_with_wine) - # check if winery, wine_name, region, country, wine_type, grape_variety are in the query because sometime AI halucinates - for i in [:grape_variety, :winery, :wine_name, :region] - content = responsedict[i] - if occursin(",", content) - content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" - content = strip.(content) - else - content = [content] - end + println(@__FILE__, " ", @__LINE__) + pprintln(responsedict) - for x in content - if !occursin("NA", responsedict[i]) && !occursin(x, input) - errornote = "$x is not mentioned in the user query, you must only use the info from the query." - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) - continue + # check if winery, wine_name, region, country, wine_type, grape_varietal's value are in the query because sometime AI halucinates + checkFlag = false + for i in attributes + j = Symbol(i) + if j ∉ [:reasoning, :tasting_notes, :occasion, :food_to_be_paired_with_wine] + # in case j is wine_price it needs to be checked differently because its value is ranged + if j == "wine_price" + if responsedict[:wine_price] != "NA" + # check whether wine_price is in ranged number + if !occursin('-', responsedict[:wine_price]) + errornote = "wine_price must be a range number" + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + checkFlag = true + break + end + + # check whether max wine_price is in the input + maxprice = split(responsedict[:wine_price], '-')[end] + if !occursin(maxprice, input) + responsedict[:wine_price] = "NA" + end + end + else + content = responsedict[j] + if occursin(",", content) + content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" + content = strip.(content) + else + content = [content] + end + + for x in content + if !occursin("NA", responsedict[j]) && !occursin(x, input) + errornote = "$x is not mentioned in the user query, you must only use the info from the query." + println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + checkFlag == true + break + end + end end end end + checkFlag == true ? continue : nothing # remove (some text) for (k, v) in responsedict @@ -624,7 +478,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< end end - #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" + #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_varietal: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" result = result[1:end-2] # remove the ending ", " @@ -785,7 +639,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< for (k, v) in responsedict if !occursin("keyword", string(k)) if v !== "NA" && (!occursin('-', v) || length(v) > 5) - errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, such as min-max." + errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, i.e. min-max." println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) continue end diff --git a/src/util.jl b/src/util.jl index e449dd6..2d13228 100644 --- a/src/util.jl +++ b/src/util.jl @@ -1,6 +1,6 @@ module util -export clearhistory, addNewMessage, vectorOfDictToText, eventdict, noises +export clearhistory, addNewMessage, vectorOfDictToText, eventdict, noises, createTimeline using UUIDs, Dates, DataStructures, HTTP, JSON3 using GeneralUtils @@ -169,31 +169,6 @@ function vectorOfDictToText(vecd::Vector; withkey=true)::String end -# function eventdict(; -# event_description::Union{String, Nothing}=nothing, -# timestamp::Union{DateTime, Nothing}=nothing, -# subject::Union{String, Nothing}=nothing, -# action_or_dialogue::Union{String, Nothing}=nothing, -# location::Union{String, Nothing}=nothing, -# equipment_used::Union{String, Nothing}=nothing, -# material_used::Union{String, Nothing}=nothing, -# outcome::Union{String, Nothing}=nothing, -# note::Union{String, Nothing}=nothing, -# ) -# return Dict{Symbol, Any}( -# :event_description=> event_description, -# :timestamp=> timestamp, -# :subject=> subject, -# :action_or_dialogue=> action_or_dialogue, -# :location=> location, -# :equipment_used=> equipment_used, -# :material_used=> material_used, -# :outcome=> outcome, -# :note=> note, -# ) -# end - - function eventdict(; event_description::Union{String, Nothing}=nothing, timestamp::Union{DateTime, Nothing}=nothing, @@ -222,6 +197,32 @@ function eventdict(; ) end + +function createTimeline(memory::T1, recent) where {T1<:AbstractVector} + totalevents = length(memory) + ind = + if totalevents > recent + start = totalevents - recent + start:totalevents + else + 1:totalevents + end + + timeline = "" + for (i, event) in enumerate(memory[ind]) + if event[:outcome] === nothing + timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" + else + timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" + end + end + + return timeline +end + + + + # """ Convert a single chat dictionary into LLM model instruct format. # # Llama 3 instruct format example From 944d9eaf2bd10c11139629416e6d41adc256624e Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Fri, 10 Jan 2025 18:08:21 +0700 Subject: [PATCH 06/16] update --- src/interface.jl | 2 +- src/llmfunction.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 808f18c..06657c2 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -162,7 +162,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen Your responsibility excludes: 1) Asking or guiding the user to make a purchase 2) Processing sales orders or engaging in any other sales-related activities - 3) Answering questions and offering additional services beyond just recommendations, such as delivery, box, gift wrapping or packaging, personalized messages. Customers can reach out to our sales at the store. + 3) Answering questions and offering additional services beyond just recommendations, such as discount, reward program, promotion, delivery, box, gift wrapping or packaging, personalized messages. For these, inform customers that they can reach out to 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 diff --git a/src/llmfunction.jl b/src/llmfunction.jl index d7b6103..fdea59f 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -426,7 +426,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< j = Symbol(i) if j ∉ [:reasoning, :tasting_notes, :occasion, :food_to_be_paired_with_wine] # in case j is wine_price it needs to be checked differently because its value is ranged - if j == "wine_price" + if j == :wine_price if responsedict[:wine_price] != "NA" # check whether wine_price is in ranged number if !occursin('-', responsedict[:wine_price]) @@ -451,7 +451,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< content = [content] end - for x in content + for x in content #BUG why x is "0-1500" if !occursin("NA", responsedict[j]) && !occursin(x, input) errornote = "$x is not mentioned in the user query, you must only use the info from the query." println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) From a29e8049a70abc837305e43b871609d1dca9cd19 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sat, 11 Jan 2025 16:57:57 +0700 Subject: [PATCH 07/16] update --- src/interface.jl | 55 ++++++++++++++++++++++++++-------------------- src/llmfunction.jl | 18 +++++++-------- src/util.jl | 2 +- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 06657c2..f09c56c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -218,7 +218,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen winenames = df[:, :wine_name] for winename in winenames if !occursin(winename, chathistory) - println("\n~~~ Yiem decisionMaker() found wines from DB ", @__FILE__, " ", @__LINE__) + println("\n~~~ Yiem decisionMaker() found wines from DB ", Dates.now(), " ", @__FILE__, " ", @__LINE__) d = Dict( :understanding=> "I understand that the customer is looking for a wine that matches their intention and budget.", :reasoning=> "I checked the inventory and found wines that match the customer's criteria. I will present the wines to the customer.", @@ -271,7 +271,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen end if count > 1 errornote = "You must use only one function" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) continue end @@ -281,16 +281,16 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] errornote = "You must use the given functions" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) continue end checkFlag = false for i ∈ [:understanding, :plan, :action_name] if length(responsedict[i]) == 0 - error("$i is empty ", @__FILE__, " ", @__LINE__) + error("$i is empty ", Dates.now(), " ", @__FILE__, " ", @__LINE__) errornote = "$i is empty" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) checkFlag = true break end @@ -303,14 +303,14 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) if length(matchkeys) > 1 errornote = "DecisionMaker has more than one key per categories" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) checkFlag = true break end end checkFlag == true ? continue : nothing - println("\n~~~ Yiem decisionMaker() ", @__FILE__, " ", @__LINE__) + println("\n~~~ Yiem decisionMaker() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) pprintln(Dict(responsedict)) # check whether an agent recommend wines before checking inventory or recommend wines @@ -337,7 +337,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen isWineInEvent == false errornote = "Note: Before recommending a wine, ensure it's in your inventory. Check your stock first." - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) continue end end @@ -503,7 +503,7 @@ function evaluator(config::T1, state::T2 showerror(io, e) errorMsg = String(take!(io)) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) + println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end end error("evaluator failed to generate an evaluation") @@ -633,7 +633,7 @@ function reflector(config::T1, state::T2)::String where {T1<:AbstractDict,T2<:Ab showerror(io, e) errorMsg = String(take!(io)) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) + println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end end error("reflector failed to generate a thought") @@ -733,7 +733,7 @@ function conversation(a::sommelier, userinput::Dict) end end -function conversation(a::companion, userinput::Dict) +function conversation(a::companion, userinput::Dict; maximumMsg=30) chatresponse = nothing if userinput[:text] == "newtopic" @@ -754,7 +754,7 @@ function conversation(a::companion, userinput::Dict) ) chatresponse = generatechat(a) - addNewMessage(a, "assistant", chatresponse) + addNewMessage(a, "assistant", chatresponse; maximumMsg=30) push!(a.memory[:events], eventdict(; @@ -871,7 +871,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh ) ) else - error("condition is not defined ", @__FILE__, " ", @__LINE__) + error("condition is not defined ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end @@ -928,6 +928,7 @@ function generatechat(a::sommelier, thoughtDict) - If the user interrupts, prioritize the user - Medium and full-bodied red wines should not be paired with spicy foods. + You should then respond to the user with: 1) Chat: Given the situation, How would you respond to the user to express your thoughts honestly and keep the conversation going smoothly? @@ -977,7 +978,7 @@ function generatechat(a::sommelier, thoughtDict) # 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__) + error("generatechat() response contain : ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end response = GeneralUtils.remove_french_accents(response) response = replace(response, '*'=>"") @@ -989,7 +990,7 @@ function generatechat(a::sommelier, thoughtDict) for i ∈ [:chat] if length(JSON3.write(responsedict[i])) == 0 - error("$i is empty ", @__FILE__, " ", @__LINE__) + error("$i is empty ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end end @@ -1006,7 +1007,7 @@ function generatechat(a::sommelier, thoughtDict) error("Context: is in text. This is not allowed") end - println("\n~~~ generatechat() ", @__FILE__, " ", @__LINE__) + println("\n~~~ generatechat() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) pprintln(Dict(responsedict)) # check whether an agent recommend wines before checking inventory or recommend wines @@ -1044,7 +1045,7 @@ function generatechat(a::sommelier, thoughtDict) showerror(io, e) errorMsg = String(take!(io)) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) + println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end end error("generatechat failed to generate a response") @@ -1137,6 +1138,12 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity - If you don't already know, find out what food will be served with wine - If you haven't already, introduce the wines you found in the database to the user first + - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - All wines in your inventory are always in stock. + - Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory. + - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. + - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Medium and full-bodied red wines should not be paired with spicy foods. You should then respond to the user with: 1) Understanding: @@ -1251,17 +1258,17 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St # check for valid response q_atleast = length(a.memory[:events]) <= 2 ? 1 : 3 if q_number < q_atleast - error("too few questions only $q_number questions are generated ", @__FILE__, " ", @__LINE__) + error("too few questions only $q_number questions are generated ", Dates.now(), " ", @__FILE__, " ", @__LINE__) # check whether "A1" is in the response, if not error. elseif !occursin("A1:", response) - error("no answer found in the response ", @__FILE__, " ", @__LINE__) + error("no answer found in the response ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end responsedict = GeneralUtils.textToDict(response, ["Understanding", "Q1"], rightmarker=":", symbolkey=true, lowercasekey=true) response = "Q1: " * responsedict[:q1] - println("\n~~~ generatequestion ", @__FILE__, " ", @__LINE__) + println("\n~~~ generatequestion ", Dates.now(), " ", @__FILE__, " ", @__LINE__) pprintln(response) return response catch e @@ -1269,7 +1276,7 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St showerror(io, e) errorMsg = String(take!(io)) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) + println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end end error("generatequestion failed to generate a response ", response) @@ -1345,7 +1352,7 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: # responsedict = GeneralUtils.textToDict(response, # ["summary", "presented", "selected"], # rightmarker=":", symbolkey=true) - println("\n~~~ generateSituationReport() ", @__FILE__, " ", @__LINE__) + println("\n~~~ generateSituationReport() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) pprintln(response) return Dict(:recap => response) @@ -1401,7 +1408,7 @@ function detectWineryName(a, text) try response = a.func[:text2textInstructLLM](prompt) - println("\n~~~ detectWineryName() ", @__FILE__, " ", @__LINE__) + println("\n~~~ detectWineryName() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) pprintln(response) responsedict = GeneralUtils.textToDict(response, ["winery_names"], @@ -1415,7 +1422,7 @@ function detectWineryName(a, text) showerror(io, e) errorMsg = String(take!(io)) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\n Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__) + println("\n Attempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end end error("detectWineryName failed to generate a response") diff --git a/src/llmfunction.jl b/src/llmfunction.jl index fdea59f..70573a1 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -291,20 +291,20 @@ julia> result = checkinventory(agent, input) function checkinventory(a::T1, input::T2 ) where {T1<:agent, T2<:AbstractString} - println("\n~~~ checkinventory order: $input ", @__FILE__, " ", @__LINE__) + println("\n~~~ checkinventory order: $input ", Dates.now(), " ", @__FILE__, " ", @__LINE__) wineattributes_1 = extractWineAttributes_1(a, input) wineattributes_2 = extractWineAttributes_2(a, input) _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" inventoryquery = "Retrieves winery, wine_name, vintage, region, country, wine_type, grape, serving_temperature, sweetness, intensity, tannin, acidity, tasting_notes, price and currency of wines that match the following criteria - {$_inventoryquery}" - println("~~~ checkinventory input: $inventoryquery ", @__FILE__, " ", @__LINE__) + println("~~~ checkinventory input: $inventoryquery ", Dates.now(), " ", @__FILE__, " ", @__LINE__) # add suppport for similarSQLVectorDB textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL], a.func[:text2textInstructLLM], insertSQLVectorDB=a.func[:insertSQLVectorDB], similarSQLVectorDB=a.func[:similarSQLVectorDB]) - println("\n~~~ checkinventory result ", @__FILE__, " ", @__LINE__) + println("\n~~~ checkinventory result ", Dates.now(), " ", @__FILE__, " ", @__LINE__) println(textresult) return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) @@ -403,7 +403,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< for word in attributes if !occursin(word, response) errornote = "$word attribute is missing in previous attempts" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) checkFlag = true break end @@ -431,7 +431,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< # check whether wine_price is in ranged number if !occursin('-', responsedict[:wine_price]) errornote = "wine_price must be a range number" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) checkFlag = true break end @@ -454,7 +454,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< for x in content #BUG why x is "0-1500" if !occursin("NA", responsedict[j]) && !occursin(x, input) errornote = "$x is not mentioned in the user query, you must only use the info from the query." - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) checkFlag == true break end @@ -624,7 +624,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< value = responsedict[keyword] if value != "NA" && !occursin(value, input) errornote = "WARNING. Keyword $keyword: $value does not appear in the input. You must use information from the input only" - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) continue end @@ -640,7 +640,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< if !occursin("keyword", string(k)) if v !== "NA" && (!occursin('-', v) || length(v) > 5) errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, i.e. min-max." - println("Attempt $attempt $errornote ", @__FILE__, " ", @__LINE__) + println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__) continue end end @@ -859,7 +859,7 @@ end # state[:isterminal] = true # state[:reward] = 1 # end -# println("--> 5 Evaluator ", @__FILE__, " ", @__LINE__) +# println("--> 5 Evaluator ", Dates.now(), " ", @__FILE__, " ", @__LINE__) # pprintln(Dict(responsedict)) # return responsedict[:score] # catch e diff --git a/src/util.jl b/src/util.jl index 2d13228..1871423 100644 --- a/src/util.jl +++ b/src/util.jl @@ -106,7 +106,7 @@ function addNewMessage(a::T1, name::String, text::T2; error("name is not in agent.availableRole $(@__LINE__)") end - #[] summarize the oldest 10 message + #[WORKING] summarize the oldest 10 message if length(a.chathistory) > maximumMsg summarize(a.chathistory) else From 2206831bab4ec63368855a4c27fede9adb87d17b Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Wed, 15 Jan 2025 06:13:18 +0700 Subject: [PATCH 08/16] update --- src/interface.jl | 150 ++++++++++++++++++++++++++++++++--------- src/llmfunction.jl | 165 +++++++++++++++++++++++++++++++++++++++++++++ src/type.jl | 1 + src/util.jl | 22 +++--- 4 files changed, 298 insertions(+), 40 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index f09c56c..1fc2e19 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -162,7 +162,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen Your responsibility excludes: 1) Asking or guiding the user to make a purchase 2) Processing sales orders or engaging in any other sales-related activities - 3) Answering questions and offering additional services beyond just recommendations, such as discount, reward program, promotion, delivery, box, gift wrapping or packaging, personalized messages. For these, inform customers that they can reach out to our sales team at the store. + 3) Answering questions and offering additional services beyond just recommendations. At each round of conversation, you will be given the current situation: Your recent events: latest 5 events of the situation @@ -180,6 +180,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen - 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. + - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. For your information: - vintage 0 means non-vintage. @@ -692,7 +693,7 @@ julia> response = ChatAgent.conversation(newAgent, "Hi! how are you?") # Signature """ -function conversation(a::sommelier, userinput::Dict) +function conversation(a::sommelier, userinput::Dict; maximumMsg=50) # place holder actionname = nothing @@ -705,7 +706,7 @@ function conversation(a::sommelier, userinput::Dict) return "Okay. What shall we talk about?" else # add usermsg to a.chathistory - addNewMessage(a, "user", userinput[:text]) + addNewMessage(a, "user", userinput[:text]; maximumMsg=maximumMsg) # add user activity to events memory push!(a.memory[:events], @@ -727,13 +728,13 @@ function conversation(a::sommelier, userinput::Dict) end end - addNewMessage(a, "assistant", chatresponse) + addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) return chatresponse end end -function conversation(a::companion, userinput::Dict; maximumMsg=30) +function conversation(a::companion, userinput::Dict; maximumMsg=50) chatresponse = nothing if userinput[:text] == "newtopic" @@ -741,7 +742,7 @@ function conversation(a::companion, userinput::Dict; maximumMsg=30) return "Okay. What shall we talk about?" else # add usermsg to a.chathistory - addNewMessage(a, "user", userinput[:text]) + addNewMessage(a, "user", userinput[:text]; maximumMsg=maximumMsg) # add user activity to events memory push!(a.memory[:events], @@ -754,7 +755,7 @@ function conversation(a::companion, userinput::Dict; maximumMsg=30) ) chatresponse = generatechat(a) - addNewMessage(a, "assistant", chatresponse; maximumMsg=30) + addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) push!(a.memory[:events], eventdict(; @@ -786,7 +787,7 @@ julia> """ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} - a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=3) + a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) thoughtDict = decisionMaker(a; recent=3) actionname = thoughtDict[:action_name] @@ -928,7 +929,6 @@ function generatechat(a::sommelier, thoughtDict) - If the user interrupts, prioritize the user - Medium and full-bodied red wines should not be paired with spicy foods. - You should then respond to the user with: 1) Chat: Given the situation, How would you respond to the user to express your thoughts honestly and keep the conversation going smoothly? @@ -1119,9 +1119,9 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St Your responsibility does not include: 1) Processing sales orders or engaging in any other sales-related activities. + 2) Answering questions and offering additional services beyond just recommendations. At each round of conversation, you will be given the current situation: - Your status: your current status Recap: recap of what has happened so far Your recent events: latest 5 events of the situation @@ -1144,6 +1144,7 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - Medium and full-bodied red wines should not be paired with spicy foods. + - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. You should then respond to the user with: 1) Understanding: @@ -1222,11 +1223,22 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St errornote = "" response = nothing # store for show when error msg show up + #[WORKING] + recap = + if a.memory[:recap] === nothing + "None" + else + if length(a.memory[:events]) > recent + GeneralUtils.dictToString(a.memory[:recap][1:end-recent]) + else + "None" + end + end + for attempt in 1:10 usermsg = """ - Your status: $(GeneralUtils.dict_to_string(a.memory[:state])) - Recap: $(a.memory[:recap]) + Recap: $recap) Your recent events: $timeline $errornote """ @@ -1283,9 +1295,86 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St end -function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 -)::Dict +# function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 +# )::Dict +# systemmsg = +# """ +# You are an assistant being in the given events. +# Your task is to writes a summary for each event in an ongoing, interleaving series. + +# At each round of conversation, you will be given the situation: +# Total events: number of events you need to summarize. +# Events timeline: ... +# Context: ... + +# You should then respond to the user with: +# event: a detailed summary for each event without exaggerated details. + +# You must only respond in format as described below: +# Event_1: ... +# Event_2: ... +# ... + +# Here are some examples: +# Event_1: The user ask me about where to buy a toy. +# Event_2: I told the user to go to the store at 2nd floor. + +# Let's begin! +# """ + +# if length(a.memory[:events]) <= skiprecent +# return Dict(:recap => "None") +# end + +# events = deepcopy(a.memory[:events][1:end-skiprecent]) + +# timeline = "" +# for (i, event) in enumerate(events) +# if event[:outcome] === nothing +# timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" +# else +# timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" +# end +# end + +# errornote = "" +# response = nothing # store for show when error msg show up + +# for attempt in 1:10 +# usermsg = """ +# Total events: $(length(events)) +# Events timeline: $timeline +# $errornote +# """ + +# _prompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# Dict(:name => "user", :text => usermsg) +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") +# prompt *= """ +# <|start_header_id|>assistant<|end_header_id|> +# """ + +# response = text2textInstructLLM(prompt) +# # responsedict = GeneralUtils.textToDict(response, +# # ["summary", "presented", "selected"], +# # rightmarker=":", symbolkey=true) +# println("\n~~~ generateSituationReport() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) +# pprintln(response) + +# return Dict(:recap => response) +# end +# error("generateSituationReport failed to generate a response ", response) +# end + +function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 + )::Dict + systemmsg = """ You are an assistant being in the given events. @@ -1312,19 +1401,21 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: """ if length(a.memory[:events]) <= skiprecent - return Dict(:recap => "None") + return nothing end - events = deepcopy(a.memory[:events][1:end-skiprecent]) + # events = deepcopy(a.memory[:events][1:end-skiprecent]) - timeline = "" - for (i, event) in enumerate(events) - if event[:outcome] === nothing - timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" - else - timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" - end - end + # timeline = "" + # for (i, event) in enumerate(events) + # if event[:outcome] === nothing + # timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" + # else + # timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" + # end + # end + + timeline = createTimeline(a.memory[:events]; skiprecent=skiprecent) errornote = "" response = nothing # store for show when error msg show up @@ -1349,19 +1440,18 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: """ response = text2textInstructLLM(prompt) - # responsedict = GeneralUtils.textToDict(response, - # ["summary", "presented", "selected"], - # rightmarker=":", symbolkey=true) + eventheader = ["Event_$i" for i in eachindex(a.memory[:events])] + responsedict = GeneralUtils.textToDict(response, eventheader, + rightmarker=":", symbolkey=true) + println("\n~~~ generateSituationReport() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) pprintln(response) - return Dict(:recap => response) + return responsedict end error("generateSituationReport failed to generate a response ", response) end - - function detectWineryName(a, text) systemmsg = diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 70573a1..cbe4fca 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -669,6 +669,171 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< end +# function concept(a::sommelier, thoughtDict) +# systemmsg = +# """ +# Your name: N/A +# Situation: +# - You are a helpful assistant +# Your vision: +# - This is a good opportunity to help the user +# Your mission: +# - To describe the concept of a conversation +# Mission's objective includes: +# - To +# Your responsibility includes: +# 1) Given the situation, convey your thoughts to the user. +# Your responsibility excludes: +# 1) Asking or guiding the user to make a purchase +# 2) Processing sales orders or engaging in any other sales-related activities +# 3) Answering questions and offering additional services beyond just recommendations, such as delivery, box, gift wrapping, personalized messages. Customers can reach out to our sales at the store. +# Your profile: +# - You are a young professional in a big company. +# - You are avid party goer +# - You like beer. +# - You know nothing about wine. +# - You have a budget of 1500usd. +# Additional information: +# - your boss like spicy food. +# - your boss is a middle-aged man. + +# At each round of conversation, you will be given the following information: +# Your ongoing conversation with the user: ... +# Context: ... +# Your thoughts: Your current thoughts in your mind + +# You MUST follow the following guidelines: +# - Do not offer additional services you didn't thought. + +# You should follow the following guidelines: +# - Focus on the latest conversation. +# - If the user interrupts, prioritize the user +# - Medium and full-bodied red wines should not be paired with spicy foods. + +# You should then respond to the user with: +# 1) Chat: Given the situation, How would you respond to the user to express your thoughts honestly and keep the conversation going smoothly? + +# You should only respond in format as described below: +# Chat: ... + +# Here are some examples of response format: +# Chat: "I see. Let me think about it. I'll get back to you with my recommendation." + +# Let's begin! +# """ + +# # a.memory[:shortmem][:available_wine] is a dataframe. +# context = +# if haskey(a.memory[:shortmem], :available_wine) +# "Available wines $(GeneralUtils.dfToString(a.memory[:shortmem][:available_wine]))" +# else +# "None" +# end + +# chathistory = vectorOfDictToText(a.chathistory) +# errornote = "" +# response = nothing # placeholder for show when error msg show up + +# for attempt in 1:10 +# usermsg = """ +# Your ongoing conversation with the user: $chathistory +# Contex: $context +# Your thoughts: $(thoughtDict[:understanding]) $(thoughtDict[:reasoning]) $(thoughtDict[:plan]) +# $errornote +# """ + +# _prompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# Dict(:name => "user", :text => usermsg) +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") +# prompt *= """ +# <|start_header_id|>assistant<|end_header_id|> +# """ + +# try +# 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 : ", Dates.now(), " ", @__FILE__, " ", @__LINE__) +# end +# response = GeneralUtils.remove_french_accents(response) +# response = replace(response, '*'=>"") +# response = replace(response, '$' => "USD") +# response = replace(response, '`' => "") +# response = GeneralUtils.remove_french_accents(response) +# responsedict = GeneralUtils.textToDict(response, ["Chat"], +# rightmarker=":", symbolkey=true, lowercasekey=true) + +# for i ∈ [:chat] +# if length(JSON3.write(responsedict[i])) == 0 +# error("$i is empty ", Dates.now(), " ", @__FILE__, " ", @__LINE__) +# end +# end + +# # check if there are more than 1 key per categories +# for i ∈ [:chat] +# matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) +# if length(matchkeys) > 1 +# error("generatechat has more than one key per categories") +# end +# end + +# # check if Context: is in chat +# if occursin("Context:", responsedict[:chat]) +# error("Context: is in text. This is not allowed") +# end + +# println("\n~~~ generatechat() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) +# 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]) +# 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 = "Previously: You recommend a wine that is not in your inventory which is not allowed." +# error("Previously: You recommend a wine that is not in your inventory which is not allowed.") +# end +# end + +# result = responsedict[:chat] + +# return result +# catch e +# io = IOBuffer() +# showerror(io, e) +# errorMsg = String(take!(io)) +# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) +# println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__) +# end +# end +# error("generatechat failed to generate a response") +# end + + + """ Attemp to correct LLM response's incorrect JSON response. # Arguments diff --git a/src/type.jl b/src/type.jl index 939b7b9..8e137b8 100644 --- a/src/type.jl +++ b/src/type.jl @@ -185,6 +185,7 @@ function sommelier( :state=> Dict{Symbol, Any}( :wine_presented_to_user=> "None", ), + :recap=> nothing, ) newAgent = sommelier( diff --git a/src/util.jl b/src/util.jl index 1871423..a562043 100644 --- a/src/util.jl +++ b/src/util.jl @@ -198,18 +198,20 @@ function eventdict(; end -function createTimeline(memory::T1, recent) where {T1<:AbstractVector} - totalevents = length(memory) - ind = - if totalevents > recent - start = totalevents - recent - start:totalevents - else - 1:totalevents - end +function createTimeline(memory::T1; skiprecent::Integer=0) where {T1<:AbstractVector} + # totalevents = length(memory) + # ind = + # if totalevents > skiprecent + # start = totalevents - skiprecent + # start:totalevents + # else + # 1:totalevents + # end + + events = memory[1:end-skiprecent] timeline = "" - for (i, event) in enumerate(memory[ind]) + for (i, event) in enumerate(events) if event[:outcome] === nothing timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" else From c7000f66b83250e879bb11223dc059d9bc4a6032 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Wed, 15 Jan 2025 08:35:25 +0700 Subject: [PATCH 09/16] update --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3b08a83..c963881 100644 --- a/Project.toml +++ b/Project.toml @@ -22,6 +22,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] DataFrames = "1.7.0" -GeneralUtils = "0.1.0" +GeneralUtils = "0.1, 0.2" LLMMCTS = "0.1.2" SQLLLM = "0.2.0" From 3fdc0adf999f2c6ee3f92dabe55906fa65b40e6d Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Thu, 16 Jan 2025 07:40:39 +0700 Subject: [PATCH 10/16] update --- src/interface.jl | 73 ++++++++++++++++++++++++++-------------------- src/llmfunction.jl | 4 +-- src/type.jl | 2 +- src/util.jl | 9 ------ 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 1fc2e19..a4787c8 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -132,17 +132,39 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 1:totalevents end - timeline = "" + recentevents = "" for (i, event) in enumerate(a.memory[:events][ind]) if event[:outcome] === nothing - timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" + recentevents *= "$i) $(event[:subject])> $(event[:actioninput])\n" else - timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" + recentevents *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" end end + #[TESTING] recap as caching # query similar result from vectorDB - similarDecision = a.func[:similarSommelierDecision](timeline) + 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 + + # 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) if similarDecision !== nothing responsedict = similarDecision @@ -213,7 +235,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen # check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX if haskey(a.memory[:shortmem], :available_wine) - # check if wine name mentioned in timeline, only check first wine name is enough + # check if wine name mentioned in recentevents, only check first wine name is enough # because agent will recommend every wines it found each time. df = a.memory[:shortmem][:available_wine] winenames = df[:, :wine_name] @@ -242,7 +264,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen usermsg = """ - Your recent events: $timeline + Your recent events: $recentevents Your Q&A: $QandA) $errornote """ @@ -345,15 +367,6 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen delete!(responsedict, :mentioned_winery) - # #CHANGE cache decision dict into vectorDB, this should be after new message is added to a.memory[:events] - # println("\n~~~ Do you want to cache decision dict? (y/n)") - # user_answer = readline() - # if user_answer == "y" - # timeline = timeline - # decisiondict = responsedict - # a.func[:insertSommelierDecision](timeline, decisiondict) - # end - return responsedict end error("DecisionMaker failed to generate a thought ", response) @@ -1223,16 +1236,21 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St errornote = "" response = nothing # store for show when error msg show up - #[WORKING] recap = - if a.memory[:recap] === nothing + if length(a.memory[:recap]) <= recent "None" else - if length(a.memory[:events]) > recent - GeneralUtils.dictToString(a.memory[:recap][1:end-recent]) - else - "None" + recapkeys = keys(a.memory[:recap]) + recapkeys_vec = [i for i in recapkeys] + recapkeys_vec = recapkeys_vec[1:end-recent] + tempmem = OrderedDict() + for (k, v) in a.memory[:recap] + if k ∈ recapkeys_vec + tempmem[k] = v + end end + + GeneralUtils.dictToString(tempmem) end for attempt in 1:10 @@ -1373,7 +1391,7 @@ end # end function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 - )::Dict + )::OrderedDict systemmsg = """ @@ -1404,16 +1422,7 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: return nothing end - # events = deepcopy(a.memory[:events][1:end-skiprecent]) - - # timeline = "" - # for (i, event) in enumerate(events) - # if event[:outcome] === nothing - # timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" - # else - # timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" - # end - # end + events = a.memory[:events][1:end-skiprecent] timeline = createTimeline(a.memory[:events]; skiprecent=skiprecent) diff --git a/src/llmfunction.jl b/src/llmfunction.jl index cbe4fca..ae85089 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -364,7 +364,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< Here are some example: User's query: red, Chenin Blanc, Riesling, 20 USD - {"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red", "grape_varietal": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "0-20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} + {"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red, white", "grape_varietal": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "0-20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} User's query: Domaine du Collier Saumur Blanc 2019, France, white, Chenin Blanc {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Chenin Blanc", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} @@ -444,7 +444,7 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< end else content = responsedict[j] - if occursin(",", content) + if occursin(',', content) content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" content = strip.(content) else diff --git a/src/type.jl b/src/type.jl index 8e137b8..31a26c3 100644 --- a/src/type.jl +++ b/src/type.jl @@ -185,7 +185,7 @@ function sommelier( :state=> Dict{Symbol, Any}( :wine_presented_to_user=> "None", ), - :recap=> nothing, + :recap=> OrderedDict{Symbol, Any}(), ) newAgent = sommelier( diff --git a/src/util.jl b/src/util.jl index a562043..0c8aec0 100644 --- a/src/util.jl +++ b/src/util.jl @@ -199,15 +199,6 @@ end function createTimeline(memory::T1; skiprecent::Integer=0) where {T1<:AbstractVector} - # totalevents = length(memory) - # ind = - # if totalevents > skiprecent - # start = totalevents - skiprecent - # start:totalevents - # else - # 1:totalevents - # end - events = memory[1:end-skiprecent] timeline = "" From 4197625e57054e8c2202ed8353c49effe8418991 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Fri, 17 Jan 2025 22:09:48 +0700 Subject: [PATCH 11/16] update --- src/interface.jl | 83 +++--------------------------------------------- 1 file changed, 4 insertions(+), 79 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index a4787c8..9442011 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -3,7 +3,8 @@ module interface export addNewMessage, conversation, decisionMaker, evaluator, reflector, generatechat, generalconversation, detectWineryName -using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization +using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, + DataFrames using GeneralUtils using ..type, ..util, ..llmfunction @@ -940,6 +941,7 @@ function generatechat(a::sommelier, thoughtDict) 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. You should then respond to the user with: @@ -1313,90 +1315,13 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St end -# function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 -# )::Dict - -# systemmsg = -# """ -# You are an assistant being in the given events. -# Your task is to writes a summary for each event in an ongoing, interleaving series. - -# At each round of conversation, you will be given the situation: -# Total events: number of events you need to summarize. -# Events timeline: ... -# Context: ... - -# You should then respond to the user with: -# event: a detailed summary for each event without exaggerated details. - -# You must only respond in format as described below: -# Event_1: ... -# Event_2: ... -# ... - -# Here are some examples: -# Event_1: The user ask me about where to buy a toy. -# Event_2: I told the user to go to the store at 2nd floor. - -# Let's begin! -# """ - -# if length(a.memory[:events]) <= skiprecent -# return Dict(:recap => "None") -# end - -# events = deepcopy(a.memory[:events][1:end-skiprecent]) - -# timeline = "" -# for (i, event) in enumerate(events) -# if event[:outcome] === nothing -# timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n" -# else -# timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" -# end -# end - -# errornote = "" -# response = nothing # store for show when error msg show up - -# for attempt in 1:10 -# usermsg = """ -# Total events: $(length(events)) -# Events timeline: $timeline -# $errornote -# """ - -# _prompt = -# [ -# Dict(:name => "system", :text => systemmsg), -# Dict(:name => "user", :text => usermsg) -# ] - -# # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") -# prompt *= """ -# <|start_header_id|>assistant<|end_header_id|> -# """ - -# response = text2textInstructLLM(prompt) -# # responsedict = GeneralUtils.textToDict(response, -# # ["summary", "presented", "selected"], -# # rightmarker=":", symbolkey=true) -# println("\n~~~ generateSituationReport() ", Dates.now(), " ", @__FILE__, " ", @__LINE__) -# pprintln(response) - -# return Dict(:recap => response) -# end -# error("generateSituationReport failed to generate a response ", response) -# end - function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 )::OrderedDict systemmsg = """ You are an assistant being in the given events. - Your task is to writes a summary for each event in an ongoing, interleaving series. + Your task is to writes a summary for each event seperately into an ongoing, interleaving series. At each round of conversation, you will be given the situation: Total events: number of events you need to summarize. From bb81b973d3f5106c239861e62337c8ffe0d9d496 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Mon, 20 Jan 2025 18:19:38 +0700 Subject: [PATCH 12/16] update --- src/interface.jl | 70 ++++++++++++++++++++++++++-------------------- src/llmfunction.jl | 8 +++++- src/util.jl | 26 +++++++++++++++-- 3 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 9442011..09eb9ce 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,7 +1,7 @@ module interface export addNewMessage, conversation, decisionMaker, evaluator, reflector, generatechat, - generalconversation, detectWineryName + generalconversation, detectWineryName, generateSituationReport using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames @@ -176,16 +176,17 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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) Help them select the best wines from your inventory that align with their preferences + 2) Help them 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 excludes: - 1) Asking or guiding the user to make a purchase + 1) Asking or guiding the user to make an order or purchase 2) Processing sales orders or engaging in any other sales-related activities - 3) Answering questions and offering additional services beyond just recommendations. + 3) Answering questions beyond just recommendations. + 4) Offering additional services beyond just recommendations. At each round of conversation, you will be given the current situation: Your recent events: latest 5 events of the situation @@ -204,7 +205,8 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen - 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. - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. - + - Only recommend + For your information: - vintage 0 means non-vintage. @@ -232,14 +234,17 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen Let's begin! """ - chathistory = vectorOfDictToText(a.chathistory) + chathistory = chatHistoryToText(a.chathistory) # check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX if haskey(a.memory[:shortmem], :available_wine) # check if wine name mentioned in recentevents, only check first wine name is enough # because agent will recommend every wines it found each time. - df = a.memory[:shortmem][:available_wine] - winenames = df[:, :wine_name] + winenames = [] + for wine in a.memory[:shortmem][:available_wine] + push!(winenames, wine["wine_name"]) + end + for winename in winenames if !occursin(winename, chathistory) println("\n~~~ Yiem decisionMaker() found wines from DB ", Dates.now(), " ", @__FILE__, " ", @__LINE__) @@ -257,6 +262,19 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen end end + context = # may b add wine name instead of the hold wine data is better + if haskey(a.memory[:shortmem], :available_wine) + winenames = [] + for (i, wine) in enumerate(a.memory[:shortmem][:available_wine]) + name = "$i) $(wine["wine_name"]) " + push!(winenames, name) + end + availableWineName = join(winenames, ',') + "You found information about the following wines in your inventory: $availableWineName" + else + "" + end + errornote = "" response = nothing # placeholder for show when error msg show up @@ -265,6 +283,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen usermsg = """ + $context Your recent events: $recentevents Your Q&A: $QandA) $errornote @@ -734,14 +753,12 @@ function conversation(a::sommelier, userinput::Dict; maximumMsg=50) # thinking loop until AI wants to communicate with the user chatresponse = nothing - for i in 1:5 + while chatresponse === nothing actionname, result = think(a) if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"] chatresponse = result - break end end - addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) return chatresponse @@ -856,22 +873,13 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh elseif actionname == "CHECKINVENTORY" if rawresponse !== nothing if haskey(a.memory[:shortmem], :available_wine) - df = a.memory[:shortmem][:available_wine] - #[TESTING] sometime df 2 df has different column size - dfCol = names(df) - rawresponse_dfCol = names(rawresponse) - if length(dfCol) > length(rawresponse_dfCol) - a.memory[:shortmem][:available_wine] = DataFrames.outerjoin(df, rawresponse, on=rawresponse_dfCol) - elseif length(dfCol) < length(rawresponse_dfCol) - a.memory[:shortmem][:available_wine] = DataFrames.outerjoin(df, rawresponse, on=dfCol) - else - a.memory[:shortmem][:available_wine] = vcat(df, rawresponse) - end + vd = GeneralUtils.dfToVectorDict(rawresponse) + a.memory[:shortmem][:available_wine] = vcat(vd, rawresponse) else - a.memory[:shortmem][:available_wine] = rawresponse + a.memory[:shortmem][:available_wine] = GeneralUtils.dfToVectorDict(rawresponse) end else - # no result, skip + println("checkinventory return nothing") end push!(a.memory[:events], @@ -889,7 +897,6 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh error("condition is not defined ", Dates.now(), " ", @__FILE__, " ", @__LINE__) end - return (actionname=actionname, result=result) end @@ -956,15 +963,15 @@ function generatechat(a::sommelier, thoughtDict) Let's begin! """ - # a.memory[:shortmem][:available_wine] is a dataframe. + # a.memory[:shortmem][:available_wine] is a vector of dictionary context = if haskey(a.memory[:shortmem], :available_wine) - "Available wines $(GeneralUtils.dfToString(a.memory[:shortmem][:available_wine]))" + "Wines previously found in your inventory: $(availableWineToText(a.memory[:shortmem][:available_wine]))" else - "None" + "N/A" end - chathistory = vectorOfDictToText(a.chathistory) + chathistory = chatHistoryToText(a.chathistory) errornote = "" response = nothing # placeholder for show when error msg show up @@ -1092,7 +1099,7 @@ function generatechat(a::companion) a.systemmsg end - chathistory = vectorOfDictToText(a.chathistory) + chathistory = chatHistoryToText(a.chathistory) response = nothing # placeholder for show when error msg show up for attempt in 1:10 @@ -1340,6 +1347,9 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: Event_1: The user ask me about where to buy a toy. Event_2: I told the user to go to the store at 2nd floor. + Event_1: The user greets the assistant by saying 'hello'. + Event_2: The assistant respond warmly and inquire about how he can assist the user. + Let's begin! """ diff --git a/src/llmfunction.jl b/src/llmfunction.jl index ae85089..1e53d73 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -307,6 +307,8 @@ function checkinventory(a::T1, input::T2 println("\n~~~ checkinventory result ", Dates.now(), " ", @__FILE__, " ", @__LINE__) println(textresult) + #[WORKING] when rawresponse is nothing, AI get errors + return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) end @@ -412,6 +414,8 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< responsedict = copy(JSON3.read(response)) + # convert + delete!(responsedict, :reasoning) delete!(responsedict, :tasting_notes) delete!(responsedict, :occasion) @@ -444,7 +448,9 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< end else content = responsedict[j] - if occursin(',', content) + if typeof(content) <: AbstractVector + content = strip.(content) + elseif occursin(',', content) content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" content = strip.(content) else diff --git a/src/util.jl b/src/util.jl index 0c8aec0..5478394 100644 --- a/src/util.jl +++ b/src/util.jl @@ -1,6 +1,7 @@ module util -export clearhistory, addNewMessage, vectorOfDictToText, eventdict, noises, createTimeline +export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, + availableWineToText using UUIDs, Dates, DataStructures, HTTP, JSON3 using GeneralUtils @@ -138,7 +139,7 @@ julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true) ``` # Signature """ -function vectorOfDictToText(vecd::Vector; withkey=true)::String +function chatHistoryToText(vecd::Vector; withkey=true)::String # Initialize an empty string to hold the final text text = "" @@ -169,6 +170,27 @@ function vectorOfDictToText(vecd::Vector; withkey=true)::String end +function availableWineToText(vecd::Vector)::String + # Initialize an empty string to hold the final text + rowtext = "" + # Loop through each dictionary in the input vector + for (i, d) in enumerate(vecd) + # Iterate over all key-value pairs in the dictionary + temp = [] + for (k, v) in d + # Append the formatted string to the text variable + t = "$k:$v" + push!(temp, t) + end + _rowtext = join(temp, ',') + rowtext *= "$i) $_rowtext " + end + + return rowtext +end + + + function eventdict(; event_description::Union{String, Nothing}=nothing, timestamp::Union{DateTime, Nothing}=nothing, From d89d4258859fff4e89e3bfea392004794a9d50d2 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Tue, 21 Jan 2025 08:28:26 +0700 Subject: [PATCH 13/16] update --- src/interface.jl | 14 +++++++------- src/type.jl | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 09eb9ce..d8b91aa 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -237,7 +237,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen chathistory = chatHistoryToText(a.chathistory) # check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX - if haskey(a.memory[:shortmem], :available_wine) + if length(a.memory[:shortmem][:available_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 = [] @@ -263,7 +263,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen end context = # may b add wine name instead of the hold wine data is better - if haskey(a.memory[:shortmem], :available_wine) + if length(a.memory[:shortmem][:available_wine]) != 0 winenames = [] for (i, wine) in enumerate(a.memory[:shortmem][:available_wine]) name = "$i) $(wine["wine_name"]) " @@ -872,11 +872,11 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh elseif actionname == "CHECKINVENTORY" if rawresponse !== nothing - if haskey(a.memory[:shortmem], :available_wine) - vd = GeneralUtils.dfToVectorDict(rawresponse) - a.memory[:shortmem][:available_wine] = vcat(vd, rawresponse) + vd = GeneralUtils.dfToVectorDict(rawresponse) + if length(a.memory[:shortmem][:available_wine]) == 0 + a.memory[:shortmem][:available_wine] = vcat(a.memory[:shortmem][:available_wine], vd) else - a.memory[:shortmem][:available_wine] = GeneralUtils.dfToVectorDict(rawresponse) + a.memory[:shortmem][:available_wine] = vd end else println("checkinventory return nothing") @@ -965,7 +965,7 @@ function generatechat(a::sommelier, thoughtDict) # a.memory[:shortmem][:available_wine] is a vector of dictionary context = - if haskey(a.memory[:shortmem], :available_wine) + if length(a.memory[:shortmem][:available_wine]) != 0 "Wines previously found in your inventory: $(availableWineToText(a.memory[:shortmem][:available_wine]))" else "N/A" diff --git a/src/type.jl b/src/type.jl index 31a26c3..6273224 100644 --- a/src/type.jl +++ b/src/type.jl @@ -180,10 +180,11 @@ function sommelier( memory = Dict{Symbol, Any}( :chatbox=> "", - :shortmem=> OrderedDict{Symbol, Any}(), + :shortmem=> OrderedDict{Symbol, Any}( + :available_wine=> [], + ), :events=> Vector{Dict{Symbol, Any}}(), :state=> Dict{Symbol, Any}( - :wine_presented_to_user=> "None", ), :recap=> OrderedDict{Symbol, Any}(), ) From 29adc077d514f8e71c7b242562339e00cca31251 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Thu, 23 Jan 2025 19:34:13 +0700 Subject: [PATCH 14/16] update --- src/interface.jl | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index d8b91aa..bb2f2c3 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -205,7 +205,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen - 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. - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. - - Only recommend + - Do not discuss other stores with the user except for your own. For your information: - vintage 0 means non-vintage. @@ -873,7 +873,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh elseif actionname == "CHECKINVENTORY" if rawresponse !== nothing vd = GeneralUtils.dfToVectorDict(rawresponse) - if length(a.memory[:shortmem][:available_wine]) == 0 + if length(a.memory[:shortmem][:available_wine]) != 0 a.memory[:shortmem][:available_wine] = vcat(a.memory[:shortmem][:available_wine], vd) else a.memory[:shortmem][:available_wine] = vd @@ -933,9 +933,10 @@ function generatechat(a::sommelier, thoughtDict) 1) Given the situation, convey your thoughts to the user. Your responsibility excludes: - 1) Asking or guiding the user to make a purchase + 1) Asking or guiding the user to make an order or purchase 2) Processing sales orders or engaging in any other sales-related activities - 3) Answering questions and offering additional services beyond just recommendations, such as delivery, box, gift wrapping, personalized messages. Customers can reach out to our sales at the store. + 3) Answering questions beyond just recommendations. + 4) Offering additional services beyond just recommendations. At each round of conversation, you will be given the current situation: Your ongoing conversation with the user: ... @@ -950,6 +951,7 @@ function generatechat(a::sommelier, thoughtDict) - If the user interrupts, prioritize the user - Be honest - Medium and full-bodied red wines should not be paired with spicy foods. + - Do not discuss other stores with the user except for your own. You should then respond to the user with: 1) Chat: Given the situation, How would you respond to the user to express your thoughts honestly and keep the conversation going smoothly? @@ -1054,8 +1056,8 @@ function generatechat(a::sommelier, thoughtDict) # then the agent is not supposed to recommend the wine if isWineInEvent == false - errornote = "Previously: You recommend a wine that is not in your inventory which is not allowed." - error("Previously: You recommend a wine 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." + error("Previously, You recommend wines that is not in your inventory which is not allowed.") end end @@ -1225,6 +1227,13 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St Let's begin! """ + context = + if length(a.memory[:shortmem][:available_wine]) != 0 + "Wines previously found in your inventory: $(availableWineToText(a.memory[:shortmem][:available_wine]))" + else + "N/A" + end + totalevents = length(a.memory[:events]) ind = if totalevents > recent @@ -1247,7 +1256,7 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St recap = if length(a.memory[:recap]) <= recent - "None" + "N/A" else recapkeys = keys(a.memory[:recap]) recapkeys_vec = [i for i in recapkeys] @@ -1267,6 +1276,7 @@ function generatequestion(a, text2textInstructLLM::Function; recent=nothing)::St """ Recap: $recap) Your recent events: $timeline + Context: $context $errornote """ From cf4cd13b1431d2750f99d8cc310b9f36c6d8369b Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sat, 25 Jan 2025 13:31:23 +0700 Subject: [PATCH 15/16] update --- src/interface.jl | 11 ++++++++--- src/type.jl | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index bb2f2c3..72f71ca 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -56,7 +56,7 @@ end - `state::T2` a game state -# Return +# Return - `thoughtDict::Dict` # Example @@ -237,11 +237,11 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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][:available_wine]) != 0 + 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][:available_wine] + for wine in a.memory[:shortmem][:found_wine] push!(winenames, wine["wine_name"]) end @@ -257,6 +257,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen 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 @@ -304,6 +305,7 @@ function decisionMaker(a::T; recent::Integer=5)::Dict{Symbol,Any} where {T<:agen response = a.func[:text2textInstructLLM](prompt) response = GeneralUtils.remove_french_accents(response) response = replace(response, '*'=>"") + response = replace(response, "<|eot_id|>"=>"") # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] count = 0 @@ -873,6 +875,8 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh elseif actionname == "CHECKINVENTORY" if rawresponse !== nothing vd = GeneralUtils.dfToVectorDict(rawresponse) + a.memory[:shortmem][:found_wine] = vd # used by decisionMaker() as a short note + if length(a.memory[:shortmem][:available_wine]) != 0 a.memory[:shortmem][:available_wine] = vcat(a.memory[:shortmem][:available_wine], vd) else @@ -1008,6 +1012,7 @@ function generatechat(a::sommelier, thoughtDict) response = replace(response, '*'=>"") response = replace(response, '$' => "USD") response = replace(response, '`' => "") + response = replace(response, "<|eot_id|>"=>"") response = GeneralUtils.remove_french_accents(response) responsedict = GeneralUtils.textToDict(response, ["Chat"], rightmarker=":", symbolkey=true, lowercasekey=true) diff --git a/src/type.jl b/src/type.jl index 6273224..477eea7 100644 --- a/src/type.jl +++ b/src/type.jl @@ -182,6 +182,7 @@ function sommelier( :chatbox=> "", :shortmem=> OrderedDict{Symbol, Any}( :available_wine=> [], + :found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines ), :events=> Vector{Dict{Symbol, Any}}(), :state=> Dict{Symbol, Any}( From b8fc23b41e442736f0f4bae83db90a7d20a14a35 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sat, 25 Jan 2025 14:21:37 +0700 Subject: [PATCH 16/16] update --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index c963881..dcef6d5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" authors = ["narawat lamaiin "] -version = "0.1.2-dev" +version = "0.1.2" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"