diff --git a/Manifest.toml b/Manifest.toml index 8bc7203..a134300 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "a5128932115e2d3aa9658af105ac712db061d523" +project_hash = "1e317787f914f6d857feb7c23bb910d1185caed9" [[deps.Accessors]] deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] @@ -97,9 +97,9 @@ uuid = "944b1d66-785c-5afd-91f1-9de20f533193" version = "0.7.8" [[deps.CommonSolve]] -git-tree-sha1 = "99ee296f88c12485402e37c2fd025f95ae097637" +git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662" uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" -version = "0.2.9" +version = "0.2.11" [[deps.Compat]] deps = ["TOML", "UUIDs"] @@ -246,9 +246,13 @@ version = "0.1.10" [[deps.FileIO]] deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "91e0e5c68d02bcdaae76d3c8ceb4361e8f28d2e9" +git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819" uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.16.5" +version = "1.20.0" +weakdeps = ["HTTP"] + + [deps.FileIO.extensions] + HTTPExt = "HTTP" [[deps.FilePathsBase]] deps = ["Compat", "Dates"] @@ -311,9 +315,9 @@ version = "1.14.0" [[deps.HTTP]] deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] -git-tree-sha1 = "eda1d37cb55d90a17d0957c75841138c88b361a1" +git-tree-sha1 = "c2c808326222b6dc4bec295a83b55f79aeec98e0" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "2.5.4" +version = "2.5.5" [[deps.HashArrayMappedTries]] git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" @@ -678,15 +682,17 @@ version = "0.4.2" [[deps.PrettyTables]] deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "624de6279ab7d94fc9f672f0068107eb6619732c" +git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "3.3.2" +version = "3.4.0" [deps.PrettyTables.extensions] + PrettyTablesExcelExt = "XLSX" PrettyTablesTypstryExt = "Typstry" [deps.PrettyTables.weakdeps] Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" + XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [[deps.Printf]] deps = ["Unicode"] @@ -767,9 +773,9 @@ version = "0.5.1+0" [[deps.Roots]] deps = ["Accessors", "CommonSolve", "Printf"] -git-tree-sha1 = "ed45bcc7cf3c8887595b973f2b1efbe91dcc50ec" +git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "3.0.1" +version = "3.0.5" [deps.Roots.extensions] RootsChainRulesCoreExt = "ChainRulesCore" @@ -1047,10 +1053,10 @@ uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" version = "1.6.1" [[deps.YiemAgent]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"] +deps = ["CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"] path = "." uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" -version = "0.6.4" +version = "0.6.5" [[deps.Zlib_jll]] deps = ["Libdl"] diff --git a/Project.toml b/Project.toml index ccb543e..ea75dbc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,9 +1,10 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" -version = "0.6.4" +version = "0.7.0" authors = ["narawat lamaiin "] [deps] +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" @@ -23,6 +24,7 @@ URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] +Base64 = "1.11.0" CSV = "0.10.15" DataFrames = "1.7.0" GeneralUtils = "0.5.1" diff --git a/etc.jl b/etc.jl index 4b46fd2..bc26067 100644 --- a/etc.jl +++ b/etc.jl @@ -1,28 +1,111 @@ +using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures +using GeneralUtils, SQLLLM, YiemAgent + +config = JSON.parsefile("./appconfig.json") +host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':') +port = parse(Int, _port) +dbname = "winedb" +user = config["externalservice"]["sommpanion_db"]["user"] +password = config["externalservice"]["sommpanion_db"]["password"] +pg_conn_str = "host=$host_url port=$port dbname=$dbname user=$user password=$password" + + function execute_sql_winedb(sql::T) where {T<:AbstractString} + host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':') + port = parse(Int, _port) + dbname = "winedb" + user = config["externalservice"]["sommpanion_db"]["user"] + password = config["externalservice"]["sommpanion_db"]["password"] + db_connection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password") + result = nothing + try + result = LibPQ.execute(db_connection, sql) + catch e + LibPQ.close(db_connection) + end + + LibPQ.close(db_connection) + return result + end + + + +sql = """ -CREATE TABLE "public"."wine" ( - "wine_id" uuid DEFAULT gen_random_uuid() NOT NULL, - "seo_name" character varying(128) NOT NULL, - "wine_name" character varying(128) NOT NULL, - "winery" character varying(128) NOT NULL, - "vintage" integer NOT NULL, - "region" character varying(128) NOT NULL, - "country" character varying(128) NOT NULL, - "wine_type" character varying(128) NOT NULL, - "grape" character varying(128) NOT NULL, - "serving_temperature" character varying(128) NOT NULL, - "intensity" integer NULL, - "sweetness" integer NULL, - "tannin" integer NULL, - "acidity" integer NULL, - "fizziness" integer NULL, - "tasting_notes" text NULL, - "image_url" jsonb NULL, - "manufacturer_sku" text NULL, - "note" text NULL, - "other_attributes" jsonb NULL, - "created_time" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NULL, - "updated_time" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NULL, - "description" text NULL, - PRIMARY KEY (wine_id) -); -""" \ No newline at end of file +SELECT T1.winery, T1.wine_name, T1.wine_id, T1.vintage, T1.region, T1.country, T1.wine_type, T1.grape, T1.serving_temperature, T1.sweetness, T1.intensity, T1.tannin, T1.acidity, T1.tasting_notes, T2.price, T2.currency, T1.image_url, T3.retailer_name, T3.retailer_id FROM "wine" AS T1 JOIN "retailer_wine" AS T2 ON T1.wine_id = T2.wine_id JOIN "retailer" AS T3 ON T2.retailer_id = T3.retailer_id WHERE T1.wine_name = 'Montrachet Grand Cru' AND T1.winery = 'Domaine Jacques Prieur' AND T3.retailer_name = 'Yiem Wines Ltd' AND T3.retailer_id = 'f54eab6b-7650-4448-b009-c53f3efbcc3b'; +""" + +textresult, sql_result_raw, _, _ = YiemAgent.SQLexecution(execute_sql_winedb, sql) +result_vec = GeneralUtils.dfToVectorDict(sql_result_raw) + +for d in result_vec + wine_name = d["wine_name"] + image_url_json_str = d["image_url"] + image_url_json_obj = JSON.parse(image_url_json) + base_url = "http://192.168.88.106:8080/" + image_base64 = + if haskey(image_url_json_obj, "bottle") + url = base_url * image_url_json_obj["bottle"] + image_data = HTTP.get(url) # vector{int} data + image_base64_string = base64encode(image_data) + else + nothing + end + d["image"] = image_base64 +end + + + + + + + + + + +using LibPQ +using Tables + +""" + update_car_regions_one_by_one(conn::LibPQ.Connection, target_word::String) + +Iterates through all rows in the 'car' table where the region is "German", +and updates them one-by-one to the `target_word`. +""" +function update_car_regions_one_by_one(pg_conn_str::String, replace_word::String , target_word::String) + conn = LibPQ.Connection(pg_conn_str) + # 1. Fetch the target rows. Assumes 'id' is the primary key. + # We select the ID to target rows individually during the update step. + select_query = "SELECT id FROM car WHERE region = '$replace_word';" + + result = execute(conn, select_query) + rows = Tables.rows(result) + + # 2. Prepare the update statement for execution reuse + # Using explicit types for parameter placeholders ($1, $2) + update_query = "UPDATE car SET region = \$1 WHERE id = \$2;" + + println("Starting one-by-one update...") + updated_count = 0 + + # 3. Iterate through rows one-by-one + for row in rows + # LibPQ row values are accessed via properties or column names + row_id = row.id + + # Execute the parameterized statement safely + execute(conn, update_query, [target_word, row_id]) + updated_count += 1 + end + + println("Successfully updated \$updated_count rows.") + return updated_count +end + + + + + + + + + diff --git a/image.png b/image.png new file mode 100644 index 0000000..c8926fc Binary files /dev/null and b/image.png differ diff --git a/src/interface.jl b/src/interface.jl index 3761adf..eacc7ec 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -75,7 +75,7 @@ OrderedDict{String, Any} with 4 entries: """ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 ) where {T<:agent} - + @info "YiemAgent decisionMaker() start " @__LINE__ # lessonDict = copy(JSON.parsefile("lesson.json")) # lesson = @@ -123,9 +123,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 response = nothing # placeholder for show when error msg show up for attempt in 1:maxattempt - if attempt > 1 - println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end + msg = Dict( "model" => "gemma-4-E4B-it-UD-Q4_K_XL", @@ -134,12 +132,13 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 ) response = a.context.text2textInstructLLM(a.id, msg) + response = GeneralUtils.clean_json_response(response) response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) response = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to response = strip(response) - + # dollar sign in Julia means string interpolation while occursin('$', response) response = replace(response, '$' => "USD") @@ -154,13 +153,26 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end + else + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue end # check whether all answer's key points are in responsedict + println("\n---") + println(responsedict) + println("---\n") 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") + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue + end + + if responsedict["action_input"] == "CHAT_BOX" && + occursin("similar", responsedict["action_input"]) + continue end @@ -172,7 +184,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # pprintln(responsedict) - + @info "YiemAgent decisionMaker() end " @__LINE__ return responsedict end error("DecisionMaker failed to generate a thought ", response) @@ -304,7 +316,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) if !ispass errornote = errormsg - println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end @@ -347,7 +359,7 @@ message => Dict( function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}}, maximumMsg=50, max_think_loop::Integer=3) - @info "YiemAgent conversation() 1 " @__LINE__ + @info "YiemAgent conversation() start " @__LINE__ userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"]) # find text in usermsg @@ -363,7 +375,7 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj clearhistory(a) return "Okay. What shall we talk about?" else - @info "YiemAgent conversation() 2 " @__LINE__ + # add usermsg to a.chathistory but how do I handle images? addNewMessage(a, "user", userinput; maximumMsg=maximumMsg) @@ -372,49 +384,111 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj while true loopcount += 1 if loopcount > max_think_loop - @info "YiemAgent conversation() 2-1 think count $loopcount " @__LINE__ - r = generatechat!(a) - @info "YiemAgent conversation() 2-2 think count $loopcount " @__LINE__ - return r - end - @info "YiemAgent conversation() 2-3 think count $loopcount " @__LINE__ - thoughtdict, result_raw = think(a) - if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__ + thoughtdict, result_raw = generatechat!(a) + + assistant_response = Dict{String, Any}( "role" => "assistant", "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] ) addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) - @info "YiemAgent conversation() 2-5 think count $loopcount " @__LINE__ - return thoughtdict["action_input"] - # elseif thoughtdict["action_name"] ∈ ["CHAT_BOX"] - # @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__ - # assistant_response = Dict{String, Any}( - # "role" => "assistant", - # "content" => [ - # Dict("type" => "text", "text" => thoughtdict["action_input"]), - # Dict( #WORKING put 1st image here - # "type" => "image_url", - # "image_url" => Dict("url" => image1_data_uri) - # ), - # Dict( #WORKING put 2nd image here - # "type" => "image_url", - # "image_url" => Dict("url" => image2_data_uri) - # ), - # ] - # ) - # addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) - # return thoughtdict["action_input"] #XXX change output from string to dict - else + items_info = [] + if haskey(a.memory["shortmem"], "items_info") + for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) + if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) + push!(items_info, item) + deleteat!(a.memory["shortmem"]["items_info"], i) + end + end + end + + response_to_frontend = Dict{String, Any}( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => thoughtdict["action_input"]), + Dict( + "type" => "items_info", + "items_info" => items_info + ), + ] + ) + + return response_to_frontend + end + + + thoughtdict, result_raw = think(a) + + if thoughtdict["action_name"] ∈ ["CHAT_BOX"] + + + assistant_response = Dict{String, Any}( + "role" => "assistant", + "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] + ) + addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) + + items_info = [] + if haskey(a.memory["shortmem"], "items_info") + for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) + if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) + push!(items_info, item) + deleteat!(a.memory["shortmem"]["items_info"], i) + end + end + end + + response_to_frontend = Dict{String, Any}( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => thoughtdict["action_input"]), + Dict( + "type" => "items_info", + "items_info" => items_info + ), + ] + ) + + """ intended message to send to frontend should have the following format. + response_to_frontend = Dict{String, Any}( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => "assistant_text_response"), + Dict( + "type" => "items_info", + "items_info" => [ + Dict( + "wine_name"=> "wine name 1", + "wine_id"=> "...", + "image"=> base64 encoded image, + ... + ), + Dict( + "wine_name"=> "wine name 2", + "wine_id"=> "...", + "image"=> base64 encoded image, + ... + ), + ] + ), + ] + ) + """ + + + return response_to_frontend + else # still in action + action_name = thoughtdict["action_name"] action_input = thoughtdict["action_input"] + action_call = Dict{String, Any}( "role" => "action_call", "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),] ) + addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg) action_result = thoughtdict["action_result"] @@ -422,7 +496,9 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj "role" => "action_result", "content" => [Dict("type" => "text", "text" => "$action_result"),] ) + addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg) + @info "YiemAgent conversation() end think count $loopcount " @__LINE__ end end end @@ -442,34 +518,44 @@ julia> """ function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0) + @info "YiemAgent think() start " @__LINE__ thoughtdict = decisionMaker(a) @info "YiemAgent think() 1 " @__LINE__ - # pprintln(thoughtdict) + @show thoughtdict + println("---\n") result_raw = nothing if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - @info "YiemAgent think() 2 " @__LINE__ + thoughtdict, result_raw = generatechat!(a) - + elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE" - @info "YiemAgent think() 3 " @__LINE__ + thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict) elseif thoughtdict["action_name"] ∈ ["WINE_PRESENTATION_GUIDELINE"] - @info "YiemAgent think() 4 " @__LINE__ + thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict) elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE" - @info "YiemAgent think() 5 " @__LINE__ + thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false) - #WORKING result_raw will be a df. i need to get images so i can send to frontend + if result_raw !== nothing && result_raw isa Vector + if haskey(a.memory["shortmem"], "items_info") + append!(a.memory["shortmem"]["items_info"], result_raw) + else + a.memory["shortmem"]["items_info"] = result_raw + end + end + else - @info "YiemAgent think() 6 " @__LINE__ + error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - println("\n") + + @info "YiemAgent think() end " @__LINE__ @show thoughtdict - @info "YiemAgent think() 7 " @__LINE__ + println("---\n") return (thoughtdict=thoughtdict, result_raw=result_raw) end @@ -546,7 +632,7 @@ end #PENDING function generatechat!(a::T; maxattempt::Integer=10 )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - + @info "YiemAgent generatechat!() start " @__LINE__ # lessonDict = copy(JSON.parsefile("lesson.json")) # lesson = @@ -629,7 +715,7 @@ function generatechat!(a::T; maxattempt::Integer=10 "action_name": "...", "action_input": "..." """ - + system_msg = Dict( "role" => "system", "content" => [ @@ -660,8 +746,7 @@ function generatechat!(a::T; maxattempt::Integer=10 response = GeneralUtils.clean_json_response(response) response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) - response = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to - + response = strip(response) responsedict = nothing @@ -673,13 +758,17 @@ function generatechat!(a::T; maxattempt::Integer=10 println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end + else + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + 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 generatechat() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end @@ -691,7 +780,8 @@ function generatechat!(a::T; maxattempt::Integer=10 # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # pprintln(responsedict) - + responsedict["action_result"] = "Action result is the next user dialogue." + @info "YiemAgent generatechat!() end " @__LINE__ return (thoughtdict=responsedict, result_raw=responsedict["action_input"]) end error("YiemAgent generatechat() failed to generate a thought ", response) diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 116e764..b47d29f 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -2,9 +2,10 @@ module llmfunction export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recommendbox, virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, - extractWineAttributes_2, paraphrase + extractWineAttributes_2, paraphrase, SQLexecution -using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures +using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures, + Base64 using GeneralUtils, SQLLLM using ..type, ..util @@ -211,7 +212,7 @@ pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemms receiverName= "text2textinstruct", mqttBroker= config["mqttServerInfo"]["broker"], mqttBrokerPort= config["mqttServerInfo"]["port"], - msgId = string(uuid4()) #CHANGE remove after testing finished + msgId = string(uuid4()) # remove after testing finished ) outgoingMsg = Dict( @@ -290,7 +291,7 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool= wineattributes_2 = extractWineAttributes_2(a, thoughtdict["action_input"]) retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency", "image_url", "retailer_name", "retailer_id"] - _inventoryquery = "$wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)" + _inventoryquery = "$(thoughtdict["action_input"]), $wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)" inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -309,47 +310,33 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool= # direct query with possible sql instead of SQLLLM. sql = generatesql(a, inventoryquery) println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n") - textresult, result_raw, _, _ = SQLexecution(a.context.executeSQL, sql) - #WORKING if result_raw != nothing, get image from image_url column of a df. - # then store in a.memory["shortmem]["image"] = OrderedDict( - # Dict( - # "wine_id"=> "wine_id, - # "name"=> "wine name", - # "image_url" => Dict("url" => data1_uri) - # ) - - # ) - - - # # 1. Read local file and encode to base64 string - # image2_path = "test/small_image.png" - # image2_bytes = read(image2_path) - # image2_base64_string = base64encode(image2_bytes) - # mime_type = "image/png" - # data2_uri = "data:$(mime_type);base64,$(image2_base64_string)" - - # # 3. Construct payload with the Data URI - # message = Dict( - # "role" => "user", - # "content" => [ - # Dict("type" => "text", "text" => "Do you know type of wine in the image?"), - # Dict( - # "type" => "image_url", - # "image_url" => Dict("url" => data1_uri) - # ) - # ] - # ) - - - - + textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql) + items = nothing + if sql_result_df !== nothing + result_vec = GeneralUtils.dfToVectorDict(sql_result_df) + # get image + for d in result_vec + image_url_json_str = d["image_url"] + image_url_json_obj = JSON.parse(image_url_json_str) + base_url = "http://192.168.88.106:8080/" + if haskey(image_url_json_obj, "bottle") + url = base_url * image_url_json_obj["bottle"] + image_data = HTTP.get(url) # vector{int} data + image_base64_string = base64encode(image_data.body) + d["image"] = image_base64_string + else + d["image"] = nothing + end + end + items = result_vec # image is added to each item + end thoughtdict["action_result"] = textresult end - return (thoughtdict=thoughtdict, result_raw=result_raw) + return (thoughtdict=thoughtdict, result_raw=items) end @@ -366,7 +353,7 @@ function generatesql(a::T, searchterm::String, - 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 search term, 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. + - Overly strict condition usually yields empth result # situation At each round of conversation, you will be given the following: @@ -711,7 +698,6 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 wine_name: name of the wine winery: name of the winery vintage: the year of the wine - region: a region, such as Burgundy, Bordeaux, Champagne, Napa Valley, Tuscany, California, Oregon, etc. Use "or" if there are multiple regions. country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States". Use "or" if there are multiple countries. wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" grape_varietal: the name of the primary grape used to make the wine @@ -720,13 +706,12 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100. occasion: the occasion the user is having the wine for food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc - _keyword suffice is the related keyword that appears in user's query. each keyword can not be used twice. + _keyword suffice is the related keyword that appears in user's query. "wine_name": "...", "winery": "...", "vintage": "...", - "region": "...", "country": "...", "wine_type": "...", "grape_varietal": "...", @@ -741,7 +726,6 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 "wine_name": "N/A", "winery": "N/A", "vintage": "N/A", - "region": "Tuscany or Napa Valley", "country": "Italy or United States", "wine_type": "red or white", "grape_varietal": "Chenin Blanc or Riesling", @@ -755,7 +739,6 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 "wine_name": "Saumur Blanc", "winery": "Domaine du Collier", "vintage": "2019", - "region": "N/A", "country": "France", "wine_type": "white", "grape_varietal": "Merlot", @@ -766,7 +749,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 "food_to_be_paired_with_wine": "N/A" """ - requiredKeys = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"] + requiredKeys = ["wine_name", "winery", "vintage", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"] errornote = "" context = """ @@ -827,15 +810,15 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 responsedict[k] = _v end - # println("\n--- extractWineAttributes_1-1()") - # @show responsedict - # @info "---\n" @__LINE__ + @info "YiemAgent extractWineAttributes_1() " @__LINE__ + @show responsedict + @info "---\n" @__LINE__ # check each attributes against each column in a database table with BM25 for (k, v) in responsedict if k ∉ ["wine_price_min", "wine_price_max"] words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k) - resolved_word = GeneralUtils.resolve_entity(v, words_catalog;threshold=0.9) + resolved_word = GeneralUtils.resolve_entity(v, words_catalog; threshold=0.9) responsedict[k] = resolved_word end end @@ -849,10 +832,11 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 end result = result[1:end-2] # remove the ending ", " - # println("\n--- extractWineAttributes_1-2()") - # @show responsedict - # @show result - # @info "---\n" @__LINE__ + + @info "YiemAgent extractWineAttributes_1() " @__LINE__ + @show result + @info "---\n" @__LINE__ + return result end error("extractWineAttributes_1() failed to get a response") @@ -1005,9 +989,6 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< for attempt in 1:10 response = a.context.text2textInstructLLM(a.id, msg) response = GeneralUtils.clean_json_response(response) - println("\n--- extractWineAttributes_2-1()") - println(response) - println("--- \n") response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) @@ -1045,9 +1026,11 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< end end result = result[1:end-2] # remove the ending ", " - println("\n--- extractWineAttributes_2-2()") - println(result) - println("--- \n") + + @info "YiemAgent extractWineAttributes_2() " @__LINE__ + @show result + @info "---\n" @__LINE__ + return result end error("extractWineAttributes_2() failed to get a response") diff --git a/src/type.jl b/src/type.jl index f97f99c..e42e8e3 100644 --- a/src/type.jl +++ b/src/type.jl @@ -252,6 +252,7 @@ function sommelier( - Your store carries only wine. - Vintage 0 means non-vintage. - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. + - User usually ask for something similar. This means you should use the search term based on the profile they like. # situation You are having conversation with a customer. @@ -285,10 +286,10 @@ function sommelier( # available actions "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. - "SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is supported search criteria including: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." + "SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD." Example query 2: "Red or white wine, medium tannin, price under 700 USD" - Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France + Example query 3: "white wine from Tuscany, Italy or Bordeaux, France "WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. "END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. """