From ef523aaa48d93ee81d4ee8052c72b0fe05353ff3 Mon Sep 17 00:00:00 2001 From: narawat Date: Mon, 20 Jul 2026 12:23:03 +0700 Subject: [PATCH] update --- Manifest.toml | 42 +++++++- Project.toml | 2 + src/interface.jl | 16 ++- src/llmfunction.jl | 245 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 284 insertions(+), 21 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index a134300..c5957f2 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "1e317787f914f6d857feb7c23bb910d1185caed9" +project_hash = "dc7878808bbc4637a12e709dd495979a784824a5" [[deps.Accessors]] deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] @@ -244,6 +244,12 @@ git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" version = "0.1.10" +[[deps.EzXML]] +deps = ["Printf", "XML2_jll"] +git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3" +uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" +version = "1.2.3" + [[deps.FileIO]] deps = ["Pkg", "Requires", "UUIDs"] git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819" @@ -500,6 +506,12 @@ version = "1.11.3+1" uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" version = "1.11.0" +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.18.0+0" + [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" @@ -828,6 +840,12 @@ git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80" uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" version = "1.4.10" +[[deps.Serde]] +deps = ["CSV", "Dates", "EzXML", "JSON", "TOML", "UUIDs", "YAML"] +git-tree-sha1 = "f397fc8779cc53e4677c2708f3802c6996f28d00" +uuid = "db9b398d-9517-45f8-9a95-92af99003e0e" +version = "3.7.2" + [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" version = "1.11.0" @@ -932,6 +950,12 @@ git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a" uuid = "88034a9c-02f8-509d-84a9-84ec65e18404" version = "1.0.0" +[[deps.StringEncodings]] +deps = ["Libiconv_jll"] +git-tree-sha1 = "b765e46ba27ecf6b44faf70df40c57aa3a547dcb" +uuid = "69024149-9ee7-55f6-a4c4-859efe599b68" +version = "0.3.7" + [[deps.StringManipulation]] deps = ["PrecompileTools"] git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5" @@ -1052,11 +1076,23 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" version = "1.6.1" +[[deps.XML2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] +git-tree-sha1 = "3f3315d89fc954a28f5b471bce698ed6e27481be" +uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +version = "2.15.3+0" + +[[deps.YAML]] +deps = ["Base64", "Dates", "Printf", "StringEncodings"] +git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403" +uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" +version = "0.4.16" + [[deps.YiemAgent]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"] +deps = ["Base64", "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.5" +version = "0.7.2" [[deps.Zlib_jll]] deps = ["Libdl"] diff --git a/Project.toml b/Project.toml index f212494..038442f 100644 --- a/Project.toml +++ b/Project.toml @@ -19,6 +19,7 @@ PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" +Serde = "db9b398d-9517-45f8-9a95-92af99003e0e" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" @@ -33,3 +34,4 @@ JSON = "1.6.1" LLMMCTS = "0.1.5" NATS = "0.1.0" SQLLLM = "0.2.8" +Serde = "3.7.2" diff --git a/src/interface.jl b/src/interface.jl index db2907b..581a0cd 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -393,8 +393,6 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj if loopcount > max_think_loop thoughtdict, result_raw = generatechat!(a) - - assistant_response = Dict{String, Any}( "role" => "assistant", "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] @@ -404,17 +402,17 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj items_info = [] send_item_ind = [] # index of the item being send to frontend if haskey(a.memory["shortmem"], "items_info") - for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) push!(items_info, deepcopy(item)) push!(send_item_ind, i) - + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ end end # remove sent items deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) - + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ end response_to_frontend = Dict{String, Any}( @@ -435,8 +433,6 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj 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"]),] @@ -446,17 +442,17 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj items_info = [] send_item_ind = [] # index of the item being send to frontend if haskey(a.memory["shortmem"], "items_info") - for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) push!(items_info, deepcopy(item)) push!(send_item_ind, i) - + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ end end # remove sent items deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) - + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ end response_to_frontend = Dict{String, Any}( diff --git a/src/llmfunction.jl b/src/llmfunction.jl index b47d29f..270cac4 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -5,7 +5,7 @@ export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recomme extractWineAttributes_2, paraphrase, SQLexecution using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures, - Base64 + Base64, Serde using GeneralUtils, SQLLLM using ..type, ..util @@ -277,14 +277,19 @@ end # Example ```jldoctest julia> using ChatAgent -julia> agent = ChatAgent.agentReflex("Jene") -julia> input = "{\"food\": \"pizza\", \"occasion\": \"anniversary\"}" -julia> result = checkinventory(agent, input) -"{"wine 1": {\"Winery\": \"Pichon Baron\", \"wine name\": \"Pauillac (Grand Cru Classé)\", \"grape variety\": \"Cabernet Sauvignon\", \"year\": 2010, \"price\": \"125 USD\", \"stock ID\": \"ar-17\"}, }" +julia> agent = YiemAgent.sommelier(...) +julia> thoughtdict = + OrderedDict{String, Any}( + "plan" => "The user is asking a very specific question about a wine (Brunello di Montalcino from Tenuta CastelGiocondo). Although the policy suggests gathering budget, wine type, and occasion, the user has provided enough specific information (name, region, producer) to attempt a direct search in the database. I will use the SEARCH_WINE_DATABASE action to check if this specific wine is in our inventory.", + "action_name" => "SEARCH_WINE_DATABASE", + "action_input" => "Brunello di Montalcino from Tenuta CastelGiocondo") ``` """ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} + + # WORKING + # look_for_wine_in_wine_database(a, thoughtdict["action_input"]) println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"]) @@ -366,9 +371,9 @@ function generatesql(a::T, searchterm::String, Fulfill the objective. # you should then respond to the user with interleaving plan, action_name, action_input - 1) "plan, Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) "action_name, Must be "RUNSQL" - 3) "action_input, The input to the action you are about to perform according to your plan. + 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. + 2) "action_name", Must be "RUNSQL" + 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. # you should only respond in JSON format as described below @@ -610,6 +615,230 @@ function generatesql(a::T, searchterm::String, error("SQLLLM DecisionMaker() failed to generate a thought \n", response) end + +""" +# Example +```jldoctest +julia> using ChatAgent +julia> agent = YiemAgent.sommelier(...) +julia> thoughtdict = + OrderedDict{String, Any}( + "plan" => "The user is asking a very specific question about a wine (Brunello di Montalcino from Tenuta CastelGiocondo). Although the policy suggests gathering budget, wine type, and occasion, the user has provided enough specific information (name, region, producer) to attempt a direct search in the database. I will use the SEARCH_WINE_DATABASE action to check if this specific wine is in our inventory.", + "action_name" => "SEARCH_WINE_DATABASE", + "action_input" => "Brunello di Montalcino from Tenuta CastelGiocondo") +``` +julia> look_for_wine_in_wine_database(agent, thoughtdict["action_input"]) +""" +function look_for_wine_in_wine_database(a::T, searchterm::String, + ; maxattempt=10 + )::String where {T<:agent} + + systemmsg = + """ + + # situation + At each round of conversation, you will be given the following: + - user search term + - database tables schema + + # objective + Consult the provided database schema (tables and columns), please map a user's natural-language search term to the appropriate database columns and tables—identify the relevant fields, operators, and values (e.g., for SQL filtering). + + # your responsibility includes + Fulfill the objective. + + # you should only respond in YAML format as described below + table_name_1: + column_name_1: + operator: "=" + value: "..." + column_name_2: + operator: "=" + value: "..." + ... + table_name_2: + column_name_1: + operator: "=" + value: "..." + column_name_2: + operator: "=" + value: "..." + ... + + # here are some example + + 4-wheel drive car with red color that will give me fast and furious emotion. No more than 7000 USD + + + car_info: # table_name + drive_type: # column_name + operator: "=" # operator is not "N/A" because drive_type column store quantitative value + value: "4-wheel" # column_value + color: + operator: "=" # operator is not "N/A" because color column store quantitative value + value: "red" + drive_feeling: + operator: "N/A" # operator is "N/A" because drive_feeling column store qualitative value + value: "fast and furious" + price_list: + price: + operator: "<" # operator is not "N/A" because drive_type column store quantitative value + value: "7000" + + """ + + + # use find_related_tables_for_user_question and inject only related table schema for a given search term + # to LLM instead of giving LLM all tables schema. + related_tables = a.context.find_related_tables_for_user_question(searchterm) + table_schema = "" + for table in related_tables + _table_schema_str = GeneralUtils.get_db_table_schema_simple(a.context.pg_conn_str, table) + table_schema_str = sprint(show, _table_schema_str) * "\n" + table_schema = table_schema * table_schema_str + end + + context = + """ + + + $table_schema + + + """ + input = context * searchterm + + 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" => input), + ] + ), + ], + "temperature" => 0.7 + ) + + for attempt in 1:maxattempt + response = a.context.text2textInstructLLM("random_id", msg) + responsedict = Serde.parse_yaml(response) + + """ + responsedict = Dict( + "wine" => Dict( + "tasting_notes" => Dict( + "operator" => "N/A", + "value" => "casual dinner" + ), + "wine_type" => Dict( + "operator" => "=", + "value" => "red" + ) + ), + "retailer_wine" => Dict( + "currency" => Dict( + "operator" => "=", "value" => "USD" + ), + "price" => Dict( + "operator" => "<", "value" => "1000" + ) + ) + ) + """ + + for (table_name, table_info_dict) in responsedict + for (column_name, v) in table_info_dict + + # + do_not_resolve_BM25_list = ["tasting_notes", "seo_name", "vintage", "grape"] + if column_name ∉ do_not_resolve_list + words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, table_name, column_name) + resolved_word = GeneralUtils.resolve_entity(v["value"], words_catalog; threshold=0.9) + table_info_dict[column_name] = resolved_word + end + end + end + + + # check each attributes against each column in a database table with BM25 and get the closest + # word match because there is a typo sometimes. + for (k, v) in responsedict + if k ∉ ["tasting_notes"] + words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k) + resolved_word = GeneralUtils.resolve_entity(v, words_catalog; threshold=0.9) + responsedict[k] = resolved_word + end + end + + + + + + + #WORKING + println("\n", responsedict) + @info "test done " @__LINE__ + error(9999) + + + + + + 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 each attributes against each column in a database table with BM25 and get the closest + # word match because there is a typo sometimes. + for (k, v) in responsedict + if k ∉ ["tasting_notes"] + words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k) + resolved_word = GeneralUtils.resolve_entity(v, words_catalog; threshold=0.9) + responsedict[k] = resolved_word + end + end + + # LLM already extract user search term against tables schema + # Ex. responsedict = Dict( + # "wine_type"=> "red", # hard constraint + # "region"=> "bordeaux", # hard constraint + # "price_max"=> "100", # hard constraint + # "tasting_notes"=> "fruity, oak" # semantic search) + + + + + + + + + + + + + + + + + return items + end + error("SQLLLM DecisionMaker() failed to generate a thought \n", response) +end + function SQLexecution(executeSQL::Function, sql::T )::NamedTuple where {T<:AbstractString} -- 2.52.0