From 5228e2cfe14f5cb2c1dd7bb5c5c85a6b74dcba6e Mon Sep 17 00:00:00 2001 From: narawat lamaiin Date: Wed, 7 Aug 2024 20:41:20 +0700 Subject: [PATCH] update --- Manifest.toml | 4 +- Project.toml | 1 + src/interface BACKUP 2.jl | 1595 +++++++++++++++++++++++++++++++++++++ src/interface.jl | 268 +++++-- src/llmfunction.jl | 60 +- test/etc.jl | 9 + test/runtest.jl | 6 +- 7 files changed, 1831 insertions(+), 112 deletions(-) create mode 100644 src/interface BACKUP 2.jl create mode 100644 test/etc.jl diff --git a/Manifest.toml b/Manifest.toml index 7136e8a..5539e2c 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.10.4" manifest_format = "2.0" -project_hash = "03625e2270b5f9b2a2b6b43af674dcefbd8f4f9d" +project_hash = "bf6c32becbc917fa1c33558e7aa59c1aac5237e3" [[deps.AliasTables]] deps = ["PtrArrays", "Random"] @@ -647,7 +647,7 @@ uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" [[deps.SQLLLM]] -deps = ["CSV", "CondaPkg", "DataFrames", "DataStructures", "Dates", "DispatchDoctor", "FileIO", "FormatCorrector", "GeneralUtils", "HTTP", "JSON3", "LLMMCTS", "LibPQ", "MQTTClient", "PrettyPrinting", "PythonCall", "Random", "Revise", "Tables", "URIs", "UUIDs"] +deps = ["CSV", "CondaPkg", "DataFrames", "DataStructures", "Dates", "DispatchDoctor", "FileIO", "FormatCorrector", "GeneralUtils", "HTTP", "JSON3", "LLMMCTS", "LibPQ", "MQTTClient", "PrettyPrinting", "PythonCall", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"] path = "/appfolder/app/privatejuliapkg/SQLLLM" uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" version = "0.1.0" diff --git a/Project.toml b/Project.toml index a4e358c..f9dfcff 100644 --- a/Project.toml +++ b/Project.toml @@ -18,5 +18,6 @@ PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" diff --git a/src/interface BACKUP 2.jl b/src/interface BACKUP 2.jl new file mode 100644 index 0000000..101b579 --- /dev/null +++ b/src/interface BACKUP 2.jl @@ -0,0 +1,1595 @@ +module interface + +export addNewMessage, conversation, decisionMaker, evaluator, reflector + # isterminal, + +using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, MQTTClient, PrettyPrinting, Serialization +using GeneralUtils, LLMMCTS +using ..type, ..util, ..llmfunction + +# ------------------------------------------------------------------------------------------------ # +# pythoncall setting # +# ------------------------------------------------------------------------------------------------ # +# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252 +# by setting the following variables, PythonCall.jl will use: +# 1. system's python and packages installed by system (via apt install) +# or 2. conda python and packages installed by conda +# if these setting are not set (comment out), PythonCall will use its own python and packages that +# installed by CondaPkg.jl (from env_preparation.jl) +# ENV["JULIA_CONDAPKG_BACKEND"] = "Null" # set condapkg backend = none +# systemPython = split(read(`which python`, String), "\n")[1] # system's python path +# ENV["JULIA_PYTHONCALL_EXE"] = systemPython # find python location with $> which python ex. raw"/root/conda/bin/python" + +# using PythonCall +# const py_agents = PythonCall.pynew() +# const py_llms = PythonCall.pynew() +# function __init__() +# # PythonCall.pycopy!(py_cv2, pyimport("cv2")) + +# # equivalent to from urllib.request import urlopen in python +# PythonCall.pycopy!(py_agents, pyimport("langchain.agents")) +# PythonCall.pycopy!(py_llms, pyimport("langchain.llms")) +# end + +# ---------------------------------------------- 100 --------------------------------------------- # + + +macro executeStringFunction(functionStr, args...) + # Parse the function string into an expression + func_expr = Meta.parse(functionStr) + + # Create a new function with the parsed expression + function_to_call = eval(Expr(:function, + Expr(:call, func_expr, args...), func_expr.args[2:end]...)) + + # Call the newly created function with the provided arguments + function_to_call(args...) +end + + +""" Think and choose action + +# Arguments + - `config::T1` + config + - `state::T2` + a game state + +# Return + - `thoughtDict::Dict` + +# Example +```jldoctest +julia> config = Dict( + :mqttServerInfo => Dict( + :description => "mqtt server info", + :port => 1883, + :broker => "mqtt.yiem.cc" + ), + :externalservice => Dict( + :text2textinstruct => Dict( + :mqtttopic => "/loadbalancer/requestingservice", + :description => "text to text service with instruct LLM", + :llminfo => Dict( + :name => "llama3instruct" + ) + ), + ) + ) + +julia> output_thoughtDict = Dict( + :thought_1 => "The customer wants to buy a bottle of wine. This is a good start!", + :action_1 => Dict{Symbol, Any}( + :action=>"Chatbox", + :input=>"What occasion are you buying the wine for?" + ), + :observation_1 => "" + ) +``` + +# TODO + - [] update docstring + - [x] implement the function + - [] implement RAG to pull similar experience + - [] use customerinfo + - [] user storeinfo + - BUG LLM recommend wine before check inventory + +# Signature +""" +function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} + + # lessonDict = copy(JSON3.read("lesson.json")) + + # lesson = + # if isempty(lessonDict) + # "" + # else + # lessons = Dict{Symbol, Any}() + # for (k, v) in lessonDict + # lessons[k] = lessonDict[k][:lesson] + # end + + # """ + # 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. + + # Here are some lessons in JSON format: + # $(JSON3.write(lessons)) + + # 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 + + # _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 are also keen to improve your recommendation with lesson(s). + + # You must follow the following criteria: + # 1) Get to know how much the user willing to spend + # 2) Get to know type of wine the user is looking for e.g. red, white, sparkling, rose, dessert, fortified + # 3) Get to know what occasion the user is buying wine for + # 4) Get to know what characteristics of wine the user is looking for + # e.g. tannin, sweetness, intensity, acidity + # 5) Get to know what food the user will have with wine + # 6) Check your inventory for the best wine that match the user preference + # 7) Recommend wine to the user + + # You should only respond with interleaving Thought, Action, Observation steps. + # Thought can reason about the current situation, and Action can be three types: + # 1) winestock[query], which you can use to find wine in your inventory. The more input data the better. + # 2) chatbox[text], which you can use to interact with the user. + # After each observation, provide the next Thought and next Action. + + # You should only respond in JSON format as describe below: + # { + # "thought": "your reasoning", + # "action": {"name": "action to take", "input": "action input"}, + # "observation": "result of the action" + # } + + # Here are some examples: + # { + # "question": "I would like to buy a sedan with 8 seats.", + # "thought_1": "Our showroom carries various vehicle model. But I'm not sure whether we have a models that fits the user demand, I need to check our inventory.", + # "action_1": {"name": "inventory", "input": "sedan with 8 seats."}, + # "observation_1": "Several model has 8 seats. Available color are black, red green" + # } + # { + # "thought": "I have a few color for the user to choose from. I will ask him what color he likes.", + # "action": {"name": "chatbox", "input": "Which color do you like?"} + # "observation": "I'll take black." + # } + + # $lesson + + # Let's begin! + + # $(JSON3.write(state[:thoughtHistory])) + # {"thought" + # """ + + # systemmsg = + # """ + # You are a helpful sommelier working for a wine store. + # Your task is to help the user choose the best wine that match the user preferences from your inventory. + # You are also eager to improve your helpfulness. + + # You must follow the following guidelines: + # - Get to know how much the user willing to spend + # - Get to know type of wine the user is looking for e.g. red, white, sparkling, rose, dessert, fortified + # - Get to know what occasion the user is buying wine for + # - Get to know what characteristics of wine the user is looking for e.g. tannin, sweetness, intensity, acidity + # - Get to know what food the user will have with wine + + # At each round of conversation, the user will give you the current situation: + # Context: ... + # Your earlier conversation with the user: ... + + # You should then respond to the user with interleaving Thought, Plan, Action and Observation: + # - thought: + # 1) State your reasoning about the current situation. + # - plan: Based on the current situation, what would you do to complete the task? Be specific. + # - action (Must be aligned with your plan): Can be one of the following functions: + # 1) CHATBOX[text], which you can use to talk with the user. "text" is in verbal English. + # 2) CHECKINVENTORY[query], which you can use to find info about wine in your inventory. "query" is a search term in verbal English. + # - observation: result of the action. + + # You should only respond in format as described below: + # thought: ... + # plan: ... + # action_name: ... + # action_input: ... + # observation: ... + + # Let's begin! + # """ + + # QandA = generatequestion(a, text2textInstructLLM) + + systemmsg = + """ + You are a internet-based, polite sommelier working for an online wine store. + You are currently talking with the user. + Your task is to understand their preferences and then recommend the best wines from your inventory that match those preferences. + + At each round of conversation, you will be given the current situation: + Inventory search result: The result of your inventory search + Your ongoing conversation with the user: ... + + You MUST follow the following guidelines: + - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know which wines your store carries until you check your inventory. + - Use the "understand-then-check" inventory strategy to understand the user, as there are many wines in the inventory. + - After recommending wines to the user, ask if there is anything else you can help with. If the user doesn't need anything else, say thank you and goodbye. + - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar. + + You should follow the following guidelines as you see fit: + - If the user interrupts, prioritize the user. + - If you don't already know, find out the user's budget. + - If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified. + - If you don't already know, find out the occasion for which the user is buying wine. + - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity. + - If you don't already know, find out what food will be served with wine. + - Recommend wines when inventory search result available + + You should then respond to the user with interleaving Thought, Plan, Action: + - thought: + 1) State your reasoning about the current situation. + - plan: Based on the current situation, state a complete plan to complete the task. Be specific. + - action_name (Must be aligned with your plan): Can be one of the following functions: + 1) CHATBOX[text], which you can use to talk with the user. "text" is in verbal English. + 2) CHECKINVENTORY[query], which you can use to check info about wine in your inventory. "query" is a search term in verbal English. + Good query example: black car, a stereo, 200 mile range, electric motor. + Good query example: How many car brand are from Asia? + 3) RECOMMEMDBOX[text], which you can use to recommend wines to the user. This function is better than CHATBOX. "text" is a recommendation. + - action_input: input to the action + - mentioning_wine: Are you mentioning specific wine name to the user? Can be "Yes" or "No" + + You should only respond in format as described below: + thought: ... + plan: ... + action_name: ... + action_input: ... + mentioning_wine: ... + + Let's begin! + """ + + context = + if length(a.memory[:shortmem]) > 0 + x = vectorOfDictToText(a.memory[:shortmem], withkey=false) + x = split(x, "More details:") + y = x[2] + else + "None" + end + chathistory = vectorOfDictToText(a.chathistory) + errornote = "" + response = nothing # placeholder for show when error msg show up + + for attempt in 1:10 + usermsg = + """ + Best matched wines from inventory:: $context + Your conversation with the user: $chathistory) + $errornote + """ + + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + """ + + try + response = a.text2textInstructLLM(prompt) + responsedict = GeneralUtils.textToDict(response, + ["thought", "plan", "action_name", "action_input", "mentioning_wine"], + rightmarker=":", symbolkey=true) + + if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY"] + error("decisionMaker didn't use the given functions ", @__LINE__) + end + + for i ∈ [:thought, :plan, :action_name] + if length(responsedict[i]) == 0 + error("$i is empty ", @__LINE__) + end + end + + # check if there are more than 1 key per categories + for i ∈ [:thought, :plan, :action_name, :action_input, :mentioning_wine] + matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) + if length(matchkeys) > 1 + error("DecisionMaker has more than one key per categories") + end + end + + println("") + println("--> Yiem decisionMaker() ", @__FILE__, " ", @__LINE__) + pprintln(responsedict) + + # check if LLM recommend wine before checking inventory + isMemEmpty = isempty(a.memory[:shortmem]) + if occursin("Yes", responsedict[:mentioning_wine]) && isMemEmpty && + responsedict[:action_name] != "CHECKINVENTORY" + errornote = "Note: You must check your inventory before recommending wine to the user." + error( "You must check your inventory before recommending wine") + else + errornote = "" + end + + delete!(responsedict, :mentioning_wine) + + return responsedict + catch e + io = IOBuffer() + showerror(io, e) + errorMsg = String(take!(io)) + st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) + println("") + println("Attempt $attempt. Error occurred: $errorMsg\n$st") + println("") + end + end + error("DecisionMaker failed to generate a thought ", response) +end +# function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} + +# # lessonDict = copy(JSON3.read("lesson.json")) + +# # lesson = +# # if isempty(lessonDict) +# # "" +# # else +# # lessons = Dict{Symbol, Any}() +# # for (k, v) in lessonDict +# # lessons[k] = lessonDict[k][:lesson] +# # end + +# # """ +# # 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. + +# # Here are some lessons in JSON format: +# # $(JSON3.write(lessons)) + +# # 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 + +# _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 are also keen to improve your recommendation with lesson(s). + +# You must follow the following criteria: +# 1) Get to know how much the user willing to spend +# 2) Get to know type of wine the user is looking for e.g. red, white, sparkling, rose, dessert, fortified +# 3) Get to know what occasion the user is buying wine for +# 4) Get to know what characteristics of wine the user is looking for +# e.g. tannin, sweetness, intensity, acidity +# 5) Get to know what food the user will have with wine +# 6) Check your inventory for the best wine that match the user preference +# 7) Recommend wine to the user + +# You should only respond with interleaving Thought, Action, Observation steps. +# Thought can reason about the current situation, and Action can be three types: +# 1) winestock[query], which you can use to find wine in your inventory. The more input data the better. +# 2) chatbox[text], which you can use to interact with the user. +# After each observation, provide the next Thought and next Action. + +# You should only respond in JSON format as describe below: +# { +# "thought": "your reasoning", +# "action": {"name": "action to take", "input": "action input"}, +# "observation": "result of the action" +# } + +# Here are some examples: +# { +# "question": "I would like to buy a sedan with 8 seats.", +# "thought_1": "Our showroom carries various vehicle model. But I'm not sure whether we have a models that fits the user demand, I need to check our inventory.", +# "action_1": {"name": "inventory", "input": "sedan with 8 seats."}, +# "observation_1": "Several model has 8 seats. Available color are black, red green" +# } +# { +# "thought": "I have a few color for the user to choose from. I will ask him what color he likes.", +# "action": {"name": "chatbox", "input": "Which color do you like?"} +# "observation": "I'll take black." +# } + +# $lesson + +# Let's begin! + +# $(JSON3.write(state[:thoughtHistory])) +# {"thought" +# """ + +# # apply LLM specific instruct format +# externalService = config[:externalservice][:text2textinstruct] +# llminfo = externalService[:llminfo] +# prompt = +# if llminfo[:name] == "llama3instruct" +# formatLLMtext_llama3instruct("system", _prompt) +# else +# error("llm model name is not defied yet $(@__LINE__)") +# end + +# msgMeta = GeneralUtils.generate_msgMeta( +# externalService[:mqtttopic], +# senderName= "decisionMaker", +# 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|>"], +# ) +# ) +# ) +# @show outgoingMsg + +# for attempt in 1:5 +# try +# response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) +# _responseJsonStr = response[:response][:text] +# expectedJsonExample = +# """ +# Here is an expected JSON format: +# { +# "thought": "...", +# "action": {"name": "...", "input": "..."}, +# "observation": "..." +# } +# """ +# responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample) +# thoughtDict = copy(JSON3.read(responseJsonStr)) + +# # check if dict has all required value +# thought::AbstractString = thoughtDict[:thought] +# actionname::AbstractString = thoughtDict[:action][:name] +# actioninput::AbstractString = thoughtDict[:action][:input] +# if actionname ∈ ["winestock", "chatbox", "recommendbox"] +# # LLM use available function +# elseif thought == "" +# error("DecisionMaker has no thought") +# elseif length(actioninput) == 0 +# error("DecisionMaker has no actioninput") +# else +# error("DecisionMaker use wrong function") +# end + +# return thoughtDict +# catch e +# io = IOBuffer() +# showerror(io, e) +# errorMsg = String(take!(io)) +# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) +# println("") +# @warn "Attempt $attempt. Error occurred: $errorMsg\n$st" +# println("") +# end +# end +# error("DecisionMaker 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 + - `a::T1` + one of Yiem's agent + - `state::T2` + a game state + +# Return + - `evaluation::Tuple{String, Integer}` + evaluation and score + +# Example +```jldoctest +julia> +``` + +# Signature +""" +function evaluator(config::T1, state::T2 + )::Tuple{String, Integer} where {T1<:AbstractDict, T2<:AbstractDict} + + 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. + + 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. + + 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 = formatLLMtext(chathistory, "llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + { + """ + + 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("") + @warn "Attempt $attempt. Error occurred: $errorMsg\n$st" + println("") + end + end + error("evaluator failed to generate an evaluation") +end + + +""" + +# Arguments + +# Return + +# Example +```jldoctest +julia> +``` + +# 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. t.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) + 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], + ) + + 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: + {"reflection": "..."} + """ + responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample) + reflectionDict = copy(JSON3.read(responseJsonStr)) + + # check if dict has all required value + dummya::AbstractString = reflectionDict[:reflection] + + 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("") + @warn "Attempt $attempt. Error occurred: $errorMsg\n$st" + println("") + end + end + error("reflector failed to generate a thought") +end + + + + + +# """ Chat with llm. + +# # Arguments +# `a::agent` +# an agent + +# # Return +# None + +# # Example +# ```jldoctest +# julia> using JSON3, UUIDs, Dates, FileIO, MQTTClient, ChatAgent +# julia> const mqttBroker = "mqtt.yiem.cc" +# julia> mqttclient, connection = MakeConnection(mqttBroker, 1883) +# julia> tools=Dict( # update input format +# "askbox"=>Dict( +# :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", +# :input => "Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}", +# :output => "" , +# :func => nothing, +# ), +# ) +# julia> msgMeta = Dict( +# :msgPurpose=> "updateStatus", +# :from=> "agent", +# :to=> "llmAI", +# :requestresponse=> "request", +# :sendto=> "", # destination topic +# :replyTo=> "agent/api/v0.1.0/txt/response", # requester ask responseer to send reply to this topic +# :repondToMsgId=> "", # responseer is responseing to this msg id +# :taskstatus=> "", # "complete", "fail", "waiting" or other status +# :timestamp=> Dates.now(), +# :msgId=> "$(uuid4())", +# ) +# julia> a = ChatAgent.agentReflex( +# "Jene", +# mqttclient, +# msgMeta, +# agentConfigTopic, # I need a function to send msg to config topic to get load balancer +# role=:sommelier, +# tools=tools +# ) +# julia> newAgent = ChatAgent.agentReact(agent) +# julia> response = ChatAgent.conversation(newAgent, "Hi! how are you?") +# ``` + +# # TODO +# - [] update docstring +# - [x] MCTS() for planning +# - [] add recap to initialState for earlier completed question +# - [WORKING] conversation loop + +# # Signature +# """ +# function conversation(a::T, userinput::Dict) where {T<:agent} +# config = deepcopy(a.config) +# pprint(config) +# if userinput[:text] == "newtopic" +# clearhistory(a) +# return "Okay. What shall we talk about?" +# else +# # add usermsg to a.chathistory +# addNewMessage(a, "user", userinput[:text]) + +# if isempty(a.plan[:currenttrajectory]) + +# # initial state +# a.plan[:currenttrajectory] = Dict{Symbol, Any}( +# # deepcopy the info to prevent modifying the info unintentionally during MCTS planning +# :customerinfo=> deepcopy(a.keywordinfo[:customerinfo]), +# :storeinfo=> deepcopy(a.keywordinfo[:storeinfo]), +# :userselect=> nothing, +# :reward=> 0, +# :isterminal=> false, +# :evaluation=> nothing, +# :lesson=> nothing, + +# :totalTrajectoryReward=> nothing, + +# # contain question, thought_1, action_1, observation_1, thought_2, ... +# :thoughtHistory=> OrderedDict{Symbol, Any}( +# #[] :recap=>, +# :question=> userinput[:text], +# ), + +# # store conversation for virtual customer because the virtual customer agent is just +# # a function and stateless. +# :virtualCustomerChatHistory=> Vector{Dict{Symbol, Any}}( +# [Dict(:name=> "user", :text=> userinput[:text])] +# ), +# ) +# else +# _, a.plan[:currenttrajectory] = makeNewState(a.plan[:currenttrajectory], +# a.plan[:activeplan][:thoughtHistory], userinput[:text], userinput[:select], +# userinput[:reward], userinput[:isterminal]) +# end +# end + +# while true +# bestNextState, besttrajectory = LLMMCTS.runMCTS(a.plan[:currenttrajectory], +# transition, config, decisionMaker, evaluator, reflector; +# totalsample=2, maxDepth=3, maxiterations=3, explorationweight=1.0) +# a.plan[:activeplan] = bestNextState + +# latestActionKey, latestActionIndice = +# GeneralUtils.findHighestIndexKey(bestNextState[:thoughtHistory], "action") +# actionname = bestNextState[:thoughtHistory][latestActionKey][:name] +# actioninput = bestNextState[:thoughtHistory][latestActionKey][:input] + +# # transition +# if actionname == "chatbox" +# # add usermsg to a.chathistory +# addNewMessage(a, "assistant", actioninput) +# return actioninput +# elseif actionname == "recommendbox" +# # add usermsg to a.chathistory +# addNewMessage(a, "assistant", actioninput) +# return actioninput +# else +# _, a.plan[:currenttrajectory] = transition(a, a.plan[:currenttrajectory], a.plan[:activeplan]) +# end +# end +# end + + + +""" Chat with llm. + +# Arguments + `a::agent` + an agent + +# Return + None + +# Example +```jldoctest +julia> using JSON3, UUIDs, Dates, FileIO, MQTTClient, ChatAgent +julia> const mqttBroker = "mqtt.yiem.cc" +julia> mqttclient, connection = MakeConnection(mqttBroker, 1883) +julia> tools=Dict( # update input format + "askbox"=>Dict( + :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", + :input => "Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}", + :output => "" , + :func => nothing, + ), + ) +julia> msgMeta = Dict( + :msgPurpose=> "updateStatus", + :from=> "agent", + :to=> "llmAI", + :requestresponse=> "request", + :sendto=> "", # destination topic + :replyTo=> "agent/api/v0.1.0/txt/response", # requester ask responseer to send reply to this topic + :repondToMsgId=> "", # responseer is responseing to this msg id + :taskstatus=> "", # "complete", "fail", "waiting" or other status + :timestamp=> Dates.now(), + :msgId=> "$(uuid4())", +) +julia> a = ChatAgent.agentReflex( + "Jene", + mqttclient, + msgMeta, + agentConfigTopic, # I need a function to send msg to config topic to get load balancer + role=:sommelier, + tools=tools + ) +julia> newAgent = ChatAgent.agentReact(agent) +julia> response = ChatAgent.conversation(newAgent, "Hi! how are you?") +``` + +# TODO + - [] update docstring + - [] add recap to initialState for earlier completed question + +# Signature +""" +function conversation(a::T, userinput::Dict) where {T<:agent} + + # place holder + actionname = nothing + result = nothing + chatresponse = nothing + + if userinput[:text] == "newtopic" + clearhistory(a) + return "Okay. What shall we talk about?" + else + # add usermsg to a.chathistory + addNewMessage(a, "user", userinput[:text]) + + # use dummy memory to check generatechat() for halucination (checking inventory) + for i in 1:3 + actionname, result = think(a) + if actionname == "CHATBOX" + break + end + end + + # thought will be added to chat model via context + chatresponse = generatechat(a.memory, a.chathistory, a.text2textInstructLLM) + + # some time LLM said to user that it (checking inventory) but it is not. + # if chatresponse want to check inventory but think() didn't checkinventory then do it + llmCheckInv = occursin("(check", chatresponse) || occursin("*check", chatresponse) || + occursin("inventory)", chatresponse) || occursin("inventory*", chatresponse) + + if llmCheckInv && actionname != "checkinventory" + actionname, result = forceInventoryCheck(a) + push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result)) + + # generate chatresponse again because we have force inventory check + chatresponse = generatechat(a.memory, a.chathistory, a.text2textInstructLLM) + else + # since chatresponse does not halucinate i.e. no (check inventory), it does not need + # to regenerate again and con be use directly + end + + addNewMessage(a, "assistant", chatresponse) + + return chatresponse + end +end + +""" + +# Arguments + +# Return + +# Example +```jldoctest +julia> +``` + +# TODO + - [] update docstring + - [x] implement the function + - [x] add try block. check result that it is expected before returning + +# Signature +""" +function think(a::T)::NamedTuple{(:actionname, :result), Tuple{String, String}} where {T<:agent} + thoughtDict = decisionMaker(a) + + actionname = thoughtDict[:action_name] + actioninput = thoughtDict[:action_input] + + # map action and input() to llm function + response = + if actionname == "CHATBOX" + (result=actioninput, errormsg=nothing, success=true) + elseif actionname == "CHECKINVENTORY" + checkinventory(a, actioninput) + else + error("undefined LLM function. Requesting $actionname") + end + + # this section allow LLM functions above to have different return values. + result = haskey(response, :result) ? response[:result] : nothing + select = haskey(response, :select) ? response[:select] : nothing + reward::Integer = haskey(response, :reward) ? response[:reward] : 0 + isterminal::Bool = haskey(response, :isterminal) ? response[:isterminal] : false + errormsg::Union{AbstractString, Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing + success::Bool = haskey(response, :success) ? response[:success] : false + + if actionname == "CHATBOX" + a.memory[:chatbox] = result + else + push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result)) + end + + return (actionname=actionname, result=result) +end + + +""" Force to think and check inventory +[TESTING] +""" +function forceInventoryCheck(a::T)::NamedTuple{(:actionname, :result), Tuple{String, String}} where {T<:agent} + println("--> forceInventoryCheck()") + thoughtDict = thinkCheckInventory(a) + actionname = thoughtDict[:action_name] + actioninput = thoughtDict[:action_input] + + # map action and input() to llm function + response = + if actionname == "CHECKINVENTORY" + checkinventory(a, actioninput) + else + error("undefined LLM function. Requesting $actionname") + end + + # this section allow LLM functions above to have different return values. + result = haskey(response, :result) ? response[:result] : nothing + select = haskey(response, :select) ? response[:select] : nothing + reward::Integer = haskey(response, :reward) ? response[:reward] : 0 + isterminal::Bool = haskey(response, :isterminal) ? response[:isterminal] : false + errormsg::Union{AbstractString, Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing + success::Bool = haskey(response, :success) ? response[:success] : false + + return (actionname=actionname, result=result) +end + +""" + [TESTING] +""" +function thinkCheckInventory(a::T)::Dict{Symbol, Any} where {T<:agent} + + systemmsg = + """ + You are a helpful sommelier working for a wine store. + Your task is to help the user choose the best wine that match the user preferences from your inventory. + + Definitions: + - observation: result of the preceding immediate action. + + At each round of conversation, the user will give you the current situation: + Context: ... + Your earlier conversation with the user: ... + + You must follow the following guidelines: + - Check inventory immediately based on what you know about the user. + + You should then respond to the user with interleaving Thought, Plan, Action and Observation: + - thought: + 1) State your reasoning about the current situation. + - plan: Based on the current situation, state a complete plan to complete the task. Be specific. + - action_name (Must be aligned with your plan): Can be one of the following functions: + 1) CHECKINVENTORY[query], which you can use to check info about wine in your inventory. "query" is a search term in verbal English. + Good query example: black car with a stereo, 200 mile range and an electric motor. + Good query example: How many car brand are from Asia? + - action_input: input to the action + + You should only respond in format as described below: + thought: ... + plan: ... + action_name: ... + action_input: ... + + Let's begin! + """ + + usermsg = + """ + Context: None + Your earlier conversation with the user: $(vectorOfDictToText(a.chathistory)) + """ + + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + """ + response = nothing # store for show when error msg show up + for attempt in 1:10 + try + response = a.text2textInstructLLM(prompt) + responsedict = GeneralUtils.textToDict(response, + ["thought", "plan", "action_name", "action_input"], + rightmarker=":", symbolkey=true) + + if responsedict[:action_name] ∉ ["CHECKINVENTORY"] + error("decisionMaker didn't use the given functions ", @__LINE__) + end + + for i ∈ [:thought, :plan, :action_name] + if length(JSON3.write(responsedict[i])) == 0 + error("$i is empty ", @__LINE__) + end + end + + # check if there are more than 1 key per categories + for i ∈ [:thought, :plan, :action_name, :action_input] + matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) + if length(matchkeys) > 1 + error("DecisionMaker has more than one key per categories") + end + end + + return responsedict + catch e + io = IOBuffer() + showerror(io, e) + errorMsg = String(take!(io)) + st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) + println("") + println("Attempt $attempt. Error occurred: $errorMsg\n$st") + println("") + end + end + error("DecisionMaker failed to generate a thought ", response) +end + + +""" + +# Arguments + - `a::T1` + one of ChatAgent's agent. + - `input::T2` +# Return + A JSON string of available wine + +# Example +```jldoctest +julia> +``` + +# TODO + - [] update docs + - [x] implement the function + +# Signature +""" +function generatechat(memory::Dict, chathistory::Vector, text2textInstructLLM::Function) + systemmsg = + """ + You are a website-based, polite sommelier working for an online wine store. + You are currently talking with the user. + Your task is to understand their preferences and then recommend the best wines from your inventory that match those preferences. + + At each round of conversation, you will be given the current situation: + Context: ... + Your ongoing conversation with the user: ... + Your current thoughts in your mind: ... + + You should then respond to the user with: + - chat: Your conversation with the user according to your thoughts. + - mentioning_wine: Are you mentioning specific wine name to the user? Can be "Yes" or "No" + + You should only respond in format as described below: + chat: ... + mentioning_wine: ... + + Let's begin! + """ + + context = + if length(memory[:shortmem]) > 0 + vectorOfDictToText(memory[:shortmem], withkey=false) + else + "None" + end + + chathistory = vectorOfDictToText(chathistory) + errornote = "" + response = nothing # placeholder for show when error msg show up + + for attempt in 1:5 + usermsg = + """ + Context: $context + Your conversation with the user: $chathistory) + Your thoughts: $(memory[:chatbox]) + $errornote + """ + + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + """ + + try + response = text2textInstructLLM(prompt) + responsedict = GeneralUtils.textToDict(response,["chat", "mentioning_wine"], + rightmarker=":", symbolkey=true) + + for i ∈ [:chat] + if length(JSON3.write(responsedict[i])) == 0 + error("$i is empty ", @__LINE__) + end + end + + # check if there are more than 1 key per categories + for i ∈ [:chat] + matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) + if length(matchkeys) > 1 + error("generatechat has more than one key per categories") + end + end + + println("") + println("--> generatechat() ", @__FILE__, " ", @__LINE__) + pprintln(responsedict) + + # check if LLM recommend wine before checking inventory + isMemEmpty = isempty(memory[:shortmem]) + if occursin("Yes", responsedict[:mentioning_wine]) && isMemEmpty + errornote = "Note: You must check your inventory before recommending wine to the user." + error( "You must check your inventory before recommending wine") + else + errornote = "" + end + + memory[:chatbox] = "" # delete content because it no longer used. + delete!(responsedict, :mentioning_wine) + result = responsedict[:chat] + + return result + catch e + io = IOBuffer() + showerror(io, e) + errorMsg = String(take!(io)) + st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) + println("") + println("Attempt $attempt. Error occurred: $errorMsg\n$st") + println("") + end + end + error("generatechat failed to generate an evaluation") +end + + +# function generatequestion(a, text2textInstructLLM::Function)::String + +# systemmsg = +# """ +# You are a helpful sommelier that generate multiple questions about the current situation. + +# At each round of conversation, you will be given the current situation: +# User query: What's the user preferences about wine? +# Your work progress: ... + +# You must follow the following guidelines: +# 1) Ask at least three questions but no more than five. +# 2) Your question should be specific, self-contained and not require any additional context. +# 3) Do not generate any question or comments at the end. + +# You should then respond to the user with: +# - Reasoning: State your detailed reasoning of the current situation +# - Q: Your question +# - A: Your answer to the question. + + +# You must only respond in format as described below: +# Reasoning: ... +# Q 1: ... +# A 1: ... +# Q 2: ... +# A 2: ... +# Q 3: ... +# A 3: ... +# ... + +# Let's begin! +# """ + +# workprogress = "" +# for (k, v) in state[:thoughtHistory] +# if k ∉ [:query] +# workprogress *= "$k: $v\n" +# end +# end + +# usermsg = +# """ +# $(context[:tablelist]) +# User query: $(state[:thoughtHistory][:question]) +# Your work progress: $workprogress +# """ + +# _prompt = +# [ +# Dict(:name=> "system", :text=> systemmsg), +# Dict(:name=> "user", :text=> usermsg) +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") +# prompt *= +# """ +# <|start_header_id|>assistant<|end_header_id|> +# """ +# response = nothing # store for show when error msg show up +# for attempt in 1:10 +# try +# response = text2textInstructLLM(prompt) +# q_number = count("Q ", response) +# if q_number < 3 +# error("too few questions only $q_number questions are generated ", @__FILE__, " ", @__LINE__) +# end +# println("--> generatequestion ", @__FILE__, " ", @__LINE__) +# pprintln(response) +# return response +# catch e +# io = IOBuffer() +# showerror(io, e) +# errorMsg = String(take!(io)) +# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) +# println("") +# println("Attempt $attempt. Error occurred: $errorMsg\n$st") +# println("") +# end +# end +# error("generatequestion failed to generate a thought ", response) +# end + + + + +# """ + +# # Arguments +# - `a::T1` +# one of Yiem's agent +# - `state::T2` +# a game state + +# # Return +# - `evaluation::Tuple{String, Integer}` +# evaluation and score + +# # Example +# ```jldoctest +# julia> +# ``` + +# # TODO +# - [] update docs +# - [] implement the function + +# # Signature +# """ +# function comparer(a::T1, state::T2)::Tuple{String, Integer} where {T1<:agent, T2<:AbstractDict} + +# _prompt = +# """ +# 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) winestock[query], which you can use to find wine in your inventory. +# 2) chatbox[text], which you can use to interact with the user. +# 3) recommendbox[answer], which returns your wine recommendation to the user. + +# 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. + +# You should only respond in JSON format as describe below: +# {"evaluation": "your evaluation", "score": "your evaluation score"} + +# Here are some examples: +# { +# "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." +# } +# {"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 +# } + +# { +# "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." +# } +# {"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!: +# $(JSON3.write(state[:thoughtHistory])) +# {"evaluation" +# """ + +# # apply LLM specific instruct format +# externalService = a.config[:externalservice][:text2textinstruct] +# llminfo = externalService[:llminfo] +# prompt = +# if llminfo[:name] == "llama3instruct" +# formatLLMtext_llama3instruct("system", _prompt) +# else +# error("llm model name is not defied yet $(@__LINE__)") +# end + +# msgMeta = GeneralUtils.generate_msgMeta( +# a.config[:externalservice][:text2textinstruct][:mqtttopic], +# senderName= "evaluator", +# senderId= a.id, +# receiverName= "text2textinstruct", +# mqttBroker= a.config[:mqttServerInfo][:broker], +# mqttBrokerPort= a.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(a, _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("") +# @warn "Attempt $attempt. Error occurred: $errorMsg\n$st" +# println("") +# end +# end +# error("evaluator failed to generate an evaluation") +# end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +end # module interface \ No newline at end of file diff --git a/src/interface.jl b/src/interface.jl index f5c5fd6..d4ef9a6 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -3,7 +3,7 @@ module interface export addNewMessage, conversation, decisionMaker, evaluator, reflector # isterminal, -using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, MQTTClient, PrettyPrinting +using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, MQTTClient, PrettyPrinting, Serialization using GeneralUtils, LLMMCTS using ..type, ..util, ..llmfunction @@ -80,7 +80,7 @@ julia> config = Dict( julia> output_thoughtDict = Dict( :thought_1 => "The customer wants to buy a bottle of wine. This is a good start!", :action_1 => Dict{Symbol, Any}( - :action=>"Chatbox", + :action=>"CHATBOX", :input=>"What occasion are you buying the wine for?" ), :observation_1 => "" @@ -93,7 +93,6 @@ julia> output_thoughtDict = Dict( - [] implement RAG to pull similar experience - [] use customerinfo - [] user storeinfo - - BUG LLM recommend wine before check inventory # Signature """ @@ -143,7 +142,7 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} # You should only respond with interleaving Thought, Action, Observation steps. # Thought can reason about the current situation, and Action can be three types: # 1) winestock[query], which you can use to find wine in your inventory. The more input data the better. - # 2) chatbox[text], which you can use to interact with the user. + # 2) CHATBOX[text], which you can use to interact with the user. # After each observation, provide the next Thought and next Action. # You should only respond in JSON format as describe below: @@ -162,7 +161,7 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} # } # { # "thought": "I have a few color for the user to choose from. I will ask him what color he likes.", - # "action": {"name": "chatbox", "input": "Which color do you like?"} + # "action": {"name": "CHATBOX", "input": "Which color do you like?"} # "observation": "I'll take black." # } @@ -210,42 +209,43 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} # Let's begin! # """ + # QandA = generatequestion(a, text2textInstructLLM) + systemmsg = """ - You are a website-based, attentive, and polite sommelier working for an online wine store. You are currently talking with the user. - Your task is to help the user choose the best wine from your inventory that matches the user preferences. + You are a internet-based, polite sommelier working for an online wine store. + You are currently talking with the user. + Your goal is to recommend the best wines from your inventory that match the user's preferences. + Your current task is to decide what action to take so that you can achieve your goal. - Definitions: - "observation" is result of the preceding immediate action. - - At each round of conversation, the user will give you the current situation: - Context: ... - Your conversation with the user: ... + At each round of conversation, you will be given the current situation: + Your ongoing conversation with the user: ... + I found the best matched wines from inventory: ... You MUST follow the following guidelines: - - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy but you won't know which wines are in stock until you check your inventory. - - Use the "get to know the user's preferences, then check inventory" strategy to help the user, as there are many wines in the inventory. - - After recommending wines to the user, ask if there is anything else you can help with, but do not offer any extra services. If the user doesn't need anything else, say thank you and goodbye. + - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know which wines your store carries until you check your inventory. + - Use the "understand-then-check" inventory strategy to understand the user, as there are many wines in the inventory. - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar. + - After the user chose the wine, end the conversation politely. You should follow the following guidelines as you see fit: - If the user interrupts, prioritize the user. - - Get to know how much the user willing to spend. - - Get to know type of wine the user is looking for e.g. red, white, sparkling, rose, dessert, fortified. - - Get to know what occasion the user is buying wine for. - - Get to know what characteristics of wine the user is looking for e.g. tannin, sweetness, intensity, acidity. - - Get to know what food will be served with wine. + - If you don't already know, find out the user's budget. + - If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified. + - If you don't already know, find out the occasion for which the user is buying wine. + - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity. + - If you don't already know, find out what food will be served with wine. - You should then respond to the user with interleaving Thought, Plan, Action and Observation: + You should then respond to the user with interleaving Thought, Plan, Action: - thought: 1) State your reasoning about the current situation. - plan: Based on the current situation, state a complete plan to complete the task. Be specific. - - action_name (Must be aligned with your plan): Can be one of the following functions: - 1) CHATBOX[text], which you can use to talk with the user. "text" is in verbal English. - 2) CHECKINVENTORY[query], which you can use to check info about wine in your inventory. "query" is a search term in verbal English. + - action_name (Must be aligned with your plan): The name of the action which can be one of the following functions: + 1) CHATBOX [input], which you can use to generate conversation in order to communicate with the user. The input is your intention for the talk. Be specific. + 2) CHECKINVENTORY [input], which you can use to check info about wine in your inventory. The input is a search term in verbal English. Good query example: black car, a stereo, 200 mile range, electric motor. Good query example: How many car brand are from Asia? - - action_input: input to the action + - action_input: input details of the action - mentioning_wine: Are you mentioning specific wine name to the user? Can be "Yes" or "No" You should only respond in format as described below: @@ -257,25 +257,26 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} Let's begin! """ - - # context = length(a.memory[:shortmem]) > 0 ? vectorOfDictToText(a.memory[:shortmem], withkey=false) : "DO not recommending wine because inventory has not been searched yet" + context = if length(a.memory[:shortmem]) > 0 - vectorOfDictToText(a.memory[:shortmem], withkey=false) + x = vectorOfDictToText(a.memory[:shortmem], withkey=false) + x = split(x, "More details:") + y = x[2] + "I have searched the inventory and this is what I found: $y" else - "None" + "" end - chathistory = vectorOfDictToText(a.chathistory) - checkinventory_flag = "" + errornote = "" response = nothing # placeholder for show when error msg show up for attempt in 1:10 usermsg = """ - Context: $context Your conversation with the user: $chathistory) - $checkinventory_flag + $context + $errornote """ _prompt = @@ -296,14 +297,19 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} responsedict = GeneralUtils.textToDict(response, ["thought", "plan", "action_name", "action_input", "mentioning_wine"], rightmarker=":", symbolkey=true) + # if occursin('[', responsedict[:action_name]) + # action_input = GeneralUtils.getStringBetweenCharacters(responsedict[:action_name], '[', ']') + # action_name = string(split(responsedict[:action_name], '[')[1]) + # end - if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY"] - error("decisionMaker didn't use the given functions ", @__LINE__) + if responsedict[:action_name] ∉ ["RECOMMEMDBOX", "CHATBOX", "CHECKINVENTORY"] + errornote = "You must use the given functions" + error("You must use the given functions ", @__FILE__, " ", @__LINE__) end for i ∈ [:thought, :plan, :action_name] if length(responsedict[i]) == 0 - error("$i is empty ", @__LINE__) + error("$i is empty ", @__FILE__, " ", @__LINE__) end end @@ -323,15 +329,19 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent} isMemEmpty = isempty(a.memory[:shortmem]) if occursin("Yes", responsedict[:mentioning_wine]) && isMemEmpty && responsedict[:action_name] != "CHECKINVENTORY" - checkinventory_flag = "Note: You must check your inventory before recommending wine to the user." + errornote = "Note: You must check your inventory before recommending wine to the user." error( "You must check your inventory before recommending wine") else - checkinventory_flag = "" + errornote = "" end delete!(responsedict, :mentioning_wine) - return responsedict + # if length(a.memory[:shortmem]) > 0 && responsedict[:action_name] != "RECOMMEMDBOX" + # errornote = "Note: You have found the best matched wines for the user. Use R them." + # error("found wines but not recommending") + # end + return responsedict catch e io = IOBuffer() showerror(io, e) @@ -390,7 +400,7 @@ end # You should only respond with interleaving Thought, Action, Observation steps. # Thought can reason about the current situation, and Action can be three types: # 1) winestock[query], which you can use to find wine in your inventory. The more input data the better. -# 2) chatbox[text], which you can use to interact with the user. +# 2) CHATBOX[text], which you can use to interact with the user. # After each observation, provide the next Thought and next Action. # You should only respond in JSON format as describe below: @@ -409,7 +419,7 @@ end # } # { # "thought": "I have a few color for the user to choose from. I will ask him what color he likes.", -# "action": {"name": "chatbox", "input": "Which color do you like?"} +# "action": {"name": "CHATBOX", "input": "Which color do you like?"} # "observation": "I'll take black." # } @@ -472,7 +482,7 @@ end # thought::AbstractString = thoughtDict[:thought] # actionname::AbstractString = thoughtDict[:action][:name] # actioninput::AbstractString = thoughtDict[:action][:input] -# if actionname ∈ ["winestock", "chatbox", "recommendbox"] +# if actionname ∈ ["winestock", "CHATBOX", "recommendbox"] # # LLM use available function # elseif thought == "" # error("DecisionMaker has no thought") @@ -562,7 +572,7 @@ function evaluator(config::T1, state::T2 "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"}, + "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: @@ -690,15 +700,15 @@ function reflector(config::T1, state::T2)::String where {T1<:AbstractDict, T2<:A { "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?"}, + "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?"}, + "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?"}, + "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.", @@ -706,7 +716,7 @@ function reflector(config::T1, state::T2)::String where {T1<:AbstractDict, T2<:A "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. t.e.g. tannin level, sweetness, intensity, acidity)"}, + "action_5": {"name": "CHATBOX", "input": "What type of wine characteristics are you looking for? (e.g. t.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.", @@ -898,7 +908,7 @@ end # actioninput = bestNextState[:thoughtHistory][latestActionKey][:input] # # transition -# if actionname == "chatbox" +# if actionname == "CHATBOX" # # add usermsg to a.chathistory # addNewMessage(a, "assistant", actioninput) # return actioninput @@ -980,18 +990,16 @@ function conversation(a::T, userinput::Dict) where {T<:agent} # add usermsg to a.chathistory addNewMessage(a, "user", userinput[:text]) - actionname, result = think(a) #[WORKING] need to wrapped in a while loop to let it think UNTIL it use chatbox. Right now it has unsync bewteen think() and generatechat() - - # -------- use dummy memory to check generatechat() for halucination (checking inventory) -------- # - mem = deepcopy(a.memory) - if actionname == "CHATBOX" - mem[:chatbox] = result - else - push!(mem[:shortmem], Dict(Symbol(actionname)=> result)) + # use dummy memory to check generatechat() for halucination (checking inventory) + for i in 1:3 + actionname, result = think(a) + if actionname == "CHATBOX" + break + end end # thought will be added to chat model via context - chatresponse = generatechat(mem, a.chathistory, a.text2textInstructLLM) + chatresponse = generatechat(a.memory, a.chathistory, a.text2textInstructLLM) # some time LLM said to user that it (checking inventory) but it is not. # if chatresponse want to check inventory but think() didn't checkinventory then do it @@ -1004,14 +1012,7 @@ function conversation(a::T, userinput::Dict) where {T<:agent} # generate chatresponse again because we have force inventory check chatresponse = generatechat(a.memory, a.chathistory, a.text2textInstructLLM) - else - if actionname == "CHATBOX" - # skip - else - push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result)) - end - # since chatresponse does not halucinate i.e. no (check inventory), it does not need # to regenerate again and con be use directly end @@ -1064,6 +1065,12 @@ function think(a::T)::NamedTuple{(:actionname, :result), Tuple{String, String}} errormsg::Union{AbstractString, Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing success::Bool = haskey(response, :success) ? response[:success] : false + if actionname == "CHATBOX" + a.memory[:CHATBOX] = result + else + push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result)) + end + return (actionname=actionname, result=result) end @@ -1217,19 +1224,17 @@ julia> function generatechat(memory::Dict, chathistory::Vector, text2textInstructLLM::Function) systemmsg = """ - You are a website-based, attentive, and polite sommelier working for an online wine store. You are currently talking with the user. - Your task is to help the user choose the best wine from your inventory that matches the user preferences. + You are a website-based, polite sommelier working for an online wine store. + You are currently talking with the user. + Your task is to understand their preferences and then recommend the best wines from your inventory that match those preferences. - At each round of conversation, the user will give you the current situation: + At each round of conversation, you will be given the current situation: Context: ... - Your earlier conversation with the user: ... + Your ongoing conversation with the user: ... Your current thoughts in your mind: ... - You must follow the following guidelines: - - Your thoughts matter. - You should then respond to the user with: - - chat: what do you want to say to the user based on the current situation + - chat: Your conversation with the user according to your thoughts. - mentioning_wine: Are you mentioning specific wine name to the user? Can be "Yes" or "No" You should only respond in format as described below: @@ -1239,18 +1244,27 @@ function generatechat(memory::Dict, chathistory::Vector, text2textInstructLLM::F Let's begin! """ - context_1 = length(memory[:shortmem]) > 0 ? vectorOfDictToText(memory[:shortmem], withkey=false) : "None" + context = + if length(memory[:shortmem]) > 0 + x = vectorOfDictToText(memory[:shortmem], withkey=false) + x = split(x, "More details:") + y = x[2] + "I have searched the inventory and this is what I found: $y" + else + "" + end + chathistory = vectorOfDictToText(chathistory) - checkinventory_flag = "" + errornote = "" response = nothing # placeholder for show when error msg show up for attempt in 1:5 usermsg = """ - Context: $context_1 - Your earlier conversation with the user: $chathistory) - Your thoughts: $(memory[:chatbox]) - $checkinventory_flag + Your conversation with the user: $chathistory) + $context + Your thoughts: $(memory[:CHATBOX]) + $errornote """ _prompt = @@ -1292,14 +1306,14 @@ function generatechat(memory::Dict, chathistory::Vector, text2textInstructLLM::F # check if LLM recommend wine before checking inventory isMemEmpty = isempty(memory[:shortmem]) if occursin("Yes", responsedict[:mentioning_wine]) && isMemEmpty - checkinventory_flag = "Note: You must check your inventory before recommending wine to the user." + errornote = "Note: You must check your inventory before recommending wine to the user." error( "You must check your inventory before recommending wine") else - checkinventory_flag = "" + errornote = "" end + memory[:CHATBOX] = "" # delete content because it no longer used. delete!(responsedict, :mentioning_wine) - result = responsedict[:chat] return result @@ -1317,6 +1331,92 @@ function generatechat(memory::Dict, chathistory::Vector, text2textInstructLLM::F end +# function generatequestion(a, text2textInstructLLM::Function)::String + +# systemmsg = +# """ +# You are a helpful sommelier that generate multiple questions about the current situation. + +# At each round of conversation, you will be given the current situation: +# User query: What's the user preferences about wine? +# Your work progress: ... + +# You must follow the following guidelines: +# 1) Ask at least three questions but no more than five. +# 2) Your question should be specific, self-contained and not require any additional context. +# 3) Do not generate any question or comments at the end. + +# You should then respond to the user with: +# - Reasoning: State your detailed reasoning of the current situation +# - Q: Your question +# - A: Your answer to the question. + + +# You must only respond in format as described below: +# Reasoning: ... +# Q 1: ... +# A 1: ... +# Q 2: ... +# A 2: ... +# Q 3: ... +# A 3: ... +# ... + +# Let's begin! +# """ + +# workprogress = "" +# for (k, v) in state[:thoughtHistory] +# if k ∉ [:query] +# workprogress *= "$k: $v\n" +# end +# end + +# usermsg = +# """ +# $(context[:tablelist]) +# User query: $(state[:thoughtHistory][:question]) +# Your work progress: $workprogress +# """ + +# _prompt = +# [ +# Dict(:name=> "system", :text=> systemmsg), +# Dict(:name=> "user", :text=> usermsg) +# ] + +# # put in model format +# prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") +# prompt *= +# """ +# <|start_header_id|>assistant<|end_header_id|> +# """ +# response = nothing # store for show when error msg show up +# for attempt in 1:10 +# try +# response = text2textInstructLLM(prompt) +# q_number = count("Q ", response) +# if q_number < 3 +# error("too few questions only $q_number questions are generated ", @__FILE__, " ", @__LINE__) +# end +# println("--> generatequestion ", @__FILE__, " ", @__LINE__) +# pprintln(response) +# return response +# catch e +# io = IOBuffer() +# showerror(io, e) +# errorMsg = String(take!(io)) +# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) +# println("") +# println("Attempt $attempt. Error occurred: $errorMsg\n$st") +# println("") +# end +# end +# error("generatequestion failed to generate a thought ", response) +# end + + + # """ @@ -1349,7 +1449,7 @@ end # labeled by environmental observations about the situation, thoughts that can reason about # the current situation and actions that can be three types: # 1) winestock[query], which you can use to find wine in your inventory. -# 2) chatbox[text], which you can use to interact with the user. +# 2) CHATBOX[text], which you can use to interact with the user. # 3) recommendbox[answer], which returns your wine recommendation to the user. # Given a question and a trajectory, evaluate its correctness and provide your reasoning and @@ -1381,7 +1481,7 @@ end # "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"}, +# "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." # } # {"evaluation": "This trajectory is incorrect as my search term should be related to a 4-colors pen with a pencil in it, diff --git a/src/llmfunction.jl b/src/llmfunction.jl index f39531f..815c5ed 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -345,6 +345,7 @@ function extractWineAttributes_1(a::T1, input::T2 - occasion: ... - food_to_be_paired_with_wine: food that the user will be served with wine - country: wine's country of origin + - region: wine's region of origin such as Burgundy, Napa Valley - grape variety: a single name of grape used to make wine. - flavors: Names of items that the wine tastes like. - aromas: wine's aroma @@ -356,6 +357,7 @@ function extractWineAttributes_1(a::T1, input::T2 occasion: ... food_to_be_paired_with_wine: ... country: ... + region: ... grape_variety: ... flavors: ... aromas: ... @@ -363,29 +365,29 @@ function extractWineAttributes_1(a::T1, input::T2 Let's begin! """ - # chathistory = vectorOfDictToText(a.chathistory) - - usermsg = - """ - User's query: $input - """ - - _prompt = - [ - Dict(:name=> "system", :text=> systemmsg), - Dict(:name=> "user", :text=> usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") - prompt *= - """ - <|start_header_id|>assistant<|end_header_id|> - """ - - attributes = ["reasoning", "wine_type", "price", "occasion", "food_to_be_paired_with_wine", "country", "grape_variety", "flavors", "aromas"] + attributes = ["reasoning", "wine_type", "price", "occasion", "food_to_be_paired_with_wine", "country", "region", "grape_variety", "flavors", "aromas"] errornote = "" for attempt in 1:5 + + usermsg = + """ + User's query: $input + $errornote + """ + + _prompt = + [ + Dict(:name=> "system", :text=> systemmsg), + Dict(:name=> "user", :text=> usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct") + prompt *= + """ + <|start_header_id|>assistant<|end_header_id|> + """ + try response = a.text2textInstructLLM(prompt) responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) @@ -397,8 +399,20 @@ function extractWineAttributes_1(a::T1, input::T2 end #[PENDING] check if grape_variety has more than 1 name - if length(split(responsedict[:grape_variety], ",")) > 1 - error("multiple name in grape_variety is not allowed") + x = length(split(responsedict[:grape_variety], ",")) * length(split(responsedict[:grape_variety], "/")) + if x > 1 + errornote = "only a single name in grape_variety is allowed" + error("only a single grape_variety name is allowed") + end + x = length(split(responsedict[:country], ",")) * length(split(responsedict[:country], "/")) + if x > 1 + errornote = "only a single name in country is allowed" + error("only a single country name is allowed") + end + x = length(split(responsedict[:region], ",")) * length(split(responsedict[:region], "/")) + if x > 1 + errornote = "only a single name in region is allowed" + error("only a single region name is allowed") end responsedict[:flavors] = replace(responsedict[:flavors], "notes"=>"") diff --git a/test/etc.jl b/test/etc.jl new file mode 100644 index 0000000..6a8159a --- /dev/null +++ b/test/etc.jl @@ -0,0 +1,9 @@ +using GeneralUtils + +response = "trajectory_evaluation:\nThe trajectory is correct so far. The thought accurately reflects the user's question, and the action taken is a valid attempt to retrieve data from the database that matches the specified criteria.\n\nanswer_evaluation:\nThe observation provides information about two red wines from Bordeaux rive droite in France, which partially answers the question. However, it does not provide a complete answer as it only lists the wine names and characteristics, but does not explicitly state whether there are any other wines that match the criteria.\n\naccepted_as_answer: No\n\nscore: 6\nThe trajectory is mostly correct, but the observation does not fully address the question.\n\nsuggestion: Consider adding more filters or parameters to the database query to retrieve a complete list of wines that match the specified criteria." + +responsedict = GeneralUtils.textToDict(response, + ["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"], + rightmarker=":", symbolkey=true) + + diff --git a/test/runtest.jl b/test/runtest.jl index c1eb0b4..e4abf2f 100644 --- a/test/runtest.jl +++ b/test/runtest.jl @@ -88,9 +88,9 @@ end main() """ -I'm having a graduation party this evening. I have unlimited budget. I want a bottle of dry red wine. -It will be a casual party with no food serving. -I'm open to suggestion since I have no specific idea about wine other than I like full bodied wine from France. +I'm joining a graduation party this evening. I have unlimited budget. I want a bottle of dry red wine. +Well, it is a small casual party with close friend and no food serving. +I'm open to suggestion since I have no specific idea about wine but I like full bodied wine from France. The latter one seems nice. """