module interface export decisionMaker, evaluator, reflector, transition, query using LibPQ, DataStructures, JSON, UUIDs, PrettyPrinting, Dates using GeneralUtils, LLMMCTS using ..util, ..llmfunction # ---------------------------------------------- 100 --------------------------------------------- # """ Think and choose action. # Arguments - `state::T2` A game state - `context` A context that will be added to decisionMaker - `text2textInstructLLM::Function` A function that handles communication to LLM service # Return - `thoughtDict::Dict{String, Any}` # Example ```jldoctest julia> using SQLLLM, GeneralUtils, UUIDs, DataStructures, PrettyPrinting julia> state = Dict( "isterminal" => false, "lesson" => nothing, "reward" => 0, "evaluation" => "None", "accepted_as_answer" => "No", "action_history" => OrderedDict{String, Any}("question" => "How many wines do you have that can be paired with lamb?"), "evaluationscore" => 0, "suggestion" => "None" ) julia> context = Dict("tablelist"=> "None") julia> function text2textInstructLLM(prompt::String) 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") ), ) ) # apply LLM specific instruct format externalService = config[:externalservice][:text2textinstruct] msgMeta = GeneralUtils.generate_msgMeta( externalService[:mqtttopic], senderName= "SQLLLM", 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|>"], :temperature=> 0.2, ) ) ) _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) response = _response[:response][:text] return response end julia> result = SQLLLM.decisionMaker(state, context, text2textInstructLLM) julia> pprintln(result) Dict( :action_input => "[\"wine_food\"]", :thought => "Since the user is asking about wine pairing, I need to find a way to connect the \"wine\" and \"food\" tables. The \"wine_food\" table seems like a good starting point.", :plan => "First, I'll get information about the \"wine_food\" table to see how it relates to the other two tables. Then, I'll use this information to craft an instruction that retrieves the wines that can be paired with lamb.", :observation => "[{\"name\": \"wine_food\", \"columns\": [\"wine_id\", \"food_id\"]}]", :action_name => "TABLEINFO" ) ``` # TODO - [] implement RAG to pull similar experience # Signature """ function decisionMaker(state::T1, text2textInstructLLM::Function, llmFormatName::String ; querySQLVectorDBF::Union{T2, Nothing}=nothing, maxattempt=10 )::Dict{String, Any} where {T1<:AbstractDict, T2<:Function} 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 """ # 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 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) think, response = GeneralUtils.extractthink(response) responsedict = nothing try _responsedict = JSON.parse(response) responsedict = GeneralUtils.dictify(_responsedict, keytype=String) catch println("\nERROR decisionMaker() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end # check whether all answer's key points are in responsedict 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 # 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 = ["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())") 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 --(not qualify response)--> $(responsedict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue 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 """ Assigns a scalar value to each new child node to be used for selec- tion and backpropagation. This value effectively quantifies the agent's progress in task completion, serving as a heuristic to steer the search algorithm towards the most promising regions of the tree. # Arguments - `state<:AbstractDict` one of Yiem's agent - `text2textInstructLLM::Function` A function that handles communication to LLM service # Return - `score::Integer` # Example ```jldoctest julia> ``` # Signature """ function evaluator(state::T1, text2textInstructLLM::Function, llmFormatName::String; maxattempt=10 ) where {T1<:AbstractDict} systemmsg = """ 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 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 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"])) """ 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 response = text2textInstructLLM("random_id", msg) response = GeneralUtils.clean_json_response(response) response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) 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) catch println("\nERROR SQLLLM evaluator() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end # 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 responsedict["score"] = parse(Int, responsedict["score"]) # convert string "5" into integer 5 catch errornote = "Your previous attempt's score has wrong format" println("\nERROR SQLLLM evaluator() Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end accepted_as_answer::AbstractString = responsedict["accepted_as_answer"] 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 end # add to state here instead to in transition() because the latter causes julia extension crash (a bug in julia extension) state["evaluation"] = "$(responsedict["trajectory_evaluation"]) $(responsedict["answer_evaluation"])" state["evaluationscore"] = responsedict["score"] state["accepted_as_answer"] = responsedict["accepted_as_answer"] state["suggestion"] = responsedict["suggestion"] # mark as terminal state when the answer is achieved if accepted_as_answer ∈ ["Yes", "yes"] # mark the state as terminal state because the evaluation say so. state["isterminal"] = true # evaluation score as reward because different answers hold different value for the user. state["reward"] = responsedict["score"] end println("\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 # function evaluator(state::T1, thoughtDict, text2textInstructLLM::Function, llmFormatName::String; # maxattempt=10 # ) where {T1<:AbstractDict} # systemmsg = # """ # You are a helpful assistant that analyzes agent's trajectory to find solutions and observations (i.e., the results of actions) to answer the user's questions. # Definitions: # "question" is the user's question # "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. # For example: # - 0 indicates that both the trajectory is incorrect, failed or errors and the observation is incorrect or failed # - 4 indicates that the trajectory are correct, but 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! # """ # #[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"] # action_history = "" # for (k, v) in state["action_history"] # action_history *= "$k: $v\n" # end # errornote = "N/A" # for attempt in 1:maxattempt # usermsg = # """ # # $action_history # # """ # context = # """ # # # thoughtDict[:context] # # P.S. $errornote # # """ # 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) # 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())") # continue # end # responsedict = GeneralUtils.textToDict(response, header; # dictKey=dictkey, symbolkey=false) # responsedict["score"] = responsedict["score"][1] # some time "6\nThe trajectories are incomplete" is generated but I only need the number. # try # responsedict["score"] = parse(Int, responsedict["score"]) # convert string "5" into integer 5 # catch # errornote = "Your previous attempt's score has wrong format" # println("\nERROR SQLLLM evaluator() Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # end # accepted_as_answer::AbstractString = responsedict["accepted_as_answer"] # if accepted_as_answer ∉ ["yes", "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 # end # # add to state here instead to in transition() because the latter causes julia extension crash (a bug in julia extension) # state["evaluation"] = "$(responsedict["trajectory_evaluation"]) $(responsedict["answer_evaluation"])" # state["evaluationscore"] = responsedict["score"] # state["accepted_as_answer"] = responsedict["accepted_as_answer"] # state["suggestion"] = responsedict["suggestion"] # # mark as terminal state when the answer is achieved # if accepted_as_answer ∈ ["Yes", "yes"] # # mark the state as terminal state because the evaluation say so. # state["isterminal"] = true # # evaluation score as reward because different answers hold different value for the user. # state["reward"] = responsedict["score"] # end # println("\nSQLLLM evaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # pprintln(Dict(responsedict)) # # # store for later training # # responsedict[:action_history] = state[:action_history] # # 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 # return responsedict["score"] # end # error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") # end """ # Arguments # Return # Example ```jldoctest julia> ``` # TODO - [] update docstring - [] 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 systemmsg = """ 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 use the wrong syntax, or use the wrong function, or refer to item that don't exist in the database. In a few sentences, Diagnose a possible reason for failure and devise a new, specific and concise lesson 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: user: { "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." } assistant: { "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." } user: { "question": "How many wines suitable to be paired with lamb?", "thought_1": "The user wants to know how many wines that can be paired with lamb, I will try to find the table that has information about pairing between wines and food items.", "action_1": {"name": "getdata", "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": "SELECT * FROM wine_food WHERE obj_description LIKE '%lamb%'"}, "observation_2": "SQL execution error: SQL syntax error. It must end with character ';'", } assistant: { "reflection": "I need to have ';' at the end of the SQL query." } Let's begin! """ usermsg = """ $(JSON.json(state[:action_history])) """ _prompt = [ Dict(:name=> "system", :text=> systemmsg), Dict(:name=> "user", :text=> usermsg) ] # put in model format prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") externalService = config[:externalservice][:text2textinstruct] # apply LLM specific instruct format externalService = config[:externalservice][:text2textinstruct] msgMeta = GeneralUtils.generate_msgMeta( externalService[:mqtttopic]; senderName= "reflector", senderId= string(uuid4()), receiverName= "text2textinstruct", mqttBrokerAddress= 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:10 try response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) _responseJsonStr = response[:response][:text] expectedJsonExample = """ Here is an expected JSON format: {"reflection": "..."} """ # responseJsonStr, errormsg, success = # FormatCorrector.jsoncorrection(config, _responseJsonStr, expectedJsonExample) if !success error("Not valid JSON") end reflectionDict = copy(JSON.parse(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 """ Get a new state # Arguments - `state<:AbstractDict` state's dictionary - `args::NamedTuple` Arguments for decisionMaker() and others # Return - `NamedTuple{(:newNodeKey, :newstate, :progressvalue), Tuple{String, T, Integer}}` # Example ```jldoctest julia> using SQLLLM, DataStructures julia> state = Dict( "isterminal" => false, "lesson" => nothing, "reward" => 0, "evaluation" => "None", "accepted_as_answer" => "No", "action_history" => OrderedDict{String, Any}("question" => "How many wines do you have that can be paired with lamb?"), "evaluationscore" => 0, "suggestion" => "None" ) ``` - 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] executeSQL::Function = args[:executeSQL] text2textInstructLLM::Function = args[:text2textInstructLLM] # insertSQLVectorDB::Function = args[:insertSQLVectorDB] querySQLVectorDBF::Function = args[:querySQLVectorDB] llmFormatName::String = args[:llmFormatName] # getting SQL from vectorDB 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 = nothing if thoughtDict["action_name"] == "RUNSQL" response = SQLexecution(executeSQL, thoughtDict["action_input"]) 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 return (newNodeKey=newNodeKey, newstate=newstate, progressvalue=progressvalue) end """ Ask the database using English language. # Arguments - `query<:AbstractString` A natural language query in English - `executeSQL::Function` A function that executes SQL queries against the database - `text2textInstructLLM::Function` A function that handles communication with a text-to-text instruction-based language model # Keyword Arguments - `insertSQLVectorDB::Union{Function, Nothing}=nothing` Optional function to insert SQL queries into a vector database for future reference - `similarSQLVectorDB::Union{Function, Nothing}=nothing` Optional function to find similar SQL queries from a vector database # Returns - `NamedTuple{(:text, :rawresponse), Tuple{Any, Any}}` - `:text`: The query result in natural language - `:rawresponse`: The raw database response # Example ```jldoctest julia> using LibPQ, JSON3, UUIDs julia> using SQLLLM, GeneralUtils julia> function executeSQL(sql) DBconnection = LibPQ.Connection("host=192.168.88.122 port=5432 dbname=xyz user=zyx password=1234") result = LibPQ.execute(DBconnection, sql) close(DBconnection) return result end julia> function text2textInstructLLM(prompt::String) 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") ), ) ) # apply LLM specific instruct format externalService = config[:externalservice][:text2textinstruct] msgMeta = GeneralUtils.generate_msgMeta( externalService[:mqtttopic], senderName= "SQLLLM", 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|>"], :temperature=> 0.2, ) ) ) _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) response = _response[:response][:text] return response end julia> query = Dict(:text=> "How many wines do you have that can be paired with lamb?") julia> result = SQLLLM.query(query, executeSQL, text2textInstructLLM) julia> println(result) ``` # Signature """ function query(query::T, executeSQL::Function, text2textInstructLLM::Function; insertSQLVectorDB::Union{Function, Nothing}=nothing, similarSQLVectorDB::Union{Function, Nothing}=nothing, llmFormatName="qwen3" ) 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] 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}( "reward"=> 0, "isterminal"=> false, "evaluation"=> "None", "evaluationscore"=> 0, "suggestion"=> "None", "accepted_as_answer"=> "No", "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"=> "..."), # ... ), ) 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 table_schema = """ create table customer ( customer_id uuid primary key default gen_random_uuid (), customer_firstname varchar(128), customer_lastname varchar(128), customer_displayname varchar(128) not null, customer_username varchar(128), customer_password varchar(128), customer_gender varchar(128), country varchar(128), telephone varchar(128), email varchar(128) not null, customer_birthdate varchar(128), note text, other_attributes jsonb, created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp, description text ); create table retailer ( retailer_id uuid primary key default gen_random_uuid (), retailer_name varchar(128) not null, retailer_username varchar(128) not null, retailer_password varchar(128) not null, retailer_address text not null, country varchar(128) not null, contact_person varchar(128) not null, telephone varchar(128) not null, email varchar(128) not null, note text, other_attributes jsonb, created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp, description text ); create table food ( food_id uuid primary key default gen_random_uuid (), food_name varchar(128) not null, country varchar(128), spiciness integer, sweetness integer, sourness integer, savoriness integer, bitterness integer, serving_temperature integer, image_url jsonb, note text, other_attributes jsonb, created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp, description text ); create table wine ( wine_id uuid primary key default gen_random_uuid (), seo_name varchar(128) not null, wine_name varchar(128) not null, winery varchar(128) not null, vintage integer not null, region varchar(128) not null, country varchar(128) not null, wine_type varchar(128) not null, grape varchar(128) not null, serving_temperature varchar(128) not null, intensity integer, sweetness integer, tannin integer, acidity integer, fizziness integer, tasting_notes text, image_url jsonb, manufacturer_sku text, note text, other_attributes jsonb, created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp, description text ); create table wine_food ( wine_id uuid references wine(wine_id), food_id uuid references food(food_id), constraint wine_food_id primary key (wine_id, food_id), created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp ); CREATE TABLE retailer_wine ( retailer_id uuid references retailer(retailer_id), wine_id uuid references wine(wine_id), constraint retailer_wine_id primary key (retailer_id, wine_id), price NUMERIC(10, 2), currency varchar(3) not null, created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp ); CREATE TABLE retailer_food ( retailer_id uuid references retailer(retailer_id), food_id uuid references food(food_id), constraint retailer_food_id primary key (retailer_id, food_id), price NUMERIC(10, 2), currency varchar(3) not null, created_time timestamptz default current_timestamp, updated_time timestamptz default current_timestamp ); """ initialstate["context"]["table_schema"] = table_schema transitionargs = ( 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, horizontalSampleSimulationPhase=1, maxSimulationDepth=1, maxiterations=1, explorationweight=1.0, earlystop=earlystop, saveSimulatedNode=true, multithread=false) # compare all high value state answer then select the best one if length(highValueState) > 1 selected = compareState(query, highValueState, text2textInstructLLM, llmFormatName) resultState = highValueState[selected] end 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["accepted_as_answer"] == "yes" # insertSQLVectorDB(resultState["question"], sql) # end # println("\n--- SQLLLM query() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # println(latest_action) # println("---") # error("SQLLLM query() end") return (result_str=latest_action["action_result"], result_raw=resultState["result_raw"]) end """ Make a new state. # Arguments # Return # Example ```jldoctest julia> ``` # Signature """ function makeNewState(currentstate::T1, thoughtDict::T2, response::NamedTuple, )::NamedTuple{(:newNodeKey, :newstate), Tuple{String, Dict{String, <:Any}}} where {T1<:AbstractDict, T2<:AbstractDict} if response[:success] thoughtDict["action_result"] = response[:result_str] else error(response[:errormsg]) end 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, )::String where {T1<:AbstractDict, T2<:AbstractString} similarSQL = if similarSQL === nothing "None" else "This is the closest matching SQL statement for a similar query: $similarSQL" end systemmsg = """ You are a SQL expert that generate multiple questions about the current situation. At each round of conversation, the user will give you the current situation: User query: ... Example: ... Your work progress: ... About the tables in the database: - Column name can be the same in different tables. Refer to column comments to get more details. - Columns represent properties of the items the table represents. For example, the 'color' column in a "dealer_car" table corresponds to the color of the dealer's car. - A junction table can be used to link tables together. You must follow the following guidelines: 1) Your question must be specific to locating each piece of information mentioned in the query and how to retrieve it. 2) Your question should be specific, self-contained and not require any additional context. 3) Some information can be accessed by joining multiple tables. 4) Do not generate any question or comments at the end. You should follow the following guidelines: - 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: 1) Q: Given the situation, "ask yourself" about the situation at least three, but no more than five, questions. 2) A: Given the situation, "answer to yourself" the best you can. - Do not generate any text after the last answer. You must only respond in format as described below: Q1: ... A1: ... Q2: ... A2: ... ... Here are some examples: Q: What information in the hints is not necessary based on the query? A: Country is not specified in the query thus it should not be included in an SQL Q: How can I modify a SQL example to fit my specific query needs? A: ... Q: Why the query failed? A: ... Q: What criteria become more restrictive as the search scope broadens and can be remove? A: In the "2019 Toyota Camry hybrid" search query, "2019" represents the most restrictive criteria because it narrows the data scope to a specific year, whereas "Toyota" and "Camry" are broader categories that allow for more general results. Q: What works and what not previously? A: ... Let's begin! """ header = ["Q1:"] dictkey = ["q1"] workprogress = "" for (k, v) in state["action_history"] if k ∉ ["query"] workprogress *= "$k: $v\n" end end response = nothing # store for show when error msg show up errornote = "N/A" for attempt in 1:maxattempt usermsg = """ $(context["tablelist"]) User query: $(state["action_history"]["question"]) Example: $similarSQL Your work progress: $workprogress P.S. $errornote /no_think """ _prompt = [ Dict(:name=> "system", :text=> systemmsg), Dict(:name=> "user", :text=> usermsg) ] # put in model format prompt = GeneralUtils.formatLLMtext(_prompt, llmFormatName) response = text2textInstructLLM(prompt, modelsize="medium") response = GeneralUtils.deFormatLLMtext(response, llmFormatName) think, response = GeneralUtils.extractthink(response) # check if response is valid q_number = count("Q", response) if q_number < 1 errornote = "Your previous attempt has too few question." println("\nERROR YiemAgent generatequestion(). Attempt $attempt/$maxattempt. $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end if occursin('`', response) response = replace(response, '`'=>"") end responsedict = GeneralUtils.textToDict(response, header; dictKey=dictkey, symbolkey=false) response = "Q1: " * responsedict["q1"] println("\nSQLLLM generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") pprintln(Dict(responsedict)) return response end error("generatequestion failed to generate a thought ", response) end end # module interface