diff --git a/Manifest.toml b/Manifest.toml index 74eb80b..53b840d 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "6e0efa362e5748de9ce219e0376be2f93d464376" +project_hash = "f82baf5953223c6185bd47af518fd540515402d1" [[deps.Accessors]] deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] @@ -268,19 +268,24 @@ deps = ["Random"] uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" version = "1.11.0" +[[deps.Gamma]] +git-tree-sha1 = "86f86b6168a016ed88e4ae4e64577b98c3b59e8e" +uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b" +version = "1.1.0" + [[deps.GeneralUtils]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "JSON", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "UUIDs"] -git-tree-sha1 = "f1bad7621e6ac2d235adc8c593b0aff87bd1d93e" +deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "HTTP", "JSON", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "UUIDs"] +git-tree-sha1 = "7c0600c166a5deb2c607018a491c04eb25969c2e" repo-rev = "main" repo-url = "https://git.yiem.cc/ton/GeneralUtils" uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -version = "0.4.2" +version = "0.4.9" [[deps.HTTP]] deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] -git-tree-sha1 = "69343dd8afb1671b84c3aa2dda511238d0919a55" +git-tree-sha1 = "eda1d37cb55d90a17d0957c75841138c88b361a1" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "2.5.0" +version = "2.5.4" [[deps.HashArrayMappedTries]] git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" @@ -288,10 +293,10 @@ uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" version = "0.2.0" [[deps.HypergeometricFunctions]] -deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] -git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" +deps = ["Gamma", "LinearAlgebra"] +git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10" uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" -version = "0.3.28" +version = "0.3.29" [[deps.ICU_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -401,9 +406,11 @@ version = "1.21.3+0" [[deps.LLMMCTS]] deps = ["GeneralUtils", "JSON", "PrettyPrinting"] -path = "../LLMMCTS" +git-tree-sha1 = "6b4f123b03c0fcce5b21c0dbcb947e8dd23f333a" +repo-rev = "main" +repo-url = "https://git.yiem.cc/ton/LLMMCTS" uuid = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" -version = "0.1.4" +version = "0.1.5" [[deps.LaTeXStrings]] git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" @@ -727,9 +734,9 @@ version = "0.5.1+0" [[deps.Roots]] deps = ["Accessors", "CommonSolve", "Printf"] -git-tree-sha1 = "91cfb1cb4f6e27557cc2df798a31eff6089a41eb" +git-tree-sha1 = "ed45bcc7cf3c8887595b973f2b1efbe91dcc50ec" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "3.0.0" +version = "3.0.1" [deps.Roots.extensions] RootsChainRulesCoreExt = "ChainRulesCore" diff --git a/Project.toml b/Project.toml index 8522217..cce2e81 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "SQLLLM" uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" -version = "0.2.4" +version = "0.2.5" authors = ["narawat lamaiin "] [deps] @@ -24,5 +24,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] Dates = "1.11.0" -GeneralUtils = "0.4.2" +GeneralUtils = "0.4.0 - 0.9.0" JSON = "1.6.1" +LLMMCTS = "0.1.5" diff --git a/README.md b/README.md index e69de29..e2ee34b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core b/core deleted file mode 100644 index 1358e6e..0000000 Binary files a/core and /dev/null differ diff --git a/src/interface.jl b/src/interface.jl index d357542..0803f2f 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -34,7 +34,7 @@ julia> state = Dict( "reward" => 0, "evaluation" => "None", "accepted_as_answer" => "No", - "thoughtHistory" => OrderedDict{String, Any}("question" => "How many wines do you have that can be paired with lamb?"), + "action_history" => OrderedDict{String, Any}("question" => "How many wines do you have that can be paired with lamb?"), "evaluationscore" => 0, "suggestion" => "None" ) @@ -102,141 +102,70 @@ Dict( # Signature """ -function decisionMaker(state::T1, additionalinfo, text2textInstructLLM::Function, llmFormatName::String +function decisionMaker(state::T1, text2textInstructLLM::Function, llmFormatName::String ; querySQLVectorDBF::Union{T2, Nothing}=nothing, maxattempt=10 )::Dict{String, Any} where {T1<:AbstractDict, T2<:Function} - systemmsg = - """ - You are a helpful assistant that find the data from a database to satisfy the user's question. - You are working under your mentor supervision and you are also eager to improve your helpfulness. + requiredKeys = ["plan", "action_name", "action_input"] + errornote = "" + # provide similar sql only for the first attempt + sql, distance = querySQLVectorDBF(state["question"]) + similarSQL_ = sql !== nothing ? sql : "None" + context = + """ + + $(GeneralUtils.dict_to_string_html(state["context"])) + + $similarSQL_ + + + $(GeneralUtils.dict_to_string_html(state["action_history"])) + + + $errornote + + + """ - For your information: - - Observation: Result of the immediately preceding action - - At each round of conversation, you will be given the following information: - context: additional information about the current situation - - You must follow the following guidelines: - - Keep SQL queries focused only on the provided information. - - You should follow the following guidelines: - - Do not create any table in the database - - A junction table can be used to link tables together. Another use case is for filtering data. - - If you can't find a single table that can be used to answer the user's query, try joining multiple tables to see if you can obtain the answer. - - Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search. - - If there is no search result from the database, remove the restrictive criteria until a search result is available, and proceed from there. - - You should then respond to the user with interleaving Comprehension, Plan, Action_name, Action_input: - 1) plan: Given the current circumstances, outline a detailed, step-by-step plan to accomplish the task. Be specific. - 2) action_name: (Typically corresponds to the execution of the first step in your plan) - Can be one of the following function names: - - 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 ';'. - 3) action_input: Input to the action - - You should only respond in JSON format as described below: - { - "plan": "...", - "action_name": "...", - "action_input": "..." - } - - Let's begin! - """ - requiredKeys = [:plan, :action_name, :action_input] - workprogress = "" - for (k, v) in state["thoughtHistory"] - if k ∉ ["question"] - workprogress *= "$k: $v\n" + # add context to text of the latest message (in the front). + # use for loop because in openai format, each msg may contain both text and image. + + for d in state["chathistory"][end]["content"] + if d["type"] == "text" + d["text"] = context * d["text"] + break end end - + response = nothing # store for show when error msg show up - errornote = "N/A" - - # provide similar sql only for the first attempt -similarSQL_ = "None" - if length(state["thoughtHistory"]) == 1 - sql, distance = querySQLVectorDBF(state["thoughtHistory"]["question"]) - similarSQL_ = sql !== nothing ? sql : "None" - end - for attempt in 1:maxattempt + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => state["chathistory"], + "temperature" => 0.7 + ) + response = text2textInstructLLM("random_id", msg) + response = GeneralUtils.clean_json_response(response) - # QandA = generatequestion(state, context, text2textInstructLLM, llmFormatName; similarSQL=similarSQL_) - - context = - """ - - This is schema of tables in the database: - $(additionalinfo[:tablelist]) - - The closest known SQL for this question is: - $similarSQL_ - - This is the query result when executing the most_relevant_SQL against a database. You can use this to see how the data are stored. - winery: Chateau Montelena, wine_name: The Montelena Estate Cabernet Sauvignon, wine_id: 97264f71-007c-4cce-a3fe-2cc88fba4d05, vintage: 2017, region: Napa Valley, country: United States, wine_type: red, grape: Cabernet Sauvignon, serving_temperature: 15 to 18 Celsius, sweetness: 1, intensity: 5, tannin: 4, acidity: 4, tasting_notes: oak, vanilla, tobacco, blackberry, plum, black cherry, leather, earthy, smoke, price: 19.95, currency: USD - - your work progress so far: - $workprogress - - This is your mentor's suggestion for the immediately preceding action and observation -$(state["suggestion"]) - - P.S. $errornote - - """ - - unformatPrompt = - [ - Dict(:name => "system", :text => systemmsg), - Dict("name" => "user", "text" => state["thoughtHistory"]["question"]) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, llmFormatName) - # add info - prompt = prompt * context - response = text2textInstructLLM(prompt) - response = GeneralUtils.deFormatLLMtext(response, llmFormatName) think, response = GeneralUtils.extractthink(response) - - # if occursin("NULL", response) - # errornote = "\nYour previous attempt contain NULL. It is not allowed in your response" - # println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - responsedict = nothing - try - responsedict = copy(JSON.parse(response)) + try + _responsedict = JSON.parse(response) + responsedict = GeneralUtils.dictify(_responsedict, keytype=String, sort_order=requiredKeys) catch - println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end - delete!(responsedict, "observation") - # remove backticks Error occurred: MethodError: no method matching occursin(::String, ::Vector{String}) if occursin("```", responsedict["action_input"]) sql = GeneralUtils.extract_triple_backtick_text(responsedict["action_input"])[1] @@ -249,7 +178,7 @@ $(state["suggestion"]) responsedict["action_input"] = sql end - toollist = ["TABLEINFO", "RUNSQL"] + toollist = ["RUNSQL"] if responsedict["action_name"] ∉ toollist errornote = "Your previous attempt has action_name that is not in the tool list" println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> $(responsedict["action_name"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -263,284 +192,16 @@ $(state["suggestion"]) continue end end - - # for i ∈ Symbol.(dictkey) - # if length(JSON3.write(responsedict[i])) == 0 - # errornote = "Your previous attempt has empty value for $i" - # println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - # end - - println("\nSQLLLM decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(Dict(responsedict)) - - # store for later training -responsedict["thoughthistory"] = state["thoughtHistory"] - responsedict["system"] = systemmsg - responsedict["prompt"] = prompt - responsedict["context"] = context - responsedict["think"] = think - - # # read sessionId - # sessionid = JSON.parse("/appfolder/app/sessionid.json") - # # save to filename ./log/decisionlog.txt - # println("saving SQLLLM decisionMaker() to disk") - # filename = "agent_decision_log_$(sessionid[:id]).json" - # filepath = "/appfolder/app/log/$filename" - # # check whether there is a file path exists before writing to it - # if !isfile(filepath) - # decisionlist = [responsedict] - # println("Creating file $filepath") - # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) - # end - # else - # # read the file and append new data - # decisionlist = copy(JSON.parse(filepath)) - # push!(decisionlist, responsedict) - # println("Appending new data to file $filepath") - # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) - # end - # end - + + # println("\nSQLLLM decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # pprintln(responsedict) + # println("---") + return responsedict end error("SQLLLM DecisionMaker() failed to generate a thought \n", response) end -# function decisionMaker(state::T1, context, text2textInstructLLM::Function, llmFormatName::String -# ; querySQLVectorDBF::Union{T2, Nothing}=nothing, maxattempt=10 -# )::Dict{String, Any} where {T1<:AbstractDict, T2<:Function} - -# # lessonDict = -# # if isfile("lesson.json") -# # lessonDict = copy(JSON.parse("lesson.json")) -# # else -# # lessonDict = nothing -# # end - -# # lessonDict = nothing - -# # lesson = -# # if lessonDict === nothing -# # "" -# # else -# # """ -# # 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(lessonDict)) - -# # 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 - -# systemmsg = -# """ -# You are a helpful assistant that find the data from a database to satisfy the user's query. -# You are also eager to improve your helpfulness. - -# For your information: -# - Observation: Result of the immediately preceding action - -# At each round of conversation, the user will give you the following: -# User Query: ... -# Example: ... -# Your Q&A: ... -# Your work progress: ... -# Evaluation: Evaluation of the immediately preceding action and observation -# Suggestion: Suggestion for the immediately preceding action and observation - -# You must follow the following guidelines: -# - Keep SQL queries focused only on the provided information. - -# You should follow the following guidelines: -# - Do not create any table in the database -# - A junction table can be used to link tables together. Another use case is for filtering data. -# - If you can't find a single table that can be used to answer the user's query, try joining multiple tables to see if you can obtain the answer. -# - If you are unable to find the requested information, kindly inform the user, "The current data in our database does not provide the specific answer to your query". -# - Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search. - -# You should then respond to the user with interleaving Comprehension, Plan, Action_name, Action_input: -# Plan: Given the current circumstances, outline a detailed, step-by-step plan to accomplish the task. Be specific. -# Action_name: (Typically corresponds to the execution of the first step in your plan) -# Can be one of the following function names: -# - 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 ';'. -# 4) Action_input: Input to the action - -# You should only respond in format as described below: -# Plan: ... -# Action_name: ... -# Action_input: ... - -# Let's begin! -# """ - -# workprogress = "" -# for (k, v) in state[:thoughtHistory] -# if k ∉ [:question] -# workprogress *= "$k: $v\n" -# end -# end - -# response = nothing # store for show when error msg show up -# errornote = "N/A" - -# # provide similar sql only for the first attempt -# similarSQL_ = "None" -# if length(state[:thoughtHistory]) == 1 -# sql, distance = querySQLVectorDBF(state[:thoughtHistory][:question]) -# similarSQL_ = sql !== nothing ? sql : "None" -# end - -# header = ["Plan:", "Action_name:", "Action_input:"] -# dictkey = ["plan", "action_name", "action_input"] - -# llmkwargs=Dict( -# :num_ctx => 32768, -# :temperature => 0.5, -# ) - -# for attempt in 1:maxattempt - -# QandA = generatequestion(state, context, text2textInstructLLM, llmFormatName; similarSQL=similarSQL_) - -# usermsg = -# """ -# $(context[:tablelist]) -# User query: $(state[:thoughtHistory][:question]) -# Example: $similarSQL_ -# Your Q&A: $QandA -# Your work progress: $workprogress -# Evaluation: $(state[:evaluation]) -# Suggestion: $(state[:suggestion]) -# P.S. $errornote -# """ - -# _prompt = -# [ -# Dict(:name=> "system", :text=> systemmsg), -# Dict(:name=> "user", :text=> usermsg) -# ] - -# # put in model format -# prompt = GeneralUtils.formatLLMtext(_prompt, llmFormatName) -# response = text2textInstructLLM(prompt; llmkwargs=llmkwargs) -# response = GeneralUtils.deFormatLLMtext(response, llmFormatName) -# think, response = GeneralUtils.extractthink(response) - -# # LLM tends to generate observation given that it is in the input -# response = -# if occursin("observation:", response) -# string(split(response, "observation:")[1]) -# elseif occursin("Observation:", response) -# string(split(response, "Observation:")[1]) -# elseif occursin("observation_", response) -# string(split(response, "observation_")[1]) -# elseif occursin("Observation_", response) -# string(split(response, "Observation_")[1]) -# else -# response -# end - -# # sometime LLM output something like **Comprehension**: which is not expected -# response = replace(response, "**"=>"") -# response = replace(response, "***"=>"") - -# # some time LLM output Plan_1: so we need to detect and replace topic numbering -# regex = r"_[0-1000]+:" -# matches = collect(eachmatch(regex, response)) -# for m in matches -# response = replace(response, string(m.match)=>":") -# end - -# if occursin("NULL", response) -# errornote = "\nYour previous attempt was NULL. This is not allowed" -# println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# # # detect if there are more than 1 key per categories -# # wordcount = GeneralUtils.countGivenWords(response, header) -# # duplicateKeywordFlag = false -# # for (i, v) in enumerate(wordcount) -# # keyword = header[i] -# # keywordNumber = v -# # if keywordNumber > 1 -# # errornote = "\nSQL query has duplicated keyword, $keyword" -# # println("Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # duplicateKeywordFlag = true -# # break -# # end -# # end -# # duplicateKeywordFlag == true ? continue : nothing - -# # check whether response has all header -# detected_kw = GeneralUtils.detect_keyword(header, response) -# if 0 ∈ values(detected_kw) -# errornote = "\nYour previous attempt did not have all points according to the required response format" -# println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# elseif sum(values(detected_kw)) > length(header) -# errornote = "\nYour previous attempt has duplicated points according to the required response format" -# println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# responsedict = GeneralUtils.textToDict(response, header; -# dictKey=dictkey, symbolkey=false) - -# delete!(responsedict, :observation) - -# # remove backticks Error occurred: MethodError: no method matching occursin(::String, ::Vector{String}) -# if occursin("```", responsedict[:action_input]) -# sql = GeneralUtils.extract_triple_backtick_text(responsedict[:action_input])[1] -# if sql[1:4] == "sql\n" -# sql = sql[5:end] -# end -# sql = split(sql, ';') # some time there are comments in the sql -# sql = sql[1] * ';' - -# responsedict[:action_input] = sql -# end - -# toollist = ["TABLEINFO", "RUNSQL"] -# if responsedict[:action_name] ∉ toollist -# errornote = "Your previous attempt has action_name that is not in the tool list" -# println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# for i in toollist -# if occursin(i, responsedict[:action_input]) -# errornote = "Your previous attempt has action_name in action_input which is not allowed" -# println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end -# end - -# for i ∈ Symbol.(dictkey) -# if length(JSON3.write(responsedict[i])) == 0 -# errornote = "Your previous attempt has empty value for $i" -# println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end -# end - -# state[:decisionMaker] = responsedict -# return responsedict -# end -# error("SQLLLM DecisionMaker() failed to generate a thought \n", response) -# 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, @@ -562,125 +223,120 @@ julia> # Signature """ -function evaluator(state::T1, thoughtDict, text2textInstructLLM::Function, llmFormatName::String; +function evaluator(state::T1, text2textInstructLLM::Function, llmFormatName::String; maxattempt=10 ) where {T1<:AbstractDict} - systemmsg = + 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 - "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 - - At each round of conversation, you will be given the following information: - trajectory: A history of how you worked on the question chronologically - evaluatee_context: The context that evaluatee use to make a decision - - You must follow the following guidelines: - - When the search returns no result, validate whether the SQL query makes sense before accepting it as a valid answer. - - You should then respond to the user with: - 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. + + At each round of conversation, the user provides the following: + - customer question + - trajectory: A history of how an agent (you) worked on the question chronologically + + + Analyze and evaluate agent's trajectory to find solutions and the results of actions to answer the user's questions according to evaluation guidelines. + + + Fulfill the objective. + + + - When the search returns no result, it usually means 1) there is simply no data. or 2) SQL condition is not correct or 3) SQL is looking at the wrong tables. + - 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 plan, action_name, action_input, and action_result. + - 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 action_input is appropriate. + 3) Accepted_as_answer: Decide whether the latest action_input is technically correct. Can be "yes" or "no" + Bad example: + question: Find cars with 4 wheels. + action_input: INSERT INTO employees + VALUES (5, 'Charlie', 'Green', '2026-06-01', 60000.00);. + Good example: + question: Find cars with a sunroof. + action_input: SELECT * FROM car_features + WHERE has_sunroof = TRUE; + 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 + - 0 indicates that both the trajectory is incorrect, failed or errors and the action_result is incorrect or failed - 4 indicates that the trajectory are correct, but no results are returned. - - 5 indicates that the trajectory are correct but the observation is incorrect or failed - - 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: what are the possible reason of this outcome, what can one learn from it and what suggestion can made? - - You should only respond in format as described below: - Trajectory_evaluation: ... - Answer_evaluation: ... - Accepted_as_answer: ... - Score: ... - Suggestion: ... - - Let's begin! + - 5 indicates that the trajectory are correct but the action_result is incorrect or failed + - 6 indicates that the trajectory are correct, but the action_result's content doesn't directly answer the question + - 8 indicates that both the trajectory are correct, and the action_result's content directly answers the question. + - 9 indicates a perfect perfomance. Both the trajectory are correct, and the action_result's content directly answers the question, surpassing your expectations. + 5) Suggestion: what are the possible reason of this outcome, what can one learn from it and what suggestion can made? + + + "trajectory_evaluation": "...", + "answer_evaluation": "...", + "accepted_as_answer": "...", + "score": "...", + "suggestion": "..." + + """ + requiredKeys = ["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"] + errornote = "" + usermsg = + """ + + $(state["context"]["table_schema"]) + + + $(state["chathistory"][2]["content"][1]["text"]) + + + $(GeneralUtils.dict_to_string_html(state["action_history"])) + """ - #[WORKING] add what I should think --> this will be the think for decisionMaker() - header = ["Trajectory_evaluation:", "Answer_evaluation:", "Accepted_as_answer:", "Score:", "Suggestion:"] - dictkey = ["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"] - thoughthistory = "" - for (k, v) in state["thoughtHistory"] - thoughthistory *= "$k: $v\n" - end - - errornote = "N/A" + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => usermsg), + ] + ), + ], + "temperature" => 0.7 + ) + for attempt in 1:maxattempt - usermsg = - """ - - $thoughthistory - - """ - context = - """ - - - thoughtDict[:context] - - P.S. $errornote - - """ + response = text2textInstructLLM("random_id", msg) + response = GeneralUtils.clean_json_response(response) - unformatPrompt = - [ - Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) - ] - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, llmFormatName) - # add info - prompt = prompt * context - - response = text2textInstructLLM(prompt, modelsize="medium") - response = GeneralUtils.deFormatLLMtext(response, llmFormatName) + response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) - - # sometime LLM output something like **Comprehension**: which is not expected - response = replace(response, "**"=>"") - response = replace(response, "***"=>"") - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - missingkeys = [k for (k, v) in detected_kw if v === nothing] - if !isempty(missingkeys) - errornote = "$missingkeys are missing from your previous response" - println("\nERROR SQLLLM extractContent_dataframe() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum([length(i) for i in values(detected_kw)]) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR SQLLLM extractContent_dataframe() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + response = String(split(response, ", action_result")[1]) # in case LLM generate action_result key which it isn't supposed to + response = strip(response) + responsedict = nothing + try + _responsedict = JSON.parse(response) + responsedict = GeneralUtils.dictify(_responsedict, keytype=String, sort_order=requiredKeys) + catch + println("\nERROR SQLLLM evaluator() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=false) + # check whether all answer's key points are in responsedict + ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR SQLLLM evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue + end responsedict["score"] = responsedict["score"][1] # some time "6\nThe trajectories are incomplete" is generated but I only need the number. try @@ -693,7 +349,7 @@ function evaluator(state::T1, thoughtDict, text2textInstructLLM::Function, llmFo accepted_as_answer::AbstractString = responsedict["accepted_as_answer"] - if accepted_as_answer ∉ ["yes", "no"] + if accepted_as_answer ∉ ["Yes", "yes", "No", "no"] errornote = "Your previous attempt's accepted_as_answer has wrong format" println("\nERROR SQLLLM evaluator() Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> $(responsedict["accepted_as_answer"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue @@ -715,45 +371,16 @@ function evaluator(state::T1, thoughtDict, text2textInstructLLM::Function, llmFo state["reward"] = responsedict["score"] end - println("\nSQLLLM evaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(Dict(responsedict)) - - # # store for later training - # responsedict[:thoughthistory] = state[:thoughtHistory] - # responsedict[:system] = systemmsg - # responsedict[:usermsg] = usermsg - # responsedict[:prompt] = prompt - # responsedict[:context] = context - # responsedict[:think] = think - - # # read sessionId - # sessionid = JSON.parse("/appfolder/app/sessionid.json") - # # save to filename ./log/decisionlog.txt - # println("saving SQLLLM evaluator() to disk") - # filename = "agent_evaluator_log_$(sessionid[:id]).json" - # filepath = "/appfolder/app/log/$filename" - # # check whether there is a file path exists before writing to it - # if !isfile(filepath) - # decisionlist = [responsedict] - # println("Creating file $filepath") - # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) - # end - # else - # # read the file and append new data - # decisionlist = copy(JSON.parse(filepath)) - # push!(decisionlist, responsedict) - # println("Appending new data to file $filepath") - # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) - # end - # end + println("\n--- SQLLLM evaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(responsedict) + println("---\n") return responsedict["score"] end error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") end + """ # Arguments @@ -839,7 +466,7 @@ function reflector(config::T1, state::T2)::String where {T1<:AbstractDict, T2<:A usermsg = """ - $(JSON3.write(state[:thoughtHistory])) + $(JSON.json(state[:action_history])) """ _prompt = @@ -930,24 +557,20 @@ julia> state = Dict( "reward" => 0, "evaluation" => "None", "accepted_as_answer" => "No", - "thoughtHistory" => OrderedDict{String, Any}("question" => "How many wines do you have that can be paired with lamb?"), + "action_history" => OrderedDict{String, Any}("question" => "How many wines do you have that can be paired with lamb?"), "evaluationscore" => 0, "suggestion" => "None" ) ``` - -# TODO - - [] add embedding of newstate and store in newstate[:embedding] - - [PENDING] should getdata() return isterminal? -# Signature -""" + - add embedding of newstate and store in newstate[:embedding] + - should getdata() return isterminal? +""" function transition(state::T, args::NamedTuple )::NamedTuple{(:newNodeKey, :newstate, :progressvalue), Tuple{String, T, Integer}} where {T<:AbstractDict} decisionMakerF::Function = args[:decisionMaker] evaluatorF::Function = args[:evaluator] # reflector::Function = args[:reflector] - context = args[:context] executeSQL::Function = args[:executeSQL] text2textInstructLLM::Function = args[:text2textInstructLLM] # insertSQLVectorDB::Function = args[:insertSQLVectorDB] @@ -955,40 +578,35 @@ function transition(state::T, args::NamedTuple llmFormatName::String = args[:llmFormatName] # getting SQL from vectorDB - thoughtDict = decisionMakerF(state, context, text2textInstructLLM, llmFormatName; - querySQLVectorDBF) - + thoughtDict = decisionMakerF(state, text2textInstructLLM, llmFormatName; + querySQLVectorDBF) + # println("\n--- SQLLLM transition() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # pprintln(thoughtDict) + # println("---") # map action and input() to llm function - response = - if thoughtDict["action_name"] == "listalltables" - # deepcopy(state["virtualCustomerChatHistory"]) because I want to keep it clean - # so that other simulation start from this same node is not contaminated with actioninput - listAllTable_json(executeSQL) - elseif thoughtDict["action_name"] == "TABLEINFO" - input = thoughtDict["action_input"] - tableinfo(executeSQL, input) - elseif thoughtDict[:action_name] == "RUNSQL" - response = SQLexecution(executeSQL, thoughtDict["action_input"]) - if response["success"] - extracted = extractContent_dataframe(response["result"], text2textInstructLLM, - thoughtDict["action_input"], llmFormatName) - (rawresponse=response[:result], result=extracted, errormsg=nothing, success=true) - else - (result=nothing, errormsg=response[:errormsg], success=false) - end - else - error("undefined LLM function. Requesting $(thoughtDict[:action_name])") - end - # this section allow LLM functions above to have different return values. - success::Bool = haskey(response, "success") ? response["success"] : false - result = success ? response["result"] : response["errormsg"] - rawresponse = haskey(response, "rawresponse") ? response["rawresponse"] : nothing - select = haskey(response, "select") ? response["select"] : nothing - reward::Integer = haskey(response, "reward") ? response["reward"] : 0 - isterminal::Bool = haskey(response, "isterminal") ? response["isterminal"] : false - newNodeKey, newstate = makeNewState(state, thoughtDict, rawresponse, JSON3.write(result), select, reward, isterminal) - progressvalue::Integer = evaluatorF(newstate, thoughtDict, text2textInstructLLM, llmFormatName) + response = nothing + if thoughtDict["action_name"] == "RUNSQL" + response = SQLexecution(executeSQL, thoughtDict["action_input"]) + # println("\n--- SQLLLM transition() response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println(response) + # println("---") + else + error("undefined LLM function. Requesting $(thoughtDict["action_name"])") + end + + newNodeKey, newstate = makeNewState(state, thoughtDict, response) + progressvalue::Integer = + if response[:success] + 8 # for faster agent response. if success just skip evaluation + else + evaluatorF(newstate, text2textInstructLLM, llmFormatName) + end + + println("\n--- SQLLLM transition() thoughtDict ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(thoughtDict) + println("---") + # error("SQLLLM transition() end") return (newNodeKey=newNodeKey, newstate=newstate, progressvalue=progressvalue) end @@ -1075,26 +693,80 @@ julia> println(result) ``` # Signature -""" +""" function query(query::T, executeSQL::Function, text2textInstructLLM::Function; insertSQLVectorDB::Union{Function, Nothing}=nothing, similarSQLVectorDB::Union{Function, Nothing}=nothing, llmFormatName="qwen3" - )::NamedTuple{(:text, :rawresponse), Tuple{Any, Any}} where {T<:AbstractString} + ) where {T<:AbstractString} # use similarSQLVectorDB to find similar SQL for the query sql, distance = similarSQLVectorDB(query) + + # if sql is really match, immediately check database then return if sql !== nothing && distance <= 1 # query vector db to get wine response = SQLexecution(executeSQL, sql) if response[:success] - # intention = Dict(:intention=> "$(thoughtDict[:plan])") - extracted = extractContent_dataframe(response[:result], text2textInstructLLM, sql, - llmFormatName) - return (text=extracted, rawresponse=response[:result]) - end + return (result_str=response[:result_str], result_raw=response[:result_raw]) + else + error(response[:errormsg]) + end end + """ + chathistory= [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => "You are a helpful assistant"), + ] + ), + ] + """ + + systemmsg = + """ + + - 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 ';'. + + + At each round of conversation, you will be given the following: + - user question + You are working under your mentor supervision and you are also eager to improve your helpfulness. + + + Consult the database search guidelines. Then find the data from a database to satisfy the user's question. + + + Fulfill the objective. + + + - Keep SQL queries focused only on the provided information. + - Do not create any table in the database + - A junction table can be used to link tables together. Another use case is for filtering data. + - If you can't find a single table that can be used to answer the user's query, try joining multiple tables to see if you can obtain the answer. + - Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search. + - If there is no search result from the database, remove the restrictive criteria until a search result is available, and proceed from there. + + + 1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific. + 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name + 3) action_input: The input to the action you are about to perform according to your plan. + After the action is executed you gets "action_result". It is the output from the action you selected. + + + "plan": "...", + "action_name": "...", + "action_input": "..." + + """ + + + # do MCTS if no data in the database # add extra context for Evaluator so that it knows the observation is from seaching a database initialstate = Dict{String, Any}( @@ -1104,22 +776,35 @@ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; "evaluationscore"=> 0, "suggestion"=> "None", "accepted_as_answer"=> "No", - "lesson"=> nothing, - - # contain question, thought_1, action_1, observation_1, thought_2, ... - "thoughtHistory"=> OrderedDict{String, Any}( - #[] :recap=>, - "question"=> query, - ), + "chathistory"=> Vector{Dict{String, Any}}(), # store system, user and assistant msg + "question"=> query, + "context"=> Dict{String, Any}(), + "action_history"=> OrderedDict{String, Any}( + # "1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), + # "2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), + # ... + ), ) - # context = Dict( - # :tablelist => listAllTable_str(executeSQL)[:result] - # ) + + systemmsg_dict = Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ) + usermsg = Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => query), + ] + ) + push!(initialstate["chathistory"], systemmsg_dict) + push!(initialstate["chathistory"], usermsg) + #XXX find a way to recreate the schema from a existing database - context = Dict( - :tablelist => - """ - create table customer ( + table_schema = + """ + create table customer ( customer_id uuid primary key default gen_random_uuid (), customer_firstname varchar(128), customer_lastname varchar(128), @@ -1167,6 +852,7 @@ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; savoriness integer, bitterness integer, serving_temperature integer, + image_url jsonb, note text, other_attributes jsonb, @@ -1181,7 +867,7 @@ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; wine_name varchar(128) not null, winery varchar(128) not null, vintage integer not null, - region varchar(128) not null, -- A field used to store the name of a wine-producing region, such as Napa Valley (California), Bordeaux, Champagne, Tuscany, etc. + region varchar(128) not null, country varchar(128) not null, wine_type varchar(128) not null, grape varchar(128) not null, @@ -1191,7 +877,9 @@ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; tannin integer, acidity integer, fizziness integer, - tasting_notes text, -- A field used to record the distinctive flavors of wine such as floral, citrus, apple, earthy, daisy, etc. + tasting_notes text, + image_url jsonb, + manufacturer_sku text, note text, other_attributes jsonb, @@ -1231,22 +919,26 @@ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; updated_time timestamptz default current_timestamp ); """ - ) + + # println("\n--- SQLLLM query() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println("---") + # error("SQLLLM query() end") + + initialstate["context"]["table_schema"] = table_schema transitionargs = ( - decisionMaker=decisionMaker, - evaluator=evaluator, - reflector=reflector, - context=context, - executeSQL=executeSQL, - text2textInstructLLM=text2textInstructLLM, - querySQLVectorDB=similarSQLVectorDB, - insertSQLVectorDB=insertSQLVectorDB, - llmFormatName=llmFormatName + executeSQL=executeSQL, + decisionMaker=decisionMaker, + evaluator=evaluator, + reflector=reflector, + text2textInstructLLM=text2textInstructLLM, + querySQLVectorDB=similarSQLVectorDB, + insertSQLVectorDB=insertSQLVectorDB, + llmFormatName=llmFormatName ) earlystop(state) = state["reward"] >= 8 ? true : false - + root, _, resultState, highValueState = LLMMCTS.runMCTS(initialstate, transition, transitionargs; horizontalSampleExpansionPhase=1, @@ -1257,34 +949,36 @@ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; earlystop=earlystop, saveSimulatedNode=true, multithread=false) - + + # error("SQLLLM query() end") + # compare all high value state answer then select the best one if length(highValueState) > 1 - # open("/appfolder/app/highValueState.json", "w") do io - # JSON3.pretty(io, highValueState) - # end selected = compareState(query, highValueState, text2textInstructLLM, llmFormatName) resultState = highValueState[selected] end - latestKey, latestInd = GeneralUtils.findHighestIndexKey(resultState[:thoughtHistory], "observation") -action_input = "action_input_$latestInd" # latest sql - sql = resultState["thoughtHistory"][action_input] - extractedTableContent = resultState["thoughtHistory"][latestKey] - # add to vectorDB only if the answer is achieved and the state is terminal + max_ind = + if length(resultState["action_history"]) == 0 + 0 + else + k = keys(resultState["action_history"]) + maximum(parse.(Int, k)) + end + latest_action = resultState["action_history"]["$max_ind"] + + #CHANGE add to vectorDB only if the answer is achieved and the state is terminal + sql = latest_action["action_input"] if insertSQLVectorDB !== nothing && resultState["isterminal"] == true && - resultState["rawresponse"] !== nothing - - insertSQLVectorDB(resultState["thoughtHistory"]["question"], sql) + resultState["accepted_as_answer"] == "yes" + insertSQLVectorDB(resultState["question"], sql) end - if extractedTableContent === nothing - println("\nSQLLLM query() return nothing ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - result = (text=extractedTableContent, rawresponse=resultState["rawresponse"]) - - return result + println("\n--- SQLLLM query() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println(resultState["result_raw"]) + println("---\n") + + return (result_str=latest_action["action_result"], result_raw=resultState["result_raw"]) end @@ -1301,41 +995,34 @@ julia> # Signature """ -function makeNewState(currentstate::T1, thoughtDict::T4, rawresponse, response::T2, select::Union{T3, Nothing}, - reward::T3, isterminal::Bool - )::NamedTuple{(:newNodeKey, :newstate), Tuple{String, Dict{String, <:Any}}} where {T1<:AbstractDict, T2<:AbstractString, T3<:Number, T4<:AbstractDict} - - keys = [:plan, :action_name, :action_input, :observation] - # latestKeys = [] +function makeNewState(currentstate::T1, thoughtDict::T2, response::NamedTuple, + )::NamedTuple{(:newNodeKey, :newstate), Tuple{String, Dict{String, <:Any}}} where {T1<:AbstractDict, T2<:AbstractDict} - currentstate_latestKey, currentstate_latestIndice = - GeneralUtils.findHighestIndexKey(currentstate["thoughtHistory"], keys[1]) - nextindice = currentstate_latestKey !== nothing ? currentstate_latestIndice + 1 : 1 - # currentstate_latestKey == "NA" ? 1 : currentstate_latestIndice + 1 - - currentstate_latestKey = makekey.(keys, nextindice) - - # add Thought, action, observation to thoughtHistory - newstate = deepcopy(currentstate) - for (x, y) in zip(keys, currentstate_latestKey) - if x != "observation" - newstate["thoughtHistory"][y] = thoughtDict[x] - else - newstate["thoughtHistory"][y] = response - end + if response[:success] + thoughtDict["action_result"] = response[:result_str] + else + error(response[:errormsg]) end -newstate["reward"] = reward - newstate["select"] = select - newstate["isterminal"] = isterminal - newstate["rawresponse"] = rawresponse # whatever return from action + newstate = deepcopy(currentstate) + max_ind = + if length(newstate["action_history"]) == 0 + 0 + else + k = keys(newstate["action_history"]) + maximum(parse.(Int, k)) + end + newstate["action_history"]["$(max_ind + 1)"] = thoughtDict + newstate["reward"] = haskey(response, :reward) ? response[:reward] : 0 + newstate["select"] = haskey(response, :select) ? response[:select] : nothing + newstate["isterminal"] = haskey(response, :isterminal) ? response[:isterminal] : false + newstate["result_raw"] = response[:result_raw] # whatever return from action newNodeKey = GeneralUtils.uuid4snakecase() return (newNodeKey=newNodeKey, newstate=newstate) end - function generatequestion(state::T1, context, text2textInstructLLM::Function, llmFormatName::String; similarSQL::Union{T2, Nothing}=nothing, maxattempt=10, @@ -1402,7 +1089,7 @@ function generatequestion(state::T1, context, text2textInstructLLM::Function, dictkey = ["q1"] workprogress = "" - for (k, v) in state["thoughtHistory"] + for (k, v) in state["action_history"] if k ∉ ["query"] workprogress *= "$k: $v\n" end @@ -1415,7 +1102,7 @@ function generatequestion(state::T1, context, text2textInstructLLM::Function, usermsg = """ $(context["tablelist"]) - User query: $(state["thoughtHistory"]["question"]) + User query: $(state["action_history"]["question"]) Example: $similarSQL Your work progress: $workprogress P.S. $errornote diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 17fc789..0cf8f60 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -481,20 +481,9 @@ julia> response = SQLLLM.SQLexecution(executeSQL, sql) # Signature """ function SQLexecution(executeSQL::Function, sql::T -) where {T<:AbstractString} + )::NamedTuple where {T<:AbstractString} try - #XXX dummy SQL. use for testing - # sql = "SELECT w.wine_name FROM wine w JOIN wine_food wf ON w.wine_id = wf.wine_id JOIN food f ON wf.food_id = f.food_id WHERE f.\"food_name\" = 'lamb';" - # sql = " SELECT w.wine_name FROM wine w JOIN food f ON f.food_name = 'lamb' JOIN wine_food wf ON w.wine_id = wf.wine_id AND f.food_id = wf.food_id GROUP BY w.wine_name ORDER BY COUNT(DISTINCT w.wine_id) DESC;" - # sql = " SELECT COUNT(DISTINCT wf.wine_id) FROM wine w JOIN wine_food wf ON w.wine_id = wf.wine_id JOIN food f ON wf.food_id = f.food_id WHERE f.food_name ILIKE '%lamb%'" - - #XXX use for package testing, remove when done - # ans = "1.schilfwein zweigelt 2.cabernet sauvignon reserve limited edition" - # ans = "There are 1500 wines that can be paired with lamb." - # ans = "1500" - # return (response=ans, errormsg=nothing, reward=1, isterminal=true) - # add LIMIT to the SQL to prevent loading large data sql = strip(sql) @@ -508,39 +497,36 @@ function SQLexecution(executeSQL::Function, sql::T else sql = sql * ";" end - println("\n~~~ SQLexecution() SQL: ", @__FILE__, " ", @__LINE__) - println(sql) - result = executeSQL(sql) df = DataFrame(result) - tablesize = size(df) row, column = tablesize if row == 0 - error("\nThe resulting table has 0 row. Please try again.") + return (result_str="The resulting table has 0 row.", result_raw=df, success=true, errormsg=nothing) elseif column > 30 - error("\nSQL execution failed. An unexpected error occurred. Please try again.") + return (result_str="There are more than 30 columns. Please be more specific.", result_raw=df, success=true, errormsg=nothing) + else + df1 = + if row > 2 + # ramdom row to pick + df[sample(1:nrow(df), 2, replace=false), :] # random select 2 rows from df + else + df + end + result = GeneralUtils.dfToString(df1) + # println("\n~~~ SQLexecution() result: ", @__FILE__, " ", @__LINE__) + # println(sql) + # println(df1) + # println("\n") + return (result_str=result, result_raw=df1, success=true, errormsg=nothing) end - - df1 = - if row > 2 - # ramdom row to pick - df[sample(1:nrow(df), 2, replace=false), :] # random select 2 rows from df - else - df - end - - println("\n~~~ SQLexecution() result: ", @__FILE__, " ", @__LINE__) - println(df1) - return (result=df1, success=true, errormsg=nothing) catch e io = IOBuffer() showerror(io, e) errorMsg = String(take!(io)) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) println(errorMsg) - response = (result=nothing, success=false, errormsg=errorMsg) - return response + return (result_str=nothing, result_raw=nothing, success=false, errormsg=errorMsg) end end @@ -559,7 +545,7 @@ end - `result::String` # Signature -""" +""" #WORKING function extractContent_dataframe(df::DataFrame, text2textInstructLLM::Function, action::String, llmFormatName::String )::String @@ -633,7 +619,7 @@ function extractContent_dataframe(df::DataFrame, text2textInstructLLM::Function, dictkey = ["about_resulting_table", "search_summary"] for i in 1:5 - response = text2textInstructLLM(prompt, modelsize="medium") + response = text2textInstructLLM("ramdom_id", prompt) response = GeneralUtils.deFormatLLMtext(response, llmFormatName) think, response = GeneralUtils.extractthink(response) @@ -653,7 +639,6 @@ function extractContent_dataframe(df::DataFrame, text2textInstructLLM::Function, responsedict = GeneralUtils.textToDict(response, header; dictKey=dictkey, symbolkey=false) - # result = dfstr result = """ Summary: $(responsedict["search_summary"]) @@ -821,7 +806,7 @@ julia> # Notes - The function makes up to 10 attempts to get a valid response from the LLM -- Each state in highValueStateList should contain a thoughtHistory with action_input and observation +- Each state in highValueStateList should contain a action_history with action_input and observation - The LLM evaluates attempts based on accuracy and relevance to the original question """ function compareState(question::String, highValueStateList::Vector{T}, @@ -866,14 +851,14 @@ potentialSolution = [] keys = ["action_input", "observation"] # extract the last action_name, action_input, observation of each state in highValueStateList and store them in a dictionary then push into potentialSolution for state in highValueStateList - thoughtHistory = state["thoughtHistory"] + action_history = state["action_history"] _, currentstate_latestIndice = - GeneralUtils.findHighestIndexKey(thoughtHistory, keys[1]) + GeneralUtils.findHighestIndexKey(action_history, keys[1]) latestKeys = makekey.(keys, currentstate_latestIndice) d = Dict() # get the last action_name, action_input, observation of currentstate for (i,v) in enumerate(keys) - d[v] = thoughtHistory[latestKeys[i]] + d[v] = action_history[latestKeys[i]] end push!(potentialSolution, d) end diff --git a/test/runtests.jl b/test/runtests.jl index 4ef0327..8498517 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -137,7 +137,7 @@ end function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} tablename = "sqlllm_decision_repository" # get embedding of the query - # query = state[:thoughtHistory][:question] + # query = state[:action_history][:question] df = findSimilarTextFromVectorDB(query, tablename, "function_input_embedding", executeSQLVectorDB) row, col = size(df) @@ -352,7 +352,7 @@ SELECT * FROM wine WHERE wine_type = 'red' AND country = 'France' AND sweetness # :evaluation => # "The user's question is to search the database for wines that have a type of \"white\", are from \"France\", and have a sweetness level of 1. The thought is correct in identifying the conditions needed to filter the wine table. The action taken is to execute a SQL query to retrieve the desired data, which is also correct. The observation provides a search summary and two search results that match the user's question. Each result includes details about the wine such as ID, name, brand, manufacturer, region, country, type, grape variety, serving temperature, intensity, sweetness, tannin, and acidity.", # :accepted_as_answer => "Yes", -# :thoughtHistory => +# :action_history => # OrderedDict{String, Any}("question" => "Search the database for wine_type: white, country: France, sweetness: 1", "thought_1" => "The user wants to search the database for wines that have a type of \"white\", are from \"France\", and have a sweetness level of 1. To achieve this, we need to filter the wine table based on these conditions.", "action_name_1" => "GETDATA", "action_input_1" => "SELECT * FROM wine WHERE wine.wine_type = 'white' AND wine.country = 'France' AND wine.sweetness = 1;", "observation_1" => "\"Search summary: The resulting table represents wines.\\nSearch result: 1) wine_id: 5b6b6df9-d87c-4f33-8995-7249c2ecc917, wine_name: corton-charlemagne grand cru, brand: domaine des croix, manufacturer: domaine des croix, region: bourgogne, country: France, wine_type: white, grape_variety: cote de beaune blanc, serving_temperature: 11 to 13 Celsius, intensity: 4, sweetness: 1, tannin: missing, acidity: 3, fizziness: missing\\n2) wine_id: 1ad27d16-ef64-4907-acf1-40631630c143, wine_name: puligny-montrachet 1er cru 'les demoiselles', brand: amiot guy, manufacturer: amiot guy, region: bourgogne, country: France, wine_type: white, grape_variety: cote de beaune blanc, serving_temperature: 11 to 13 Celsius, intensity: 4, sweetness: 1, tannin: missing, acidity: 3, fizziness: missing\\n\\n\""), # :evaluationscore => 9, # :select => nothing,