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..b8a7aa7 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.6.5" 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..473f48c 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() 1 " @__LINE__ # lessonDict = copy(JSON.parsefile("lesson.json")) # lesson = @@ -110,7 +110,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 """ - + @info "YiemAgent decisionMaker() 2 " @__LINE__ # 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 a.chathistory[end]["content"] @@ -123,28 +123,27 @@ 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 + @info "YiemAgent decisionMaker() 3 " @__LINE__ msg = Dict( "model" => "gemma-4-E4B-it-UD-Q4_K_XL", "messages" => a.chathistory, "temperature" => 0.7 ) - + @info "YiemAgent decisionMaker() 4 " @__LINE__ response = a.context.text2textInstructLLM(a.id, msg) + @info "YiemAgent decisionMaker() 5 " @__LINE__ 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) - + @info "YiemAgent decisionMaker() 6 " @__LINE__ # dollar sign in Julia means string interpolation while occursin('$', response) response = replace(response, '$' => "USD") end - + @info "YiemAgent decisionMaker() 7 " @__LINE__ responsedict = nothing if occursin(requiredKeys[2], response) try @@ -155,14 +154,24 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 continue end end - + @info "YiemAgent decisionMaker() 8 " @__LINE__ # check whether all answer's key points are in responsedict + println("\n---") + println(responsedict) + println("---\n") ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + @info "YiemAgent decisionMaker() 9 " @__LINE__ if !ispass errornote = errormsg println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end + @info "YiemAgent decisionMaker() 10 " @__LINE__ + if responsedict["action_input"] == "CHAT_BOX" && + occursin("similar", responsedict["action_input"]) + @info "illegal CHAT_BOX search term" @__LINE__ + continue + end # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] # errornote = "Your previous attempt didn't use the given functions" @@ -172,7 +181,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # pprintln(responsedict) - + @info "YiemAgent decisionMaker() 11 " @__LINE__ return responsedict end error("DecisionMaker failed to generate a thought ", response) @@ -380,49 +389,85 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj @info "YiemAgent conversation() 2-3 think count $loopcount " @__LINE__ thoughtdict, result_raw = think(a) + @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__ if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__ + @info "YiemAgent conversation() 2-5 think count $loopcount " @__LINE__ + 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 + ), + ] + ) + + """ 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, + ... + ), + ] + ), + ] + ) + """ + + @info "YiemAgent conversation() 2-6 think count $loopcount " @__LINE__ + return response_to_frontend + else # still in action + @info "YiemAgent conversation() 2-7 think count $loopcount " @__LINE__ action_name = thoughtdict["action_name"] action_input = thoughtdict["action_input"] + @info "YiemAgent conversation() 2-8 think count $loopcount " @__LINE__ action_call = Dict{String, Any}( "role" => "action_call", "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),] ) + @info "YiemAgent conversation() 2-9 think count $loopcount " @__LINE__ addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg) - + @info "YiemAgent conversation() 2-10 think count $loopcount " @__LINE__ action_result = thoughtdict["action_result"] actionresult = Dict{String, Any}( "role" => "action_result", "content" => [Dict("type" => "text", "text" => "$action_result"),] ) + @info "YiemAgent conversation() 2-11 think count $loopcount " @__LINE__ addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg) + @info "YiemAgent conversation() 2-12 think count $loopcount " @__LINE__ end end end @@ -442,15 +487,16 @@ 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() 1-1 " @__LINE__ thoughtdict = decisionMaker(a) - @info "YiemAgent think() 1 " @__LINE__ + @info "YiemAgent think() 1-2 " @__LINE__ # pprintln(thoughtdict) result_raw = nothing if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - @info "YiemAgent think() 2 " @__LINE__ + @info "YiemAgent think() 2-1 " @__LINE__ thoughtdict, result_raw = generatechat!(a) - + @info "YiemAgent think() 2-2 " @__LINE__ elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE" @info "YiemAgent think() 3 " @__LINE__ thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict) @@ -462,7 +508,14 @@ function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, 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())") @@ -629,7 +682,7 @@ function generatechat!(a::T; maxattempt::Integer=10 "action_name": "...", "action_input": "..." """ - + @info "YiemAgent generatechat!() 1 " @__LINE__ system_msg = Dict( "role" => "system", "content" => [ @@ -644,7 +697,7 @@ function generatechat!(a::T; maxattempt::Integer=10 errornote = "N/A" response = nothing # placeholder for show when error msg show up - + @info "YiemAgent generatechat!() 2 " @__LINE__ for attempt in 1:maxattempt if attempt > 1 println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -660,10 +713,9 @@ 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) + response = strip(response) + @info "YiemAgent generatechat!() 3 " @__LINE__ responsedict = nothing if occursin(requiredKeys[2], response) try @@ -673,8 +725,12 @@ 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 + @info "YiemAgent generatechat!() 4 " @__LINE__ # check whether all answer's key points are in responsedict ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) if !ispass @@ -682,7 +738,7 @@ function generatechat!(a::T; maxattempt::Integer=10 println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end - + @info "YiemAgent generatechat!() 5 " @__LINE__ # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] # errornote = "Your previous attempt didn't use the given functions" # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -691,7 +747,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!() 6 " @__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..3c3fb65 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( @@ -286,11 +287,11 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool= )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"]) + # wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"]) 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_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: @@ -720,7 +707,7 @@ 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": "...", @@ -827,15 +814,15 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 responsedict[k] = _v end - # println("\n--- extractWineAttributes_1-1()") - # @show responsedict - # @info "---\n" @__LINE__ + println("\n--- extractWineAttributes_1-1()") + @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 +836,9 @@ 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__ + println("\n--- extractWineAttributes_1-2()") + @show result + @info "---\n" @__LINE__ return result end error("extractWineAttributes_1() 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. """