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 2c976a7..afb05a8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,9 +1,10 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" authors = ["narawat lamaiin "] -version = "0.1.4" +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/fast_inference_guideline.txt b/fast_inference_guideline.txt new file mode 100644 index 0000000..bcf5564 --- /dev/null +++ b/fast_inference_guideline.txt @@ -0,0 +1,72 @@ +To make **LLM-driven inference** fast while maintaining its dynamic capabilities, there are a few practices or approaches to avoid, as they could lead to performance bottlenecks or inefficiencies. Here's what *not* to do: + +--- + +### **1. Avoid Using Overly Large Models for Every Query** +While larger LLMs like GPT-4 provide high accuracy and nuanced responses, they may slow down real-time processing due to their computational complexity. Instead: +- Use distilled or smaller models (e.g., GPT-3.5 Turbo or fine-tuned versions) for faster inference without compromising much on quality. + +--- + +### **2. Avoid Excessive Entity Preprocessing** +Don’t rely on overly complicated preprocessing steps (like advanced NER models or regex-heavy pipelines) to extract entities from the query before invoking the LLM. This could add latency. Instead: +- Design efficient prompts that allow the LLM to extract entities and generate responses simultaneously. + +--- + +### **3. Avoid Asking the LLM Multiple Separate Questions** +Running the LLM for multiple subtasks—for example, entity extraction first and response generation second—can significantly slow down the pipeline. Instead: +- Create prompts that combine tasks into one pass, e.g., *"Identify the city name and generate a weather response for this query: 'What's the weather in London?'"*. + +--- + +### **4. Don’t Overload the LLM with Context History** +Excessively lengthy conversation history or irrelevant context in your prompts can slow down inference times. Instead: +- Provide only the relevant context for each query, trimming unnecessary parts of the conversation. + +--- + +### **5. Avoid Real-Time Dependence on External APIs** +Using external APIs to fetch supplementary data (e.g., weather details or location info) during every query can introduce latency. Instead: +- Pre-fetch API data asynchronously and use the LLM to integrate it dynamically into responses. + +--- + +### **6. Avoid Running LLM on Underpowered Hardware** +Running inference on CPUs or low-spec GPUs will result in slower response times. Instead: +- Deploy the LLM on optimized infrastructure (e.g., high-performance GPUs like NVIDIA A100 or cloud platforms like Azure AI) to reduce latency. + +--- + +### **7. Skip Lengthy Generative Prompts** +Avoid prompts that encourage the LLM to produce overly detailed or verbose responses, as these take longer to process. Instead: +- Use concise prompts that focus on generating actionable or succinct answers. + +--- + +### **8. Don’t Ignore Optimization Techniques** +Failing to optimize your LLM setup can drastically impact performance. For example: +- Avoid skipping techniques like model quantization (reducing numerical precision to speed up inference) or distillation (training smaller models). + +--- + +### **9. Don’t Neglect Response Caching** +While you may not want a full caching system to avoid sunk costs, dismissing lightweight caching entirely can impact speed. Instead: +- Use temporary session-based caching for very frequent queries, without committing to a full-fledged cache infrastructure. + +--- + +### **10. Avoid One-Size-Fits-All Solutions** +Applying the same LLM inference method to all queries—whether simple or complex—will waste processing resources. Instead: +- Route basic queries to faster, specialized models and use the LLM for nuanced or multi-step queries only. + +--- + +### Summary: Focus on Efficient Design +By avoiding these pitfalls, you can ensure that LLM-driven inference remains fast and responsive: +- Optimize prompts. +- Use smaller models for simpler queries. +- Run the LLM on high-performance hardware. +- Trim unnecessary preprocessing or contextual steps. + +Would you like me to help refine a prompt or suggest specific tools to complement your implementation? Let me know! \ No newline at end of file diff --git a/src/interface.jl b/src/interface.jl index 6cfb9c4..33f955c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -97,7 +97,7 @@ julia> output_thoughtDict = Dict( # Signature """ -function decisionMaker(a::T; recent::Integer=10 +function decisionMaker(a::T; recent::Integer=10, maxattempt=10 ) where {T<:agent} # lessonDict = copy(JSON3.read("lesson.json")) @@ -151,7 +151,7 @@ function decisionMaker(a::T; recent::Integer=10 end end - recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) + # recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) # similarDecision = a.func[:similarSommelierDecision](recentrecap) similarDecision = nothing #CHANGE @@ -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 @@ -238,25 +179,87 @@ function decisionMaker(a::T; recent::Integer=10 errornote = "" response = nothing # placeholder for show when error msg show up - for attempt in 1:10 + #[PENDING] add 1) 3 decisions samples 2) compare and choose the best decision (correct tolls etc) + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + ) + for attempt in 1:maxattempt if attempt > 1 - println("\nYiemAgent decisionMaker() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + llmkwargs[:temperature] += 0.1 end - QandA = generatequestion(a, a.func[:text2textInstructLLM]; recent=3) - - usermsg = + QandA = generatequestion(a, a.func[:text2textInstructLLM], timeline) + 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. + - If you are unable to locate the desired item after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + + For your information: + - Your store carries only wine. + - Vintage 0 means non-vintage. + + You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input: + 1) Thought: Articulate your current understanding and consider the current situation. + 2) Plan: Based on the current situation, state a complete action plan to complete the task. Be specific. + 3) Action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the following tool names: + - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. + - CHECKINVENTORY allows you to check information about wines you want in your inventory. The input should be a specific search term in verbal English. A good search term should include details such as price range, winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. + Invalid query example: red wine that pair well with spicy food. + - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. + - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is "NA". + 4) Action_input: The input to the action you are about to perform. This should be aligned with the plan + + + You should only respond in format as described below: + Thought: ... + Plan: ... + Action_name: ... + Action_input: ... + + Let's begin! + $context - Your recent events: $timeline - Your Q&A: $QandA) - $errornote + Your recent events: + $timeline + Your Q&A: + $QandA + P.S. $errornote """ unformatPrompt = [ Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) ] #BUG found wine is "count 0" invalid return from CHECKINVENTORY() @@ -273,7 +276,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") @@ -287,13 +290,13 @@ function decisionMaker(a::T; recent::Integer=10 # end # change qwen format put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt; formatname="qwen") - - response = a.func[:text2textInstructLLM](prompt) + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + response = a.func[:text2textInstructLLM](prompt; senderId=a.id, llmkwargs=llmkwargs) response = GeneralUtils.remove_french_accents(response) response = replace(response, "**"=>"") response = replace(response, "***"=>"") response = replace(response, "<|eot_id|>"=>"") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] count = 0 @@ -304,7 +307,7 @@ function decisionMaker(a::T; recent::Integer=10 end if count > 1 errornote = "You must use only one function" - println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -315,11 +318,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("\nERROR YiemAgent 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("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -328,7 +331,7 @@ function decisionMaker(a::T; recent::Integer=10 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())") + println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -336,7 +339,7 @@ function decisionMaker(a::T; recent::Integer=10 for i ∈ Symbol.(dictkey) if length(responsedict[i]) == 0 errornote = "$i is empty" - println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") checkFlag = true break end @@ -349,7 +352,7 @@ function decisionMaker(a::T; recent::Integer=10 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())") + println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") checkFlag = true break end @@ -360,8 +363,8 @@ 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" - println("\nYiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + errornote = "In your previous attempt, action_input for CHECKINVENTORY function is invalid" + println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -386,316 +389,529 @@ 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 decisionMaker() $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 decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue end + # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println("\n$response") - - + responsedict[:prompt] = prompt 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 + +# header = ["Thought:", "Plan:", "Action_name:", "Action_input:"] +# dictkey = ["thought", "plan", "action_name", "action_input"] -# # TODO -# - [] update docstring -# - [x] implement the function -# - [x] add try block. check result that it is expected before returning - -# # Signature -# """ -# function reflector(config::T1, state::T2)::String where {T1<:AbstractDict,T2<:AbstractDict} -# # https://github.com/andyz245/LanguageAgentTreeSearch/blob/main/hotpot/hotpot.py - -# _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. - -# You should only respond in JSON format as describe below: -# {"reflection": "your relection"} - -# 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", - -# "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.", - -# "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.", - -# "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." -# } - -# Let's begin! - -# Previous trial: -# $(JSON3.write(state[:thoughtHistory])) -# {"reflection" -# """ - -# # 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) +# 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 -# # check if dict has all required value -# dummya::AbstractString = reflectionDict[:reflection] +# 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 -# 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())") +# 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. +# - If you are unable to locate the desired item after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + +# For your information: +# - Your store carries only wine. +# - Vintage 0 means non-vintage. + +# 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! + +# $context +# Your recent events: +# $timeline +# Your Q&A: +# $QandA +# P.S. $errornote +# """ + +# unformatPrompt = +# [ +# Dict(:name => "system", :text => systemmsg), +# ] + +# # 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, "qwen3") +# response = a.func[:text2textInstructLLM](prompt) +# response = GeneralUtils.remove_french_accents(response) +# response = replace(response, "**"=>"") +# response = replace(response, "***"=>"") +# response = replace(response, "<|eot_id|>"=>"") +# response = GeneralUtils.deFormatLLMtext(response, "qwen3") + +# # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] +# count = 0 +# for i ∈ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] +# if occursin(i, response) +# count += 1 +# end +# end +# if count > 1 +# errornote = "You must use only one function" +# println("\nERROR YiemAgent decisionMaker() $errornote\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # check whether response has all header +# detected_kw = GeneralUtils.detect_keyword(header, response) +# kwvalue = [i for i in values(detected_kw)] +# zeroind = findall(x -> x == 0, kwvalue) +# missingkeys = [header[i] for i in zeroind] +# if 0 ∈ values(detected_kw) +# errornote = "$missingkeys are missing from your previous response" +# println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# elseif sum(values(detected_kw)) > length(header) +# errornote = "Your previous attempt has duplicated points" +# println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# responsedict = GeneralUtils.textToDict(response, header; +# dictKey=dictkey, symbolkey=true) + +# if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] +# errornote = "Your previous attempt didn't use the given functions" +# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# checkFlag = false +# for i ∈ Symbol.(dictkey) +# if length(responsedict[i]) == 0 +# errornote = "$i is empty" +# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# checkFlag = true +# break +# end +# end +# checkFlag == true ? continue : nothing + +# # check if there are more than 1 key per categories +# checkFlag = false +# for i ∈ Symbol.(dictkey) +# matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) +# if length(matchkeys) > 1 +# errornote = "Your previous attempt has more than one key per categories" +# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# checkFlag = true +# break +# end +# end +# checkFlag == true ? continue : nothing + +# # check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or +# # "pair well" in it because it is not a valid query. +# detected_kw = GeneralUtils.detect_keyword(["pair", "pairs", "pairing", "well"], responsedict[:action_input]) +# if responsedict[:action_name] == "CHECKINVENTORY" && sum(values(detected_kw)) != 0 +# errornote = "In your previous attempt, action_input for CHECKINVENTORY function is invalid" +# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# 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 decisionMaker() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end +# end + +# delete!(responsedict, :mentioned_winery) +# responsedict[:systemmsg] = systemmsg +# responsedict[:unformatPrompt] = unformatPrompt +# responsedict[:QandA] = QandA + +# # check whether responsedict[:action_input] is the same as previous dialogue +# if responsedict[:action_input] == a.chathistory[end][:text] +# errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." +# println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # println("\n$response") + +# responsedict[:prompt] = prompt +# 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, "qwen3") + + header = ["Trajectory_evaluation:", "Answer_evaluation:", "Accepted_as_answer:", "Score:", "Suggestion:"] + dictkey = ["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"] + + response = text2textInstructLLM(prompt; modelsize="medium", senderId=a.id) + + # sometime LLM output something like **Comprehension**: which is not expected + response = replace(response, "**"=>"") + response = replace(response, "***"=>"") + response = GeneralUtils.deFormatLLMtext(response, "qwen3") + + # check whether response has all header + detected_kw = GeneralUtils.detect_keyword(header, response) + if 0 ∈ values(detected_kw) + errornote = "\nSQL evaluator() response does not have all header" + println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "\nSQL evaluator() response has duplicated header" + println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + + 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("\nERROR Evaluator() ", @__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. # Arguments @@ -769,6 +985,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 +1004,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 +1022,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) @@ -839,7 +1059,7 @@ julia> # Signature """ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} - a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) + # a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) thoughtDict = decisionMaker(a; recent=5) actionname = thoughtDict[:action_name] @@ -883,8 +1103,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh # ) # ) # result = chatresponse - if actionname ∈ ["CHATBOX", "ENDCONVERSATION"] - # chatresponse = generatechat(a, thoughtDict) + if actionname ∈ ["CHATBOX"] push!(a.memory[:events], eventdict(; event_description="the assistant talks to the user.", @@ -896,6 +1115,19 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh ) ) result = actioninput + elseif actionname ∈ ["ENDCONVERSATION"] + chatresponse = generatechat(a, thoughtDict) + push!(a.memory[:events], + eventdict(; + event_description="the assistant talks to the user.", + timestamp=Dates.now(), + subject="assistant", + thought=thoughtDict, + actionname=actionname, + actioninput=chatresponse, + ) + ) + result = chatresponse elseif actionname ∈ ["PRESENTBOX"] chatresponse = presentbox(a, thoughtDict) push!(a.memory[:events], @@ -914,7 +1146,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 @@ -931,8 +1163,8 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh subject= "assistant", thought=thoughtDict, actionname=actionname, - actioninput= "I searched the database with this query: $actioninput", - outcome= "This is what I've found in the database, $result" + actioninput= "I found something in the database using this SQL: $actioninput", + outcome= "This is what I found:, $result" ) ) else @@ -943,36 +1175,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! """ @@ -1000,7 +1224,7 @@ function presentbox(a::sommelier, thoughtDict) if attempt > 1 # use to prevent LLM generate the same respond over and over println("\nYiemAgent presentbox() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - # llmkwargs[:temperature] = 0.1 * attempt + # llmkwargs[:temperature] += 0.1 else # yourthought1 = yourthought end @@ -1020,21 +1244,28 @@ function presentbox(a::sommelier, thoughtDict) ] # 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 = "Your previous response contains 'response:' which is not allowed" + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + #[WORKING] update this function to use new llm + response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + # 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") response = replace(response, '`' => "") response = replace(response, "<|eot_id|>"=>"") response = GeneralUtils.remove_french_accents(response) + response = GeneralUtils.deFormatLLMtext(response, "qwen3") # check whether response has all header detected_kw = GeneralUtils.detect_keyword(header, response) @@ -1082,17 +1313,16 @@ function presentbox(a::sommelier, thoughtDict) # if wine is mentioned but not in timeline or shortmem, # then the agent is not supposed to recommend the wine if isWineInEvent == false - errornote = "Your previous response recommends wines that is not in your inventory which is not allowed" + errornote = "Your previous response recommended wines that is not in your inventory which is not allowed" println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end end result = responsedict[:dialogue] - return result end - error("generatechat failed to generate a response") + error("presentbox() failed to generate a response") end @@ -1164,7 +1394,7 @@ function generatechat(a::sommelier, thoughtDict) end chathistory = chatHistoryToText(a.chathistory) - errornote = "" + errornote = "N/A" response = nothing # placeholder for show when error msg show up yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" @@ -1172,24 +1402,24 @@ function generatechat(a::sommelier, thoughtDict) llmkwargs=Dict( :num_ctx => 32768, - :temperature => 0.1, + :temperature => 0.2, ) for attempt in 1:10 if attempt > 1 # use to prevent LLM generate the same respond over and over println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - llmkwargs[:temperature] = 0.1 * attempt + llmkwargs[:temperature] += 0.1 else yourthought1 = yourthought end usermsg = """ - $errornote Additional info: $context Your ongoing conversation with the user: $chathistory Your thoughts: $yourthought1 + P.S. $errornote """ _prompt = @@ -1199,16 +1429,19 @@ function generatechat(a::sommelier, thoughtDict) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs) + response = GeneralUtils.deFormatLLMtext(response, "qwen3") # 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" println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue elseif occursin("Your thoughts:", response) || occursin("your thoughts:", response) errornote = "You don't need to put 'Your thoughts:' in your response" println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue end response = GeneralUtils.remove_french_accents(response) response = replace(response, '*'=>"") @@ -1242,7 +1475,7 @@ function generatechat(a::sommelier, thoughtDict) continue end - println("\ngeneratechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nYiemAgent generatechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") pprintln(Dict(responsedict)) # check whether an agent recommend wines before checking inventory or recommend wines @@ -1276,219 +1509,88 @@ 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! -# """ - -# 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 +# modify it to work with customer object +function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) + response = nothing # placeholder for show when error msg show up + errornote = "N/A" + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + ) + for attempt in 1:maxattempt + if attempt > 1 + println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - 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 * "\nP.S. $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, a.llmFormatName) + # replace user and assistant with partner name + prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") + prompt = replace(prompt, "|>assistant"=>"|>$(a.name)") + + response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) + response = replace(response, "<|im_start|>"=> "") + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + + # 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") end -function generatequestion(a, text2textInstructLLM::Function; - recent::Integer=5)::String +function generatequestion(a, text2textInstructLLM::Function, timeline)::String systemmsg = """ @@ -1507,7 +1609,6 @@ function generatequestion(a, text2textInstructLLM::Function; 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 info: - Recap: recap of what has happened so far Additional info: ... Your recent events: latest 5 events of the situation @@ -1534,7 +1635,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: "Ask yourself" at least five, but no more than ten, questions about the situation from your perspective. 3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions You must only respond in format as described below: @@ -1550,8 +1651,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? @@ -1576,8 +1681,6 @@ function generatequestion(a, text2textInstructLLM::Function; A: ... Q: Do I have what the user is looking for in our stock? A: ... - Q: Did I introduce what I found in our inventory to the user already? - A: According to my conversation with the user, not yet. Q: Am I certain about the information I'm going to share with the user, or should I verify the information first? A: ... Q: What should I do? @@ -1586,6 +1689,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! """ @@ -1600,46 +1705,45 @@ function generatequestion(a, text2textInstructLLM::Function; "N/A" end - recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) - recentevents = a.memory[:events][recent_ind] - timeline = createTimeline(recentevents; eventindex=recent_ind) + # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) + # recentevents = a.memory[:events][recent_ind] + # timeline = createTimeline(recentevents; eventindex=recent_ind) errornote = "" response = nothing # store for show when error msg show up - recap = - if length(a.memory[:recap]) <= recent - "N/A" - else - recapkeys = keys(a.memory[:recap]) - recapkeys_vec = [i for i in recapkeys] - recapkeys_vec = recapkeys_vec[1:end-recent] - tempmem = OrderedDict() - for (k, v) in a.memory[:recap] - if k ∈ recapkeys_vec - tempmem[k] = v - end - end + # recap = + # if length(a.memory[:recap]) <= recent + # "N/A" + # else + # recapkeys = keys(a.memory[:recap]) + # recapkeys_vec = [i for i in recapkeys] + # recapkeys_vec = recapkeys_vec[1:end-recent] + # tempmem = OrderedDict() + # for (k, v) in a.memory[:recap] + # if k ∈ recapkeys_vec + # tempmem[k] = v + # end + # end - GeneralUtils.dictToString(tempmem) - end + # GeneralUtils.dictToString(tempmem) + # end llmkwargs=Dict( :num_ctx => 32768, - :temperature => 0.2, + :temperature => 0.5, ) for attempt in 1:10 if attempt > 1 println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - llmkwargs[:temperature] = 0.1 * attempt + llmkwargs[:temperature] += 0.1 end usermsg = """ - Recap: $recap) Additional info: $context Your recent events: $timeline - $errornote + P.S. $errornote """ _prompt = @@ -1649,9 +1753,11 @@ function generatequestion(a, text2textInstructLLM::Function; ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") - response = text2textInstructLLM(prompt, modelsize="medium", llmkwargs=llmkwargs) + response = text2textInstructLLM(prompt; + modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, "qwen3") # make sure generatequestion() don't have wine name that is not from retailer inventory # check whether an agent recommend wines before checking inventory or recommend wines # outside its inventory @@ -1674,7 +1780,8 @@ function generatequestion(a, text2textInstructLLM::Function; # 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 mentioned wines that is not in your inventory which is not allowed." + errornote = "Your previous attempt mentioned wines that are not in your inventory which is not allowed." + println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end end @@ -1683,12 +1790,12 @@ function generatequestion(a, text2textInstructLLM::Function; # check for valid response if q_number < 1 - errornote = "Your previous response has too few questions." + errornote = "Your previous attempt has too few questions." println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue # check whether "A1" is in the response, if not error. elseif !occursin("A1:", response) - errornote = "Your previous response does not have A1:" + errornote = "Your previous attempt does not have A1:" println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -1768,7 +1875,7 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: usermsg = """ Total events: $(length(events)) Events timeline: $timeline - $errornote + P.S. $errornote """ _prompt = @@ -1778,9 +1885,10 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") - response = text2textInstructLLM(prompt) + response = text2textInstructLLM(prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, "qwen3") # check whether response has all header detected_kw = GeneralUtils.detect_keyword(header, response) @@ -1849,36 +1957,29 @@ function detectWineryName(a, text) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") - try - response = a.func[:text2textInstructLLM](prompt) - println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(response) + response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, "qwen3") + println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(response) - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - if 0 ∈ values(detected_kw) - errornote = "\nYiemAgent detectWineryName() response does not have all header" - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYiemAgent detectWineryName() response has duplicated header" - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - - result = responsedict[:winery_names] - - return result - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("\n Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # check whether response has all header + detected_kw = GeneralUtils.detect_keyword(header, response) + if 0 ∈ values(detected_kw) + errornote = "\nYiemAgent detectWineryName() response does not have all header" + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "\nYiemAgent detectWineryName() response has duplicated header" + continue end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + + result = responsedict[:winery_names] + + return result end error("detectWineryName failed to generate a response") end diff --git a/src/llmfunction.jl b/src/llmfunction.jl index c16bdaf..aac3629 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -296,13 +296,14 @@ 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], - a.func[:text2textInstructLLM], + a.func[:text2textInstructLLM]; insertSQLVectorDB=a.func[:insertSQLVectorDB], - similarSQLVectorDB=a.func[:similarSQLVectorDB]) + similarSQLVectorDB=a.func[:similarSQLVectorDB], + llmFormatName="qwen3") println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println(textresult) @@ -330,7 +331,8 @@ julia> # Signature """ -function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} +function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 + )::String where {T1<:agent, T2<:AbstractString} systemmsg = """ @@ -358,45 +360,50 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< Occasion: the occasion the user is having the wine for Food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc - You should only respond in format as described below: - Thought: ... - Wine_name: ... - Winery: ... - Vintage: ... - Region: ... - Country: ... - Wine_type: - Grape_varietal: ... - Tasting_notes: ... - Wine_price: ... - Occasion: ... - Food_to_be_paired_with_wine: ... - - Here are some example: - User's query: red, Chenin Blanc, Riesling, 20 USD - {"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red, white", "grape_varietal": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "0-20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} + You should only respond in format as described below: + Thought: ... + Wine_name: ... + Winery: ... + Vintage: ... + Region: ... + Country: ... + Wine_type: + Grape_varietal: ... + Tasting_notes: ... + Wine_price: ... + Occasion: ... + Food_to_be_paired_with_wine: ... + + Here are some example: + User's query: red, Chenin Blanc, Riesling, 20 USD + {"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red, white", "grape_varietal": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "0-20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} - User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot - {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Merlot", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} + User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot + {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Merlot", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} - Let's begin! + Let's begin! """ header = ["Thought:", "Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price:", "Occasion:", "Food_to_be_paired_with_wine:"] dictkey = ["thought", "wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] - errornote = "" + errornote = "N/A" - for attempt in 1:10 - #[WORKING] I should add generatequestion() + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + ) + + for attempt in 1:maxattempt + #[PENDING] I should add generatequestion() if attempt > 1 - println("\nYiemAgent extractWineAttributes_1() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nYiemAgent extractWineAttributes_1() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end usermsg = """ User's query: $input - $errornote + P.S. $errornote """ _prompt = @@ -406,29 +413,33 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") - response = a.func[:text2textInstructLLM](prompt) + prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") + response = a.func[:text2textInstructLLM](prompt; + modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) response = GeneralUtils.remove_french_accents(response) + response = GeneralUtils.deFormatLLMtext(response, "granite3") # check wheter all attributes are in the response checkFlag = false for word in header if !occursin(word, response) - errornote = "$word attribute is missing in previous attempts" - println("Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + errornote = "In your previous attempts, the $word attribute is missing. Please try again." + println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") checkFlag = true break end end checkFlag == true ? continue : nothing - # check whether response has all header + # check whether response has all answer's key points detected_kw = GeneralUtils.detect_keyword(header, response) if 0 ∈ values(detected_kw) - errornote = "\nYiemAgent extractWineAttributes_1() response does not have all header" + errornote = "In your previous attempts, the response does not have all answer's key points" + println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif sum(values(detected_kw)) > length(header) - errornote = "\nYiemAgent extractWineAttributes_1() response has duplicated header" + errornote = "In your previous attempts, the response has duplicated answer's key points" + println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end responsedict = GeneralUtils.textToDict(response, header; @@ -452,8 +463,8 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< if responsedict[:wine_price] != "NA" # check whether wine_price is in ranged number if !occursin('-', responsedict[:wine_price]) - errornote = "wine_price must be a range number" - println("ERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + errornote = "In your previous attempt, the 'wine_price' was not set to a ranged number. Please adjust it accordingly." + println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") checkFlag = true break end @@ -467,8 +478,8 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2< end # price range like 100-100 is not good if minprice == maxprice - errornote = "wine_price with minimum equals to maximum is not valid" - println("ERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + errornote = "In your previous attempt, you inputted 'wine_price' with a 'minimum' value equaling the 'maximum', which is not valid." + println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") checkFlag = true break end @@ -568,14 +579,12 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< 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 or there isn't any, mark with 'NA' to indicate this. Additionally, words like 'any' or 'unlimited' mean no information is available. 2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. 3) Do not generate other comments. - - - + You should then respond to the user with: Sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine. Sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2 Acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine. @@ -584,9 +593,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< Tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3 Intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine. Intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4 - - - + You should only respond in format as described below: Sweetness_keyword: ... Sweetness: ... Acidity_keyword: ... @@ -595,9 +602,8 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< Tannin: ... Intensity_keyword: ... Intensity: ... - - - + + Here are some examples: User's query: I want a wine with a medium-bodied, low acidity, medium tannin. Sweetness_keyword: NA Sweetness: NA @@ -617,7 +623,6 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< Tannin: NA Intensity_keyword: NA Intensity: NA - Let's begin! """ @@ -630,7 +635,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< """ $conversiontable User's query: $input - $errornote + P.S. $errornote """ _prompt = @@ -640,17 +645,20 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") response = a.func[:text2textInstructLLM](prompt) + response = GeneralUtils.deFormatLLMtext(response, "granite3") - # check whether response has all header + # check whether response has all answer's key points detected_kw = GeneralUtils.detect_keyword(header, response) if 0 ∈ values(detected_kw) - errornote = "\nYiemAgent extractWineAttributes_2() response does not have all header" + errornote = "In your previous attempt does not have all answer's key points" + println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue elseif sum(values(detected_kw)) > length(header) - errornote = "\nYiemAgent extractWineAttributes_2() response has duplicated header" + errornote = "In your previous attempt has duplicated answer's key points" + println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -662,8 +670,8 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword value = responsedict[keyword] if value != "NA" && !occursin(value, input) - errornote = "WARNING. Keyword $keyword: $value does not appear in the input. You must use information from the input only" - println("Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + errornote = "In your previous attempt, keyword $keyword: $value does not appear in the input. You must use information from the input only" + println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end @@ -679,7 +687,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< if !occursin("keyword", string(k)) if v !== "NA" && (!occursin('-', v) || length(v) > 5) errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, i.e. min-max." - println("Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end end @@ -755,7 +763,7 @@ function paraphrase(text2textInstructLLM::Function, text::String) for attempt in 1:10 usermsg = """ Text: $text - $errornote + P.S. $errornote """ _prompt = @@ -765,10 +773,11 @@ function paraphrase(text2textInstructLLM::Function, text::String) ] # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") + prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") try response = text2textInstructLLM(prompt) + response = GeneralUtils.deFormatLLMtext(response, "granite3") # 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" @@ -780,13 +789,13 @@ function paraphrase(text2textInstructLLM::Function, text::String) response = replace(response, '`' => "") response = GeneralUtils.remove_french_accents(response) - # check whether response has all header + # check whether response has all answer's key points detected_kw = GeneralUtils.detect_keyword(header, response) if 0 ∈ values(detected_kw) - errornote = "\nYiemAgent paraphrase() response does not have all header" + errornote = "\nYiemAgent paraphrase() response does not have all answer's key points" continue elseif sum(values(detected_kw)) > length(header) - errornote = "\nnYiemAgent paraphrase() response has duplicated header" + errornote = "\nnYiemAgent paraphrase() response has duplicated answer's key points" continue end @@ -984,7 +993,7 @@ end # ] # # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen") +# prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") # prompt *= # """ # <|start_header_id|>assistant<|end_header_id|> diff --git a/src/type.jl b/src/type.jl index 477eea7..31f6156 100644 --- a/src/type.jl +++ b/src/type.jl @@ -9,11 +9,48 @@ 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 + llmFormatName::String +end + +function companion( + func::NamedTuple # NamedTuple of functions + ; + systemmsg::Union{String, Nothing}= nothing, + name::String= "Assistant", + id::String= GeneralUtils.uuid4snakecase(), + maxHistoryMsg::Integer= 20, + chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + llmFormatName::String= "granite3" + ) + + if systemmsg === nothing + systemmsg = + """ + Your name: $name + Your sex: Female + Your role: You are a helpful assistant. + You should follow the following guidelines: + - Focus on the latest conversation. + - Your like to be short and concise. + + Let's begin! + """ + 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 +59,31 @@ 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, + llmFormatName + ) return newAgent end + """ A sommelier agent. # Arguments @@ -134,19 +157,10 @@ 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 + llmFormatName::String end function sommelier( @@ -157,6 +171,7 @@ function sommelier( retailername::String= "retailer_name", maxHistoryMsg::Integer= 20, chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + llmFormatName::String= "granite3" ) tools = Dict( # update input format @@ -170,16 +185,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 @@ -198,7 +214,8 @@ function sommelier( maxHistoryMsg, chathistory, memory, - func + func, + llmFormatName ) return newAgent diff --git a/src/util.jl b/src/util.jl index 41d6270..dd13f97 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 @@ -310,221 +313,34 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin end +# function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing +# ) where {T1<:AbstractVector} +# # Initialize empty timeline string +# timeline = "" + +# # Determine which indices to use - either provided range or full length +# ind = +# if eventindex !== nothing +# [eventindex...] +# else +# 1:length(events) +# end -# """ 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") +# # Iterate through events and format each one +# for (i, event) in zip(ind, events) +# # If no outcome exists, format without outcome +# subject = titlecase(event[:subject]) +# if event[:outcome] === nothing + +# timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput])\n" +# # If outcome exists, include it in formatting +# else +# timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput]) Action output: $(event[:outcome])\n" # end # end - -# return (success=success, result=result) + +# # Return formatted timeline string +# return timeline # end @@ -555,11 +371,6 @@ end - - - - - diff --git a/test/test_1.jl b/test/test_1.jl index 6500feb..f779d5c 100644 --- a/test/test_1.jl +++ b/test/test_1.jl @@ -36,7 +36,12 @@ function executeSQLVectorDB(sql) return result end -function text2textInstructLLM(prompt::String; maxattempt::Integer=2, modelsize::String="medium") +function text2textInstructLLM(prompt::String; maxattempt::Integer=3, modelsize::String="medium", + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.1, + ) + ) msgMeta = GeneralUtils.generate_msgMeta( config[:externalservice][:loadbalancer][:mqtttopic]; msgPurpose="inference", @@ -51,10 +56,7 @@ function text2textInstructLLM(prompt::String; maxattempt::Integer=2, modelsize:: :msgMeta => msgMeta, :payload => Dict( :text => prompt, - :kwargs => Dict( - :num_ctx => 16384, - :temperature => 0.2, - ) + :kwargs => llmkwargs ) ) @@ -195,7 +197,7 @@ function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::In row, col = size(df) distance = row == 0 ? Inf : df[1, :distance] if row == 0 || distance > maxdistance # no close enough SQL stored in the database - recentevents_embedding = a.func[:getEmbedding](recentevents)[1] + recentevents_embedding = getEmbedding(recentevents)[1] recentevents = replace(recentevents, "'" => "") decision_json = JSON3.write(decision) decision_base64 = base64encode(decision_json)