From 68c2c2f12b7e9f256d0c1fa027e82fd0a4b3cc43 Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sat, 17 May 2025 12:18:25 +0700 Subject: [PATCH] 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}(