Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3ab288cfe | |||
| 17b0974d82 | |||
| 8fed0b5e8f | |||
| 567d6b79d7 | |||
| 74be3e5717 | |||
| 402b6fcadd | |||
| ef523aaa48 |
+39
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
julia_version = "1.12.6"
|
julia_version = "1.12.6"
|
||||||
manifest_format = "2.0"
|
manifest_format = "2.0"
|
||||||
project_hash = "1e317787f914f6d857feb7c23bb910d1185caed9"
|
project_hash = "dc7878808bbc4637a12e709dd495979a784824a5"
|
||||||
|
|
||||||
[[deps.Accessors]]
|
[[deps.Accessors]]
|
||||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||||
@@ -244,6 +244,12 @@ git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec"
|
|||||||
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
|
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
|
||||||
version = "0.1.10"
|
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.FileIO]]
|
||||||
deps = ["Pkg", "Requires", "UUIDs"]
|
deps = ["Pkg", "Requires", "UUIDs"]
|
||||||
git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819"
|
git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819"
|
||||||
@@ -500,6 +506,12 @@ version = "1.11.3+1"
|
|||||||
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
||||||
version = "1.11.0"
|
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.LinearAlgebra]]
|
||||||
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
|
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
|
||||||
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||||
@@ -828,6 +840,12 @@ git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80"
|
|||||||
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
|
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
|
||||||
version = "1.4.10"
|
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]]
|
[[deps.Serialization]]
|
||||||
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -932,6 +950,12 @@ git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a"
|
|||||||
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
|
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
|
||||||
version = "1.0.0"
|
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.StringManipulation]]
|
||||||
deps = ["PrecompileTools"]
|
deps = ["PrecompileTools"]
|
||||||
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
|
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
|
||||||
@@ -1052,11 +1076,23 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
|
|||||||
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
|
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
|
||||||
version = "1.6.1"
|
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.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 = "."
|
path = "."
|
||||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||||
version = "0.6.5"
|
version = "0.7.2"
|
||||||
|
|
||||||
[[deps.Zlib_jll]]
|
[[deps.Zlib_jll]]
|
||||||
deps = ["Libdl"]
|
deps = ["Libdl"]
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "YiemAgent"
|
name = "YiemAgent"
|
||||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||||
version = "0.7.2"
|
version = "0.7.4"
|
||||||
authors = ["narawat lamaiin <narawat@outlook.com>"]
|
authors = ["narawat lamaiin <narawat@outlook.com>"]
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
@@ -19,6 +19,7 @@ PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
|||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
||||||
|
Serde = "db9b398d-9517-45f8-9a95-92af99003e0e"
|
||||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||||
@@ -33,3 +34,4 @@ JSON = "1.6.1"
|
|||||||
LLMMCTS = "0.1.5"
|
LLMMCTS = "0.1.5"
|
||||||
NATS = "0.1.0"
|
NATS = "0.1.0"
|
||||||
SQLLLM = "0.2.8"
|
SQLLLM = "0.2.8"
|
||||||
|
Serde = "3.7.2"
|
||||||
|
|||||||
@@ -109,3 +109,92 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function generate_wine_retail_sql(conditions::Dict{String, Any})::String
|
||||||
|
# 1. Base SQL structure
|
||||||
|
base_query = """
|
||||||
|
SELECT
|
||||||
|
w.winery,
|
||||||
|
w.wine_name,
|
||||||
|
w.wine_id,
|
||||||
|
w.vintage,
|
||||||
|
w.region,
|
||||||
|
w.country,
|
||||||
|
w.wine_type,
|
||||||
|
w.grape,
|
||||||
|
w.serving_temperature,
|
||||||
|
w.sweetness,
|
||||||
|
w.intensity,
|
||||||
|
w.tannin,
|
||||||
|
w.acidity,
|
||||||
|
w.tasting_notes,
|
||||||
|
rw.price,
|
||||||
|
rw.currency,
|
||||||
|
w.image_url,
|
||||||
|
NULL AS retailer_name,
|
||||||
|
rw.retailer_id
|
||||||
|
FROM wine AS w
|
||||||
|
JOIN retailer_wine AS rw
|
||||||
|
ON w.wine_id = rw.wine_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 2. Dynamic WHERE Clause Builder
|
||||||
|
where_clauses = String[]
|
||||||
|
|
||||||
|
# Iterate over each table condition provided
|
||||||
|
for (table_name, table_conditions) in conditions
|
||||||
|
|
||||||
|
# Determine table alias
|
||||||
|
alias = if table_name == "wine"
|
||||||
|
"w"
|
||||||
|
elseif table_name == "retailer_wine"
|
||||||
|
"rw"
|
||||||
|
else
|
||||||
|
continue # Skip unsupported tables
|
||||||
|
end
|
||||||
|
|
||||||
|
# Process condition dictionaries
|
||||||
|
if isa(table_conditions, Dict) && !isempty(table_conditions)
|
||||||
|
|
||||||
|
for (column_name, filter_details) in table_conditions
|
||||||
|
|
||||||
|
if isa(filter_details, Dict) && haskey(filter_details, "operator")
|
||||||
|
op = filter_details["operator"]
|
||||||
|
raw_val = filter_details["value"]
|
||||||
|
|
||||||
|
# --- Value Type Handling ---
|
||||||
|
# Use tryparse instead of try/catch for cleaner, faster parsing
|
||||||
|
final_val = raw_val
|
||||||
|
|
||||||
|
if op in ("=", "<", ">", "<=", ">=")
|
||||||
|
str_val = string(raw_val)
|
||||||
|
num_val = tryparse(Float64, str_val)
|
||||||
|
|
||||||
|
if !isnothing(num_val)
|
||||||
|
final_val = isinteger(num_val) ? round(Int, num_val) : num_val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- SQL Formatting ---
|
||||||
|
if isa(final_val, Number)
|
||||||
|
clause = "$(alias).$(column_name) $(op) $(final_val)"
|
||||||
|
else
|
||||||
|
# Escape single quotes within string values
|
||||||
|
escaped_val = replace(string(final_val), "'" => "''")
|
||||||
|
clause = "$(alias).$(column_name) $(op) '$(escaped_val)'"
|
||||||
|
end
|
||||||
|
|
||||||
|
push!(where_clauses, clause)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3. Assemble Final Query
|
||||||
|
where_sql = isempty(where_clauses) ? "" : "WHERE " * join(where_clauses, " AND ")
|
||||||
|
|
||||||
|
return string(base_query, where_sql, ";")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-30
@@ -4,7 +4,7 @@ export addNewMessage, conversation, decisionMaker, reflector, generatechat,
|
|||||||
generalconversation, detectWineryName, generateSituationReport
|
generalconversation, detectWineryName, generateSituationReport
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, CSV
|
DataFrames, Serde
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..util, ..llmfunction
|
using ..type, ..util, ..llmfunction
|
||||||
|
|
||||||
@@ -122,22 +122,16 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3
|
|||||||
errornote = "N/A"
|
errornote = "N/A"
|
||||||
response = nothing # placeholder for show when error msg show up
|
response = nothing # placeholder for show when error msg show up
|
||||||
|
|
||||||
for attempt in 1:maxattempt
|
msg = Dict(
|
||||||
|
|
||||||
|
|
||||||
msg = Dict(
|
|
||||||
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
|
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
|
||||||
"messages" => a.chathistory,
|
"messages" => a.chathistory,
|
||||||
"temperature" => 0.7
|
"temperature" => 0.7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for attempt in 1:maxattempt
|
||||||
response = a.context.text2textInstructLLM(a.id, msg)
|
response = a.context.text2textInstructLLM(a.id, msg)
|
||||||
|
|
||||||
response = GeneralUtils.clean_json_response(response)
|
|
||||||
response = GeneralUtils.remove_french_accents(response)
|
response = GeneralUtils.remove_french_accents(response)
|
||||||
think, response = GeneralUtils.extractthink(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
|
# dollar sign in Julia means string interpolation
|
||||||
while occursin('$', response)
|
while occursin('$', response)
|
||||||
@@ -145,16 +139,10 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3
|
|||||||
end
|
end
|
||||||
|
|
||||||
responsedict = nothing
|
responsedict = nothing
|
||||||
if occursin(requiredKeys[2], response)
|
try
|
||||||
try
|
responsedict = Serde.parse_yaml(response)
|
||||||
_responsedict = JSON.parse(response)
|
catch e
|
||||||
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
|
println("\nERROR YiemAgent decisionMaker() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||||||
catch
|
|
||||||
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
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -393,8 +381,6 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
|
|||||||
if loopcount > max_think_loop
|
if loopcount > max_think_loop
|
||||||
|
|
||||||
thoughtdict, result_raw = generatechat!(a)
|
thoughtdict, result_raw = generatechat!(a)
|
||||||
|
|
||||||
|
|
||||||
assistant_response = Dict{String, Any}(
|
assistant_response = Dict{String, Any}(
|
||||||
"role" => "assistant",
|
"role" => "assistant",
|
||||||
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
|
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
|
||||||
@@ -404,17 +390,17 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
|
|||||||
items_info = []
|
items_info = []
|
||||||
send_item_ind = [] # index of the item being send to frontend
|
send_item_ind = [] # index of the item being send to frontend
|
||||||
if haskey(a.memory["shortmem"], "items_info")
|
if haskey(a.memory["shortmem"], "items_info")
|
||||||
|
|
||||||
for (i, item) in enumerate(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"])
|
if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"])
|
||||||
push!(items_info, deepcopy(item))
|
push!(items_info, deepcopy(item))
|
||||||
push!(send_item_ind, i)
|
push!(send_item_ind, i)
|
||||||
|
@info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# remove sent items
|
# remove sent items
|
||||||
deleteat!(a.memory["shortmem"]["items_info"], send_item_ind)
|
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
|
end
|
||||||
|
|
||||||
response_to_frontend = Dict{String, Any}(
|
response_to_frontend = Dict{String, Any}(
|
||||||
@@ -435,8 +421,6 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
|
|||||||
thoughtdict, result_raw = think(a)
|
thoughtdict, result_raw = think(a)
|
||||||
|
|
||||||
if thoughtdict["action_name"] ∈ ["CHAT_BOX"]
|
if thoughtdict["action_name"] ∈ ["CHAT_BOX"]
|
||||||
|
|
||||||
|
|
||||||
assistant_response = Dict{String, Any}(
|
assistant_response = Dict{String, Any}(
|
||||||
"role" => "assistant",
|
"role" => "assistant",
|
||||||
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
|
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
|
||||||
@@ -446,17 +430,17 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
|
|||||||
items_info = []
|
items_info = []
|
||||||
send_item_ind = [] # index of the item being send to frontend
|
send_item_ind = [] # index of the item being send to frontend
|
||||||
if haskey(a.memory["shortmem"], "items_info")
|
if haskey(a.memory["shortmem"], "items_info")
|
||||||
|
|
||||||
for (i, item) in enumerate(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"])
|
if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"])
|
||||||
push!(items_info, deepcopy(item))
|
push!(items_info, deepcopy(item))
|
||||||
push!(send_item_ind, i)
|
push!(send_item_ind, i)
|
||||||
|
@info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# remove sent items
|
# remove sent items
|
||||||
deleteat!(a.memory["shortmem"]["items_info"], send_item_ind)
|
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
|
end
|
||||||
|
|
||||||
response_to_frontend = Dict{String, Any}(
|
response_to_frontend = Dict{String, Any}(
|
||||||
|
|||||||
+565
-227
@@ -5,7 +5,7 @@ export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recomme
|
|||||||
extractWineAttributes_2, paraphrase, SQLexecution
|
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
|
Base64, Serde, LibPQ
|
||||||
using GeneralUtils, SQLLLM
|
using GeneralUtils, SQLLLM
|
||||||
using ..type, ..util
|
using ..type, ..util
|
||||||
|
|
||||||
@@ -277,24 +277,18 @@ end
|
|||||||
# Example
|
# Example
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> using ChatAgent
|
julia> using ChatAgent
|
||||||
julia> agent = ChatAgent.agentReflex("Jene")
|
julia> agent = YiemAgent.sommelier(...)
|
||||||
julia> input = "{\"food\": \"pizza\", \"occasion\": \"anniversary\"}"
|
julia> thoughtdict =
|
||||||
julia> result = checkinventory(agent, input)
|
OrderedDict{String, Any}(
|
||||||
"{"wine 1": {\"Winery\": \"Pichon Baron\", \"wine name\": \"Pauillac (Grand Cru Classé)\", \"grape variety\": \"Cabernet Sauvignon\", \"year\": 2010, \"price\": \"125 USD\", \"stock ID\": \"ar-17\"}, }"
|
"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
|
function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
||||||
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
|
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
|
||||||
|
|
||||||
println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||||
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 = "$(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())")
|
|
||||||
|
|
||||||
if useSQLLLM
|
if useSQLLLM
|
||||||
# add suppport for similarSQLVectorDB
|
# add suppport for similarSQLVectorDB
|
||||||
textresult, result_raw = SQLLLM.query(
|
textresult, result_raw = SQLLLM.query(
|
||||||
@@ -308,7 +302,8 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
|
|||||||
else
|
else
|
||||||
|
|
||||||
# direct query with possible sql instead of SQLLLM.
|
# direct query with possible sql instead of SQLLLM.
|
||||||
sql = generatesql(a, inventoryquery)
|
sql = predefined_wine_search_sql(a, thoughtdict["action_input"])
|
||||||
|
# sql = generatesql(a, inventoryquery)
|
||||||
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
|
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
|
||||||
textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql)
|
textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql)
|
||||||
|
|
||||||
@@ -366,9 +361,9 @@ function generatesql(a::T, searchterm::String,
|
|||||||
Fulfill the objective.
|
Fulfill the objective.
|
||||||
|
|
||||||
# you should then respond to the user with interleaving plan, action_name, action_input
|
# 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.
|
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"
|
2) "action_name", Must be "RUNSQL"
|
||||||
3) "action_input, The input to the action you are about to perform according to your plan.
|
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.
|
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
|
# you should only respond in JSON format as described below
|
||||||
@@ -610,6 +605,203 @@ function generatesql(a::T, searchterm::String,
|
|||||||
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
||||||
end
|
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> predefined_wine_search_sql(agent, thoughtdict["action_input"])
|
||||||
|
"""
|
||||||
|
function predefined_wine_search_sql(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
|
||||||
|
<user>
|
||||||
|
4-wheel drive car with red color that will give me fast and furious emotion. No more than 7000 USD
|
||||||
|
</user>
|
||||||
|
<assistant>
|
||||||
|
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"
|
||||||
|
</assistant>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = get_db_table_schema_simple_with_samples(a.context.pg_conn_str, table)
|
||||||
|
|
||||||
|
# _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 =
|
||||||
|
"""
|
||||||
|
<internal_context_for_assistant>
|
||||||
|
<database_table_schema>
|
||||||
|
$table_schema
|
||||||
|
</database_table_schema>
|
||||||
|
</internal_context_for_assistant>
|
||||||
|
"""
|
||||||
|
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 = nothing
|
||||||
|
try
|
||||||
|
responsedict = Serde.parse_yaml(response)
|
||||||
|
catch e
|
||||||
|
println("\nERROR YiemAgent predefined_wine_search_sql() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
# println("\n ", table_schema)
|
||||||
|
println("\n ", responsedict)
|
||||||
|
@info "before BM25 " @__LINE__
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
bucket = classify_column(a.context.pg_conn_str, table_name, column_name)
|
||||||
|
|
||||||
|
if bucket == "fuzzy_correction"
|
||||||
|
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]["value"] = resolved_word
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# filter for column that will be used for hard condition (SQL where clause)
|
||||||
|
# column with "N/A" operator will be used in vector search
|
||||||
|
vector_search_words = ""
|
||||||
|
for (table_name, table_dict) in responsedict
|
||||||
|
for (column_name, column_dict) in table_dict
|
||||||
|
if column_dict["operator"] ∉ ["=","<>","!=",">","<",">=","<=","!<","!>","<=>"]
|
||||||
|
vector_search_words = vector_search_words * column_dict["value"] * ", "
|
||||||
|
delete!(table_dict, column_name)
|
||||||
|
|
||||||
|
# remove table from responsedict if there is no column to used
|
||||||
|
if length(responsedict[table_name]) == 0
|
||||||
|
delete!(responsedict, table_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
println("")
|
||||||
|
pprintln(responsedict)
|
||||||
|
@info "predefined_wine_search_sql() " @__LINE__
|
||||||
|
|
||||||
|
#WORKING do vector searched
|
||||||
|
println("")
|
||||||
|
@show vector_search_words
|
||||||
|
|
||||||
|
sql = predefined_wine_search_sql(responsedict)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
end
|
||||||
|
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
||||||
|
end
|
||||||
|
|
||||||
function SQLexecution(executeSQL::Function, sql::T
|
function SQLexecution(executeSQL::Function, sql::T
|
||||||
)::NamedTuple where {T<:AbstractString}
|
)::NamedTuple where {T<:AbstractString}
|
||||||
|
|
||||||
@@ -660,6 +852,64 @@ function SQLexecution(executeSQL::Function, sql::T
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function DEPRECIATED_search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
||||||
|
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
|
||||||
|
|
||||||
|
# XXX
|
||||||
|
predefined_wine_search_sql(a, thoughtdict["action_input"])
|
||||||
|
|
||||||
|
println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||||
|
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 = "$(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())")
|
||||||
|
|
||||||
|
if useSQLLLM
|
||||||
|
# add suppport for similarSQLVectorDB
|
||||||
|
textresult, result_raw = SQLLLM.query(
|
||||||
|
inventoryquery,
|
||||||
|
a.context.executeSQL,
|
||||||
|
a.context.text2textInstructLLM;
|
||||||
|
insertSQLVectorDB=a.context.insertSQLVectorDB,
|
||||||
|
similarSQLVectorDB=a.context.similarSQLVectorDB,
|
||||||
|
llmFormatName="qwen3")
|
||||||
|
thoughtdict["action_result"] = textresult
|
||||||
|
else
|
||||||
|
|
||||||
|
# direct query with possible sql instead of SQLLLM.
|
||||||
|
sql = generatesql(a, inventoryquery)
|
||||||
|
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
|
||||||
|
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=items)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -1036,213 +1286,183 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
|
|||||||
error("extractWineAttributes_2() failed to get a response")
|
error("extractWineAttributes_2() failed to get a response")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function predefined_wine_search_sql(conditions::Dict{String, Any})::String
|
||||||
function paraphrase(text2textInstructLLM::Function, text::String)
|
# 1. Base SQL structure
|
||||||
systemmsg =
|
base_query =
|
||||||
"""
|
"""
|
||||||
Your name: N/A
|
SELECT
|
||||||
Your vision:
|
w.winery,
|
||||||
- You are a helpful assistant who help the user to paraphrase their text.
|
w.wine_name,
|
||||||
Your mission:
|
w.wine_id,
|
||||||
- To help paraphrase the user's text
|
w.vintage,
|
||||||
Mission's objective includes:
|
w.region,
|
||||||
- To help paraphrase the user's text
|
w.country,
|
||||||
Your responsibility includes:
|
w.wine_type,
|
||||||
1) To help paraphrase the user's text
|
w.grape,
|
||||||
Your responsibility does NOT includes:
|
w.serving_temperature,
|
||||||
1) N/A
|
w.sweetness,
|
||||||
Your profile:
|
w.intensity,
|
||||||
- N/A
|
w.tannin,
|
||||||
Additional information:
|
w.acidity,
|
||||||
- N/A
|
w.tasting_notes,
|
||||||
|
rw.price,
|
||||||
At each round of conversation, you will be given the following information:
|
rw.currency,
|
||||||
Text: The user's given text
|
w.image_url,
|
||||||
|
r.retailer_name,
|
||||||
You MUST follow the following guidelines:
|
rw.retailer_id
|
||||||
- N/A
|
FROM wine AS w
|
||||||
|
JOIN retailer_wine AS rw ON w.wine_id = rw.wine_id
|
||||||
You should follow the following guidelines:
|
JOIN retailer AS r ON rw.retailer_id = r.retailer_id
|
||||||
- N/A
|
|
||||||
|
|
||||||
You should then respond to the user with:
|
|
||||||
Paraphrase: Paraphrased text
|
|
||||||
|
|
||||||
You should only respond in format as described below:
|
|
||||||
Paraphrase: ...
|
|
||||||
|
|
||||||
Let's begin!
|
|
||||||
"""
|
|
||||||
#[PENDING] use JSON the same as extractWineAttributes_1 is better. change this function to use the same format use decisionMaker
|
|
||||||
header = ["Paraphrase:"]
|
|
||||||
dictkey = ["paraphrase"]
|
|
||||||
|
|
||||||
errornote = "N/A"
|
|
||||||
response = nothing # placeholder for show when error msg show up
|
|
||||||
|
|
||||||
|
|
||||||
for attempt in 1:10
|
|
||||||
usermsg = """
|
|
||||||
Text: $text
|
|
||||||
P.S. $errornote
|
|
||||||
"""
|
|
||||||
|
|
||||||
_prompt =
|
|
||||||
[
|
|
||||||
Dict("name" => "system", "text" => systemmsg),
|
|
||||||
Dict("name" => "user", "text" => usermsg)
|
|
||||||
]
|
|
||||||
|
|
||||||
# put in model format
|
|
||||||
prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)
|
|
||||||
|
|
||||||
try
|
|
||||||
response = text2textInstructLLM(prompt)
|
|
||||||
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
|
|
||||||
think, response = GeneralUtils.extractthink(response)
|
|
||||||
# sometime the model response like this "here's how I would respond: ..."
|
|
||||||
if occursin("respond:", response)
|
|
||||||
errornote = "You don't need to intro your response"
|
|
||||||
error("\nparaphrase() response contain : ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
end
|
|
||||||
response = GeneralUtils.remove_french_accents(response)
|
|
||||||
response = replace(response, '*'=>"")
|
|
||||||
response = replace(response, '$' => "USD")
|
|
||||||
response = replace(response, '`' => "")
|
|
||||||
response = GeneralUtils.remove_french_accents(response)
|
|
||||||
|
|
||||||
# check whether response has all answer's key points
|
|
||||||
detected_kw = GeneralUtils.detect_keyword(header, response)
|
|
||||||
if 0 ∈ values(detected_kw)
|
|
||||||
errornote = "\nYiemAgent paraphrase() response does not have all answer's key points"
|
|
||||||
continue
|
|
||||||
elseif sum(values(detected_kw)) > length(header)
|
|
||||||
errornote = "\nnYiemAgent paraphrase() response has duplicated answer's key points"
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
responsedict = GeneralUtils.textToDict(response, header;
|
|
||||||
dictKey=dictkey, symbolkey=true)
|
|
||||||
|
|
||||||
for i ∈ [:paraphrase]
|
|
||||||
if length(JSON.json(responsedict[i])) == 0
|
|
||||||
error("$i is empty ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# check if there are more than 1 key per categories
|
|
||||||
for i ∈ [:paraphrase]
|
|
||||||
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
|
|
||||||
if length(matchkeys) > 1
|
|
||||||
error("paraphrase() has more than one key per categories")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
println("\nparaphrase() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
pprintln(Dict(responsedict))
|
|
||||||
|
|
||||||
result = responsedict["paraphrase"]
|
|
||||||
|
|
||||||
return result
|
|
||||||
catch e
|
|
||||||
io = IOBuffer()
|
|
||||||
showerror(io, e)
|
|
||||||
errorMsg = String(take!(io))
|
|
||||||
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
|
|
||||||
println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
error("paraphrase() failed to generate a response")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
""" Attemp to correct LLM response's incorrect JSON response.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `a::T1`
|
|
||||||
one of Yiem's agent
|
|
||||||
- `input::T2`
|
|
||||||
text to be send to virtual wine customer
|
|
||||||
|
|
||||||
# Return
|
|
||||||
- `correctjson::String`
|
|
||||||
corrected json string
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```jldoctest
|
|
||||||
julia>
|
|
||||||
```
|
|
||||||
|
|
||||||
# Signature
|
|
||||||
"""
|
"""
|
||||||
function jsoncorrection(config::T1, input::T2, correctJsonExample::T3;
|
|
||||||
maxattempt::Integer=3
|
|
||||||
) where {T1<:AbstractDict, T2<:AbstractString, T3<:AbstractString}
|
|
||||||
|
|
||||||
incorrectjson = deepcopy(input)
|
# 2. Dynamic WHERE Clause Builder
|
||||||
correctjson = nothing
|
where_clauses = String[]
|
||||||
|
|
||||||
for attempt in 1:maxattempt
|
# Iterate over each table condition provided
|
||||||
try
|
for (table_name, table_conditions) in conditions
|
||||||
d = copy(JSON.parsefile(incorrectjson))
|
|
||||||
correctjson = incorrectjson
|
|
||||||
return correctjson
|
|
||||||
catch e
|
|
||||||
@warn "Attempting to correct JSON string. Attempt $attempt"
|
|
||||||
e = """$e"""
|
|
||||||
if occursin("EOF", e)
|
|
||||||
e = split(e, "EOF")[1] * "EOF"
|
|
||||||
end
|
|
||||||
incorrectjson = deepcopy(input)
|
|
||||||
_prompt =
|
|
||||||
"""
|
|
||||||
Your goal are:
|
|
||||||
1) Use the expected JSON format as a guideline to check why the given JSON string failed to load and provide a corrected version that can be loaded by Python's json.load function.
|
|
||||||
2) Provide Corrected JSON string only. Do not provide any other info.
|
|
||||||
|
|
||||||
$correctJsonExample
|
# Determine table alias
|
||||||
|
alias = if table_name == "wine"
|
||||||
|
"w"
|
||||||
|
elseif table_name == "retailer_wine"
|
||||||
|
"rw"
|
||||||
|
else
|
||||||
|
continue # Skip unsupported tables
|
||||||
|
end
|
||||||
|
|
||||||
Let's begin!
|
# Process condition dictionaries
|
||||||
Given JSON string: $incorrectjson
|
if isa(table_conditions, Dict) && !isempty(table_conditions)
|
||||||
The given JSON string failed to load previously because: $e
|
|
||||||
Corrected JSON string:
|
|
||||||
"""
|
|
||||||
|
|
||||||
# apply LLM specific instruct format
|
for (column_name, filter_details) in table_conditions
|
||||||
externalService = config["externalservice"]["text2textinstruct"]
|
|
||||||
llminfo = externalService["llminfo"]
|
|
||||||
prompt =
|
|
||||||
if llminfo["name"] == "llama3instruct"
|
|
||||||
formatLLMtext_llama3instruct("system", _prompt)
|
|
||||||
else
|
|
||||||
error("llm model name is not defied yet $(@__LINE__)")
|
|
||||||
end
|
|
||||||
|
|
||||||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
if isa(filter_details, Dict) && haskey(filter_details, "operator")
|
||||||
msgMeta = GeneralUtils.generate_msgMeta(
|
op = filter_details["operator"]
|
||||||
externalService["mqtttopic"],
|
raw_val = filter_details["value"]
|
||||||
senderName= "jsoncorrection",
|
|
||||||
senderId= string(uuid4()),
|
|
||||||
receiverName= "text2textinstruct",
|
|
||||||
mqttBroker= config["mqttServerInfo"]["broker"],
|
|
||||||
mqttBrokerPort= config["mqttServerInfo"]["port"],
|
|
||||||
)
|
|
||||||
|
|
||||||
outgoingMsg = Dict(
|
# --- Value Type Handling ---
|
||||||
"msgMeta"=> msgMeta,
|
# Use tryparse instead of try/catch for cleaner, faster parsing
|
||||||
"payload"=> Dict(
|
final_val = raw_val
|
||||||
"text"=> prompt,
|
|
||||||
"kwargs"=> Dict(
|
if op in ("=", "<", ">", "<=", ">=")
|
||||||
"max_tokens"=> 512,
|
str_val = string(raw_val)
|
||||||
"stop"=> ["<|eot_id|>"],
|
num_val = tryparse(Float64, str_val)
|
||||||
)
|
|
||||||
)
|
if !isnothing(num_val)
|
||||||
)
|
final_val = isinteger(num_val) ? round(Int, num_val) : num_val
|
||||||
result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
end
|
||||||
incorrectjson = result[:response][:text]
|
end
|
||||||
|
|
||||||
|
# --- SQL Formatting ---
|
||||||
|
if isa(final_val, Number)
|
||||||
|
clause = "$(alias).$(column_name) $(op) $(final_val)"
|
||||||
|
else
|
||||||
|
# Escape single quotes within string values
|
||||||
|
escaped_val = replace(string(final_val), "'" => "''")
|
||||||
|
clause = "$(alias).$(column_name) $(op) '$(escaped_val)'"
|
||||||
|
end
|
||||||
|
|
||||||
|
push!(where_clauses, clause)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
# 3. Assemble Final Query
|
||||||
|
where_sql = isempty(where_clauses) ? "" : "WHERE " * join(where_clauses, " AND ")
|
||||||
|
|
||||||
|
return string(base_query, where_sql, ";")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function get_db_table_schema_simple_with_samples(pg_conn_str::String, table_name::String;
|
||||||
|
schema_name::String="public")::String
|
||||||
|
conn = LibPQ.Connection(pg_conn_str)
|
||||||
|
return get_db_table_schema_simple_with_samples(conn, table_name; schema_name=schema_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_db_table_schema_simple_with_samples(conn, table_name::String; schema_name::String="public", sample_count::Int=3)::String
|
||||||
|
# 1. SQL query for catalog metadata
|
||||||
|
meta_sql = """
|
||||||
|
SELECT
|
||||||
|
a.attname AS column_name,
|
||||||
|
format_type(a.atttypid, a.atttypmod) AS data_type,
|
||||||
|
pg_get_expr(def.adbin, def.adrelid) AS default_value,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT pg_get_constraintdef(p.oid)
|
||||||
|
FROM pg_catalog.pg_constraint p
|
||||||
|
WHERE p.conrelid = c.oid AND a.attnum = ANY(p.conkey)
|
||||||
|
LIMIT 1), ''
|
||||||
|
) AS constraint_definition
|
||||||
|
FROM pg_catalog.pg_attribute a
|
||||||
|
JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
|
||||||
|
JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
|
||||||
|
LEFT JOIN pg_catalog.pg_attrdef def ON def.adrelid = c.oid AND def.adnum = a.attnum
|
||||||
|
WHERE c.relname = \$1
|
||||||
|
AND n.nspname = \$2
|
||||||
|
AND a.attnum > 0
|
||||||
|
AND NOT a.attisdropped
|
||||||
|
ORDER BY a.attnum;
|
||||||
|
"""
|
||||||
|
|
||||||
|
meta_res = DataFrame(execute(conn, meta_sql, [table_name, schema_name]))
|
||||||
|
|
||||||
|
if nrow(meta_res) == 0
|
||||||
|
error("Table '$schema_name.$table_name' not found.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. Build single dynamic query to fetch non-null samples for all columns
|
||||||
|
sample_selects = String[]
|
||||||
|
for row in eachrow(meta_res)
|
||||||
|
c_name = row.column_name
|
||||||
|
push!(sample_selects, """
|
||||||
|
(SELECT json_agg(s."$c_name")
|
||||||
|
FROM (
|
||||||
|
SELECT "$c_name"
|
||||||
|
FROM "$schema_name"."$table_name"
|
||||||
|
WHERE "$c_name" IS NOT NULL
|
||||||
|
LIMIT $sample_count
|
||||||
|
) s
|
||||||
|
) AS "$c_name"
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
sample_sql = "SELECT " * join(sample_selects, ",\n ") * ";"
|
||||||
|
sample_df = DataFrame(execute(conn, sample_sql))
|
||||||
|
|
||||||
|
# 3. Build DDL definitions with inline sample comments
|
||||||
|
ddl_lines = String[]
|
||||||
|
constraints = String[]
|
||||||
|
|
||||||
|
for row in eachrow(meta_res)
|
||||||
|
col_name = row.column_name
|
||||||
|
data_type = row.data_type
|
||||||
|
default_val = ismissing(row.default_value) ? "" : " DEFAULT " * row.default_value
|
||||||
|
|
||||||
|
col_def = " \"$col_name\" $data_type$default_val"
|
||||||
|
|
||||||
|
# Fetch sample data for this column from the single-row sample DataFrame
|
||||||
|
samples_comment = ""
|
||||||
|
if nrow(sample_df) > 0
|
||||||
|
raw_samples = sample_df[1, Symbol(col_name)]
|
||||||
|
samples_str = ismissing(raw_samples) || isnothing(raw_samples) ? "[]" : string(raw_samples)
|
||||||
|
samples_comment = " -- Samples: $samples_str"
|
||||||
|
end
|
||||||
|
|
||||||
|
push!(ddl_lines, col_def * samples_comment)
|
||||||
|
|
||||||
|
# Handle table-level constraints
|
||||||
|
con_def = ismissing(row.constraint_definition) ? "" : row.constraint_definition
|
||||||
|
if !isempty(con_def) && !(con_def in constraints)
|
||||||
|
push!(constraints, " " * con_def)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
all_definitions = vcat(ddl_lines, constraints)
|
||||||
|
body = join(all_definitions, ",\n")
|
||||||
|
|
||||||
|
return "CREATE TABLE \"$schema_name\".\"$table_name\" (\n$body\n);"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -1254,27 +1474,145 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function classify_column(pg_conn_str::String, table_name::String, column_name::String;
|
||||||
|
sample_size::Integer=1000)
|
||||||
|
conn = LibPQ.Connection(pg_conn_str)
|
||||||
|
return classify_column(conn, table_name, column_name; sample_size=sample_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function classify_column(conn::LibPQ.Connection, table_name::String, column_name::String; sample_size::Int=1000)
|
||||||
|
# 1. Fetch BOTH data_type and udt_name (User Defined Type name)
|
||||||
|
meta_query = """
|
||||||
|
SELECT data_type, udt_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = lower('$(table_name)')
|
||||||
|
AND column_name = lower('$(column_name)');
|
||||||
|
"""
|
||||||
|
|
||||||
|
pg_type = "unknown"
|
||||||
|
udt_name = "unknown"
|
||||||
|
|
||||||
|
try
|
||||||
|
df = DataFrame(LibPQ.execute(conn, meta_query))
|
||||||
|
if !isempty(df)
|
||||||
|
pg_type = df[1, :data_type]
|
||||||
|
udt_name = df[1, :udt_name]
|
||||||
|
end
|
||||||
|
catch e
|
||||||
|
@error "Failed to fetch metadata for $table_name.$column_name" exception=e
|
||||||
|
return "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. FAST-TRACK: Check for pgvector FIRST
|
||||||
|
# pgvector registers as "USER-DEFINED" in data_type, but "vector" in udt_name
|
||||||
|
if udt_name == "vector"
|
||||||
|
return "semantic_search"
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3. FAST-TRACK: Hard rules for standard non-text Postgres types
|
||||||
|
if pg_type in ["integer", "bigint", "smallint", "numeric", "real",
|
||||||
|
"double precision", "boolean", "date",
|
||||||
|
"timestamp without time zone", "timestamp with time zone", "uuid"]
|
||||||
|
return "exact_or_range"
|
||||||
|
end
|
||||||
|
|
||||||
|
# 4. SAMPLE: Get text statistics for remaining text columns
|
||||||
|
stats_query = """
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::int AS total_count,
|
||||||
|
COUNT(DISTINCT $(column_name)::text)::int AS unique_count,
|
||||||
|
COALESCE(AVG(LENGTH($(column_name)::text)), 0)::float AS avg_len,
|
||||||
|
COALESCE(STDDEV(LENGTH($(column_name)::text)), 0)::float AS std_len
|
||||||
|
FROM (
|
||||||
|
SELECT $(column_name)
|
||||||
|
FROM $(table_name)
|
||||||
|
WHERE $(column_name) IS NOT NULL
|
||||||
|
LIMIT $sample_size
|
||||||
|
) AS sampled_data;
|
||||||
|
"""
|
||||||
|
|
||||||
|
try
|
||||||
|
df = DataFrame(LibPQ.execute(conn, stats_query))
|
||||||
|
if isempty(df) || df[1, :total_count] == 0
|
||||||
|
return "unknown"
|
||||||
|
end
|
||||||
|
|
||||||
|
total = df[1, :total_count]
|
||||||
|
unique = df[1, :unique_count]
|
||||||
|
avg_len = df[1, :avg_len]
|
||||||
|
std_len = df[1, :std_len]
|
||||||
|
ratio = unique / total
|
||||||
|
|
||||||
|
# 5. HEURISTICS: Route the column_name to the correct text bucket
|
||||||
|
return classify_text_column(unique, ratio, avg_len, std_len)
|
||||||
|
|
||||||
|
catch e
|
||||||
|
@warn "Failed to sample column_name $table_name.$column_name" exception=e
|
||||||
|
return "unknown"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The Decision Tree for Text Columns (Unchanged, but kept for completeness)
|
||||||
|
function classify_text_column(unique_count::Integer, ratio::Float64, avg_len::Float64, std_len::Float64)
|
||||||
|
if avg_len > 60 && std_len > 25
|
||||||
|
return "full_text_search"
|
||||||
|
end
|
||||||
|
if ratio > 0.90 && avg_len < 40
|
||||||
|
return "exact_or_regex"
|
||||||
|
end
|
||||||
|
if unique_count <= 100
|
||||||
|
return "fuzzy_correction"
|
||||||
|
end
|
||||||
|
if ratio > 0.10 && avg_len < 35
|
||||||
|
return "fuzzy_correction"
|
||||||
|
end
|
||||||
|
if avg_len < 60
|
||||||
|
return "fuzzy_correction"
|
||||||
|
end
|
||||||
|
return "full_text_search"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function harvest_entity_catalog(pg_conn_str::String, table::String, column::String)
|
||||||
|
conn = LibPQ.Connection(pg_conn_str)
|
||||||
|
return harvest_entity_catalog(conn, table, column)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function harvest_entity_catalog_with_pg_type(conn::LibPQ.Connection, table::String, column::String)
|
||||||
|
try
|
||||||
|
# 1. Query the actual data
|
||||||
|
data_query = "SELECT DISTINCT $(column) FROM $(table) WHERE $(column) IS NOT NULL;"
|
||||||
|
df = DataFrame(LibPQ.execute(conn, data_query))
|
||||||
|
values = String.(strip.(string.(df[!, 1])))
|
||||||
|
|
||||||
|
# 2. Query the database schema for the column's data type
|
||||||
|
# Note: Postgres stores unquoted table/column names in lowercase
|
||||||
|
type_query = """
|
||||||
|
SELECT data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = lower('$(table)')
|
||||||
|
AND column_name = lower('$(column)');
|
||||||
|
"""
|
||||||
|
type_df = DataFrame(LibPQ.execute(conn, type_query))
|
||||||
|
pg_type = isempty(type_df) ? "unknown" : type_df[1, 1]
|
||||||
|
|
||||||
|
return (values = values, type = pg_type)
|
||||||
|
|
||||||
|
catch e
|
||||||
|
@error "Failed to harvest catalog" exception=e
|
||||||
|
return (values = String[], type = "unknown")
|
||||||
|
finally
|
||||||
|
close(conn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# result = harvest_entity_catalog_with_pg_type(conn, "users", "created_at")
|
||||||
|
# println(result.values) # ["2023-01-01", "2023-02-15"]
|
||||||
|
# println(result.type) # "timestamp without time zone"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-77
@@ -23,75 +23,6 @@ end
|
|||||||
|
|
||||||
abstract type agent end
|
abstract type agent end
|
||||||
|
|
||||||
mutable struct companion <: agent
|
|
||||||
name::String # agent name
|
|
||||||
id::String # agent id
|
|
||||||
systemmsg::String # system message
|
|
||||||
tools::Dict # tools
|
|
||||||
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
|
|
||||||
chathistory::Vector{Dict{String, Any}}
|
|
||||||
memory::Dict{String, Any}
|
|
||||||
context::NamedTuple # NamedTuple of functions
|
|
||||||
llmFormatName::String
|
|
||||||
end
|
|
||||||
|
|
||||||
function companion(
|
|
||||||
context::agentcontext # NamedTuple of functions
|
|
||||||
;
|
|
||||||
name::String= "Assistant",
|
|
||||||
id::String= GeneralUtils.uuid4snakecase(),
|
|
||||||
maxHistoryMsg::Integer= 20,
|
|
||||||
chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(),
|
|
||||||
llmFormatName::String= "granite3",
|
|
||||||
systemmsg::String=
|
|
||||||
"""
|
|
||||||
Your name: $name
|
|
||||||
Your sex: Female
|
|
||||||
Your role: You are a helpful assistant.
|
|
||||||
You should follow the following guidelines:
|
|
||||||
- Focus on the latest conversation.
|
|
||||||
- Your like to be short and concise.
|
|
||||||
|
|
||||||
Let's begin!
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = Dict( # update input format
|
|
||||||
"CHAT_BOX"=> Dict(
|
|
||||||
"description" => "- CHAT_BOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Memory
|
|
||||||
Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
|
|
||||||
NO "system" message in chathistory because I want to add it at the inference time
|
|
||||||
chathistory= [
|
|
||||||
Dict("name"=>"user", "text"=> "Wassup!", "timestamp"=> Dates.now()),
|
|
||||||
Dict("name"=>"assistant", "text"=> "Hi I'm your assistant.", "timestamp"=> Dates.now()),
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
memory = Dict{String, Any}(
|
|
||||||
"events"=> Vector{Dict{String, Any}}(),
|
|
||||||
"state"=> Dict{String, Any}(), # state of the agent
|
|
||||||
"recap"=> OrderedDict{String, Any}(), # recap summary of the conversation
|
|
||||||
)
|
|
||||||
|
|
||||||
newAgent = companion(
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
systemmsg,
|
|
||||||
tools,
|
|
||||||
maxHistoryMsg,
|
|
||||||
chathistory,
|
|
||||||
memory,
|
|
||||||
context,
|
|
||||||
llmFormatName
|
|
||||||
)
|
|
||||||
|
|
||||||
return newAgent
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
mutable struct sommelier <: agent
|
mutable struct sommelier <: agent
|
||||||
name::String # agent name
|
name::String # agent name
|
||||||
id::String # agent id
|
id::String # agent id
|
||||||
@@ -210,11 +141,7 @@ function sommelier(
|
|||||||
memory = Dict{String, Any}(
|
memory = Dict{String, Any}(
|
||||||
"shortmem"=> OrderedDict{String, Any}(),
|
"shortmem"=> OrderedDict{String, Any}(),
|
||||||
"scratchpad"=> "",
|
"scratchpad"=> "",
|
||||||
"events"=> Vector{Dict{String, Any}}(),
|
|
||||||
"state"=> Dict{String, Any}(
|
|
||||||
),
|
|
||||||
"recap"=> OrderedDict{String, Any}(),
|
"recap"=> OrderedDict{String, Any}(),
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
newAgent = sommelier(
|
newAgent = sommelier(
|
||||||
@@ -279,10 +206,10 @@ function sommelier(
|
|||||||
3) "action_input", The input to the action you are about to perform according to your plan.
|
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.
|
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 (not Markdown format)
|
# you should only respond in YAML format as described below
|
||||||
"plan": "...",
|
plan: "..."
|
||||||
"action_name": "...",
|
action_name: "..."
|
||||||
"action_input": "..."
|
action_input: "..."
|
||||||
|
|
||||||
# available actions
|
# 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.
|
"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.
|
||||||
|
|||||||
Reference in New Issue
Block a user