From 48a3704f6d82fe089b2a7504ec44a8c6481bc82a Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Sun, 13 Apr 2025 21:46:54 +0700 Subject: [PATCH] update --- Manifest.toml | 78 ++- Project.toml | 5 +- src/interface.jl | 1170 +++++++++++++++++++++++--------------------- src/llmfunction.jl | 2 +- src/type.jl | 114 +++-- src/util.jl | 291 ++--------- 6 files changed, 778 insertions(+), 882 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index c0973e6..bc87211 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.2" +julia_version = "1.11.4" manifest_format = "2.0" -project_hash = "b483014657ef9f0fde60d7258585b291d6f0eeca" +project_hash = "cb7f3c57318e927e8ac4dc2dea9acdcace566ed1" [[deps.AliasTables]] deps = ["PtrArrays", "Random"] @@ -120,9 +120,9 @@ version = "1.11.0" [[deps.Distributions]] deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] -git-tree-sha1 = "3101c32aab536e7a27b1763c0797dba151b899ad" +git-tree-sha1 = "0b4190661e8a4e51a842070e7dd4fae440ddb7f4" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" -version = "0.25.113" +version = "0.25.118" [deps.Distributions.extensions] DistributionsChainRulesCoreExt = "ChainRulesCore" @@ -158,9 +158,9 @@ version = "0.1.10" [[deps.FileIO]] deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "2dd20384bf8c6d411b5c7370865b1e9b26cb2ea3" +git-tree-sha1 = "b66970a70db13f45b7e57fbda1736e1cf72174ea" uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.16.6" +version = "1.17.0" weakdeps = ["HTTP"] [deps.FileIO.extensions] @@ -168,9 +168,9 @@ weakdeps = ["HTTP"] [[deps.FilePathsBase]] deps = ["Compat", "Dates"] -git-tree-sha1 = "7878ff7172a8e6beedd1dea14bd27c3c6340d361" +git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2" uuid = "48062228-2e41-5def-b9a4-89aafe57970f" -version = "0.9.22" +version = "0.9.24" weakdeps = ["Mmap", "Test"] [deps.FilePathsBase.extensions] @@ -200,11 +200,9 @@ version = "1.11.0" [[deps.GeneralUtils]] deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "JSON3", "MQTTClient", "PrettyPrinting", "Random", "SHA", "UUIDs"] -git-tree-sha1 = "978d9a5c3fc30205dd72d4a2a2ed4fa85ebee5cf" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/GeneralUtils" +path = "/appfolder/app/dev/GeneralUtils" uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -version = "0.1.0" +version = "0.2.3" [[deps.HTTP]] deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] @@ -214,9 +212,9 @@ version = "1.10.13" [[deps.HypergeometricFunctions]] deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] -git-tree-sha1 = "b1c2585431c382e3fe5805874bda6aea90a95de9" +git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" -version = "0.3.25" +version = "0.3.28" [[deps.ICU_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] @@ -260,9 +258,9 @@ uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" version = "1.3.0" [[deps.IrrationalConstants]] -git-tree-sha1 = "630b497eafcc20001bba38a4651b327dcfc491d2" +git-tree-sha1 = "e2222959fbc6c19554dc15174c81bf7bf3aa691c" uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" -version = "0.2.2" +version = "0.2.4" [[deps.IterTools]] git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023" @@ -305,12 +303,10 @@ uuid = "b39eb1a6-c29a-53d7-8c32-632cd16f18da" version = "1.19.3+0" [[deps.LLMMCTS]] -deps = ["GeneralUtils", "JSON3"] -git-tree-sha1 = "d8c653b8fafbd3757b7332985efaf1fdb8b6fe97" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/LLMMCTS" +deps = ["GeneralUtils", "JSON3", "PrettyPrinting"] +path = "/appfolder/app/dev/LLMMCTS" uuid = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" -version = "0.1.2" +version = "0.1.4" [[deps.LaTeXStrings]] git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" @@ -370,9 +366,9 @@ version = "1.11.0" [[deps.LogExpFunctions]] deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "a2d09619db4e765091ee5c6ffe8872849de0feea" +git-tree-sha1 = "13ca9e2586b89836fd20cccf56e57e2b9ae7f38f" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.28" +version = "0.3.29" [deps.LogExpFunctions.extensions] LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -475,7 +471,7 @@ version = "0.3.27+1" [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.1+2" +version = "0.8.1+4" [[deps.OpenSSL]] deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] @@ -493,7 +489,7 @@ version = "3.0.15+1" deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1" uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" -version = "0.5.5+0" +version = "0.5.5+2" [[deps.OrderedCollections]] git-tree-sha1 = "12f1439c4f986bb868acda6ea33ebc78e19b95ad" @@ -502,9 +498,9 @@ version = "1.7.0" [[deps.PDMats]] deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] -git-tree-sha1 = "949347156c25054de2db3b166c52ac4728cbad65" +git-tree-sha1 = "48566789a6d5f6492688279e22445002d171cf76" uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" -version = "0.11.31" +version = "0.11.33" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] @@ -556,15 +552,15 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" version = "1.11.0" [[deps.PtrArrays]] -git-tree-sha1 = "77a42d78b6a92df47ab37e177b2deac405e1c88f" +git-tree-sha1 = "1d36ef11a9aaf1e8b74dacc6a731dd1de8fd493d" uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" -version = "1.2.1" +version = "1.3.0" [[deps.QuadGK]] deps = ["DataStructures", "LinearAlgebra"] -git-tree-sha1 = "cda3b045cf9ef07a08ad46731f5a3165e56cf3da" +git-tree-sha1 = "9da16da70037ba9d701192e27befedefb91ec284" uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" -version = "2.11.1" +version = "2.11.2" [deps.QuadGK.extensions] QuadGKEnzymeExt = "Enzyme" @@ -623,11 +619,9 @@ version = "0.7.0" [[deps.SQLLLM]] deps = ["CSV", "DataFrames", "DataStructures", "Dates", "FileIO", "GeneralUtils", "HTTP", "JSON3", "LLMMCTS", "LibPQ", "PrettyPrinting", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"] -git-tree-sha1 = "45e660e44de0950a5e5f92d467298d8b768b6023" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/SQLLLM" +path = "/appfolder/app/dev/SQLLLM" uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" -version = "0.2.0" +version = "0.2.4" [[deps.SQLStrings]] git-tree-sha1 = "55de0530689832b1d3d43491ee6b67bd54d3323c" @@ -672,9 +666,9 @@ version = "1.11.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "2f5d4697f21388cbe1ff299430dd169ef97d7e14" +git-tree-sha1 = "64cca0c26b4f31ba18f13f6c12af7c85f478cfde" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.4.0" +version = "2.5.0" [deps.SpecialFunctions.extensions] SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -699,16 +693,16 @@ uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" version = "1.7.0" [[deps.StatsBase]] -deps = ["DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] -git-tree-sha1 = "5cf7606d6cef84b543b483848d4ae08ad9832b21" +deps = ["AliasTables", "DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "29321314c920c26684834965ec2ce0dacc9cf8e5" uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.34.3" +version = "0.34.4" [[deps.StatsFuns]] deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] -git-tree-sha1 = "b423576adc27097764a90e163157bcfc9acf0f46" +git-tree-sha1 = "35b09e80be285516e52c9054792c884b9216ae3c" uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" -version = "1.3.2" +version = "1.4.0" [deps.StatsFuns.extensions] StatsFunsChainRulesCoreExt = "ChainRulesCore" diff --git a/Project.toml b/Project.toml index 74ea0a4..afb05a8 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["narawat lamaiin "] version = "0.2.0" [deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -21,7 +22,5 @@ URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] +CSV = "0.10.15" DataFrames = "1.7.0" -GeneralUtils = "0.1, 0.2" -LLMMCTS = "0.1.2" -SQLLLM = "0.2.0" diff --git a/src/interface.jl b/src/interface.jl index 6cfb9c4..65730ca 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -159,68 +159,9 @@ function decisionMaker(a::T; recent::Integer=10 responsedict = similarDecision return responsedict else - systemmsg = - """ - Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s 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: - Your recent events: latest 5 events of the situation - Your Q&A: the question and answer you have asked yourself - - 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. - - Sometimes, the item a user desires might not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - For your information: - - Your store carries only wine. - - Vintage 0 means non-vintage. - - 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 function names: - - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. - - CHECKINVENTORY which you can use to check info about wine you want in your inventory. The input is a search term is verbal english and it should includes - winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, wine price, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. - Invalid query example: red wine that pair well with spicy food. - - 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 when the user has finished their conversation with you, so that you can properly end the conversation. Input is "NA". - 4) Action_input: input of the action - - You should only respond in format as described below: - Thought: ... - Plan: ... - Action_name: ... - Action_input: ... - - Let's begin! - """ - + header = ["Thought:", "Plan:", "Action_name:", "Action_input:"] dictkey = ["thought", "plan", "action_name", "action_input"] - - chathistory = chatHistoryToText(a.chathistory) context = # may b add wine name instead of the hold wine data is better if length(a.memory[:shortmem][:available_wine]) != 0 @@ -244,19 +185,75 @@ function decisionMaker(a::T; recent::Integer=10 end QandA = generatequestion(a, a.func[:text2textInstructLLM]; recent=3) - - usermsg = + systemmsg = """ - $context - Your recent events: $timeline - Your Q&A: $QandA) + 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: + Your recent events: latest 5 events of the situation + Your Q&A: the question and answer you have asked yourself + + 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. + - Sometimes, the item a user desires might not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + + For your information: + - Your store carries only wine. + - Vintage 0 means non-vintage. + + 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 function 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 winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, wine price, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. + Invalid query example: red wine that pair well with spicy food. + - 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 when the user has finished their conversation with you, so that you can properly end the conversation. 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! + $errornote + $context + Your recent events: + $timeline + Your Q&A: + $QandA """ unformatPrompt = [ Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) ] #BUG found wine is "count 0" invalid return from CHECKINVENTORY() @@ -273,7 +270,7 @@ function decisionMaker(a::T; recent::Integer=10 # 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 that 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.", + # :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") @@ -288,7 +285,6 @@ function decisionMaker(a::T; recent::Integer=10 # change qwen format put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt; formatname="qwen") - response = a.func[:text2textInstructLLM](prompt) response = GeneralUtils.remove_french_accents(response) response = replace(response, "**"=>"") @@ -315,11 +311,11 @@ function decisionMaker(a::T; recent::Integer=10 missingkeys = [header[i] for i in zeroind] if 0 ∈ values(detected_kw) errornote = "$missingkeys are missing from your previous response" - println("\nYiemAgent decisionMaker() $errornote:\n $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nYiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif sum(values(detected_kw)) > length(header) - errornote = "Your response has duplicated points" - println("\nYiemAgent decisionMaker() $errornote: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + errornote = "Your previous attempt has duplicated points" + println("\nYiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -360,7 +356,7 @@ function decisionMaker(a::T; recent::Integer=10 # "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 = "Your previous attempt has invalid query" + errornote = "In your previous attempt, action_input for CHECKINVENTORY function is invalid" println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -386,315 +382,516 @@ function decisionMaker(a::T; recent::Integer=10 end end - # if wine is mentioned but not in timeline or shortmem, # then the agent is not supposed to recommend the wine - if responsedict[:action_name] == "CHATBOX" && - isWineInEvent == false - - errornote = "Note: Before recommending a wine, ensure it's in your inventory. Check your stock first." - println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + if isWineInEvent == false + errornote = "You recommended wines that are not in your inventory before. Please only recommend wines that you have previously found in your inventory." + println("\nERROR YiemAgent generatechat() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end end delete!(responsedict, :mentioned_winery) responsedict[:systemmsg] = systemmsg - responsedict[:usermsg] = usermsg responsedict[:unformatPrompt] = unformatPrompt responsedict[:QandA] = QandA - # store responsedict in decisionlog.csv. if it is the first time, create the file - if !isfile("/appfolder/app/decisionlog.csv") - CSV.write(decisionlog, responsedict) - else - CSV.write(decisionlog, responsedict, append=true) + # check whether responsedict[:action_input] is the same as previous dialogue + if responsedict[:action_input] == a.chathistory[end][:text] + errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." + + println("\nERROR YiemAgent generatechat() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue end - - + println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\n$response") return responsedict end error("DecisionMaker failed to generate a thought ", response) end end +# function decisionMaker(a::T; recent::Integer=10 +# ) where {T<:agent} +# # lessonDict = copy(JSON3.read("lesson.json")) -# """ Assigns a scalar value to each new child node to be used for selec- -# tion and backpropagation. This value effectively quantifies the agent's progress in task completion, -# serving as a heuristic to steer the search algorithm towards the most promising regions of the tree. +# # lesson = +# # if isempty(lessonDict) +# # "" +# # else +# # lessons = Dict{Symbol, Any}() +# # for (k, v) in lessonDict +# # lessons[k] = lessonDict[k][:lesson] +# # end -# # Arguments -# - `a::T1` -# one of Yiem's agent -# - `state::T2` -# a game state +# # """ +# # You have attempted to help the user before and failed, either because your reasoning for the +# # recommendation was incorrect or your response did not exactly match the user expectation. +# # The following lesson(s) give a plan to avoid failing to help the user in the same way you +# # did previously. Use them to improve your strategy to help the user. -# # Return -# - `evaluation::Tuple{String, Integer}` -# evaluation and score +# # Here are some lessons in JSON format: +# # $(JSON3.write(lessons)) -# # Example -# ```jldoctest -# julia> -# ``` +# # When providing the thought and action for the current trial, that into account these failed +# # trajectories and make sure not to repeat the same mistakes and incorrect answers. +# # """ +# # end -# # Signature -# """ -# function evaluator(config::T1, state::T2 -# )::Tuple{String,Integer} where {T1<:AbstractDict,T2<:AbstractDict} +# recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) +# recentevents = a.memory[:events][recent_ind] +# timeline = createTimeline(recentevents; eventindex=recent_ind) -# systemmsg = -# """ -# Analyze the trajectories of a solution to a question answering task. The trajectories are -# labeled by environmental observations about the situation, thoughts that can reason about -# the current situation and actions that can be three types: -# 1) CHECKINVENTORY[query], which you can use to find wine in your inventory. -# 2) CHATBOX[text], which you can use to interact with the user. +# # recap as caching +# # query similar result from vectorDB +# recapkeys = keys(a.memory[:recap]) +# _recapkeys_vec = [i for i in recapkeys] -# Given a question and a trajectory, evaluate its correctness and provide your reasoning and -# analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories -# can be correct if the thoughts and actions so far are correct, even if the answer is not found -# yet. Do not generate additional thoughts or actions. Then ending with the correctness score s -# where s is an integer from 0 to 10. +# # 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 -# You should only respond in JSON format as describe below: -# {"evaluation": "your evaluation", "score": "your evaluation score"} - -# Here are some examples: -# user: -# { -# "question": "I'm looking for a sedan with an automatic driving feature.", -# "thought_1": "I have many types of sedans in my inventory, each with diverse features.", -# "thought_2": "But there is only 1 model that has the feature customer wanted.", -# "thought_3": "I should check our inventory first to see if we have it.", -# "action_1": {"name": "inventory", "input": "Yiem model A"}, -# "observation_1": "Yiem model A is in stock." -# } -# assistant -# { -# "evaluation": "This trajectory is correct as it is reasonable to check an inventory for info provided in the question. -# It is also better to have simple searches corresponding to a single entity, making this the best action.", -# "score": 10 -# } - -# user: -# { -# "question": "Do you have an all-in-one pen with 4 colors and a pencil for sale?", -# "thought_1": "Let me check our inventory first to see if I have it.", -# "action_1": {"name": "inventory", "input": "pen with 4 color and a pencil."}, -# "observation_1": "I found {1: "Pilot Dr. grip 4-in-1 pen", 2: "Rotting pencil"}", -# "thought_2": "Ok, I have what the user is asking. Let's tell the user.", -# "action_2": {"name": "CHATBOX", "input": "Yes, we do have a Pilot Dr. grip 4-in-1 pen and a Rotting pencil"}, -# "observation_1": "This is not what I wanted." -# } -# assistant: -# { -# "evaluation": "This trajectory is incorrect as my search term should be related to a 4-colors pen with a pencil in it, -# not a pen and a pencil seperately. A better search term should have been a 4-colors pen with a pencil, all-in-one.", -# "score": 0 -# } - -# Let's begin! -# """ - -# usermsg = """ -# $(JSON3.write(state[:thoughtHistory])) -# """ - -# chathistory = -# [ -# Dict(:name => "system", :text => systemmsg), -# Dict(:name => "user", :text => usermsg) -# ] - -# # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") - -# pprint(prompt) -# externalService = config[:externalservice][:text2textinstruct] - - -# # apply LLM specific instruct format -# externalService = config[:externalservice][:text2textinstruct] - -# msgMeta = GeneralUtils.generate_msgMeta( -# externalService[:mqtttopic], -# senderName="evaluator", -# senderId=string(uuid4()), -# receiverName="text2textinstruct", -# mqttBroker=config[:mqttServerInfo][:broker], -# mqttBrokerPort=config[:mqttServerInfo][:port], -# ) - -# outgoingMsg = Dict( -# :msgMeta => msgMeta, -# :payload => Dict( -# :text => prompt, -# :kwargs => Dict( -# :max_tokens => 512, -# :stop => ["<|eot_id|>"], -# ) -# ) -# ) - -# for attempt in 1:5 -# try -# response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) -# _responseJsonStr = response[:response][:text] -# expectedJsonExample = """ -# Here is an expected JSON format: -# {"evaluation": "...", "score": "..."} -# """ -# responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample) -# evaluationDict = copy(JSON3.read(responseJsonStr)) - -# # check if dict has all required value -# dummya::AbstractString = evaluationDict[:evaluation] -# dummyb::Integer = evaluationDict[:score] - -# return (evaluationDict[:evaluation], evaluationDict[:score]) -# catch e -# io = IOBuffer() -# showerror(io, e) -# errorMsg = String(take!(io)) -# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) -# println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # get recent recap +# _recentrecap = OrderedDict() +# for (k, v) in a.memory[:recap] +# if k ∈ _recentRecapKeys +# _recentrecap[k] = v # end # end -# error("evaluator failed to generate an evaluation") -# end - - -# """ - -# # Arguments -# # Return +# recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) +# # similarDecision = a.func[:similarSommelierDecision](recentrecap) +# similarDecision = nothing #CHANGE -# # Example -# ```jldoctest -# julia> -# ``` +# if similarDecision !== nothing +# responsedict = similarDecision +# return responsedict +# else +# systemmsg = +# """ +# Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s 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 -# # TODO -# - [] update docstring -# - [x] implement the function -# - [x] add try block. check result that it is expected before returning +# 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 -# # Signature -# """ -# function reflector(config::T1, state::T2)::String where {T1<:AbstractDict,T2<:AbstractDict} -# # https://github.com/andyz245/LanguageAgentTreeSearch/blob/main/hotpot/hotpot.py +# 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. -# _prompt = -# """ -# You are a helpful sommelier working for a wine store. -# Your goal is to recommend the best wine from your inventory that match the user preferences. -# You will be given a question and a trajectory of the previous help you've done for a user. -# You were unsuccessful in helping the user either because you guessed the wrong answer with Finish[answer], or you didn't know the user enough. -# In a few sentences, Diagnose a possible reason for failure and devise a new, concise, high level plan that aims to mitigate the same failure. -# Use complete sentences. +# At each round of conversation, the user will gives you with the following information: +# Your recent events: latest 5 events of the situation +# Your Q&A: the question and answer you have asked yourself -# You should only respond in JSON format as describe below: -# {"reflection": "your relection"} +# 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. -# Here are some examples: -# Previous Trial: -# { -# "question": "Hello, I would like a get a bottle of wine", -# "thought_1": "A customer wants to buy a bottle of wine. Before making a recommendation, I need to know more about their preferences.", -# "action_1": {"name": "CHATBOX", "input": "What is the occasion for which you're buying this wine?"}, -# "observation_1": "We are holding a wedding party", +# 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. +# - Sometimes, the item a user desires might not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. -# "thought_2": "A wedding party, that's a great occasion! The customer might be looking for a celebratory drink. Let me ask some more questions to narrow down the options.", -# "action_2": {"name": "CHATBOX", "input": "What type of food will you be serving at the wedding?"}, -# "observation_2": "It will be Thai dishes.", +# For your information: +# - Your store carries only wine. +# - Vintage 0 means non-vintage. -# "thought_3": "With Thai food, I should recommend a wine that complements its spicy and savory flavors. And since it's a celebratory occasion, the customer might prefer a full-bodied wine.", -# "action_3": {"name": "CHATBOX", "input": "What is your budget for this bottle of wine?"}, -# "observation_3": "I would spend up to 50 bucks.", +# 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 function 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 winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, wine price, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. +# Invalid query example: red wine that pair well with spicy food. +# - 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 when the user has finished their conversation with you, so that you can properly end the conversation. Input is "NA". +# 4) Action_input: input of the action -# "thought_4": "Now that I have some more information, it's time to narrow down the options.", -# "action_4": {"name": "winestock", "input": "red wine with full body, pairs well with spicy food, budget \$50"}, -# "observation_4": "I found the following wines in our stock: \n{\n 1: El Enemigo Cabernet Franc 2019\n2: Tantara Chardonnay 2017\n\n}\n", - -# "thought_5": "Now that I have a list of potential wines, I need to know more about the customer's taste preferences.", -# "action_5": {"name": "CHATBOX", "input": "What type of wine characteristics are you looking for? (e.g. tannin level, sweetness, intensity, acidity)"}, -# "observation_5": "I like full-bodied red wine with low tannin.", - -# "thought_6": "Now that I have more information about the customer's preferences, it's time to make a recommendation.", -# "action_6": {"name": "recommendbox", "input": "El Enemigo Cabernet Franc 2019"}, -# "observation_6": "I don't like the one you recommend. I want dry wine." -# } - -# { -# "reflection": "I asked the user about the occasion, food type, and budget, and then searched for wine in the inventory right away. However, I should have asked the user for the specific wine type and their preferences in order to gather more information before making a recommendation." -# } +# You should only respond in format as described below: +# Thought: ... +# Plan: ... +# Action_name: ... +# Action_input: ... # Let's begin! +# """ -# Previous trial: -# $(JSON3.write(state[:thoughtHistory])) -# {"reflection" -# """ +# header = ["Thought:", "Plan:", "Action_name:", "Action_input:"] +# dictkey = ["thought", "plan", "action_name", "action_input"] + +# chathistory = chatHistoryToText(a.chathistory) -# # apply LLM specific instruct format -# externalService = config[:externalservice][:text2textinstruct] -# llminfo = externalService[:llminfo] -# prompt = -# if llminfo[:name] == "llama3instruct" -# formatLLMtext_llama3instruct("system", _prompt) +# 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 -# error("llm model name is not defied yet $(@__LINE__)") +# "" # end -# msgMeta = GeneralUtils.generate_msgMeta( -# a.config[:externalservice][:text2textinstruct][:mqtttopic], -# senderName="reflector", -# senderId=string(uuid4()), -# receiverName="text2textinstruct", -# mqttBroker=config[:mqttServerInfo][:broker], -# mqttBrokerPort=config[:mqttServerInfo][:port], -# ) +# errornote = "" +# response = nothing # placeholder for show when error msg show up -# outgoingMsg = Dict( -# :msgMeta => msgMeta, -# :payload => Dict( -# :text => prompt, -# :kwargs => Dict( -# :max_tokens => 512, -# :stop => ["<|eot_id|>"], -# ) -# ) -# ) +# for attempt in 1:10 +# if attempt > 1 +# println("\nYiemAgent decisionMaker() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# end -# for attempt in 1:5 -# try -# response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) -# _responseJsonStr = response[:response][:text] -# expectedJsonExample = """ -# Here is an expected JSON format: -# {"reflection": "..."} -# """ -# responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample) -# reflectionDict = copy(JSON3.read(responseJsonStr)) +# QandA = generatequestion(a, a.func[:text2textInstructLLM]; recent=3) -# # check if dict has all required value -# dummya::AbstractString = reflectionDict[:reflection] +# usermsg = +# """ +# $context +# Your recent events: $timeline +# Your Q&A: $QandA) +# $errornote +# """ -# return reflectionDict[:reflection] -# catch e -# io = IOBuffer() -# showerror(io, e) -# errorMsg = String(take!(io)) -# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) -# println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# unformatPrompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# Dict(:name => "user", :text => usermsg) +# ] + +# #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 +# prompt = GeneralUtils.formatLLMtext(unformatPrompt; formatname="qwen") + +# response = a.func[:text2textInstructLLM](prompt) +# response = GeneralUtils.remove_french_accents(response) +# response = replace(response, "**"=>"") +# response = replace(response, "***"=>"") +# response = replace(response, "<|eot_id|>"=>"") + +# # 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("\nYiemAgent decisionMaker() $errornote ", @__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 decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# elseif sum(values(detected_kw)) > length(header) +# errornote = "Your previous attempt has duplicated points" +# println("\nYiemAgent 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" +# println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# checkFlag = false +# for i ∈ Symbol.(dictkey) +# if length(responsedict[i]) == 0 +# errornote = "$i is empty" +# println("\nYiemAgent 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("\nYiemAgent 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. +# 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("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# println("\nYiem decisionMaker() ", @__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, response) +# if mentioned_winery != "None" +# mentioned_winery = String.(strip.(split(mentioned_winery, ","))) + +# # check whether the wine is in event +# isWineInEvent = false +# for winename in mentioned_winery +# for event in a.memory[:events] +# if event[:outcome] !== nothing && occursin(winename, event[:outcome]) +# isWineInEvent = true +# break +# end +# end +# end + +# # then the agent is not supposed to recommend the wine +# if isWineInEvent == false +# errornote = "You recommended wines that are not in your inventory before. Please only recommend wines that you have previously found in your inventory." +# println("\nERROR YiemAgent generatechat() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end +# end + +# delete!(responsedict, :mentioned_winery) +# responsedict[:systemmsg] = systemmsg +# responsedict[:usermsg] = usermsg +# responsedict[:unformatPrompt] = unformatPrompt +# responsedict[:QandA] = QandA + +# return responsedict # end +# error("DecisionMaker failed to generate a thought ", response) # end -# error("reflector failed to generate a thought") # end +""" Assigns a scalar value to each new child node to be used for selec- +tion and backpropagation. This value effectively quantifies the agent's progress in task completion, +serving as a heuristic to steer the search algorithm towards the most promising regions of the tree. + +# Arguments + - `state<:AbstractDict` + one of Yiem's agent + - `text2textInstructLLM::Function` + A function that handles communication to LLM service + +# Return + - `score::Integer` + +# Example +```jldoctest +julia> +``` + +# TODO + - [WORKING] Implement the function. + + +# Signature +""" +function evaluator(state::T1, text2textInstructLLM::Function + ) where {T1<:AbstractDict} + + 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. + + + + 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. + + + + Trajectory_evaluation: ... + Answer_evaluation: ... + Accepted_as_answer: ... + Score: ... + Suggestion: ... + + + Let's begin! + """ + + thoughthistory = "" + for (k, v) in state[:thoughtHistory] + thoughthistory *= "$k: $v\n" + end + + errornote = "" + + for attempt in 1:10 + errorFlag = false + + usermsg = + """ + Trajectory: $thoughthistory + Error_note: $errornote + """ + + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + + 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") + + # sometime LLM output something like **Comprehension**: which is not expected + response = replace(response, "**"=>"") + response = replace(response, "***"=>"") + + # 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" + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "\nSQL evaluator() response has duplicated header" + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + + responsedict[:score] = responsedict[:score][1] # some time "6\nThe trajectories are incomplete" is generated but I only need the number. + try + responsedict[:score] = parse(Int, responsedict[:score]) # convert string "5" into integer 5 + catch + continue + end + + accepted_as_answer::AbstractString = responsedict[:accepted_as_answer] + + if accepted_as_answer ∉ ["Yes", "No"] # [PENDING] add errornote into the prompt + error("generated accepted_as_answer has wrong format") + 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] + + # 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("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(Dict(responsedict)) + + return responsedict[:score] + end + error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") +end + """ Chat with llm. @@ -769,6 +966,7 @@ function conversation(a::sommelier, userinput::Dict; maximumMsg=50) event_description="the user talks to the assistant.", timestamp=Dates.now(), subject="user", + actionname="CHATBOX", actioninput=userinput[:text], ) ) @@ -787,7 +985,10 @@ function conversation(a::sommelier, userinput::Dict; maximumMsg=50) end end -function conversation(a::companion, userinput::Dict; maximumMsg=50) +function conversation(a::companion, userinput::Dict; + converPartnerName::Union{String, Nothing}=nothing, + maximumMsg=50) + chatresponse = nothing if userinput[:text] == "newtopic" @@ -802,11 +1003,11 @@ function conversation(a::companion, userinput::Dict; maximumMsg=50) eventdict(; event_description="the user talks to the assistant.", timestamp=Dates.now(), - subject="user", + subject=a.name, actioninput=userinput[:text], ) ) - chatresponse = generatechat(a) + chatresponse = generatechat(a; converPartnerName=converPartnerName) addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) @@ -914,7 +1115,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh if rawresponse !== nothing vd = GeneralUtils.dfToVectorDict(rawresponse) 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) else @@ -943,36 +1144,28 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh end -#[WORKING] function presentbox(a::sommelier, thoughtDict) 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: Additional info: additional information Chat history: your ongoing conversation with the user Wine name: name if wines you found. - - + 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. - - - Dialogue: your wine presentation to the user - - - Dialogue: ... - + 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! """ @@ -1023,12 +1216,18 @@ function presentbox(a::sommelier, thoughtDict) prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") response = a.func[:text2textInstructLLM](prompt) - # sometime the model response like this "here's how I would respond: ..." - if occursin("respond:", response) - errornote = "Your previous response contains 'response:' which is not allowed" + + # 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, '*'=>"") response = replace(response, '$' => "USD") @@ -1276,211 +1475,80 @@ function generatechat(a::sommelier, thoughtDict) end error("generatechat failed to generate a response") end -# function generatechat(a::sommelier, thoughtDict) -# systemmsg = -# """ -# -# Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. -# -# -# You have some thinking in mind while you are talking with the user. -# -# -# Concentrate on your thoughts and articulate them clearly. Keep the conversation remains engaging. -# -# -# - 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. -# - 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. -# -# -# Your ongoing conversation with the user: ... -# Additional info: ... -# Your thoughts: Your current thoughts in your mind -# -# -# - Do not offer additional services you didn't think. -# - Focus on plan. -# -# -# - Focus on the latest conversation. -# - If the user interrupts, prioritize the user -# - Be honest -# - Medium and full-bodied red wines should not be paired with spicy foods. -# -# -# Chat: ... -# -# -# Your ongoing conversation with the user: "user> hello, I need a new car\n" -# 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 to the user." -# Chat: "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! -# """ +#[WORKING] modify it to work with customer object +function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing) -# header = ["Chat:"] -# dictkey = ["chat"] - -# # 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 - -# chathistory = chatHistoryToText(a.chathistory) -# errornote = "" -# response = nothing # placeholder for show when error msg show up - -# yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" -# yourthought1 = nothing - -# for attempt in 1:10 - -# if attempt > 1 # use to prevent LLM generate the same respond over and over -# yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) -# else -# yourthought1 = yourthought -# end - -# usermsg = """ -# -# $chathistory -# -# -# $context -# -# -# $yourthought1 -# -# $errornote -# """ - -# _prompt = -# [ -# Dict(:name => "system", :text => systemmsg), -# Dict(:name => "user", :text => usermsg) -# ] - -# # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") - -# response = a.func[:text2textInstructLLM](prompt) -# # sometime the model response like this "here's how I would respond: ..." -# if occursin("respond:", response) -# errornote = "You don't need to intro your response" -# error("generatechat() response contain : ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# end -# response = GeneralUtils.remove_french_accents(response) -# response = replace(response, '*'=>"") -# response = replace(response, '$' => "USD") -# response = replace(response, '`' => "") -# response = replace(response, "<|eot_id|>"=>"") -# response = GeneralUtils.remove_french_accents(response) - -# # check whether response has all header -# detected_kw = GeneralUtils.detect_keyword(header, response) -# if 0 ∈ values(detected_kw) -# errornote = "\nYiemAgent generatechat() response does not have all header" -# continue -# elseif sum(values(detected_kw)) > length(header) -# errornote = "\nnYiemAgent generatechat() response has duplicated header" -# continue -# end - -# responsedict = GeneralUtils.textToDict(response, header; -# dictKey=dictkey, symbolkey=true) - -# # check if Context: is in chat -# if occursin("Context:", responsedict[:chat]) -# error("Context: is in text. This is not allowed") -# end - -# println("\ngeneratechat() ", @__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[:chat]) -# if mentioned_winery != "None" -# mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - -# # check whether the wine is in event -# isWineInEvent = false -# for winename in mentioned_winery -# for event in a.memory[:events] -# if event[:outcome] !== nothing && occursin(winename, event[:outcome]) -# isWineInEvent = true -# break -# end -# end -# end - -# # if wine is mentioned but not in timeline or shortmem, -# # then the agent is not supposed to recommend the wine -# if isWineInEvent == false - -# errornote = "Previously, You recommend wines that is not in your inventory which is not allowed." -# error("Previously, You recommend wines that is not in your inventory which is not allowed.") -# end -# end - -# result = responsedict[:chat] - -# return result -# end -# error("generatechat failed to generate a response") -# end - - -function generatechat(a::companion) - systemmsg = - if a.systemmsg === nothing - systemmsg = - """ - You are a helpful assistant. - You are currently talking with the user. - Your goal includes: - 1) Help the user as best as you can - - At each round of conversation, you will be given the following information: - Your ongoing conversation with the user: ... - - You should then respond to the user with: - 1) chat: Given the information, what would you say to the user? - - You should only respond in JSON format as described below: - {"chat": ...} - - Let's begin! - """ - else - a.systemmsg + # header = ["Dialogue:"] + # dictkey = ["dialogue"] + + response = nothing # placeholder for show when error msg show up + errornote = "" + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.3, + ) + for attempt in 1:10 + if attempt > 1 + println("\nYiemAgent generatechat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - chathistory = chatHistoryToText(a.chathistory) - response = nothing # placeholder for show when error msg show up - - for attempt in 1:10 - usermsg = """ - Your ongoing conversation with the user: $chathistory - """ - + systemmsg = a.systemmsg * "\nError note: $errornote\n" _prompt = [ Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) ] + 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; formatname="qwen") - response = a.text2textInstructLLM(prompt) + _prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") + prompt = replace(prompt, "|>assistant"=>"|>$(a.name)") + response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs) + response = replace(response, "<|im_start|>"=> "") + + # 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 + + + + #[WORKING] some time it copy exactly the same text as previous conversation partner msg. + + # # 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 end error("generatechat failed to generate a response") @@ -1534,7 +1602,7 @@ function generatequestion(a, text2textInstructLLM::Function; You should then respond to the user with: 1) Thought: State your thought about the current situation - 2) Q: Given the situation, "ask yourself" at least five, but no more than twenty, questions + 2) Q: Given the situation, "ask yourself" at least five, but no more than ten, questions 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: @@ -1550,8 +1618,12 @@ function generatequestion(a, text2textInstructLLM::Function; A: The user is asking for a MPV car with 7-seat Q: What do I know? A: The user is looking for a car with 7-seat. Our dealer sell these kind of cars - Q: What I do not know? - A: I don't know about the user budget, car's color, powertrain and other user's preferences. + Q: What brands the user prefer? + A: I don't know. The user didn't mentioned that. Let's find out. + Q: What else do I need to know before proceeding? + A: I don't know about the user budget, car's color, and other user's preferences yet. Let's find out more about the user's preferences. + Q: I'm still lacking information regarding the user's preferences for the powertrain. I've asked the user twice already, but perhaps they're not familiar with this. What should I do. + A: I'll proceed without asking the user about the powertrain. Q: The user is buying for her husband, should I dig in to get more information? A: Yes, I should. So that I have better idea about the user's preferences. Q: Why the user saying this? @@ -1586,6 +1658,8 @@ function generatequestion(a, text2textInstructLLM::Function; 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. + + Let's begin! """ diff --git a/src/llmfunction.jl b/src/llmfunction.jl index c16bdaf..dbc02f6 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -296,7 +296,7 @@ function checkinventory(a::T1, input::T2 wineattributes_2 = extractWineAttributes_2(a, input) _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" - inventoryquery = "Retrieves winery, wine_name, vintage, region, country, wine_type, grape, serving_temperature, sweetness, intensity, tannin, acidity, tasting_notes, price and currency of wines that match the following criteria - {$_inventoryquery}" + 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], diff --git a/src/type.jl b/src/type.jl index 477eea7..d593d06 100644 --- a/src/type.jl +++ b/src/type.jl @@ -9,11 +9,53 @@ using GeneralUtils abstract type agent end - mutable struct companion <: agent + name::String # agent name id::String # agent id - systemmsg::Union{String, Nothing} + systemmsg::String # system message + tools::Dict # tools maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized + chathistory::Vector{Dict{Symbol, Any}} + memory::Dict{Symbol, Any} + func::NamedTuple # NamedTuple of functions +end + +function companion( + func::NamedTuple # NamedTuple of functions + ; + systemmsg::Union{String, Nothing}= nothing, + name::String= "Assistant", + id::String= string(uuid4()), + maxHistoryMsg::Integer= 20, + chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + ) + + if systemmsg === nothing + systemmsg = + """ + Your name: $name + + 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. + + You should then respond to the user with: + Dialogue: Given the situation, what would you say to the sommelier? + + You should only respond format as described below: + Dialogue: ... + + Let's begin! + """ + end + + tools = Dict( # update input format + "CHATBOX"=> Dict( + :description => "- CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific.", + ), + ) """ Memory Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 @@ -22,45 +64,30 @@ mutable struct companion <: agent Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()), Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()), ] - """ - chathistory::Vector{Dict{Symbol, Any}} - memory::Dict{Symbol, Any} - - # communication function - text2textInstructLLM::Function -end - -function companion( - text2textInstructLLM::Function - ; - id::String= string(uuid4()), - systemmsg::Union{String, Nothing}= nothing, - maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), - ) - memory = Dict{Symbol, Any}( - :chatbox=> "", - :shortmem=> OrderedDict{Symbol, Any}(), - :events=> Vector{Dict{Symbol, Any}}(), - :state=> Dict{Symbol, Any}(), - ) + :events=> Vector{Dict{Symbol, Any}}(), + :state=> Dict{Symbol, Any}(), # state of the agent + :recap=> OrderedDict{Symbol, Any}(), # recap summary of the conversation + ) newAgent = companion( - id, - systemmsg, - maxHistoryMsg, - chathistory, - memory, - text2textInstructLLM - ) + name, + id, + systemmsg, + tools, + maxHistoryMsg, + chathistory, + memory, + func + ) return newAgent end + """ A sommelier agent. # Arguments @@ -134,16 +161,6 @@ mutable struct sommelier <: agent retailername::String tools::Dict maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - - """ 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()), - ] - - """ chathistory::Vector{Dict{Symbol, Any}} memory::Dict{Symbol, Any} func # NamedTuple of functions @@ -170,16 +187,17 @@ function sommelier( :input => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", :output => """Output are wines that match the search query in JSON format.""", ), - # "finalanswer"=> Dict( - # :description => "Useful for when you are ready to recommend wines to the user.", - # :input => """{\"finalanswer\": \"some text\"}.{\"finalanswer\": \"I recommend Zena Crown Vista\"}""", - # :output => "" , - # :func => nothing, - # ), ) + """ 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}( - :chatbox=> "", :shortmem=> OrderedDict{Symbol, Any}( :available_wine=> [], :found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines diff --git a/src/util.jl b/src/util.jl index 41d6270..175480e 100644 --- a/src/util.jl +++ b/src/util.jl @@ -154,11 +154,11 @@ function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String # Loop through each dictionary in the input vector for d in elements # Extract the 'name' and 'text' keys from the dictionary - name = d[:name] + name = titlecase(d[:name]) _text = d[:text] # Append the formatted string to the text variable - text *= "$name:> $_text \n" + text *= "$name> $_text \n" end else # Loop through each dictionary in the input vector @@ -239,19 +239,22 @@ function eventdict(; outcome::Union{String, Nothing}=nothing, note::Union{String, Nothing}=nothing, ) - return Dict{Symbol, Any}( - :event_description=> event_description, - :timestamp=> timestamp, - :subject=> subject, - :thought=> thought, - :actionname=> actionname, - :actioninput=> actioninput, - :location=> location, - :equipment_used=> equipment_used, - :material_used=> material_used, - :outcome=> outcome, - :note=> note, - ) + + d = Dict{Symbol, Any}( + :event_description=> event_description, + :timestamp=> timestamp, + :subject=> subject, + :thought=> thought, + :actionname=> actionname, + :actioninput=> actioninput, + :location=> location, + :equipment_used=> equipment_used, + :material_used=> material_used, + :outcome=> outcome, + :note=> note, + ) + + return d end @@ -281,6 +284,35 @@ timeline = createTimeline(events) # 2) Assistant> Hi there! with a smile """ +# 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, event) in zip(ind, events) +# # If no outcome exists, format without outcome +# if event[:outcome] === nothing +# timeline *= "Event_$i $(event[:subject])> $(event[:actioninput])\n" +# # If outcome exists, include it in formatting +# else +# timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\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 @@ -297,11 +329,13 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin # 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 $(event[:subject])> $(event[:actioninput])\n" + + 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 $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" + timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput]) Action output: $(event[:outcome])\n" end end @@ -311,229 +345,6 @@ end -# """ Convert a single chat dictionary into LLM model instruct format. - -# # Llama 3 instruct format example -# <|system|> -# You are a helpful AI assistant.<|end|> -# <|user|> -# I am going to Paris, what should I see?<|end|> -# <|assistant|> -# Paris, the capital of France, is known for its stunning architecture, art museums."<|end|> -# <|user|> -# What is so great about #1?<|end|> -# <|assistant|> - - -# # Arguments -# - `name::T` -# message owner name e.f. "system", "user" or "assistant" -# - `text::T` - -# # Return -# - `formattedtext::String` -# text formatted to model format - -# # Example -# ```jldoctest -# julia> using Revise -# julia> using YiemAgent -# julia> d = Dict(:name=> "system",:text=> "You are a helpful, respectful and honest assistant.",) -# julia> formattedtext = YiemAgent.formatLLMtext_phi3instruct(d[:name], d[:text]) - -# ``` - -# Signature -# """ -# function formatLLMtext_phi3instruct(name::T, text::T) where {T<:AbstractString} -# formattedtext = -# """ -# <|$name|> -# $text<|end|>\n -# """ - -# return formattedtext -# end - - -# """ Convert a single chat dictionary into LLM model instruct format. - -# # Llama 3 instruct format example -# <|begin_of_text|> -# <|start_header_id|>system<|end_header_id|> -# You are a helpful assistant. -# <|eot_id|> -# <|start_header_id|>user<|end_header_id|> -# Get me an icecream. -# <|eot_id|> -# <|start_header_id|>assistant<|end_header_id|> -# Go buy it yourself at 7-11. -# <|eot_id|> - -# # Arguments -# - `name::T` -# message owner name e.f. "system", "user" or "assistant" -# - `text::T` - -# # Return -# - `formattedtext::String` -# text formatted to model format - -# # Example -# ```jldoctest -# julia> using Revise -# julia> using YiemAgent -# julia> d = Dict(:name=> "system",:text=> "You are a helpful, respectful and honest assistant.",) -# julia> formattedtext = YiemAgent.formatLLMtext_llama3instruct(d[:name], d[:text]) -# "<|begin_of_text|>\n <|start_header_id|>system<|end_header_id|>\n You are a helpful, respectful and honest assistant.\n <|eot_id|>\n" -# ``` - -# Signature -# """ -# function formatLLMtext_llama3instruct(name::T, text::T) where {T<:AbstractString} -# formattedtext = -# if name == "system" -# """ -# <|begin_of_text|> -# <|start_header_id|>$name<|end_header_id|> -# $text -# <|eot_id|> -# """ -# else -# """ -# <|start_header_id|>$name<|end_header_id|> -# $text -# <|eot_id|> -# """ -# end - -# return formattedtext -# end - - - -# """ Convert a chat messages in vector of dictionary into LLM model instruct format. - -# # Arguments -# - `messages::Vector{Dict{Symbol, T}}` -# message owner name e.f. "system", "user" or "assistant" -# - `formatname::T` -# format name to be used - -# # Return -# - `formattedtext::String` -# text formatted to model format - -# # Example -# ```jldoctest -# julia> using Revise -# julia> using YiemAgent -# julia> chatmessage = [ -# Dict(:name=> "system",:text=> "You are a helpful, respectful and honest assistant.",), -# Dict(:name=> "user",:text=> "list me all planets in our solar system.",), -# Dict(:name=> "assistant",:text=> "I'm sorry. I don't know. You tell me.",), -# ] -# julia> formattedtext = YiemAgent.formatLLMtext(chatmessage, "llama3instruct") -# "<|begin_of_text|>\n <|start_header_id|>system<|end_header_id|>\n You are a helpful, respectful and honest assistant.\n <|eot_id|>\n <|start_header_id|>user<|end_header_id|>\n list me all planets in our solar system.\n <|eot_id|>\n <|start_header_id|>assistant<|end_header_id|>\n I'm sorry. I don't know. You tell me.\n <|eot_id|>\n" -# ``` - -# # Signature -# """ -# function formatLLMtext(messages::Vector{Dict{Symbol, T}}, -# formatname::String="llama3instruct") where {T<:Any} -# f = if formatname == "llama3instruct" -# formatLLMtext_llama3instruct -# elseif formatname == "mistral" -# # not define yet -# elseif formatname == "phi3instruct" -# formatLLMtext_phi3instruct -# else -# error("$formatname template not define yet") -# end - -# str = "" -# for t in messages -# str *= f(t[:name], t[:text]) -# end - -# # add <|assistant|> so that the model don't generate it and I don't need to clean it up later -# if formatname == "phi3instruct" -# str *= "<|assistant|>\n" -# end - -# return str -# end - - -# """ - -# Arguments\n -# ----- - -# Return\n -# ----- - -# Example\n -# ----- -# ```jldoctest -# julia> -# ``` - -# TODO\n -# ----- -# [] update docstring -# [PENDING] implement the function - -# Signature\n -# ----- -# """ -# function iterativeprompting(a::T, prompt::String, verification::Function) where {T<:agent} -# msgMeta = GeneralUtils.generate_msgMeta( -# a.config[:externalService][:text2textinstruct], -# senderName= "iterativeprompting", -# senderId= a.id, -# receiverName= "text2textinstruct", -# ) - -# outgoingMsg = Dict( -# :msgMeta=> msgMeta, -# :payload=> Dict( -# :text=> prompt, -# ) -# ) - -# success = nothing -# result = nothing -# critique = "" - -# # iteration loop -# while true -# # send prompt to LLM -# response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) -# error("--> iterativeprompting") -# # check for correctness and get feedback -# success, _critique = verification(response) - -# if success -# result = response -# break -# else -# # add critique to prompt -# critique *= _critique * "\n" -# replace!(prompt, "Critique: ..." => "Critique: $critique") -# end -# end - -# return (success=success, result=result) -# end - - - - - - - -