From 1fc5dfe820b5a787ae1c61888cddee6f4c22408f Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Fri, 2 May 2025 15:27:29 +0700 Subject: [PATCH 1/9] mark new version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index afb05a8..1f417cd 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" authors = ["narawat lamaiin "] -version = "0.2.0" +version = "0.3.0" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" From a0152a3c29ae731b027739bd4be0b734565ee8f9 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sun, 4 May 2025 20:56:17 +0700 Subject: [PATCH 2/9] update --- src/interface.jl | 283 +++++++++++++++++++++++++-------------------- src/llmfunction.jl | 172 ++++++++++++++------------- src/type.jl | 90 ++++++++++++-- src/util.jl | 68 ++++++----- 4 files changed, 370 insertions(+), 243 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 33f955c..18fc8db 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -125,8 +125,10 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 # """ # end - recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) + recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent; includelatest=true) recentevents = a.memory[:events][recent_ind] + recentEventsDict = createEventsLog(recentevents; eventindex=recent_ind) + timeline = createTimeline(recentevents; eventindex=recent_ind) # recap as caching @@ -160,23 +162,23 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 return responsedict else - header = ["Thought:", "Plan:", "Action_name:", "Action_input:"] - dictkey = ["thought", "plan", "action_name", "action_input"] + header = ["Plan:", "Action_name:", "Action_input:"] + dictkey = ["plan", "action_name", "action_input"] - context = # may b add wine name instead of the hold wine data is better - if length(a.memory[:shortmem][:available_wine]) != 0 - winenames = [] - for (i, wine) in enumerate(a.memory[:shortmem][:available_wine]) - name = "$i) $(wine["wine_name"]) " - push!(winenames, name) - end - availableWineName = join(winenames, ',') - "Available wines you've found in your inventory so far: $availableWineName" - else - "" - end + # context = # may b add wine name instead of the hold wine data is better + # if length(a.memory[:shortmem][:available_wine]) != 0 + # winenames = [] + # for (i, wine) in enumerate(a.memory[:shortmem][:available_wine]) + # name = "$i) $(wine["wine_name"]) " + # push!(winenames, name) + # end + # availableWineName = join(winenames, ',') + # "Available wines you've found in your inventory so far: $availableWineName" + # else + # "" + # end - errornote = "" + errornote = "N/A" response = nothing # placeholder for show when error msg show up #[PENDING] add 1) 3 decisions samples 2) compare and choose the best decision (correct tolls etc) @@ -184,10 +186,10 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 :num_ctx => 32768, :temperature => 0.5, ) + for attempt in 1:maxattempt if attempt > 1 println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - llmkwargs[:temperature] += 0.1 end QandA = generatequestion(a, a.func[:text2textInstructLLM], timeline) @@ -208,8 +210,7 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. At each round of conversation, you will be given the following information: - Your recent events: latest 5 events of the situation - Your Q&A: the question and answer you have asked yourself + Q&A: the question and answer you have asked yourself about the current situation You must follow the following guidelines: - Focus on the latest event @@ -228,75 +229,49 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 For your information: - Your store carries only wine. - Vintage 0 means non-vintage. + - All wine in your inventory has no organic, sustainability and sulfite information. You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input: - 1) Thought: Articulate your current understanding and consider the current situation. 2) Plan: Based on the current situation, state a complete action plan to complete the task. Be specific. 3) Action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the following tool names: - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. - - CHECKINVENTORY allows you to check information about wines you want in your inventory. The input should be a specific search term in verbal English. A good search term should include details such as price range, winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. - Invalid query example: red wine that pair well with spicy food. + - CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input is search criteria. Supported search parameters include: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD." - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is "NA". 4) Action_input: The input to the action you are about to perform. This should be aligned with the plan - You should only respond in format as described below: - Thought: ... Plan: ... Action_name: ... Action_input: ... Let's begin! - - $context - Your recent events: - $timeline - Your Q&A: - $QandA - P.S. $errornote """ + assistantinfo = + """ + + Q&A: $QandA + P.S. $errornote + + """ + unformatPrompt = [ Dict(:name => "system", :text => systemmsg), ] - #BUG found wine is "count 0" invalid return from CHECKINVENTORY() - # check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX - # if length(a.memory[:shortmem][:found_wine]) != 0 - # # check if wine name mentioned in recentevents, only check first wine name is enough - # # because agent will recommend every wines it found each time. - # winenames = [] - # for wine in a.memory[:shortmem][:found_wine] - # push!(winenames, wine["wine_name"]) - # end - - # for winename in winenames - # if !occursin(winename, chathistory) - # println("\nYiem decisionMaker() found wines from DB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # d = Dict( - # :thought=> "The user is looking for a wine tahat matches their intention and budget. I've checked the inventory and found wines that match the customer's criteria. I will present the wines to the customer.", - # :plan=> "1) I'll provide detailed introductions of the wines I just found to the user. 2) I'll explain how the wine could match the user's intention and what its effects might mean for the user's experience. 3) If multiple wines are available, I'll highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. 4) I'll provide my personal recommendation.", - # :action_name=> "PRESENTBOX", - # :action_input=> "I need to present to the user the following wines: $winenames") - # a.memory[:shortmem][:found_wine] = [] # clear because PRESENTBOX command is issued. This is to prevent decisionMaker() keep presenting the same wines - # result = (systemmsg=systemmsg, usermsg=usermsg, unformatPrompt=unformatPrompt, result=d) - # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(Dict(d)) - # return result - # end - # end - # end - - # change qwen format put in model format + unformatPrompt = vcat(unformatPrompt, recentEventsDict) + # put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * assistantinfo + response = a.func[:text2textInstructLLM](prompt; senderId=a.id, llmkwargs=llmkwargs) response = GeneralUtils.remove_french_accents(response) - response = replace(response, "**"=>"") - response = replace(response, "***"=>"") - response = replace(response, "<|eot_id|>"=>"") - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName; ) # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] count = 0 @@ -359,14 +334,14 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 end checkFlag == true ? continue : nothing - # check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or - # "pair well" in it because it is not a valid query. - detected_kw = GeneralUtils.detect_keyword(["pair", "pairs", "pairing", "well"], responsedict[:action_input]) - if responsedict[:action_name] == "CHECKINVENTORY" && sum(values(detected_kw)) != 0 - errornote = "In your previous attempt, action_input for CHECKINVENTORY function is invalid" - println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end + # # check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or + # # "pair well" in it because it is not a valid query. + # detected_kw = GeneralUtils.detect_keyword(["pair", "pairs", "pairing", "well"], responsedict[:action_input]) + # if responsedict[:action_name] == "CHECKINVENTORY" && sum(values(detected_kw)) != 0 + # errornote = "In your previous attempt, action_input for CHECKINVENTORY function was $(responsedict[:action_name]). It was not specific enough." + # println("\nERROR YiemAgent decisionMaker() $errornote => $(responsedict[:action_input]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") pprintln(Dict(responsedict)) @@ -497,7 +472,7 @@ end # "" # end -# errornote = "" +# errornote = "N/A" # response = nothing # placeholder for show when error msg show up # for attempt in 1:10 @@ -830,7 +805,7 @@ function evaluator(state::T1, text2textInstructLLM::Function thoughthistory *= "$k: $v\n" end - errornote = "" + errornote = "N/A" for attempt in 1:10 errorFlag = false @@ -1004,7 +979,7 @@ function conversation(a::sommelier, userinput::Dict; maximumMsg=50) end end -function conversation(a::companion, userinput::Dict; +function conversation(a::Union{companion, virtualcustomer}, userinput::Dict; converPartnerName::Union{String, Nothing}=nothing, maximumMsg=50) @@ -1022,7 +997,7 @@ function conversation(a::companion, userinput::Dict; eventdict(; event_description="the user talks to the assistant.", timestamp=Dates.now(), - subject=a.name, + subject="user", actioninput=userinput[:text], ) ) @@ -1075,7 +1050,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh (result=actioninput, errormsg=nothing, success=true) elseif actionname == "ENDCONVERSATION" x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time." - (result=x, errormsg=nothing, success=true) + (result=actioninput, errormsg=nothing, success=true) else error("undefined LLM function. Requesting $actionname") end @@ -1213,7 +1188,7 @@ function presentbox(a::sommelier, thoughtDict) end chathistory = chatHistoryToText(a.chathistory) - errornote = "" + errornote = "N/A" response = nothing # placeholder for show when error msg show up # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" @@ -1224,7 +1199,6 @@ function presentbox(a::sommelier, thoughtDict) if attempt > 1 # use to prevent LLM generate the same respond over and over println("\nYiemAgent presentbox() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - # llmkwargs[:temperature] += 0.1 else # yourthought1 = yourthought end @@ -1245,9 +1219,10 @@ function presentbox(a::sommelier, thoughtDict) # put in model format prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - #[WORKING] update this function to use new llm + # update this function to use new llm response = a.func[:text2textInstructLLM](prompt; senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) # check whether response has not-allowed words notallowed = ["respond:", "user>", "user:"] detected_kw = GeneralUtils.detect_keyword(notallowed, response) @@ -1397,19 +1372,18 @@ function generatechat(a::sommelier, thoughtDict) errornote = "N/A" response = nothing # placeholder for show when error msg show up - yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" + yourthought = thoughtDict[:plan] yourthought1 = nothing llmkwargs=Dict( :num_ctx => 32768, - :temperature => 0.2, + :temperature => 0.5, ) for attempt in 1:10 if attempt > 1 # use to prevent LLM generate the same respond over and over println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - llmkwargs[:temperature] += 0.1 else yourthought1 = yourthought end @@ -1429,9 +1403,10 @@ function generatechat(a::sommelier, thoughtDict) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs) - response = GeneralUtils.deFormatLLMtext(response, "qwen3") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) # sometime the model response like this "here's how I would respond: ..." if occursin("respond:", response) @@ -1475,8 +1450,8 @@ function generatechat(a::sommelier, thoughtDict) continue end - println("\nYiemAgent generatechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(Dict(responsedict)) + # println("\nYiemAgent generatechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # pprintln(Dict(responsedict)) # check whether an agent recommend wines before checking inventory or recommend wines # outside its inventory @@ -1510,7 +1485,7 @@ function generatechat(a::sommelier, thoughtDict) error("generatechat failed to generate a response") end -# modify it to work with customer object + function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) response = nothing # placeholder for show when error msg show up errornote = "N/A" @@ -1548,6 +1523,7 @@ function generatechat(a::companion; converPartnerName::Union{String, Nothing}=no response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) response = replace(response, "<|im_start|>"=> "") response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) # check whether LLM just repeat the previous dialogue for msg in a.chathistory @@ -1558,33 +1534,91 @@ function generatechat(a::companion; converPartnerName::Union{String, Nothing}=no end end + return response + end + error("generatechat failed to generate a response") +end - #[WORKING] some time it copy exactly the same text as previous conversation partner msg. - +# modify it to work with customer object +function generatechat(a::virtualcustomer; + converPartnerName::Union{String, Nothing}=nothing, maxattempt=10, recentEventNum=10 + ) + recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentEventNum; includelatest=true) + recentevents = a.memory[:events][recent_ind] + recentEventsDict = createEventsLog(recentevents; eventindex=recent_ind) + response = nothing # placeholder for show when error msg show up + errornote = "N/A" + header = ["Dialogue:", "Role"] + dictkey = ["dialogue", "role"] + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + ) + + for attempt in 1:maxattempt + if attempt > 1 + println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + assistantinfo = + """ + + P.S. $errornote + + """ + + unformatPrompt = + [ + Dict(:name => "system", :text => a.systemmsg), + ] + unformatPrompt = vcat(unformatPrompt, recentEventsDict) + # put in model format + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * assistantinfo + + response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) + response = replace(response, "<|im_start|>"=> "") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) + + # check whether LLM just repeat the previous dialogue + for msg in a.chathistory + if msg[:text] == response + errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." + println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + end + + # check whether response has all header + detected_kw = GeneralUtils.detect_keyword(header, response) + kwvalue = [i for i in values(detected_kw)] + zeroind = findall(x -> x == 0, kwvalue) + missingkeys = [header[i] for i in zeroind] + if 0 ∈ values(detected_kw) + errornote = "$missingkeys are missing from your previous response" + println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "Your previous attempt has duplicated points" + println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + if responsedict[:role] == "no" + errornote = "In your previous attempt you said $(responsedict[:dialogue]) which you, as a customer of a wine store, are not supposed to speak." + println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end - # # check whether response has all header - # detected_kw = GeneralUtils.detect_keyword(header, response) - # kwvalue = [i for i in values(detected_kw)] - # zeroind = findall(x -> x == 0, kwvalue) - # missingkeys = [header[i] for i in zeroind] - # if 0 ∈ values(detected_kw) - # errornote = "$missingkeys are missing from your previous response" - # println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # elseif sum(values(detected_kw)) > length(header) - # errornote = "Your previous attempt has duplicated points" - # println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # responsedict = GeneralUtils.textToDict(response, header; - # dictKey=dictkey, symbolkey=true) # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") # println("\n $response") - return response + return responsedict[:dialogue] end error("generatechat failed to generate a response") end @@ -1635,11 +1669,10 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String You should then respond to the user with: 1) Thought: State your thought about the current situation - 2) Q: "Ask yourself" at least five, but no more than ten, questions about the situation from your perspective. + 2) Q: "Ask yourself" at least three, but no more than five, questions about the situation from your perspective. 3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions You must only respond in format as described below: - Thought: ... Q1: ... A1: ... Q2: ... @@ -1671,6 +1704,8 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String A: No. I need more information from the user including ... Q: What else do I need to know? A: ... + Q: Should I present my item to the user? + A: Not yet, I will need to check my inventory first. Q: Should I check our inventory now? A: ... Q: What the user intend to do with the car? @@ -1689,14 +1724,14 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String A: ... Q: what kind of car suitable for off-road trip? A: A four-wheel drive SUV is a good choice for off-road trips. - - + Q: What car specification would satisfy the user's needs? + A: The user is seeking an eco-friendly vehicle that accommodates seven passengers, including seniors and children, with prioritized accessibility and efficient refueling. While electric vehicles (EVs) offer eco-friendly benefits, their long charging times make hybrid models more practical for fast refueling. Additionally, a lower ground level is essential for ease of entry/exit for seniors and children. A hybrid multi-purpose vehicle (MPV) emerges as the optimal solution, balancing sustainability, seating capacity, accessibility, and refueling efficiency. Let's begin! """ - header = ["Thought:", "Q1:"] - dictkey = ["thought", "q1"] + header = ["Q1:"] + dictkey = ["q1"] context = if length(a.memory[:shortmem][:available_wine]) != 0 @@ -1708,7 +1743,7 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) # recentevents = a.memory[:events][recent_ind] # timeline = createTimeline(recentevents; eventindex=recent_ind) - errornote = "" + errornote = "N/A" response = nothing # store for show when error msg show up # recap = @@ -1736,7 +1771,6 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String for attempt in 1:10 if attempt > 1 println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - llmkwargs[:temperature] += 0.1 end usermsg = @@ -1753,11 +1787,13 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) response = text2textInstructLLM(prompt; modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, "qwen3") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) + # make sure generatequestion() don't have wine name that is not from retailer inventory # check whether an agent recommend wines before checking inventory or recommend wines # outside its inventory @@ -1816,7 +1852,7 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String dictKey=dictkey, symbolkey=true) response = "Q1: " * responsedict[:q1] println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(response) + try pprintln(response) catch e println(response) end return response end @@ -1865,7 +1901,7 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: events = a.memory[:events][ind] timeline = createTimeline(events) - errornote = "" + errornote = "N/A" response = nothing # store for show when error msg show up for attempt in 1:10 if attempt > 1 # use to prevent LLM generate the same respond over and over @@ -1909,7 +1945,7 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: dictKey=dictkey, symbolkey=true) println("\ngenerateSituationReport() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(response) + try pprintln(response) catch e println(response) end return responsedict end @@ -1957,12 +1993,13 @@ function detectWineryName(a, text) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) response = a.func[:text2textInstructLLM](prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, "qwen3") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(response) + try pprintln(response) catch e println(response) end # check whether response has all header detected_kw = GeneralUtils.detect_keyword(header, response) diff --git a/src/llmfunction.jl b/src/llmfunction.jl index aac3629..d58e08c 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -342,26 +342,24 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 User's query: ... You must follow the following guidelines: - - 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. + - If specific information required in the preference form is not available in the query or there isn't any, mark with "N/A" to indicate this. Additionally, words like 'any' or 'unlimited' mean no information is available. - Do not generate other comments. You should then respond to the user with: - Thought: 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 (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" + Country: a country where 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_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 range of wine. + Tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc + Wine_price_range: price range of wine. Example: For price 10-20, price range will be "10 to 20". For price 100, price range will be 0 to 100. 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 format as described below: - Thought: ... Wine_name: ... Winery: ... Vintage: ... @@ -370,41 +368,41 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 Wine_type: Grape_varietal: ... Tasting_notes: ... - Wine_price: ... + Wine_price_range: ... Occasion: ... Food_to_be_paired_with_wine: ... 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, 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: red, Chenin Blanc, Riesling, 20 USD from Tuscany, Italy or Napa Valley, USA + Wine_name: N/A. Winery: N/A. Vintage: N/A. Region: Tuscany, Napa Valley. Country: Italy, United States. Wine_type: red, white. Grape_varietal: Chenin Blanc, Riesling. Tasting_notes: citrus. Wine_price_range: 0 to 20. Occasion: N/A. Food_to_be_paired_with_wine: N/A + User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot - {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Merlot", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} - + Winery: Domaine du Collier. Wine_name: Saumur Blanc. Vintage: 2019. Region: Saumur. Country: France. Wine_type: white. Grape_varietal: Merlot. Tasting_notes: plum. Wine_price_range: N/A. Occasion: N/A. Food_to_be_paired_with_wine: N/A. + Let's begin! """ - header = ["Thought:", "Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price:", "Occasion:", "Food_to_be_paired_with_wine:"] - dictkey = ["thought", "wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] + header = ["Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price_range:", "Occasion:", "Food_to_be_paired_with_wine:"] + dictkey = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_range", "occasion", "food_to_be_paired_with_wine"] errornote = "N/A" llmkwargs=Dict( :num_ctx => 32768, - :temperature => 0.5, + :temperature => 0.2, ) for attempt in 1:maxattempt - #[PENDING] I should add generatequestion() - - if attempt > 1 - println("\nYiemAgent extractWineAttributes_1() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - usermsg = - """ - User's query: $input - P.S. $errornote - """ + """ + $input + """ + assistantinfo = + """ + + P.S. $errornote + + """ _prompt = [ @@ -413,23 +411,30 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + # add info + prompt = prompt * assistantinfo + response = a.func[:text2textInstructLLM](prompt; modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) response = GeneralUtils.remove_french_accents(response) - response = GeneralUtils.deFormatLLMtext(response, "granite3") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) - # check wheter all attributes are in the response - checkFlag = false - for word in header - if !occursin(word, response) - errornote = "In your previous attempts, the $word attribute is missing. Please try again." - println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - checkFlag = true - break - end + # check whether response has all header + detected_kw = GeneralUtils.detect_keyword(header, response) + kwvalue = [i for i in values(detected_kw)] + zeroind = findall(x -> x == 0, kwvalue) + missingkeys = [header[i] for i in zeroind] + if 0 ∈ values(detected_kw) + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "Your previous attempt has duplicated points" + println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue end - checkFlag == true ? continue : nothing # check whether response has all answer's key points detected_kw = GeneralUtils.detect_keyword(header, response) @@ -440,6 +445,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 elseif sum(values(detected_kw)) > length(header) errornote = "In your previous attempts, the response has duplicated answer's key points" println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println(response) continue end responsedict = GeneralUtils.textToDict(response, header; @@ -460,29 +466,29 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 if j ∉ [:thought, :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" + if responsedict[:wine_price] != "N/A" # check whether wine_price is in ranged number - if !occursin('-', responsedict[:wine_price]) - errornote = "In your previous attempt, the 'wine_price' was not set to a ranged number. Please adjust it accordingly." + if !occursin("to", responsedict[:wine_price]) + errornote = "In your previous attempt, the 'wine_price' was set to $(responsedict[:wine_price]) which is not a correct format. Please adjust it accordingly." println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") checkFlag = true break end - # check whether max wine_price is in the input - pricerange = split(responsedict[:wine_price], '-') - minprice = pricerange[1] - maxprice = pricerange[end] - if !occursin(maxprice, input) - responsedict[:wine_price] = "NA" - end - # price range like 100-100 is not good - if minprice == maxprice - errornote = "In your previous attempt, you inputted 'wine_price' with a 'minimum' value equaling the 'maximum', which is not valid." - println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - checkFlag = true - break - end + # # check whether max wine_price is in the input + # pricerange = split(responsedict[:wine_price], '-') + # minprice = pricerange[1] + # maxprice = pricerange[end] + # if !occursin(maxprice, input) + # responsedict[:wine_price] = "N/A" + # end + # # price range like 100-100 is not good + # if minprice == maxprice + # errornote = "In your previous attempt, you inputted 'wine_price' with a 'minimum' value equaling the 'maximum', which is not valid." + # println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # checkFlag = true + # break + # end end else content = responsedict[j] @@ -517,7 +523,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 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) + if !occursin("N/A", v) && v != "" && !occursin("none", v) && !occursin("None", v) result *= "$k: $v, " end end @@ -580,7 +586,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< sweetness, acidity, tannin, intensity 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. + 1) If specific information required in the preference form is not available in the query or there isn't any, mark with 'N/A' 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. @@ -605,8 +611,8 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< Here are some examples: User's query: I want a wine with a medium-bodied, low acidity, medium tannin. - Sweetness_keyword: NA - Sweetness: NA + Sweetness_keyword: N/A + Sweetness: N/A Acidity_keyword: low acidity Acidity: 1-2 Tannin_keyword: medium tannin @@ -615,20 +621,20 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< 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 + Sweetness_keyword: N/A + Sweetness: N/A + Acidity_keyword: N/A + Acidity: N/A + Tannin_keyword: N/A + Tannin: N/A + Intensity_keyword: N/A + Intensity: N/A Let's begin! """ header = ["Sweetness_keyword:", "Sweetness:", "Acidity_keyword:", "Acidity:", "Tannin_keyword:", "Tannin:", "Intensity_keyword:", "Intensity:"] dictkey = ["sweetness_keyword", "sweetness", "acidity_keyword", "acidity", "tannin_keyword", "tannin", "intensity_keyword", "intensity"] - errornote = "" + errornote = "N/A" for attempt in 1:10 usermsg = @@ -645,10 +651,11 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) response = a.func[:text2textInstructLLM](prompt) - response = GeneralUtils.deFormatLLMtext(response, "granite3") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) # check whether response has all answer's key points detected_kw = GeneralUtils.detect_keyword(header, response) @@ -669,23 +676,23 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< for i in ["sweetness", "acidity", "tannin", "intensity"] keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword value = responsedict[keyword] - if value != "NA" && !occursin(value, input) + if value != "N/A" && !occursin(value, input) errornote = "In your previous attempt, keyword $keyword: $value does not appear in the input. You must use information from the input only" println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - # if value == "NA" then responsedict[i] = "NA" - # e.g. if sweetness_keyword == "NA" then sweetness = "NA" - if value == "NA" - responsedict[Symbol(i)] = "NA" + # if value == "N/A" then responsedict[i] = "N/A" + # e.g. if sweetness_keyword == "N/A" then sweetness = "N/A" + if value == "N/A" + responsedict[Symbol(i)] = "N/A" end end # some time LLM not put integer range for (k, v) in responsedict if !occursin("keyword", string(k)) - if v !== "NA" && (!occursin('-', v) || length(v) > 5) + if v !== "N/A" && (!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("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue @@ -693,10 +700,10 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< end end - # some time LLM says NA-2. Need to convert NA to 1 + # some time LLM says N/A-2. Need to convert N/A to 1 for (k, v) in responsedict - if occursin("NA", v) && occursin("-", v) - new_v = replace(v, "NA"=>"1") + if occursin("N/A", v) && occursin("-", v) + new_v = replace(v, "N/A"=>"1") responsedict[k] = new_v end end @@ -704,7 +711,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< result = "" for (k, v) in responsedict # some time LLM generate text with "(some comment)". this line removes it - if !occursin("NA", v) + if !occursin("N/A", v) result *= "$k: $v, " end end @@ -756,7 +763,7 @@ function paraphrase(text2textInstructLLM::Function, text::String) header = ["Paraphrase:"] dictkey = ["paraphrase"] - errornote = "" + errornote = "N/A" response = nothing # placeholder for show when error msg show up @@ -773,11 +780,12 @@ function paraphrase(text2textInstructLLM::Function, text::String) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) try response = text2textInstructLLM(prompt) - response = GeneralUtils.deFormatLLMtext(response, "granite3") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) # 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" diff --git a/src/type.jl b/src/type.jl index 31f6156..871cec5 100644 --- a/src/type.jl +++ b/src/type.jl @@ -1,6 +1,6 @@ module type -export agent, sommelier, companion +export agent, sommelier, companion, virtualcustomer using Dates, UUIDs, DataStructures, JSON3 using GeneralUtils @@ -24,17 +24,13 @@ end function companion( func::NamedTuple # NamedTuple of functions ; - systemmsg::Union{String, Nothing}= nothing, name::String= "Assistant", id::String= GeneralUtils.uuid4snakecase(), maxHistoryMsg::Integer= 20, chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), - llmFormatName::String= "granite3" - ) - - if systemmsg === nothing - systemmsg = - """ + llmFormatName::String= "granite3", + systemmsg::String= + """ Your name: $name Your sex: Female Your role: You are a helpful assistant. @@ -43,8 +39,8 @@ function companion( - Your like to be short and concise. Let's begin! - """ - end + """, + ) tools = Dict( # update input format "CHATBOX"=> Dict( @@ -222,6 +218,80 @@ function sommelier( end +mutable struct virtualcustomer <: agent + name::String # agent name + id::String # agent id + systemmsg::String # system message + tools::Dict + maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized + chathistory::Vector{Dict{Symbol, Any}} + memory::Dict{Symbol, Any} + func # NamedTuple of functions + llmFormatName::String +end + +function virtualcustomer( + func, # NamedTuple of functions + ; + name::String= "Assistant", + id::String= string(uuid4()), + maxHistoryMsg::Integer= 20, + chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + llmFormatName::String= "granite3", + systemmsg::String= + """ + Your name: $name + Your sex: Female + Your role: You are a helpful assistant. + You should follow the following guidelines: + - Focus on the latest conversation. + - Your like to be short and concise. + + Let's begin! + """, + ) + + tools = Dict( # update input format + "chatbox"=> Dict( + :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", + :input => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", + :output => "" , + ), + ) + + """ Memory + Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 + NO "system" message in chathistory because I want to add it at the inference time + chathistory= [ + Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()), + Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()), + ] + """ + memory = Dict{Symbol, Any}( + :shortmem=> OrderedDict{Symbol, Any}( + ), + :events=> Vector{Dict{Symbol, Any}}(), + :state=> Dict{Symbol, Any}( + ), + :recap=> OrderedDict{Symbol, Any}(), + ) + + newAgent = virtualcustomer( + name, + id, + systemmsg, + tools, + maxHistoryMsg, + chathistory, + memory, + func, + llmFormatName + ) + + return newAgent +end + + diff --git a/src/util.jl b/src/util.jl index dd13f97..488ec4a 100644 --- a/src/util.jl +++ b/src/util.jl @@ -1,7 +1,7 @@ module util export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, - availableWineToText + availableWineToText, createEventsLog using UUIDs, Dates, DataStructures, HTTP, JSON3 using GeneralUtils @@ -313,35 +313,47 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin end -# function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing -# ) where {T1<:AbstractVector} -# # Initialize empty timeline string -# timeline = "" - -# # Determine which indices to use - either provided range or full length -# ind = -# if eventindex !== nothing -# [eventindex...] -# else -# 1:length(events) -# end +function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothing + ) where {T1<:AbstractVector} + # Initialize empty log array + log = Dict{Symbol, String}[] + + # Determine which indices to use - either provided range or full length + ind = + if eventindex !== nothing + [eventindex...] + else + 1:length(events) + end + + # Iterate through events and format each one + for (i, event) in zip(ind, events) + # If no outcome exists, format without outcome + if event[:outcome] === nothing + subject = event[:subject] + actioninput = event[:actioninput] + d = Dict{Symbol, String}(:name=>subject, :text=>actioninput) + push!(log, d) + else + subject = event[:subject] + actioninput = event[:actioninput] + outcome = event[:outcome] + str = "$subject: $actioninput $outcome" + d = Dict{Symbol, String}(:name=>subject, :text=>str) + push!(log, d) + end + end + + return log +end + + + + + + -# # Iterate through events and format each one -# for (i, event) in zip(ind, events) -# # If no outcome exists, format without outcome -# subject = titlecase(event[:subject]) -# if event[:outcome] === nothing - -# timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput])\n" -# # If outcome exists, include it in formatting -# else -# timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput]) Action output: $(event[:outcome])\n" -# end -# end -# # Return formatted timeline string -# return timeline -# end From d0c26e52e8f6a17ccaa1cb1f90e5237044680a33 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Wed, 14 May 2025 21:21:35 +0700 Subject: [PATCH 3/9] update --- src/interface.jl | 888 ++++++++++++++++++++++++++------------------- src/llmfunction.jl | 191 ++++++---- src/type.jl | 3 +- src/util.jl | 42 ++- 4 files changed, 659 insertions(+), 465 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 18fc8db..b404e08 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -125,9 +125,11 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 # """ # end - recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent; includelatest=true) + recent_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recent; includelatest=true) recentevents = a.memory[:events][recent_ind] - recentEventsDict = createEventsLog(recentevents; eventindex=recent_ind) + recentchat = createChatLog(a.chathistory[recent_ind]; index=recent_ind) + # recentEventsDict = createEventsLog(recentevents; index=recent_ind) + timeline = createTimeline(recentevents; eventindex=recent_ind) @@ -161,10 +163,6 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 responsedict = similarDecision return responsedict else - - header = ["Plan:", "Action_name:", "Action_input:"] - dictkey = ["plan", "action_name", "action_input"] - # context = # may b add wine name instead of the hold wine data is better # if length(a.memory[:shortmem][:available_wine]) != 0 # winenames = [] @@ -178,83 +176,85 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 # "" # end + systemmsg = + """ + Your name is $(a.name). You are a sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision. + Your goal includes: + 1) Establish a connection with the customer by greeting them warmly + 2) Guide them to select the best wines only from your store's inventory that align with their preferences. + + Your responsibility includes: + 1) According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the goal + 2) Value your mentor's suggestions. + + Your responsibility does NOT includes: + 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. + 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. + 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + + + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock + - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. + - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Spicy foods should not be paired with medium and full-bodied red wines. + - We do not sell organic, sustainability and sulfite wine. + + + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + + At each round of conversation, you will be given the following information: + Database search result: the result of a database search using SQL commands you have found so far + + You should then respond to the user with interleaving Plan, Action_name, Action_input: + 1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific. + 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available tool name + 3) action_input: The input to the action you are about to perform according to your plan. + + You should only respond in JSON format as described below: + { + "plan": "...", + "action_name": "...", + "action_input": "..." + } + + Let's begin! + """ + requiredKeys = [:plan, :action_name, :action_input] + database_search_result = + if length(a.memory[:shortmem][:db_search_result]) != 0 + availableWineToText(a.memory[:shortmem][:db_search_result]) + else + "N/A" + end + errornote = "N/A" response = nothing # placeholder for show when error msg show up - #[PENDING] add 1) 3 decisions samples 2) compare and choose the best decision (correct tolls etc) - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) - for attempt in 1:maxattempt if attempt > 1 println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - QandA = generatequestion(a, a.func[:text2textInstructLLM], timeline) - systemmsg = + # QandA = generatequestion(a, a.func[:text2textInstructLLM], timeline) + + context = """ - Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. - Your goal includes: - 1) Establish a connection with the customer by greeting them warmly - 2) Guide them to select the best wines only from your store's inventory that align with their preferences - - Your responsibility includes: - 1) Make an informed decision about what you need to do to achieve the goal - 2) Thanks the user when they don't need any further assistance and invite them to comeback next time - - Your responsibility does NOT includes: - 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - At each round of conversation, you will be given the following information: - Q&A: the question and answer you have asked yourself about the current situation - - You must follow the following guidelines: - - Focus on the latest event - - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - All wines in your inventory are always in stock - - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. This will help you guide the conversation naturally while gathering essential insights. Once you have this information, you can efficiently check your inventory for the best match. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - Spicy foods should not be paired with medium and full-bodied red wines. - - You should follow the following guidelines: - - When searching an inventory, search as broadly as possible based on the information you have gathered so far. - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - For your information: - - Your store carries only wine. - - Vintage 0 means non-vintage. - - All wine in your inventory has no organic, sustainability and sulfite information. - - You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input: - 2) Plan: Based on the current situation, state a complete action plan to complete the task. Be specific. - 3) Action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the following tool names: - - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. - - CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input is search criteria. Supported search parameters include: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD." - - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. - - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is "NA". - 4) Action_input: The input to the action you are about to perform. This should be aligned with the plan - - You should only respond in format as described below: - Plan: ... - Action_name: ... - Action_input: ... - - Let's begin! - """ - - assistantinfo = - """ - - Q&A: $QandA - P.S. $errornote - + + + - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. + - CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD." + - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. + - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is "NA". + + Remark: $errornote + Database search result: $database_search_result + """ unformatPrompt = @@ -262,47 +262,73 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 Dict(:name => "system", :text => systemmsg), ] - unformatPrompt = vcat(unformatPrompt, recentEventsDict) + unformatPrompt = vcat(unformatPrompt, recentchat) # put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) # add info - prompt = prompt * assistantinfo + prompt = prompt * context - response = a.func[:text2textInstructLLM](prompt; senderId=a.id, llmkwargs=llmkwargs) + response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) response = GeneralUtils.remove_french_accents(response) + # response = replace(response, '$'=>"USD") think, response = GeneralUtils.extractthink(response) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName; ) - - # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] - count = 0 - for i ∈ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] - if occursin(i, response) - count += 1 - end - end - if count > 1 - errornote = "You must use only one function" - println("\nERROR YiemAgent decisionMaker() $errornote\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + + responsedict = nothing + try + responsedict = copy(JSON3.read(response)) + catch + println("\nERROR YiemAgent decisionMaker() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) + # check whether all answer's key points are in responsedict + _responsedictKey = keys(responsedict) + responsedictKey = [i for i in _responsedictKey] # convert into a list + is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + + if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) + errornote = "Your previous attempt has more key points than answer's required key points." + println("\nERROR YiemAgent decisionMaker() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif !all(is_requiredKeys_in_responsedictKey) + zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) + missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "Your previous attempt has duplicated points" - println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) + # # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] + # count = 0 + # for i ∈ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] + # if occursin(i, response) + # count += 1 + # end + # end + # if count > 1 + # errornote = "You must use only one function" + # println("\nERROR YiemAgent decisionMaker() $errornote\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end + + # # check whether response has all header + # detected_kw = GeneralUtils.detect_keyword(header, response) + # kwvalue = [i for i in values(detected_kw)] + # zeroind = findall(x -> x == 0, kwvalue) + # missingkeys = [header[i] for i in zeroind] + # if 0 ∈ values(detected_kw) + # errornote = "$missingkeys are missing from your previous response" + # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # elseif sum(values(detected_kw)) > length(header) + # errornote = "Your previous attempt has duplicated points" + # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end + + # responsedict = GeneralUtils.textToDict(response, header; + # dictKey=dictkey, symbolkey=true) if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] errornote = "Your previous attempt didn't use the given functions" @@ -310,29 +336,29 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 continue end - checkFlag = false - for i ∈ Symbol.(dictkey) - if length(responsedict[i]) == 0 - errornote = "$i is empty" - println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - checkFlag = true - break - end - end - checkFlag == true ? continue : nothing + # checkFlag = false + # for i ∈ Symbol.(dictkey) + # if length(responsedict[i]) == 0 + # errornote = "$i is empty" + # println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # checkFlag = true + # break + # end + # end + # checkFlag == true ? continue : nothing - # check if there are more than 1 key per categories - checkFlag = false - for i ∈ Symbol.(dictkey) - matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) - if length(matchkeys) > 1 - errornote = "Your previous attempt has more than one key per categories" - println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - checkFlag = true - break - end - end - checkFlag == true ? continue : nothing + # # check if there are more than 1 key per categories + # checkFlag = false + # for i ∈ Symbol.(dictkey) + # matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) + # if length(matchkeys) > 1 + # errornote = "Your previous attempt has more than one key per categories" + # println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # checkFlag = true + # break + # end + # end + # checkFlag == true ? continue : nothing # # check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or # # "pair well" in it because it is not a valid query. @@ -375,7 +401,7 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 delete!(responsedict, :mentioned_winery) responsedict[:systemmsg] = systemmsg responsedict[:unformatPrompt] = unformatPrompt - responsedict[:QandA] = QandA + # responsedict[:QandA] = QandA # check whether responsedict[:action_input] is the same as previous dialogue if responsedict[:action_input] == a.chathistory[end][:text] @@ -384,8 +410,47 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 continue end - # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # println("\n$response") + println("\nYiemAgent decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\n$response") + + #[WORKING] + evaluationdict = evaluator(a, timeline, responsedict, context) + if evaluationdict[:good_decision] == "no" + mentor_comment = evaluationdict[:suggestion] + errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" + println("\nERROR YiemAgent decisionMaker() - $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # store for later training + responsedict[:system] = systemmsg + responsedict[:recentchat] = recentchat + responsedict[:prompt] = prompt + responsedict[:context] = context + responsedict[:think] = think + # responsedict[:QandA] = QandA + + # # save to filename ./log/decisionlog.txt + # println("\nsaving YiemAgent decisionMaker() to disk ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # filename = "agent_decision_log_$(a.id).json" + # filepath = "/appfolder/app/log/$filename" + # # check whether there is a file path exists before writing to it + # if !isfile(filepath) + # decisionlist = [responsedict] + # println("Creating file $filepath") + # open(filepath, "w") do io + # JSON3.pretty(io, decisionlist) + # end + # else + # # read the file and append new data + # decisionlist = copy(JSON3.read(filepath)) + # push!(decisionlist, responsedict) + # println("Appending new data to file $filepath") + # open(filepath, "w") do io + # JSON3.pretty(io, decisionlist) + # end + # end + # println("\nYiemAgent decisionMaker() saved to disk is done. agent $(a.id)") responsedict[:prompt] = prompt return responsedict @@ -735,153 +800,169 @@ julia> # Signature """ -function evaluator(state::T1, text2textInstructLLM::Function - ) where {T1<:AbstractDict} +function evaluator(a::T1, timeline, decisiondict, evaluateecontext + ) where {T1<:agent} systemmsg = """ - You are a helpful assistant that analyzes agent's trajectory to find solutions and observations (i.e., the results of actions) to answer the user's questions. - - Definitions: - "question" is the user's question - "understanding" is agent's understanding about the current situation - "reasoning" is agent's step-by-step reasoning about the current situation - "plan" is agent's plan to complete the task from the current situation - "action_name" is the name of the action taken, which can be one of the following functions: - - RUNSQL, which you can use to execute SQL against the database. Action_input for this function must be a single SQL query to be executed against the database. - For more effective text search, it's necessary to use case-insensitivity and the ILIKE operator. - Do not wrap the SQL as it will be executed against the database directly and SQL must be ended with ';'. - "action_input" is the input to the action - "observation" is result of the preceding immediate action - - - Trajectory: ... - Error_note: error note from your previous attempt - - - - - When the search returns no result, validate whether the SQL query makes sense before accepting it as a valid answer. - - + + - You are a master sommelier of an online wine store. + + + - Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding. + + + - To improve a trainee sommelier decision based on the store policy and guidelines. + + + - trajectory: A conversation between your trainee and the customer that have occurred up until now + - evaluatee_context: The context that evaluatee use to make a decision + - evaluatee_decision: The decision made by the evaluatee, consists of the following elements: + "plan" is the trainee's plan + "action_name" is the name of the action taken, which can be one of the available tool name. + "action_input" is the input to the action. + + + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock + - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. + - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Spicy foods should not be paired with medium and full-bodied red wines. + - We do not sell organic, sustainability and sulfite wine. + + + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + + + - Use only infomation provided by the store policy and guidelines as a bedrocks for your response. + + + - N/A + - 1) Trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. - - Evaluate the correctness of each section and the overall trajectory based on the given question. - - Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation. - - Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached. - - Do not generate additional thoughts or actions. - 2) Answer_evaluation: - - Focus only on the matter mentioned in the question and comprehensively analyze how the latest observation's details addresses the question - 3) Accepted_as_answer: Decide whether the latest observation's content answers the question. Can be "Yes" or "No" - Bad example (The observation didn't answers the question): - question: Find cars with 4 wheels. - observation: There are an apple in the table. - Good example (The observation answers the question): - question: Find cars with a stereo. - observation: There are 1 cars in the table. 1) brand: Toyota, model: yaris, color: black. - 4) Score: Correctness score s where s is a single integer between 0 to 9. - For example: - - 0 indicates that both the trajectory is incorrect, failed or errors and the observation is incorrect or failed - - 4 indicates that the trajectory are correct but the observation is incorrect or failed - - 5 indicates that the trajectory are correct, but no results are returned. - - 6 indicates that the trajectory are correct, but the observation's content doesn't directly answer the question - - 8 indicates that both the trajectory are correct, and the observation's content directly answers the question. - - 9 indicates a perfect perfomance. Both the trajectory are correct, and the observation's content directly answers the question, surpassing your expectations. - 5) Suggestion: if accepted_as_answer is "No", provide suggestion. - + 1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. + - Evaluate the correctness of each section and the overall trajectory based on the given question. + - Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation. + - Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached. + - Do not generate additional thoughts or actions. + 2) decision_evaluation: + - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. + 3) good_decision: Decide whether the decision make sense under the circumstances. Can be "yes" or "no" + 4) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. - - Trajectory_evaluation: ... - Answer_evaluation: ... - Accepted_as_answer: ... - Score: ... - Suggestion: ... + + + { + "trajectory_evaluation": "..." + "decision_evaluation": "..." + "good_decision": "..." + "suggestion": "..." + } Let's begin! """ - - thoughthistory = "" - for (k, v) in state[:thoughtHistory] - thoughthistory *= "$k: $v\n" - end - + requiredKeys = [:trajectory_evaluation, :decision_evaluation, :good_decision, :suggestion] errornote = "N/A" for attempt in 1:10 - errorFlag = false + evaluateecontext = replace(evaluateecontext, "" => "") + evaluateecontext = replace(evaluateecontext, "" => "") - usermsg = + context = """ - Trajectory: $thoughthistory - Error_note: $errornote + + + $timeline + + + $evaluateecontext + + + {plan: $(decisiondict[:plan]), action_name: $(decisiondict[:action_name]), action_input: $(decisiondict[:action_input])} + + P.S. $errornote + """ - _prompt = - [ - Dict(:name=> "system", :text=> systemmsg), - Dict(:name=> "user", :text=> usermsg) - ] + unformatPrompt = + [ + Dict(:name => "system", :text => systemmsg), + ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context + println("") + println(prompt) - header = ["Trajectory_evaluation:", "Answer_evaluation:", "Accepted_as_answer:", "Score:", "Suggestion:"] - dictkey = ["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"] - - response = text2textInstructLLM(prompt; modelsize="medium", senderId=a.id) - - # sometime LLM output something like **Comprehension**: which is not expected - response = replace(response, "**"=>"") - response = replace(response, "***"=>"") - response = GeneralUtils.deFormatLLMtext(response, "qwen3") - - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - if 0 ∈ values(detected_kw) - errornote = "\nSQL evaluator() response does not have all header" - println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nSQL evaluator() response has duplicated header" - println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) + response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + response = GeneralUtils.remove_french_accents(response) + # response = replace(response, '$'=>"USD") + think, response = GeneralUtils.extractthink(response) - responsedict[:score] = responsedict[:score][1] # some time "6\nThe trajectories are incomplete" is generated but I only need the number. + responsedict = nothing try - responsedict[:score] = parse(Int, responsedict[:score]) # convert string "5" into integer 5 + responsedict = copy(JSON3.read(response)) catch + println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - accepted_as_answer::AbstractString = responsedict[:accepted_as_answer] + # check whether all answer's key points are in responsedict + _responsedictKey = keys(responsedict) + responsedictKey = [i for i in _responsedictKey] # convert into a list + is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - if accepted_as_answer ∉ ["Yes", "No"] # [PENDING] add errornote into the prompt - error("generated accepted_as_answer has wrong format") + if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) + errornote = "Your previous attempt has more key points than answer's required key points." + println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif !all(is_requiredKeys_in_responsedictKey) + zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) + missingkeys = [requiredKeys[i] for i in zeroind] + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue end - # add to state here instead to in transition() because the latter causes julia extension crash (a bug in julia extension) - state[:evaluation] = "$(responsedict[:trajectory_evaluation]) $(responsedict[:answer_evaluation])" - state[:evaluationscore] = responsedict[:score] - state[:accepted_as_answer] = responsedict[:accepted_as_answer] - state[:suggestion] = responsedict[:suggestion] + # if accepted_as_answer ∉ ["yes", "no"] # [PENDING] add errornote into the prompt + # error("generated accepted_as_answer has wrong format") + # end - # mark as terminal state when the answer is achieved - if accepted_as_answer == "Yes" - - # mark the state as terminal state because the evaluation say so. - state[:isterminal] = true - - # evaluation score as reward because different answers hold different value for the user. - state[:reward] = responsedict[:score] - end - println("\nERROR Evaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") pprintln(Dict(responsedict)) - return responsedict[:score] + # # read sessionId + # sessionid = a.id + # # save to filename ./log/decisionlog.txt + # println("saving SQLLLM evaluator() to disk") + # filename = "agent_evaluator_log_$(sessionid[:id]).json" + # filepath = "/appfolder/app/log/$filename" + # # check whether there is a file path exists before writing to it + # if !isfile(filepath) + # decisionlist = [responsedict] + # println("Creating file $filepath") + # open(filepath, "w") do io + # JSON3.pretty(io, decisionlist) + # end + # else + # # read the file and append new data + # decisionlist = copy(JSON3.read(filepath)) + # push!(decisionlist, responsedict) + # println("Appending new data to file $filepath") + # open(filepath, "w") do io + # JSON3.pretty(io, decisionlist) + # end + # end + + return responsedict end error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") end @@ -892,7 +973,7 @@ end # Arguments `a::agent` an agent - + # Return None @@ -1119,16 +1200,14 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh elseif actionname == "CHECKINVENTORY" if rawresponse !== nothing - vd = GeneralUtils.dfToVectorDict(rawresponse) + vd = GeneralUtils.dfToVectorDict(rawresponse) # comes in dataframe format a.memory[:shortmem][:found_wine] = vd # used by decisionMaker() as a short note - #[PENDING] a.memory[:shortmem][:available_wine] should be OrderedDict instead of vector if dict so that no duplicate item are listed? - if length(a.memory[:shortmem][:available_wine]) != 0 - a.memory[:shortmem][:available_wine] = vcat(a.memory[:shortmem][:available_wine], vd) + #[PENDING] a.memory[:shortmem][:available_wine] should be OrderedDict instead of vector dict so that no duplicate item are listed? + if length(a.memory[:shortmem][:db_search_result]) != 0 + a.memory[:shortmem][:db_search_result] = vcat(a.memory[:shortmem][:db_search_result], vd) else - a.memory[:shortmem][:available_wine] = vd + a.memory[:shortmem][:db_search_result] = vd end - else - println("checkinventory return nothing") end push!(a.memory[:events], @@ -1142,6 +1221,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh outcome= "This is what I found:, $result" ) ) + else error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end @@ -1150,7 +1230,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh end -function presentbox(a::sommelier, thoughtDict) +function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) systemmsg = """ Your profile: @@ -1160,102 +1240,123 @@ function presentbox(a::sommelier, thoughtDict) Your mission: Present the wines to the customer in a way that keep the conversation smooth and engaging. At each round of conversation, you will be given the following information: - Additional info: additional information + Database search result: the result of a database search using SQL commands you have found so far Chat history: your ongoing conversation with the user - Wine name: name if wines you found. + Wine name: name of wines you are going to introduce. You should follow the following guidelines: - Provide detailed introductions of the wines you've found to the user. - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. - Provide your personal recommendation and provide a brief explanation of why you recommend it. You should then respond to the user with: - Dialogue: Your presentation to the user + dialogue: Your presentation to the user You should only respond in format as described below: - Dialogue: ... + { + "dialogue": "..." + } Let's begin! """ - - header = ["Dialogue:"] - dictkey = ["dialogue"] - - # a.memory[:shortmem][:available_wine] is a vector of dictionary - 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 + requiredKeys = [:dialogue] + database_search_result = + if length(a.memory[:shortmem][:db_search_result]) != 0 + availableWineToText(a.memory[:shortmem][:db_search_result]) + else + "N/A" + end chathistory = chatHistoryToText(a.chathistory) errornote = "N/A" response = nothing # placeholder for show when error msg show up + + # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" # yourthought1 = nothing - for attempt in 1:10 + for attempt in 1:maxtattempt - if attempt > 1 # use to prevent LLM generate the same respond over and over - println("\nYiemAgent presentbox() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - else - # yourthought1 = yourthought - end - - usermsg = + context = """ - $errornote - Additional info: $context + + Database search result: $database_search_result Chat history: $chathistory Wine name: $(thoughtDict[:action_input]) + P.S. $errornote + """ - _prompt = + unformatPrompt = [ Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - # update this function to use new llm + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context + response = a.func[:text2textInstructLLM](prompt; senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - # check whether response has not-allowed words - notallowed = ["respond:", "user>", "user:"] - detected_kw = GeneralUtils.detect_keyword(notallowed, response) - # list all keys that have 1 value in detected_kw dictionary - k = [key for (key, value) in detected_kw if value == 1] - if length(k) > 0 - errornote = "In your previous attempt, you have $k in your response which is not allowed." - println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - response = GeneralUtils.remove_french_accents(response) + # response = replace(response, '$'=>"USD") + think, response = GeneralUtils.extractthink(response) + response = replace(response, '*'=>"") response = replace(response, '$' => "USD") response = replace(response, '`' => "") response = replace(response, "<|eot_id|>"=>"") - response = GeneralUtils.remove_french_accents(response) - response = GeneralUtils.deFormatLLMtext(response, "qwen3") - - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - if 0 ∈ values(detected_kw) - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + + responsedict = nothing + try + responsedict = copy(JSON3.read(response)) + catch + println("\nERROR YiemAgent presentbox() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) + # check whether all answer's key points are in responsedict + _responsedictKey = keys(responsedict) + responsedictKey = [i for i in _responsedictKey] # convert into a list + is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + + if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) + errornote = "Your previous attempt has more key points than answer's required key points." + println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif !all(is_requiredKeys_in_responsedictKey) + zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) + missingkeys = [requiredKeys[i] for i in zeroind] + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # # check whether response has not-allowed words + # notallowed = ["respond:", "user>", "user:"] + # detected_kw = GeneralUtils.detect_keyword(notallowed, response) + # # list all keys that have 1 value in detected_kw dictionary + # k = [key for (key, value) in detected_kw if value == 1] + # if length(k) > 0 + # errornote = "In your previous attempt, you have $k in your response which is not allowed." + # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end + + # # check whether response has all header + # detected_kw = GeneralUtils.detect_keyword(header, response) + # if 0 ∈ values(detected_kw) + # errornote = "$missingkeys are missing from your previous response" + # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # elseif sum(values(detected_kw)) > length(header) + # errornote = "\nYour previous attempt has duplicated points according to the required response format" + # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end + + # responsedict = GeneralUtils.textToDict(response, header; + # dictKey=dictkey, symbolkey=true) # check if Context: is in dialogue if occursin("Context:", responsedict[:dialogue]) @@ -1321,7 +1422,7 @@ julia> # Signature """ -function generatechat(a::sommelier, thoughtDict) +function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) systemmsg = """ Your role: @@ -1335,7 +1436,6 @@ function generatechat(a::sommelier, thoughtDict) - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. At each round of conversation, you will be given the following: - Additional info: ... Your ongoing conversation with the user: ... Your thoughts: Your current thoughts in your mind You must follow the following guidelines: @@ -1345,68 +1445,92 @@ function generatechat(a::sommelier, thoughtDict) - If the user interrupts, prioritize the user - Be honest You should then respond to the user with: - Dialogue: what you want to say to the user - You should only respond in format as described below: - Dialogue: ... + dialogue: what you want to say to the user + You should only respond in JSON format as described below: + { + "dialogue": "..." + } Here are some examples: - Additional info: "Car previously found in your inventory: 1) Toyota Camry 2020 2) Honda Civic 2021 3) Ford Mustang 2022" - Your thoughts: "I should recommend the car we have found in our inventory to the user." Your ongoing conversation with the user: "user> hello, I need a new car\n" - Dialogue: "We have a variety of cars available, including the Toyota Camry 2020, the Honda Civic 2021, and the Ford Mustang 2022. Which one would you like to see?" + Your thoughts: "I should recommend the car we have found in our inventory to the user." + {"dialogue": "We have a variety of cars available, including the Toyota Camry 2020, the Honda Civic 2021, and the Ford Mustang 2022. Which one would you like to see?"} Let's begin! """ - - header = ["Dialogue:"] - dictkey = ["dialogue"] + requiredKeys = [:dialogue] # a.memory[:shortmem][:available_wine] is a vector of dictionary - 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 + # 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 chathistory = chatHistoryToText(a.chathistory) errornote = "N/A" response = nothing # placeholder for show when error msg show up yourthought = thoughtDict[:plan] - yourthought1 = nothing + # yourthought1 = nothing - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) + for attempt in 1:maxattempt + # if attempt > 1 # use to prevent LLM generate the same respond over and over + # println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) + # else + # yourthought1 = yourthought + # end - for attempt in 1:10 - if attempt > 1 # use to prevent LLM generate the same respond over and over - println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - else - yourthought1 = yourthought - end - - usermsg = + context = """ - Additional info: $context + Your ongoing conversation with the user: $chathistory - Your thoughts: $yourthought1 + Your thoughts: $yourthought P.S. $errornote + """ - _prompt = + unformatPrompt = [ Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs) + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context + + response = a.func[:text2textInstructLLM](prompt; senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + response = GeneralUtils.remove_french_accents(response) + # response = replace(response, '$'=>"USD") think, response = GeneralUtils.extractthink(response) + + responsedict = nothing + try + responsedict = copy(JSON3.read(response)) + catch + println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # check whether all answer's key points are in responsedict + _responsedictKey = keys(responsedict) + responsedictKey = [i for i in _responsedictKey] # convert into a list + is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + + if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) + errornote = "Your previous attempt has more key points than answer's required key points." + println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif !all(is_requiredKeys_in_responsedictKey) + zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) + missingkeys = [requiredKeys[i] for i in zeroind] + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end # sometime the model response like this "here's how I would respond: ..." if occursin("respond:", response) @@ -1425,30 +1549,32 @@ function generatechat(a::sommelier, thoughtDict) response = replace(response, "<|eot_id|>"=>"") response = GeneralUtils.remove_french_accents(response) - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end + - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) + # # check whether response has all header + # detected_kw = GeneralUtils.detect_keyword(header, response) + # kwvalue = [i for i in values(detected_kw)] + # zeroind = findall(x -> x == 0, kwvalue) + # missingkeys = [header[i] for i in zeroind] + # if 0 ∈ values(detected_kw) + # errornote = "$missingkeys are missing from your previous response" + # println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # elseif sum(values(detected_kw)) > length(header) + # errornote = "\nYour previous attempt has duplicated points according to the required response format" + # println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end - # check if Context: is in dialogue - if occursin("Context:", responsedict[:dialogue]) - errornote = "Your previous response contains 'Context:' which is not allowed" - println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end + # responsedict = GeneralUtils.textToDict(response, header; + # dictKey=dictkey, symbolkey=true) + + # # check if Context: is in dialogue + # if occursin("Context:", responsedict[:dialogue]) + # errornote = "Your previous response contains 'Context:' which is not allowed" + # println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end # println("\nYiemAgent generatechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # pprintln(Dict(responsedict)) @@ -1546,7 +1672,7 @@ function generatechat(a::virtualcustomer; ) recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentEventNum; includelatest=true) recentevents = a.memory[:events][recent_ind] - recentEventsDict = createEventsLog(recentevents; eventindex=recent_ind) + recentEventsDict = createEventsLog(recentevents; index=recent_ind) response = nothing # placeholder for show when error msg show up errornote = "N/A" header = ["Dialogue:", "Role"] @@ -1560,11 +1686,11 @@ function generatechat(a::virtualcustomer; if attempt > 1 println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - assistantinfo = + context = """ - + P.S. $errornote - + """ unformatPrompt = @@ -1575,7 +1701,7 @@ function generatechat(a::virtualcustomer; # put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) # add info - prompt = prompt * assistantinfo + prompt = prompt * context response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) response = replace(response, "<|im_start|>"=> "") @@ -1618,14 +1744,13 @@ function generatechat(a::virtualcustomer; # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") # println("\n $response") - return responsedict[:dialogue] + return responsedict[:dialogue] end error("generatechat failed to generate a response") end function generatequestion(a, text2textInstructLLM::Function, timeline)::String - systemmsg = """ Your role: @@ -1733,12 +1858,13 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String header = ["Q1:"] dictkey = ["q1"] - context = - if length(a.memory[:shortmem][:available_wine]) != 0 - "Available wines you've found in your inventory so far: $(availableWineToText(a.memory[:shortmem][:available_wine]))" - else - "N/A" - end + # context = + # if length(a.memory[:shortmem][:available_wine]) != 0 + # "Available wines you've found in your inventory so far: $(availableWineToText(a.memory[:shortmem][:available_wine]))" + # else + # "N/A" + # end + database_search_result = a.memory[:shortmem][:db_search_result] # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) # recentevents = a.memory[:events][recent_ind] @@ -1775,7 +1901,7 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String usermsg = """ - Additional info: $context + Additional info: $database_search_result Your recent events: $timeline P.S. $errornote """ diff --git a/src/llmfunction.jl b/src/llmfunction.jl index d58e08c..78a522f 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -339,7 +339,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 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 following: - User's query: ... + - The query: the query provided by the user. You must follow the following guidelines: - If specific information required in the preference form is not available in the query or there isn't any, mark with "N/A" to indicate this. @@ -347,121 +347,167 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 - Do not generate other comments. You should then respond to the user with: - Wine_name: name of the wine - Winery: name of the winery - Vintage: the year of the wine - Region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc - Country: a country where 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_varietal: the name of the primary grape used to make the wine - Tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc - Wine_price_range: price range of wine. Example: For price 10-20, price range will be "10 to 20". For price 100, price range will be 0 to 100. - 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 + wine_name: name of the wine + winery: name of the winery + vintage: the year of the wine + region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc + country: a country where 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_varietal: the name of the primary grape used to make the wine + tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc + wine_price_min: minimum price range of wine. Example: For wine price 20, wine_price_min will be 0. For wine price 10 to 100, wine_price_min will be 10. + wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100. + 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 format as described below: - Wine_name: ... - Winery: ... - Vintage: ... - Region: ... - Country: ... - Wine_type: - Grape_varietal: ... - Tasting_notes: ... - Wine_price_range: ... - Occasion: ... - Food_to_be_paired_with_wine: ... + You should only respond in JSON format as described below: + { + "wine_name": "...", + "winery": "...", + "vintage": "...", + "region": "...", + "country": "...", + "wine_type": "...", + "grape_varietal": "...", + "tasting_notes": "...", + "wine_price_min": "...", + "wine_price_max": "...", + "occasion": "...", + "food_to_be_paired_with_wine": "..." + } Here are some example: User's query: red, Chenin Blanc, Riesling, 20 USD from Tuscany, Italy or Napa Valley, USA - Wine_name: N/A. Winery: N/A. Vintage: N/A. Region: Tuscany, Napa Valley. Country: Italy, United States. Wine_type: red, white. Grape_varietal: Chenin Blanc, Riesling. Tasting_notes: citrus. Wine_price_range: 0 to 20. Occasion: N/A. Food_to_be_paired_with_wine: N/A + { + "wine_name": "N/A", + "winery": "N/A", + "vintage": "N/A", + "region": "Tuscany, Napa Valley", + "country": "Italy, United States", + "wine_type": "red, white", + "grape_varietal": "Chenin Blanc, Riesling", + "tasting_notes": "citrus", + "wine_price_min": "0", + "wine_price_max": "20", + "occasion": "N/A", + "food_to_be_paired_with_wine": "N/A" + } User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot - Winery: Domaine du Collier. Wine_name: Saumur Blanc. Vintage: 2019. Region: Saumur. Country: France. Wine_type: white. Grape_varietal: Merlot. Tasting_notes: plum. Wine_price_range: N/A. Occasion: N/A. Food_to_be_paired_with_wine: N/A. + { + "wine_name": "Saumur Blanc", + "winery": "Domaine du Collier", + "vintage": "2019", + "region": "Saumur", + "country": "France", + "wine_type": "white", + "grape_varietal": "Merlot", + "tasting_notes": "plum", + "wine_price_min": "N/A", + "wine_price_max": "N/A", + "occasion": "N/A", + "food_to_be_paired_with_wine": "N/A" + } Let's begin! """ - header = ["Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price_range:", "Occasion:", "Food_to_be_paired_with_wine:"] - dictkey = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_range", "occasion", "food_to_be_paired_with_wine"] + requiredKeys = [:wine_name, :winery, :vintage, :region, :country, :wine_type, :grape_varietal, :tasting_notes, :wine_price_min, :wine_price_max, :occasion, :food_to_be_paired_with_wine] errornote = "N/A" - - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.2, - ) for attempt in 1:maxattempt usermsg = """ $input """ - assistantinfo = + context = """ - + P.S. $errornote - + + /no_think """ - _prompt = + unformatPrompt = [ Dict(:name=> "system", :text=> systemmsg), Dict(:name=> "user", :text=> usermsg) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) # add info - prompt = prompt * assistantinfo + prompt = prompt * context - response = a.func[:text2textInstructLLM](prompt; - modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) - response = GeneralUtils.remove_french_accents(response) + response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) + responsedict = nothing + try + responsedict = copy(JSON3.read(response)) + catch + println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # check whether all answer's key points are in responsedict + _responsedictKey = keys(responsedict) + responsedictKey = [i for i in _responsedictKey] # convert into a list + is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + + if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) + errornote = "Your previous attempt has more key points than answer's required key points." + println("\nERROR YiemAgent extractWineAttributes_1() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif !all(is_requiredKeys_in_responsedictKey) + zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) + missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "Your previous attempt has duplicated points" - println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent extractWineAttributes_1() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end + + # # check whether response has all header + # detected_kw = GeneralUtils.detect_keyword(header, response) + # kwvalue = [i for i in values(detected_kw)] + # zeroind = findall(x -> x == 0, kwvalue) + # missingkeys = [header[i] for i in zeroind] + # if 0 ∈ values(detected_kw) + # errornote = "$missingkeys are missing from your previous response" + # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # elseif sum(values(detected_kw)) > length(header) + # errornote = "Your previous attempt has duplicated points" + # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end - # check whether response has all answer's key points - detected_kw = GeneralUtils.detect_keyword(header, response) - if 0 ∈ values(detected_kw) - errornote = "In your previous attempts, the response does not have all answer's key points" - println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "In your previous attempts, the response has duplicated answer's key points" - println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - println(response) - continue - end - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) + # # check whether response has all answer's key points + # detected_kw = GeneralUtils.detect_keyword(header, response) + # if 0 ∈ values(detected_kw) + # errornote = "In your previous attempts, the response does not have all answer's key points" + # println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # elseif sum(values(detected_kw)) > length(header) + # errornote = "In your previous attempts, the response has duplicated answer's key points" + # println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println(response) + # continue + # end + # responsedict = GeneralUtils.textToDict(response, header; + # dictKey=dictkey, symbolkey=true) delete!(responsedict, :thought) delete!(responsedict, :tasting_notes) delete!(responsedict, :occasion) delete!(responsedict, :food_to_be_paired_with_wine) - println(@__FILE__, " ", @__LINE__) - pprintln(responsedict) - # 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 dictkey + for i in requiredKeys j = Symbol(i) if j ∉ [:thought, :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 @@ -759,7 +805,8 @@ function paraphrase(text2textInstructLLM::Function, text::String) Let's begin! """ - + #[WORKING] use JSON3 the same as extractWineAttributes_1 is better + #[WORKING] change this function to use the same format use decisionMater header = ["Paraphrase:"] dictkey = ["paraphrase"] diff --git a/src/type.jl b/src/type.jl index 871cec5..530238d 100644 --- a/src/type.jl +++ b/src/type.jl @@ -193,8 +193,7 @@ function sommelier( """ memory = Dict{Symbol, Any}( :shortmem=> OrderedDict{Symbol, Any}( - :available_wine=> [], - :found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines + :db_search_result=> Any[], ), :events=> Vector{Dict{Symbol, Any}}(), :state=> Dict{Symbol, Any}( diff --git a/src/util.jl b/src/util.jl index 488ec4a..2f18772 100644 --- a/src/util.jl +++ b/src/util.jl @@ -1,7 +1,7 @@ module util export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, - availableWineToText, createEventsLog + availableWineToText, createEventsLog, createChatLog using UUIDs, Dates, DataStructures, HTTP, JSON3 using GeneralUtils @@ -301,10 +301,10 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin for (i, event) in zip(ind, events) # If no outcome exists, format without outcome if event[:outcome] === nothing - timeline *= "Event_$i $(event[:subject])> $(event[:actioninput])\n" + timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" # If outcome exists, include it in formatting else - timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" + timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" end end @@ -313,15 +313,15 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin end -function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothing +function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing ) where {T1<:AbstractVector} # Initialize empty log array log = Dict{Symbol, String}[] # Determine which indices to use - either provided range or full length ind = - if eventindex !== nothing - [eventindex...] + if index !== nothing + [index...] else 1:length(events) end @@ -338,7 +338,7 @@ function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothi subject = event[:subject] actioninput = event[:actioninput] outcome = event[:outcome] - str = "$subject: $actioninput $outcome" + str = "Action: $actioninput Outcome: $outcome" d = Dict{Symbol, String}(:name=>subject, :text=>str) push!(log, d) end @@ -348,6 +348,31 @@ function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothi end +function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing + ) where {T1<:AbstractVector} + # Initialize empty log array + log = Dict{Symbol, String}[] + + # Determine which indices to use - either provided range or full length + ind = + if index !== nothing + [index...] + else + 1:length(chatdict) + end + + # Iterate through events and format each one + for (i, event) in zip(ind, chatdict) + subject = event[:name] + text = event[:text] + d = Dict{Symbol, String}(:name=>subject, :text=>text) + push!(log, d) + end + + return log +end + + @@ -382,9 +407,6 @@ end - - - From 3e79c0bfed9d7dc6db72cd5cb1323a7954adf7bf Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Fri, 16 May 2025 10:26:50 +0700 Subject: [PATCH 4/9] update --- src/interface.jl | 378 +++++++++++++++++++++++++++++++++++++-------- src/llmfunction.jl | 2 +- src/util.jl | 49 +++++- 3 files changed, 353 insertions(+), 76 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index b404e08..429d4f8 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -97,7 +97,7 @@ julia> output_thoughtDict = Dict( # Signature """ -function decisionMaker(a::T; recent::Integer=10, maxattempt=10 +function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 ) where {T<:agent} # lessonDict = copy(JSON3.read("lesson.json")) @@ -125,35 +125,42 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 # """ # end - recent_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recent; includelatest=true) - recentevents = a.memory[:events][recent_ind] - recentchat = createChatLog(a.chathistory[recent_ind]; index=recent_ind) + recentevents_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentevents; + includelatest=true) + timeline = createTimeline(a.memory[:events]; eventindex=recentevents_ind) + + recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; + includelatest=true) + recentchat = createChatLog(a.chathistory; index=recentchat_ind) # recentEventsDict = createEventsLog(recentevents; index=recent_ind) + #BUG timeline only cover event 1-9 out of 10 events while recentchat cover 1-9 because + # recent_ind is based on chathistory. i should ind based on events. The reason is events always + # have more than chathistory due to CHECKINVENTORY() function which store in events BUT NOT in + # chathistory + - timeline = createTimeline(recentevents; eventindex=recent_ind) + # # recap as caching + # # query similar result from vectorDB + # recapkeys = keys(a.memory[:recap]) + # _recapkeys_vec = [i for i in recapkeys] - # recap as caching - # query similar result from vectorDB - recapkeys = keys(a.memory[:recap]) - _recapkeys_vec = [i for i in recapkeys] + # # select recent keys + # _recentRecapKeys = + # if length(a.memory[:recap]) <= 3 # 1st message is a user's hello msg + # _recapkeys_vec + # elseif length(a.memory[:recap]) > 3 + # l = length(a.memory[:recap]) + # _recapkeys_vec[l-2:l] + # end - # select recent keys - _recentRecapKeys = - if length(a.memory[:recap]) <= 3 # 1st message is a user's hello msg - _recapkeys_vec - elseif length(a.memory[:recap]) > 3 - l = length(a.memory[:recap]) - _recapkeys_vec[l-2:l] - end - - # get recent recap - _recentrecap = OrderedDict() - for (k, v) in a.memory[:recap] - if k ∈ _recentRecapKeys - _recentrecap[k] = v - end - end + # # get recent recap + # _recentrecap = OrderedDict() + # for (k, v) in a.memory[:recap] + # if k ∈ _recentRecapKeys + # _recentrecap[k] = v + # end + # end # recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) # similarDecision = a.func[:similarSommelierDecision](recentrecap) @@ -415,7 +422,7 @@ function decisionMaker(a::T; recent::Integer=10, maxattempt=10 #[WORKING] evaluationdict = evaluator(a, timeline, responsedict, context) - if evaluationdict[:good_decision] == "no" + if evaluationdict[:approved] == "no" mentor_comment = evaluationdict[:suggestion] errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" println("\nERROR YiemAgent decisionMaker() - $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -851,7 +858,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext - Do not generate additional thoughts or actions. 2) decision_evaluation: - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. - 3) good_decision: Decide whether the decision make sense under the circumstances. Can be "yes" or "no" + 3) approved: Decide whether to let the trainee proceed or not. Can be "yes" or "no" 4) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. @@ -859,14 +866,14 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext { "trajectory_evaluation": "..." "decision_evaluation": "..." - "good_decision": "..." + "approved": "..." "suggestion": "..." } Let's begin! """ - requiredKeys = [:trajectory_evaluation, :decision_evaluation, :good_decision, :suggestion] + requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approved, :suggestion] errornote = "N/A" for attempt in 1:10 @@ -1116,7 +1123,7 @@ julia> """ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} # a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) - thoughtDict = decisionMaker(a; recent=5) + thoughtDict = decisionMaker(a; recentevents=5) actionname = thoughtDict[:action_name] actioninput = thoughtDict[:action_input] @@ -1230,30 +1237,39 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh end -function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) +function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentevents::Integer=10) + recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; + includelatest=true) + recentchat = createChatLog(a.chathistory; index=recentchat_ind) systemmsg = """ - Your profile: - Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. - Situation: - You have checked the inventory and found wines. - Your mission: - Present the wines to the customer in a way that keep the conversation smooth and engaging. - At each round of conversation, you will be given the following information: + + Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. + + + You have checked the inventory and found wines that may match what the user wants + + + Present the wines to the user in a way that keep the conversation smooth and engaging. + + + Name of the wines that needs to be introduced: name of wines you are going to introduce to the user Database search result: the result of a database search using SQL commands you have found so far - Chat history: your ongoing conversation with the user - Wine name: name of wines you are going to introduce. - You should follow the following guidelines: + + - Provide detailed introductions of the wines you've found to the user. - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. - Provide your personal recommendation and provide a brief explanation of why you recommend it. - You should then respond to the user with: + + dialogue: Your presentation to the user - You should only respond in format as described below: + + { "dialogue": "..." } + Let's begin! """ @@ -1265,7 +1281,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) "N/A" end - chathistory = chatHistoryToText(a.chathistory) + # chathistory = chatHistoryToText(a.chathistory) errornote = "N/A" response = nothing # placeholder for show when error msg show up @@ -1279,9 +1295,8 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) context = """ + Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) Database search result: $database_search_result - Chat history: $chathistory - Wine name: $(thoughtDict[:action_input]) P.S. $errornote """ @@ -1291,6 +1306,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) Dict(:name => "system", :text => systemmsg), ] + unformatPrompt = vcat(unformatPrompt, recentchat) # put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) # add info @@ -1311,7 +1327,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) try responsedict = copy(JSON3.read(response)) catch - println("\nERROR YiemAgent presentbox() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent presentbox() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1400,6 +1416,176 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) end error("presentbox() failed to generate a response") end +# function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) +# systemmsg = +# """ +# Your profile: +# Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. +# Situation: +# You have checked the inventory and found wines. +# Your mission: +# Present the wines to the customer in a way that keep the conversation smooth and engaging. +# At each round of conversation, you will be given the following information: +# Database search result: the result of a database search using SQL commands you have found so far +# Chat history: your ongoing conversation with the user +# Name of the wines that needs to be introduced: name of wines you are going to introduce to the user. +# You should follow the following guidelines: +# - Provide detailed introductions of the wines you've found to the user. +# - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. +# - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. +# - Provide your personal recommendation and provide a brief explanation of why you recommend it. +# You should then respond to the user with: +# dialogue: Your presentation to the user +# You should only respond in format as described below: +# { +# "dialogue": "..." +# } + +# Let's begin! +# """ +# requiredKeys = [:dialogue] +# database_search_result = +# if length(a.memory[:shortmem][:db_search_result]) != 0 +# availableWineToText(a.memory[:shortmem][:db_search_result]) +# else +# "N/A" +# end + +# chathistory = chatHistoryToText(a.chathistory) +# errornote = "N/A" +# response = nothing # placeholder for show when error msg show up + + + +# # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" +# # yourthought1 = nothing + +# for attempt in 1:maxtattempt + +# context = +# """ +# +# Database search result: $database_search_result +# Chat history: $chathistory +# Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) +# P.S. $errornote +# +# """ + +# unformatPrompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) +# # add info +# prompt = prompt * context + +# response = a.func[:text2textInstructLLM](prompt; senderId=a.id) +# response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) +# response = GeneralUtils.remove_french_accents(response) +# # response = replace(response, '$'=>"USD") +# think, response = GeneralUtils.extractthink(response) + +# response = replace(response, '*'=>"") +# response = replace(response, '$' => "USD") +# response = replace(response, '`' => "") +# response = replace(response, "<|eot_id|>"=>"") + +# responsedict = nothing +# try +# responsedict = copy(JSON3.read(response)) +# catch +# println("\nERROR YiemAgent presentbox() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # check whether all answer's key points are in responsedict +# _responsedictKey = keys(responsedict) +# responsedictKey = [i for i in _responsedictKey] # convert into a list +# is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + +# if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) +# errornote = "Your previous attempt has more key points than answer's required key points." +# println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# elseif !all(is_requiredKeys_in_responsedictKey) +# zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) +# missingkeys = [requiredKeys[i] for i in zeroind] +# errornote = "$missingkeys are missing from your previous response" +# println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # # check whether response has not-allowed words +# # notallowed = ["respond:", "user>", "user:"] +# # detected_kw = GeneralUtils.detect_keyword(notallowed, response) +# # # list all keys that have 1 value in detected_kw dictionary +# # k = [key for (key, value) in detected_kw if value == 1] +# # if length(k) > 0 +# # errornote = "In your previous attempt, you have $k in your response which is not allowed." +# # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # continue +# # end + +# # # check whether response has all header +# # detected_kw = GeneralUtils.detect_keyword(header, response) +# # if 0 ∈ values(detected_kw) +# # errornote = "$missingkeys are missing from your previous response" +# # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # continue +# # elseif sum(values(detected_kw)) > length(header) +# # errornote = "\nYour previous attempt has duplicated points according to the required response format" +# # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # continue +# # end + +# # responsedict = GeneralUtils.textToDict(response, header; +# # dictKey=dictkey, symbolkey=true) + +# # check if Context: is in dialogue +# if occursin("Context:", responsedict[:dialogue]) +# errornote = "Your previous response contains 'Context:' which is not allowed" +# println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# println("\nYiemAgent presentbox() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# pprintln(Dict(responsedict)) + +# # check whether an agent recommend wines before checking inventory or recommend wines +# # outside its inventory +# # ask LLM whether there are any winery mentioned in the response +# mentioned_winery = detectWineryName(a, responsedict[:dialogue]) +# if mentioned_winery != "None" +# mentioned_winery = String.(strip.(split(mentioned_winery, ","))) + +# # check whether the wine is in event +# isWineInEvent = false +# for winename in mentioned_winery +# for event in a.memory[:events] +# if event[:outcome] !== nothing && occursin(winename, event[:outcome]) +# isWineInEvent = true +# break +# end +# end +# end + +# # if wine is mentioned but not in timeline or shortmem, +# # then the agent is not supposed to recommend the wine +# if isWineInEvent == false +# errornote = "Your previous response recommended wines that is not in your inventory which is not allowed" +# println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end +# end + +# result = responsedict[:dialogue] +# return result +# end +# error("presentbox() failed to generate a response") +# end @@ -1547,7 +1733,7 @@ function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) response = replace(response, '$' => "USD") response = replace(response, '`' => "") response = replace(response, "<|eot_id|>"=>"") - response = GeneralUtils.remove_french_accents(response) + @@ -1612,35 +1798,39 @@ function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) end -function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) +function generatechat(a::companion; recentevents::Integer=10, + converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) + + recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; + includelatest=true); + recentchat = createChatLog(a.chathistory; index=recentchat_ind) + response = nothing # placeholder for show when error msg show up errornote = "N/A" - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) + for attempt in 1:maxattempt if attempt > 1 println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - systemmsg = a.systemmsg * "\nP.S. $errornote\n" - _prompt = - [ - Dict(:name => "system", :text => systemmsg), - ] - for i in a.chathistory - tempdict = Dict{Symbol, String}() - for j in keys(i) - if j ∉ [:timestamp] - tempdict[j] = i[j] - end - end - _prompt = vcat(_prompt, tempdict) - end + context = + """ + + P.S. $errornote + + """ + unformatPrompt = + [ + Dict(:name => "system", :text => a.systemmsg), + ] + + unformatPrompt = vcat(unformatPrompt, recentchat) # put in model format - _prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context + # replace user and assistant with partner name prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") @@ -1664,6 +1854,58 @@ function generatechat(a::companion; converPartnerName::Union{String, Nothing}=no end error("generatechat failed to generate a response") end +# function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) +# response = nothing # placeholder for show when error msg show up +# errornote = "N/A" +# llmkwargs=Dict( +# :num_ctx => 32768, +# :temperature => 0.5, +# ) +# for attempt in 1:maxattempt +# if attempt > 1 +# println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# end + +# systemmsg = a.systemmsg * "\nP.S. $errornote\n" +# _prompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# ] +# for i in a.chathistory +# tempdict = Dict{Symbol, String}() +# for j in keys(i) +# if j ∉ [:timestamp] +# tempdict[j] = i[j] +# end +# end +# _prompt = vcat(_prompt, tempdict) +# end + +# # put in model format +# _prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + +# # replace user and assistant with partner name +# prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") +# prompt = replace(prompt, "|>assistant"=>"|>$(a.name)") + +# response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) +# response = replace(response, "<|im_start|>"=> "") +# response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) +# think, response = GeneralUtils.extractthink(response) + +# # check whether LLM just repeat the previous dialogue +# for msg in a.chathistory +# if msg[:text] == response +# errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." +# println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end +# end + +# return response +# end +# error("generatechat failed to generate a response") +# end # modify it to work with customer object diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 78a522f..0b10ad9 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -307,7 +307,7 @@ function checkinventory(a::T1, input::T2 println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println(textresult) - + #[PENDING] if there is no wine id, it is not a valid result return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) end diff --git a/src/util.jl b/src/util.jl index 2f18772..c5ef652 100644 --- a/src/util.jl +++ b/src/util.jl @@ -297,20 +297,53 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin 1:length(events) end - # Iterate through events and format each one - for (i, event) in zip(ind, events) + #[WORKING] Iterate through events and format each one + for i in ind + event = events[i] # If no outcome exists, format without outcome - if event[:outcome] === nothing - timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" + # if event[:actionname] == "CHATBOX" + # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n" + # elseif event[:actionname] == "CHECKINVENTORY" && event[:outcome] === nothing + # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" # If outcome exists, include it in formatting - else + if event[:actionname] == "CHECKINVENTORY" timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" + else + timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n" end end # Return formatted timeline string return timeline end +# function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing +# ) where {T1<:AbstractVector} +# # Initialize empty timeline string +# timeline = "" + +# # Determine which indices to use - either provided range or full length +# ind = +# if eventindex !== nothing +# [eventindex...] +# else +# 1:length(events) +# end + +# # Iterate through events and format each one +# for i in ind +# event = events[i] +# # If no outcome exists, format without outcome +# if event[:outcome] === nothing +# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" +# # If outcome exists, include it in formatting +# else +# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" +# end +# end + +# # Return formatted timeline string +# return timeline +# end function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing @@ -327,7 +360,8 @@ function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing end # Iterate through events and format each one - for (i, event) in zip(ind, events) + for i in ind + event = events[i] # If no outcome exists, format without outcome if event[:outcome] === nothing subject = event[:subject] @@ -362,7 +396,8 @@ function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing end # Iterate through events and format each one - for (i, event) in zip(ind, chatdict) + for i in ind + event = chatdict[i] subject = event[:name] text = event[:text] d = Dict{Symbol, String}(:name=>subject, :text=>text) From 68c2c2f12b7e9f256d0c1fa027e82fd0a4b3cc43 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sat, 17 May 2025 12:18:25 +0700 Subject: [PATCH 5/9] update --- src/interface.jl | 74 ++++++++++++---------- src/llmfunction.jl | 149 +++++++++++++++++++++++++++------------------ src/type.jl | 1 + 3 files changed, 133 insertions(+), 91 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 429d4f8..5c51ecb 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -97,7 +97,7 @@ julia> output_thoughtDict = Dict( # Signature """ -function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 +function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 ) where {T<:agent} # lessonDict = copy(JSON3.read("lesson.json")) @@ -133,13 +133,8 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 includelatest=true) recentchat = createChatLog(a.chathistory; index=recentchat_ind) # recentEventsDict = createEventsLog(recentevents; index=recent_ind) - - #BUG timeline only cover event 1-9 out of 10 events while recentchat cover 1-9 because - # recent_ind is based on chathistory. i should ind based on events. The reason is events always - # have more than chathistory due to CHECKINVENTORY() function which store in events BUT NOT in - # chathistory - + # # recap as caching # # query similar result from vectorDB # recapkeys = keys(a.memory[:recap]) @@ -215,7 +210,7 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 - Vintage 0 means non-vintage. At each round of conversation, you will be given the following information: - Database search result: the result of a database search using SQL commands you have found so far + context: additional information about the current situation You should then respond to the user with interleaving Plan, Action_name, Action_input: 1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific. @@ -253,14 +248,14 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 """ - - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. + - CHATBOX which you can use to talk with the user. Be specific. - CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD." - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. - - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is "NA". + - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is a dialogue where you wrap up the conversation, thank the user, and invite them to return next time. + $(a.memory[:shortmem][:scratchpad]) Remark: $errornote - Database search result: $database_search_result """ @@ -1123,22 +1118,22 @@ julia> """ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} # a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) - thoughtDict = decisionMaker(a; recentevents=5) + thoughtDict = decisionMaker(a) actionname = thoughtDict[:action_name] actioninput = thoughtDict[:action_input] # map action and input() to llm function response = - if actionname == "CHATBOX" + if actionname == "CHATBOX" || actionname == "ENDCONVERSATION" (result=thoughtDict[:plan], errormsg=nothing, success=true) elseif actionname == "CHECKINVENTORY" checkinventory(a, actioninput) elseif actionname == "PRESENTBOX" (result=actioninput, errormsg=nothing, success=true) - elseif actionname == "ENDCONVERSATION" - x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time." - (result=actioninput, errormsg=nothing, success=true) + # elseif actionname == "ENDCONVERSATION" + # x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time." + # (result=actioninput, errormsg=nothing, success=true) else error("undefined LLM function. Requesting $actionname") end @@ -1166,7 +1161,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh # ) # ) # result = chatresponse - if actionname ∈ ["CHATBOX"] + if actionname ∈ ["CHATBOX", "ENDCONVERSATION"] push!(a.memory[:events], eventdict(; event_description="the assistant talks to the user.", @@ -1178,19 +1173,19 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh ) ) result = actioninput - elseif actionname ∈ ["ENDCONVERSATION"] - chatresponse = generatechat(a, thoughtDict) - push!(a.memory[:events], - eventdict(; - event_description="the assistant talks to the user.", - timestamp=Dates.now(), - subject="assistant", - thought=thoughtDict, - actionname=actionname, - actioninput=chatresponse, - ) - ) - result = chatresponse + # elseif actionname ∈ ["ENDCONVERSATION"] + # chatresponse = generatechat(a, thoughtDict) + # push!(a.memory[:events], + # eventdict(; + # event_description="the assistant talks to the user.", + # timestamp=Dates.now(), + # subject="assistant", + # thought=thoughtDict, + # actionname=actionname, + # actioninput=chatresponse, + # ) + # ) + # result = chatresponse elseif actionname ∈ ["PRESENTBOX"] chatresponse = presentbox(a, thoughtDict) push!(a.memory[:events], @@ -1215,6 +1210,21 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh else a.memory[:shortmem][:db_search_result] = vd end + + # add to scratchpad + a.memory[:shortmem][:scratchpad] *= + """ + + I searched the database with this search term: $actioninput This is what I found: $result + + """ + else + a.memory[:shortmem][:scratchpad] *= + """ + + I searched the database with this search term: $actioninput This is what I found: $result + + """ end push!(a.memory[:events], @@ -1224,7 +1234,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh subject= "assistant", thought=thoughtDict, actionname=actionname, - actioninput= "I found something in the database using this SQL: $actioninput", + actioninput= "I search the database with this search term: $actioninput", outcome= "This is what I found:, $result" ) ) @@ -1296,7 +1306,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev """ Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) - Database search result: $database_search_result + $(a.memory[:shortmem][:scratchpad]) P.S. $errornote """ diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 0b10ad9..c9f2552 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -500,16 +500,24 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 # responsedict = GeneralUtils.textToDict(response, header; # dictKey=dictkey, symbolkey=true) + removekeys = [:thought, :tasting_notes, :occasion, :food_to_be_paired_with_wine, :vintage] + for i in removekeys + delete!(responsedict, i) + end + + + delete!(responsedict, :thought) delete!(responsedict, :tasting_notes) delete!(responsedict, :occasion) delete!(responsedict, :food_to_be_paired_with_wine) + delete!(responsedict, :vintage) # 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 requiredKeys j = Symbol(i) - if j ∉ [:thought, :tasting_notes, :occasion, :food_to_be_paired_with_wine] + if j ∉ removekeys # 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] != "N/A" @@ -592,7 +600,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< conversiontable = """ - + Intensity level: 1 to 2: May correspond to "light-bodied" or a similar description. 2 to 3: May correspond to "med light bodied", "medium light" or a similar description. @@ -617,16 +625,16 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< 3 to 4: May correspond to "medium acidity" or a similar description. 4 to 5: May correspond to "semi high acidity" or a similar description. 4 to 5: May correspond to "high acidity" or a similar description. - + """ 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: - Conversion Table: ... - User's query: ... + At each round of conversation, you will be given the following information: + conversion_table: a conversion table that maps descriptive words to their corresponding integer levels + query: the words from the user's query that describe their preferences The preference form requires the following information: sweetness, acidity, tannin, intensity @@ -637,86 +645,109 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< 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: - 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 - You should only respond in format as described below: - Sweetness_keyword: ... - Sweetness: ... - Acidity_keyword: ... - Acidity: ... - Tannin_keyword: ... - Tannin: ... - Intensity_keyword: ... - Intensity: ... + 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 + You should only respond in JSON format 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: N/A - Sweetness: N/A - Acidity_keyword: low acidity - Acidity: 1-2 - Tannin_keyword: medium tannin - Tannin: 3-4 - Intensity_keyword: medium-bodied - Intensity: 3-4 + { + "sweetness_keyword": "N/A", + "sweetness": "N/A", + "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: N/A - Sweetness: N/A - Acidity_keyword: N/A - Acidity: N/A - Tannin_keyword: N/A - Tannin: N/A - Intensity_keyword: N/A - Intensity: N/A + { + "sweetness_keyword": "N/A", + "sweetness": "N/A", + "acidity_keyword": "N/A", + "acidity": "N/A", + "tannin_keyword": "N/A", + "tannin": "N/A", + "intensity_keyword": "N/A", + "intensity": "N/A" + } Let's begin! """ - header = ["Sweetness_keyword:", "Sweetness:", "Acidity_keyword:", "Acidity:", "Tannin_keyword:", "Tannin:", "Intensity_keyword:", "Intensity:"] - dictkey = ["sweetness_keyword", "sweetness", "acidity_keyword", "acidity", "tannin_keyword", "tannin", "intensity_keyword", "intensity"] + requiredKeys = [:sweetness_keyword, :sweetness, :acidity_keyword, :acidity, :tannin_keyword, :tannin, :intensity_keyword, :intensity] + + # header = ["Sweetness_keyword:", "Sweetness:", "Acidity_keyword:", "Acidity:", "Tannin_keyword:", "Tannin:", "Intensity_keyword:", "Intensity:"] + # dictkey = ["sweetness_keyword", "sweetness", "acidity_keyword", "acidity", "tannin_keyword", "tannin", "intensity_keyword", "intensity"] errornote = "N/A" for attempt in 1:10 - usermsg = + context = """ $conversiontable - User's query: $input + + $input + P.S. $errornote + /no_think """ - _prompt = + unformatPrompt = [ Dict(:name=> "system", :text=> systemmsg), - Dict(:name=> "user", :text=> usermsg) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context - response = a.func[:text2textInstructLLM](prompt) + response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) - - # check whether response has all answer's key points - detected_kw = GeneralUtils.detect_keyword(header, response) - if 0 ∈ values(detected_kw) - errornote = "In your previous attempt does not have all answer's key points" - println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "In your previous attempt has duplicated answer's key points" - println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + + responsedict = nothing + try + responsedict = copy(JSON3.read(response)) + catch + println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) + # check whether all answer's key points are in responsedict + _responsedictKey = keys(responsedict) + responsedictKey = [i for i in _responsedictKey] # convert into a list + is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] + + if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) + errornote = "Your previous attempt has more key points than answer's required key points." + println("\nERROR YiemAgent extractWineAttributes_2() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif !all(is_requiredKeys_in_responsedictKey) + zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) + missingkeys = [requiredKeys[i] for i in zeroind] + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent extractWineAttributes_2() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end # check whether each describing keyword is in the input to prevent halucination for i in ["sweetness", "acidity", "tannin", "intensity"] diff --git a/src/type.jl b/src/type.jl index 530238d..b21d69c 100644 --- a/src/type.jl +++ b/src/type.jl @@ -194,6 +194,7 @@ function sommelier( memory = Dict{Symbol, Any}( :shortmem=> OrderedDict{Symbol, Any}( :db_search_result=> Any[], + :scratchpad=> "", #[PENDING] should be a dict e.g. Dict(:database_search_result=>Dict(:wines=> "", :search_query=> "")) ), :events=> Vector{Dict{Symbol, Any}}(), :state=> Dict{Symbol, Any}( From 3a88e0e7d4658db047ac99ecc2b5258b93212a4d Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sat, 17 May 2025 21:36:29 +0700 Subject: [PATCH 6/9] update --- src/interface.jl | 12 ++++++------ src/llmfunction.jl | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 5c51ecb..ae79aba 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -99,7 +99,7 @@ julia> output_thoughtDict = Dict( """ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 ) where {T<:agent} - + println("\nExecuting YiemAgent decisionMaker()") # lessonDict = copy(JSON3.read("lesson.json")) # lesson = @@ -859,9 +859,9 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext { - "trajectory_evaluation": "..." - "decision_evaluation": "..." - "approved": "..." + "trajectory_evaluation": "...", + "decision_evaluation": "...", + "approved": "...", "suggestion": "..." } @@ -1215,14 +1215,14 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh a.memory[:shortmem][:scratchpad] *= """ - I searched the database with this search term: $actioninput This is what I found: $result + I searched the database with this search term: $actioninput \nThis is what I found: $result """ else a.memory[:shortmem][:scratchpad] *= """ - I searched the database with this search term: $actioninput This is what I found: $result + I searched the database with this search term: $actioninput \nThis is what I found: $result """ end diff --git a/src/llmfunction.jl b/src/llmfunction.jl index c9f2552..8442726 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -295,7 +295,9 @@ function checkinventory(a::T1, input::T2 wineattributes_1 = extractWineAttributes_1(a, input) wineattributes_2 = extractWineAttributes_2(a, input) - _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" + #CHANGE if you want to add retailer name + # _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" + _inventoryquery = "$wineattributes_1, $wineattributes_2" inventoryquery = "Retrieves winery, wine_name, wine_id, 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("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # add suppport for similarSQLVectorDB @@ -304,7 +306,7 @@ function checkinventory(a::T1, input::T2 insertSQLVectorDB=a.func[:insertSQLVectorDB], similarSQLVectorDB=a.func[:similarSQLVectorDB], llmFormatName="qwen3") - + #[PENDING] sometime wine data comeout {wine name, price, wine_id} and nothing else. I need to make sure that wine data include all of its attributes println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println(textresult) #[PENDING] if there is no wine id, it is not a valid result @@ -671,11 +673,11 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< "sweetness_keyword": "N/A", "sweetness": "N/A", "acidity_keyword": "low acidity", - "acidity": 1-2, + "acidity": "1-2", "tannin_keyword": "medium tannin", - "tannin": 3-4, + "tannin": "3-4", "intensity_keyword": "medium-bodied", - "intensity": 3-4 + "intensity": "3-4" } User's query: German red wine, under 100, pairs with spicy food From 919d8ec85eb72043ed5b36db43b7696e8237a62b Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sun, 18 May 2025 17:21:51 +0700 Subject: [PATCH 7/9] update --- src/interface.jl | 35 +++++++++++++++++++++------------ src/llmfunction.jl | 49 +++++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index ae79aba..c5b5ad2 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -249,8 +249,9 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 - CHATBOX which you can use to talk with the user. Be specific. - - CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD." + - CHECKWINE allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + Example query 1: "Dry, full-bodied red wine from Burgundy region, France or Tuscany region, Italy. Merlot varietal. price 100 to 1000 USD." + Example query 2: "Red wine, bold intensity, price under 300 USD" - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is a dialogue where you wrap up the conversation, thank the user, and invite them to return next time. @@ -332,9 +333,9 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 # responsedict = GeneralUtils.textToDict(response, header; # dictKey=dictkey, symbolkey=true) - if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] + if responsedict[:action_name] ∉ ["CHATBOX", "CHECKWINE", "PRESENTBOX", "ENDCONVERSATION"] errornote = "Your previous attempt didn't use the given functions" - println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --> $(responsedict[:action_name])", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -420,7 +421,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 if evaluationdict[:approved] == "no" mentor_comment = evaluationdict[:suggestion] errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" - println("\nERROR YiemAgent decisionMaker() - $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -814,7 +815,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext - Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding. - - To improve a trainee sommelier decision based on the store policy and guidelines. + - Improve a trainee sommelier decision based on the store policy and guidelines while ensuring seamless interactions between the trainee and customers. - trajectory: A conversation between your trainee and the customer that have occurred up until now @@ -1084,7 +1085,7 @@ function conversation(a::Union{companion, virtualcustomer}, userinput::Dict; actioninput=userinput[:text], ) ) - chatresponse = generatechat(a; converPartnerName=converPartnerName) + chatresponse = generatechat(a; converPartnerName=converPartnerName, recentEventNum=20) addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) @@ -1127,8 +1128,8 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh response = if actionname == "CHATBOX" || actionname == "ENDCONVERSATION" (result=thoughtDict[:plan], errormsg=nothing, success=true) - elseif actionname == "CHECKINVENTORY" - checkinventory(a, actioninput) + elseif actionname == "CHECKWINE" + checkwine(a, actioninput) elseif actionname == "PRESENTBOX" (result=actioninput, errormsg=nothing, success=true) # elseif actionname == "ENDCONVERSATION" @@ -1200,7 +1201,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh ) result = chatresponse - elseif actionname == "CHECKINVENTORY" + elseif actionname == "CHECKWINE" if rawresponse !== nothing vd = GeneralUtils.dfToVectorDict(rawresponse) # comes in dataframe format a.memory[:shortmem][:found_wine] = vd # used by decisionMaker() as a short note @@ -1417,6 +1418,17 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev if isWineInEvent == false errornote = "Your previous response recommended wines that is not in your inventory which is not allowed" println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + s = """ + Perfect choice! The Colgin Tychson Hill Vineyard Cabernet Sauvignon (2014) is an excellent match for your criteria. Here's why: + + Boldness & Flavor: This wine delivers intense blackberry, black cherry, and dark fruit notes, layered with vanilla, oak, and earthy undertones. Its high intensity (rated 5/5) ensures a rich, full-bodied experience that's both powerful and balanced. + + Family-Owned Legacy: Produced by Colgin Cellars, a renowned Napa Valley family winery, this vintage reflects their commitment to quality and tradition. While not a limited-edition release, it's a highly regarded, consistently excellent Cabernet Sauvignon. + + Gift-Ready & Affordable: Priced at USD144 (well under your USD250 budget), it comes in a sleek, gift-ready box—perfect for impressing friends or loved ones. + + Why I Recommend It: It perfectly balances your desire for bold fruit, oak, and a presentable format without sacrificing quality. If you're curious about alternatives, the 2017 Hunter Glenn Cabernet (also USD159) shares similar intensity but lacks specific tasting notes. However, the 2014 Tychson Hill is a more complete match for your criteria. Enjoy your selection!""" + continue end end @@ -1923,8 +1935,7 @@ function generatechat(a::virtualcustomer; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10, recentEventNum=10 ) recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentEventNum; includelatest=true) - recentevents = a.memory[:events][recent_ind] - recentEventsDict = createEventsLog(recentevents; index=recent_ind) + recentEventsDict = createEventsLog(a.memory[:events]; index=recent_ind) response = nothing # placeholder for show when error msg show up errornote = "N/A" header = ["Dialogue:", "Role"] diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 8442726..ad7a242 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -1,6 +1,6 @@ module llmfunction -export virtualWineUserChatbox, jsoncorrection, checkinventory, # recommendbox, +export virtualWineUserChatbox, jsoncorrection, checkwine, # recommendbox, virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, extractWineAttributes_2, paraphrase @@ -288,28 +288,45 @@ julia> result = checkinventory(agent, input) # Signature """ -function checkinventory(a::T1, input::T2 +function checkwine(a::T1, input::T2; maxattempt::Int=3 ) where {T1<:agent, T2<:AbstractString} println("\ncheckinventory order: $input ", @__FILE__, ":", @__LINE__, " $(Dates.now())") wineattributes_1 = extractWineAttributes_1(a, input) wineattributes_2 = extractWineAttributes_2(a, input) - #CHANGE if you want to add retailer name - # _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" - _inventoryquery = "$wineattributes_1, $wineattributes_2" - inventoryquery = "Retrieves winery, wine_name, wine_id, 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("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # add suppport for similarSQLVectorDB - textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL], - a.func[:text2textInstructLLM]; - insertSQLVectorDB=a.func[:insertSQLVectorDB], - similarSQLVectorDB=a.func[:similarSQLVectorDB], - llmFormatName="qwen3") - #[PENDING] sometime wine data comeout {wine name, price, wine_id} and nothing else. I need to make sure that wine data include all of its attributes + # placeholder + textresult = nothing + rawresponse = nothing + + for i in 1:maxattempt + + #CHANGE if you want to add retailer name + # _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" + _inventoryquery = "$wineattributes_1, $wineattributes_2" + + retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency"] + inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" + println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # add suppport for similarSQLVectorDB + textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL], + a.func[:text2textInstructLLM]; + insertSQLVectorDB=a.func[:insertSQLVectorDB], + similarSQLVectorDB=a.func[:similarSQLVectorDB], + llmFormatName="qwen3") + # check if all of retrieve_attributes appears in textresult + isin = [occursin(x, textresult) for x in retrieve_attributes] + if !all(isin) && !occursin("The resulting table has 0 row", textresult) + errornote = "Not all of $retrieve_attributes appear in search result" + println("\nERROR YiemAgent checkwine() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + else + break + end + end println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println(textresult) - #[PENDING] if there is no wine id, it is not a valid result + return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) end @@ -352,7 +369,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 wine_name: name of the wine winery: name of the winery vintage: the year of the wine - region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc + region: a region, such as Burgundy, Bordeaux, Champagne, Napa Valley, Tuscany, California, Oregon, etc country: a country where 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_varietal: the name of the primary grape used to make the wine From 3444f000625db08112a9068e7070558223b29c74 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Mon, 19 May 2025 21:10:04 +0700 Subject: [PATCH 8/9] update --- src/interface.jl | 85 ++++++++++++++++++++++++++++-------------------- src/type.jl | 1 + 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index c5b5ad2..99fdc5f 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -193,22 +193,6 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock - - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. - - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - Spicy foods should not be paired with medium and full-bodied red wines. - - We do not sell organic, sustainability and sulfite wine. - - - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - At each round of conversation, you will be given the following information: context: additional information about the current situation @@ -247,6 +231,21 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 context = """ + + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock + - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. + - Once the user has selected their wine, if you haven't already, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Spicy foods should not be paired with medium and full-bodied red wines. + - We do not sell organic, sustainability and sulfite wine. + + + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + - CHATBOX which you can use to talk with the user. Be specific. - CHECKWINE allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. @@ -292,13 +291,13 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent decisionMaker() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote ----not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent decisionMaker() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -335,7 +334,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 if responsedict[:action_name] ∉ ["CHATBOX", "CHECKWINE", "PRESENTBOX", "ENDCONVERSATION"] errornote = "Your previous attempt didn't use the given functions" - println("\nERROR YiemAgent decisionMaker() $errornote --> $(responsedict[:action_name])", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --not qualify response--> $(responsedict[:action_name])", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -402,9 +401,6 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 end delete!(responsedict, :mentioned_winery) - responsedict[:systemmsg] = systemmsg - responsedict[:unformatPrompt] = unformatPrompt - # responsedict[:QandA] = QandA # check whether responsedict[:action_input] is the same as previous dialogue if responsedict[:action_input] == a.chathistory[end][:text] @@ -421,17 +417,26 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 if evaluationdict[:approved] == "no" mentor_comment = evaluationdict[:suggestion] errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" - println("\nERROR YiemAgent decisionMaker() $errornote --> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --not qualify response--> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end # store for later training responsedict[:system] = systemmsg + responsedict[:unformatPrompt] = unformatPrompt responsedict[:recentchat] = recentchat responsedict[:prompt] = prompt responsedict[:context] = context responsedict[:think] = think + responsedict[:response] = response # responsedict[:QandA] = QandA + + # check whether there is a file path exists before writing to it + if !haskey(a.memory[:shortmem], :decisionlog) + a.memory[:shortmem][:decisionlog] = [responsedict] + else + push!(a.memory[:shortmem][:decisionlog], responsedict) + end # # save to filename ./log/decisionlog.txt # println("\nsaving YiemAgent decisionMaker() to disk ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -454,6 +459,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 # end # end # println("\nYiemAgent decisionMaker() saved to disk is done. agent $(a.id)") + responsedict[:prompt] = prompt return responsedict @@ -825,21 +831,21 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext "action_name" is the name of the action taken, which can be one of the available tool name. "action_input" is the input to the action. - + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - If you found wines in the store's database, they are in stock - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. - - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Once the user has selected their wine, if you haven't already, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - Spicy foods should not be paired with medium and full-bodied red wines. - We do not sell organic, sustainability and sulfite wine. - - + + - Encourage the customer to explore different options and try new things. - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - Your store carries only wine. - Vintage 0 means non-vintage. - + - Use only infomation provided by the store policy and guidelines as a bedrocks for your response. @@ -925,13 +931,13 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1349,13 +1355,13 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1530,13 +1536,13 @@ end # if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) # errornote = "Your previous attempt has more key points than answer's required key points." -# println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # elseif !all(is_requiredKeys_in_responsedictKey) # zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) # missingkeys = [requiredKeys[i] for i in zeroind] # errornote = "$missingkeys are missing from your previous response" -# println("\nERROR YiemAgent presentbox() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # end @@ -1730,13 +1736,13 @@ function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -2003,6 +2009,13 @@ function generatechat(a::virtualcustomer; continue end + # check if the dialogue is the same as the previous one + if responsedict[:dialogue] == a.chathistory[end][:text] + errornote = "In your previous attempt you said $(responsedict[:dialogue]) which was the same as the previous one." + println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") diff --git a/src/type.jl b/src/type.jl index b21d69c..242da74 100644 --- a/src/type.jl +++ b/src/type.jl @@ -200,6 +200,7 @@ function sommelier( :state=> Dict{Symbol, Any}( ), :recap=> OrderedDict{Symbol, Any}(), + ) newAgent = sommelier( From e5248130219f2d5cf73b956f04d3d1c882af25a5 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Mon, 26 May 2025 07:05:14 +0700 Subject: [PATCH 9/9] update --- src/interface.jl | 55 ++++++++++++++++++---------------------------- src/llmfunction.jl | 23 ++++++++++++++----- src/util.jl | 2 +- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 99fdc5f..eea2c7c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -236,13 +236,15 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 - If you found wines in the store's database, they are in stock - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. - - Once the user has selected their wine, if you haven't already, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. + - If the user is ending the conversation, say goodbye and invite them to come back next time. - Spicy foods should not be paired with medium and full-bodied red wines. - We do not sell organic, sustainability and sulfite wine. + - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - Your store carries only wine. - Vintage 0 means non-vintage. @@ -291,13 +293,13 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent decisionMaker() $errornote ----not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote ----(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent decisionMaker() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -334,7 +336,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 if responsedict[:action_name] ∉ ["CHATBOX", "CHECKWINE", "PRESENTBOX", "ENDCONVERSATION"] errornote = "Your previous attempt didn't use the given functions" - println("\nERROR YiemAgent decisionMaker() $errornote --not qualify response--> $(responsedict[:action_name])", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict[:action_name])", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -412,12 +414,11 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 println("\nYiemAgent decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\n$response") - #[WORKING] evaluationdict = evaluator(a, timeline, responsedict, context) if evaluationdict[:approved] == "no" mentor_comment = evaluationdict[:suggestion] errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" - println("\nERROR YiemAgent decisionMaker() $errornote --not qualify response--> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -831,26 +832,12 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext "action_name" is the name of the action taken, which can be one of the available tool name. "action_input" is the input to the action. - - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock - - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check 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. Other wine characteristics useful for CHECKINVENTORY tools are allowed. - - Once the user has selected their wine, if you haven't already, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - Spicy foods should not be paired with medium and full-bodied red wines. - - We do not sell organic, sustainability and sulfite wine. - - - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - Use only infomation provided by the store policy and guidelines as a bedrocks for your response. - - N/A + - The trainee's plan, action_name, and action_input must be logically consistent + - The trainee's action_input should be in a proper format as specified by the tools. 1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. @@ -860,16 +847,16 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext - Do not generate additional thoughts or actions. 2) decision_evaluation: - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. - 3) approved: Decide whether to let the trainee proceed or not. Can be "yes" or "no" - 4) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. + 3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. + 4) approved: Based on suggestion, decide whether to let the trainee proceed with the decision. Can be "yes" or "no" { "trajectory_evaluation": "...", "decision_evaluation": "...", + "suggestion": "...", "approved": "...", - "suggestion": "..." } @@ -931,13 +918,13 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1355,13 +1342,13 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1536,13 +1523,13 @@ end # if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) # errornote = "Your previous attempt has more key points than answer's required key points." -# println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # elseif !all(is_requiredKeys_in_responsedictKey) # zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) # missingkeys = [requiredKeys[i] for i in zeroind] # errornote = "$missingkeys are missing from your previous response" -# println("\nERROR YiemAgent presentbox() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # end @@ -1736,13 +1723,13 @@ function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif !all(is_requiredKeys_in_responsedictKey) zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) missingkeys = [requiredKeys[i] for i in zeroind] errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --not qualify response--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end diff --git a/src/llmfunction.jl b/src/llmfunction.jl index ad7a242..4a8febb 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -4,7 +4,7 @@ export virtualWineUserChatbox, jsoncorrection, checkwine, # recommendbox, virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, extractWineAttributes_2, paraphrase -using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates +using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames using GeneralUtils, SQLLLM using ..type, ..util @@ -316,7 +316,11 @@ function checkwine(a::T1, input::T2; maxattempt::Int=3 llmFormatName="qwen3") # check if all of retrieve_attributes appears in textresult isin = [occursin(x, textresult) for x in retrieve_attributes] - if !all(isin) && !occursin("The resulting table has 0 row", textresult) + # check if rawresponse type is DataFrame so that I can check for column + if typeof(rawresponse) == DataFrame && + !occursin("The resulting table has 0 row", textresult) && + !all(isin) + errornote = "Not all of $retrieve_attributes appear in search result" println("\nERROR YiemAgent checkwine() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue @@ -402,10 +406,10 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 "wine_name": "N/A", "winery": "N/A", "vintage": "N/A", - "region": "Tuscany, Napa Valley", - "country": "Italy, United States", - "wine_type": "red, white", - "grape_varietal": "Chenin Blanc, Riesling", + "region": "Tuscany or Napa Valley", + "country": "Italy or United States", + "wine_type": "red or white", + "grape_varietal": "Chenin Blanc or Riesling", "tasting_notes": "citrus", "wine_price_min": "0", "wine_price_max": "20", @@ -804,6 +808,13 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< end end + # delete some key words from responsedict + for (k, v) in responsedict + if k ∈ [:sweetness_keyword, :acidity_keyword, :tannin_keyword, :intensity_keyword] + delete!(responsedict, k) + end + end + result = "" for (k, v) in responsedict # some time LLM generate text with "(some comment)". this line removes it diff --git a/src/util.jl b/src/util.jl index c5ef652..763d37c 100644 --- a/src/util.jl +++ b/src/util.jl @@ -306,7 +306,7 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin # elseif event[:actionname] == "CHECKINVENTORY" && event[:outcome] === nothing # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" # If outcome exists, include it in formatting - if event[:actionname] == "CHECKINVENTORY" + if event[:actionname] == "CHECKWINE" timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" else timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n"