diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 2622f6a..d99f4de 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -1,7 +1,7 @@ module llmfunction export virtualWineUserChatbox, jsoncorrection, checkinventory, - virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes + virtualWineUserRecommendbox, userChatbox, userRecommendbox using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs using GeneralUtils, SQLLLM @@ -365,12 +365,12 @@ julia> result = checkinventory(agent, input) function checkinventory(a::T1, input::T2 )::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent, T2<:AbstractString} - wineattributes = extractWineAttributes(a, input) + wineattributes_1 = extractWineAttributes_1(a, input) + wineattributes_2 = extractWineAttributes_2(a, input) - # replace because SQLLLM didn't know what food_paired means - result = replace(wineattributes, "food_pairing" => "food_to_be_paired_with_wine") + inventoryquery = "$wineattributes_1, $wineattributes_2" - result = SQLLLM.query(result, a.executeSQL, a.text2textInstructLLM) + result = SQLLLM.query(inventoryquery, a.executeSQL, a.text2textInstructLLM) return result end @@ -394,145 +394,51 @@ julia> # Signature """ -function extractWineAttributes(a::T1, input::T2 +function extractWineAttributes_1(a::T1, input::T2 )::String where {T1<:agent, T2<:AbstractString} - converstiontable = - """ - Conversion Table: - Intensity level: - Level 1: May correspond to "light-bodied" or a similar description. - Level 2: May correspond to "med light", "medium light" or a similar description. - Level 3: May correspond to "medium" or a similar description. - Level 4: May correspond to "med full", "medium full" or a similar description. - Level 5: May correspond to "full" or a similar description. - Sweetness level: - Level 1: May correspond to "dry", "no sweet" or a similar description. - Level 2: May correspond to "off dry", "less sweet" or a similar description. - Level 3: May correspond to "semi sweet" or a similar description. - Level 4: May correspond to "sweet" or a similar description. - Level 5: May correspond to "very sweet" or a similar description. - Tannin level: - Level 1: May correspond to "low tannin" or a similar description. - Level 2: May correspond to "semi low tannin" or a similar description. - Level 3: May correspond to "medium tannin" or a similar description. - Level 4: May correspond to "semi high tannin" or a similar description. - Level 5: May correspond to "high tannin" or a similar description. - Acidity level: - Level 1: May correspond to "low acidity" or a similar description. - Level 2: May correspond to "semi low acidity" or a similar description. - Level 3: May correspond to "medium acidity" or a similar description. - Level 4: May correspond to "semi high acidity" or a similar description. - Level 5: May correspond to "high acidity" or a similar description. - """ - - # systemmsg = - # """ - # As an attentive sommelier, your task is to determine the user's wine preferred levels of sweetness, intensity, tannin, acidity and other criteria based on the user query. - - # At each round of conversation, the user will give you the current situation: - # Conversion Table: ... - # Query: ... - - # You must follow the following guidelines: - # 1) If specific information is not available in the query, use "NA" to indicate this. Also words like "any" or "unlimited" means no information available. - # 2) Use converstion table to convert sweetness, acidity, tannin, intensity describing word into an integer. - # 3) Do not generate other comments. - - # You should then respond to the user with the following points: - # - wine_type: Can be one of: red, white, sparkling, rose, dessert or fortified - # - price: ... - # - occasion: ... - # - food_pairing: food that will be served with wine - # - country: wine's country of origin - # - grape_variety: ... - # - tasting_notes: wine's flavors - # - sweetness: S where S is an integer indicating sweetness level - # - acidity: A where A is an integer indicating acidity level - # - tannin: T where T is an integer indicating tannin level - # - intensity: I where I is an integer indicating intensity level - - # You should only respond in format as described below: - # repeat: repeat the user's query - # wine_type: ... - # price: ... - # occasion: ... - # food_pairing: ... - # country: ... - # grape_variety: ... - # tasting_notes: ... - # sweetness: - # acidity: ... - # tannin: ... - # intensity: ... - - # Here are some examples: - - # user: "price < 25, for wedding party, full-bodied white wine with sweetness level 2, apple and honey notes, low tannin level and medium acidity level, Pizza, France, Riesling" - # assistant: repeat: ... \n wine_type: white\n price: less than 25\n occasion: wedding party\n food_pairing: Pizza\n country: France\n grape_variety: Riesling\n tasting_notes: apple, honey\n sweetness: 2\n acidity: 3\n tannin: 1\n intensity: 5 - - # user: body=full-bodied, dry, acidity=medium and low tannin - # assistant: repeat: ... \n wine_type: NA\n price: NA\n occasion: NA\n food_pairing: NA\n country: NA\n grape_variety: NA\n tasting_notes: NA\n sweetness: 1\n acidity: 3\n tannin: 1\n intensity: 5 - - # Let's begin! - # """ - systemmsg = """ - As an attentive sommelier, your task is to fillout the form based on the user query. The form is about the user's wine preference. + As an helpful sommelier, your task is to fill the user's preference form based on the user query. At each round of conversation, the user will give you the current situation: - Conversion Table: ... - Query: ... + User query: ... + + The preference form requires the following information: + wine_type, price, occasion, food_to_be_paired_with_wine, country, grape_variety, wine_notes. You must follow the following guidelines: - 1) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. - 2) If specific information required in the form is not available in the query, use 'NA' to indicate this. Additionally, words like 'any' or 'unlimited' mean no information is available. + 1) If specific information required in the preference form is not available in the query, use 'NA' to indicate this. + Additionally, words like 'any' or 'unlimited' mean no information is available. + 2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. 3) Do not generate other comments. You should then respond to the user with the following points: - 1) reasoning: State your reasoning about the current situation. - 2) wine_type: Can be one of: red, white, sparkling, rose, dessert or fortified - 3) price: ... - 4) occasion: ... - 5) food_to_be_paired_with_wine: food that will be served with wine - 6) country: wine's country of origin - 7) grape_variety: ... - 8) tasting_notes: wine's flavors - 9) sweetness: S where S is an integer indicating sweetness level - 10) acidity: A where A is an integer indicating acidity level - 11) tannin: T where T is an integer indicating tannin level - 12) intensity: I where I is an integer indicating intensity level + - wine_type: Can be one of: red, white, sparkling, rose, dessert or fortified + - price: ... + - occasion: ... + - food_to_be_paired_with_wine: food that will be served with wine + - country: wine's country of origin + - grape_variety: ... + - wine_notes: + Bad example (these words are not wine notes - descriptive words): dry, sour, full bodied, etc. + Good example (these words are wine notes - descriptive words): floral, citrus, earthy, fruity, tropical, nutty, etc. - You should only respond in format as described below: - reasoning: ... + You should only respond in the form as described below: wine_type: ... price: ... occasion: ... food_to_be_paired_with_wine: ... country: ... grape_variety: ... - tasting_notes: ... - sweetness: - acidity: ... - tannin: ... - intensity: ... - - Here are some examples: - - user: price < 25, for wedding party, full-bodied white wine with sweetness level 2, apple and honey notes, low tannin level and medium acidity level, Pizza, France, Riesling - assistant: reasoning: ... \n wine_type: white\n price: less than 25\n occasion: wedding party\n food_to_be_paired_with_wine: Pizza\n country: France\n grape_variety: Riesling\n tasting_notes: apple, honey\n sweetness: 2\n acidity: 3\n tannin: 1\n intensity: 5 - - user: body=full-bodied, dry, acidity=medium and low tannin - assistant: reasoning: ... \n wine_type: NA\n price: NA\n occasion: NA\n food_to_be_paired_with_wine: NA\n country: NA\n grape_variety: NA\n tasting_notes: NA\n sweetness: 1\n acidity: 3\n tannin: 1\n intensity: 5 + wine_notes: ... Let's begin! """ usermsg = """ - Conversion Table: $converstiontable - Query: $input + User query: $input """ _prompt = @@ -548,7 +454,7 @@ function extractWineAttributes(a::T1, input::T2 <|start_header_id|>assistant<|end_header_id|> """ - attributes = ["reasoning", "wine_type", "price", "occasion", "food_to_be_paired_with_wine", "country", "grape_variety", "sweetness", "acidity", "tannin", "intensity"] + attributes = ["wine_type", "price", "occasion", "food_to_be_paired_with_wine", "country", "grape_variety"] for attempt in 1:5 try @@ -561,17 +467,9 @@ function extractWineAttributes(a::T1, input::T2 end end - # check if there are more than 1 key per categories - for i ∈ attributes - matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) - if length(matchkeys) > 1 - error("generatechat has more than one key per categories") - end - end - result = "" for (k, v) in responsedict - if k != :reasoning && !occursin("NA", v) + if !occursin("NA", v) result *= "$k: $v, " end end @@ -592,6 +490,215 @@ function extractWineAttributes(a::T1, input::T2 error("wineattributes_wordToNumber() failed to get a response") end + +function extractWineAttributes_2(a::T1, input::T2 + )::String where {T1<:agent, T2<:AbstractString} + + converstiontable = + """ + Conversion Table: + Intensity level: + 1: May correspond to "light-bodied" or a similar description. + 2: May correspond to "med light", "medium light" or a similar description. + 3: May correspond to "medium" or a similar description. + 4: May correspond to "med full", "medium full" or a similar description. + 5: May correspond to "full" or a similar description. + Sweetness level: + 1: May correspond to "dry", "no sweet" or a similar description. + 2: May correspond to "off dry", "less sweet" or a similar description. + 3: May correspond to "semi sweet" or a similar description. + 4: May correspond to "sweet" or a similar description. + 5: May correspond to "very sweet" or a similar description. + Tannin level: + 1: May correspond to "low tannin" or a similar description. + 2: May correspond to "semi low tannin" or a similar description. + 3: May correspond to "medium tannin" or a similar description. + 4: May correspond to "semi high tannin" or a similar description. + 5: May correspond to "high tannin" or a similar description. + Acidity level: + 1: May correspond to "low acidity" or a similar description. + 2: May correspond to "semi low acidity" or a similar description. + 3: May correspond to "medium acidity" or a similar description. + 4: May correspond to "semi high acidity" or a similar description. + 5: May correspond to "high acidity" or a similar description. + """ + + systemmsg = + """ + As an helpful sommelier, your task is to fill the user's preference form based on the user query. + + At each round of conversation, the user will give you the current situation: + Conversion Table: ... + User query: ... + + The preference form requires the following information: + 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, use 'NA' to indicate this. + Additionally, words like 'any' or 'unlimited' mean no information is available. + 2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. + 3) Do not generate other comments. + + You should then respond to the user with the following points: + - sweetness: S where S is an integer indicating sweetness level + - acidity: D where D is an integer indicating acidity level + - tannin: T where T is an integer indicating tannin level + - intensity: I where I is an integer indicating intensity level + + You should only respond in the form as described below: + sweetness: ... + acidity: ... + tannin: ... + intensity: ... + + Let's begin! + """ + + usermsg = + """ + Conversion Table: $converstiontable + User query: $input + """ + + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + """ + + attributes = ["sweetness", "acidity", "tannin", "intensity"] + + for attempt in 1:5 + try + response = a.text2textInstructLLM(prompt) + responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) + + for i ∈ attributes + if length(JSON3.write(responsedict[Symbol(i)])) == 0 + error("$i is empty ", @__LINE__) + end + end + + result = "" + for (k, v) in responsedict + if !occursin("NA", v) + result *= "$k: $v, " + end + end + + result = result[1:end-2] # remove the ending ", " + + return result + catch e + io = IOBuffer() + showerror(io, e) + errorMsg = String(take!(io)) + st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) + println("") + println("Attempt $attempt. Error occurred: $errorMsg\n$st") + println("") + end + end + error("wineattributes_wordToNumber() failed to get a response") +end + +# function recheckWineAttributes(a::T1, input::T2 +# )::String where {T1<:agent, T2<:AbstractString} + +# systemmsg = +# """ +# As an helpful sommelier, your task is to check what preferences the user mentioned in the query. + +# At each round of conversation, the user will give you the current situation: +# User query: ... + +# The preferences are: +# - wine type: red, white, sparkling, rose, dessert, fortified +# - price +# - occasion +# - food to be paired with wine +# - country +# - grape_variety +# - tasting_notes +# - sweetness +# - acidity +# - tannin +# - intensity: wine body e.g. full bodied, light bodied + +# You should then respond to the user with the following points: +# 1) mentioned_preferences: list all the preferences in the user's query. +# Good example: mentioned_preferences: "price", "country", "wine type" + +# You should only respond in the form as described below: +# mentioned_preferences: ... + +# Let's begin! +# """ + +# usermsg = +# """ +# User query: $input +# """ + +# _prompt = +# [ +# Dict(:name=> "system", :text=> systemmsg), +# Dict(:name=> "user", :text=> usermsg) +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") +# prompt *= +# """ +# <|start_header_id|>assistant<|end_header_id|> +# """ + +# attributes = ["mentioned_preferences"] + +# for attempt in 1:5 +# try +# response = a.text2textInstructLLM(prompt) +# responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) + +# for i ∈ attributes +# if length(JSON3.write(responsedict[Symbol(i)])) == 0 +# error("$i is empty ", @__LINE__) +# end +# end + +# result = "" +# for (k, v) in responsedict +# if !occursin("NA", v) +# result *= "$k: $v, " +# end +# end + +# result = result[1:end-2] # remove the ending ", " + +# return result +# catch e +# io = IOBuffer() +# showerror(io, e) +# errorMsg = String(take!(io)) +# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) +# println("") +# println("Attempt $attempt. Error occurred: $errorMsg\n$st") +# println("") +# end +# end +# error("wineattributes_wordToNumber() failed to get a response") +# end + + + """ Attemp to correct LLM response's incorrect JSON response. # Arguments diff --git a/test/runtest.jl b/test/runtest.jl index 86fcc90..6fd5dd9 100644 --- a/test/runtest.jl +++ b/test/runtest.jl @@ -120,6 +120,14 @@ end input = "query=\"medium-bodied dry white wine\"" +# input = "the customer is looking for a medium-bodied, dry white wine." +result = YiemAgent.checkinventory(a, input) + + + + + + + -result = YiemAgent.extractWineAttributes(a, input)