Merge pull request 'v0.7.4-add_vector_search' (#33) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #33
This commit was merged in pull request #33.
This commit is contained in:
+5
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
julia_version = "1.12.6"
|
||||
manifest_format = "2.0"
|
||||
project_hash = "dc7878808bbc4637a12e709dd495979a784824a5"
|
||||
project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6"
|
||||
|
||||
[[deps.Accessors]]
|
||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||
@@ -300,11 +300,11 @@ version = "1.1.0"
|
||||
|
||||
[[deps.GeneralUtils]]
|
||||
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"]
|
||||
git-tree-sha1 = "a75a088ee8e5faf10f554ca00748e0e6ca58d1ca"
|
||||
git-tree-sha1 = "93293126d24d3929ef6a5067f347bc28c6582c71"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
|
||||
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||
version = "0.5.1"
|
||||
version = "0.5.10"
|
||||
|
||||
[[deps.Graphs]]
|
||||
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
|
||||
@@ -1089,10 +1089,10 @@ uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
|
||||
version = "0.4.16"
|
||||
|
||||
[[deps.YiemAgent]]
|
||||
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"]
|
||||
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serde", "Serialization", "URIs", "UUIDs"]
|
||||
path = "."
|
||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||
version = "0.7.2"
|
||||
version = "0.7.4"
|
||||
|
||||
[[deps.Zlib_jll]]
|
||||
deps = ["Libdl"]
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
Base64 = "1.11.0"
|
||||
CSV = "0.10.15"
|
||||
DataFrames = "1.7.0"
|
||||
GeneralUtils = "0.5.1"
|
||||
GeneralUtils = "0.5.10"
|
||||
HTTP = "2.4.0"
|
||||
JSON = "1.6.1"
|
||||
LLMMCTS = "0.1.5"
|
||||
|
||||
@@ -1,200 +1,10 @@
|
||||
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)
|
||||
# check if this column has vector embedding. if there is one, seach vector version instead
|
||||
column_name_embedding = column_name * "_embedding"
|
||||
if occursin(column_name_embedding, tables_schema[column_name_embedding])
|
||||
vector_column = Dict(
|
||||
"table_name"=> table_name,
|
||||
"column_name"=> column_name_embedding,
|
||||
"operator"=> "vector_similarity",
|
||||
"value"=> column_obj["value"]
|
||||
)
|
||||
end
|
||||
|
||||
LibPQ.close(db_connection)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
sql =
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
+81
-19
@@ -122,10 +122,70 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3
|
||||
errornote = "N/A"
|
||||
response = nothing # placeholder for show when error msg show up
|
||||
|
||||
"""
|
||||
{
|
||||
"model": "your-model.gguf",
|
||||
"messages": [ ... ],
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "agent_action",
|
||||
"strict": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"think": {
|
||||
"type": "string",
|
||||
"description": "Your step-by-step reasoning process. Explain why you are choosing this action."
|
||||
},
|
||||
"action_name": {
|
||||
"type": "string",
|
||||
"enum": ["search_web", "get_weather", "calculate_math"],
|
||||
"description": "The exact name of the tool to execute."
|
||||
},
|
||||
"action_input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": ["string", "null"], "description": "For search_web" },
|
||||
"location": { "type": ["string", "null"], "description": "For get_weather" },
|
||||
"equation": { "type": ["string", "null"], "description": "For calculate_math" }
|
||||
},
|
||||
"required": ["query", "location", "equation"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["think", "action_name", "action_input"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# strict output format
|
||||
response_format = Dict(
|
||||
"type"=> "json_schema",
|
||||
"json_schema"=> Dict(
|
||||
"name"=> "user_profile",
|
||||
"strict"=> true,
|
||||
"schema"=> Dict(
|
||||
"type"=> "object",
|
||||
"properties"=> Dict(
|
||||
"plan"=> Dict("type"=> "string"),
|
||||
"action_name"=> Dict("type"=> "string"),
|
||||
"action_input"=> Dict("type"=> "string"),
|
||||
),
|
||||
"required"=> ["plan", "action_name", "action_input"],
|
||||
"additionalProperties"=> false
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
msg = Dict(
|
||||
"model"=> "gemma-4-E4B-it-UD-Q4_K_XL",
|
||||
"messages"=> a.chathistory,
|
||||
"temperature" => 0.7
|
||||
"temperature"=> 0.7,
|
||||
"response_format"=> response_format,
|
||||
)
|
||||
|
||||
for attempt in 1:maxattempt
|
||||
@@ -138,25 +198,27 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3
|
||||
response = replace(response, '$' => "USD")
|
||||
end
|
||||
|
||||
responsedict = nothing
|
||||
try
|
||||
responsedict = Serde.parse_yaml(response)
|
||||
catch e
|
||||
println("\nERROR YiemAgent decisionMaker() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||||
continue
|
||||
end
|
||||
# responsedict = nothing
|
||||
# try
|
||||
# responsedict = Serde.parse_yaml(response)
|
||||
# catch e
|
||||
# println("\nERROR YiemAgent decisionMaker() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||||
# continue
|
||||
# end
|
||||
|
||||
# check whether all answer's key points are in responsedict
|
||||
println("\n---")
|
||||
println(responsedict)
|
||||
println("---\n")
|
||||
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
|
||||
# # check whether all answer's key points are in responsedict
|
||||
# println("\n---")
|
||||
# println(responsedict)
|
||||
# println("---\n")
|
||||
# ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
|
||||
|
||||
if !ispass
|
||||
errornote = errormsg
|
||||
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||||
continue
|
||||
end
|
||||
# if !ispass
|
||||
# errornote = errormsg
|
||||
# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||||
# continue
|
||||
# end
|
||||
|
||||
responsedict = JSON.parse(response)
|
||||
|
||||
if responsedict["action_input"] == "CHAT_BOX" &&
|
||||
occursin("similar", responsedict["action_input"])
|
||||
@@ -564,7 +626,7 @@ function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict,
|
||||
end
|
||||
|
||||
@info "YiemAgent think() end " @__LINE__
|
||||
@show thoughtdict
|
||||
# @show thoughtdict
|
||||
println("---\n")
|
||||
return (thoughtdict=thoughtdict, result_raw=result_raw)
|
||||
end
|
||||
|
||||
+240
-216
@@ -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, Serde, LibPQ
|
||||
Base64, Serde, LibPQ, NATS
|
||||
using GeneralUtils, SQLLLM
|
||||
using ..type, ..util
|
||||
|
||||
@@ -302,11 +302,47 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
|
||||
else
|
||||
|
||||
# direct query with possible sql instead of SQLLLM.
|
||||
sql = predefined_wine_search_sql(a, thoughtdict["action_input"])
|
||||
hard_conditions, vector_search = wine_search_term_classification(a, thoughtdict["action_input"])
|
||||
|
||||
# do hard filter
|
||||
# sql = generatesql(a, inventoryquery)
|
||||
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
|
||||
sql = predefined_wine_search_sql(hard_conditions)
|
||||
@info "\nsql: $sql, \nvector_search: $vector_search"
|
||||
textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql)
|
||||
|
||||
# do vector search
|
||||
vector_search_str = ""
|
||||
for i in vector_search
|
||||
vector_search_str = vector_search_str * " " * i["value"]
|
||||
end
|
||||
vector_search_str = String(strip(vector_search_str))
|
||||
vector_search_str = GeneralUtils.removestring(vector_search_str, ["%"])
|
||||
@show vector_search
|
||||
@show vector_search_str
|
||||
|
||||
|
||||
config = a.context.agentconfig
|
||||
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"
|
||||
|
||||
#WORKING
|
||||
# df = GeneralUtils.find_text_vector_similarity(
|
||||
# vector_search_str,
|
||||
# "wine",
|
||||
# "tasting_notes_embedding",
|
||||
# GeneralUtils.execute_postgres_sql(pg_conn_str, sql), #BUG input pair (F, arg)
|
||||
# a.context.getTextEmbedding([vector_search_str]) #BUG input pair (F, arg)
|
||||
# )
|
||||
|
||||
|
||||
# @show df
|
||||
# error(888888)
|
||||
|
||||
|
||||
items = nothing
|
||||
if sql_result_df !== nothing
|
||||
result_vec = GeneralUtils.dfToVectorDict(sql_result_df)
|
||||
@@ -619,9 +655,9 @@ julia> thoughtdict =
|
||||
```
|
||||
julia> predefined_wine_search_sql(agent, thoughtdict["action_input"])
|
||||
"""
|
||||
function predefined_wine_search_sql(a::T, searchterm::String,
|
||||
function wine_search_term_classification(a::T, searchterm::String,
|
||||
; maxattempt=10
|
||||
)::String where {T<:agent}
|
||||
) where {T<:agent}
|
||||
|
||||
systemmsg =
|
||||
"""
|
||||
@@ -637,43 +673,47 @@ function predefined_wine_search_sql(a::T, searchterm::String,
|
||||
# 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: "..."
|
||||
...
|
||||
# You must output your response as a JSON object containing a single key: "extracted_info".
|
||||
The "extracted_info" key must contain an array of objects. Each object must contain:
|
||||
1) "table_name": The name of the table.
|
||||
2) "column_name": The specific column being filtered.
|
||||
3) "operator": The comparison operator (e.g., "=", ">").
|
||||
4) "value": The value to compare against.
|
||||
|
||||
If the user does not specify any filters, return an empty array for "extracted_info": {"extracted_info": []}.
|
||||
|
||||
# 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"
|
||||
{
|
||||
"extracted_info": [
|
||||
{
|
||||
"table_name": "car_info",
|
||||
"column_name": "drive_type",
|
||||
"operator": "=",
|
||||
"value": "4-wheel"
|
||||
},
|
||||
{
|
||||
"table_name": "car_info",
|
||||
"column_name": "color",
|
||||
"operator": "=",
|
||||
"value": "red"
|
||||
},
|
||||
{
|
||||
"table_name": "car_info",
|
||||
"column_name": "drive_feeling",
|
||||
"operator": "=",
|
||||
"value": "fast and furious"
|
||||
},
|
||||
{
|
||||
"table_name": "price_list",
|
||||
"column_name": "price",
|
||||
"operator": "<",
|
||||
"value": "7000"
|
||||
}
|
||||
}
|
||||
</assistant>
|
||||
"""
|
||||
|
||||
@@ -700,6 +740,48 @@ function predefined_wine_search_sql(a::T, searchterm::String,
|
||||
"""
|
||||
input = context * searchterm
|
||||
|
||||
response_format = Dict(
|
||||
"type" => "json_schema",
|
||||
"json_schema" => Dict(
|
||||
"name" => "extracted_conditions",
|
||||
"strict" => true,
|
||||
"schema" => Dict(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"extracted_info" => Dict(
|
||||
"type" => "array",
|
||||
"items" => Dict(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"table_name" => Dict(
|
||||
"type" => "string",
|
||||
"description" => "The name of the database table."
|
||||
),
|
||||
"column_name" => Dict(
|
||||
"type" => "string",
|
||||
"description" => "The name of the column to filter on."
|
||||
),
|
||||
"operator" => Dict(
|
||||
"type" => "string",
|
||||
"enum" => ["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "IS NULL", "IS NOT NULL"],
|
||||
"description" => "The SQL comparison operator."
|
||||
),
|
||||
"value" => Dict(
|
||||
"type" => ["string", "null"],
|
||||
"description" => "The value to compare against. Use null for IS NULL/IS NOT NULL."
|
||||
)
|
||||
),
|
||||
"required" => ["table_name", "column_name", "operator", "value"],
|
||||
"additionalProperties" => false
|
||||
)
|
||||
)
|
||||
),
|
||||
"required" => ["extracted_info"],
|
||||
"additionalProperties" => false
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
msg = Dict(
|
||||
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
|
||||
"messages" => [
|
||||
@@ -716,92 +798,142 @@ function predefined_wine_search_sql(a::T, searchterm::String,
|
||||
]
|
||||
),
|
||||
],
|
||||
"temperature" => 0.7
|
||||
"temperature" => 0.7,
|
||||
"response_format"=> response_format,
|
||||
)
|
||||
|
||||
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
|
||||
responsedict = JSON.parse(response)
|
||||
# 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"
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
# to ensure user input is correct
|
||||
for entry in responsedict["extracted_info"]
|
||||
table_name = entry["table_name"]::String
|
||||
column_name = entry["column_name"]::String
|
||||
|
||||
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
|
||||
resolved_word = GeneralUtils.resolve_entity(entry["value"], words_catalog; threshold=0.9)
|
||||
entry["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
|
||||
# column with non-standard 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)
|
||||
hard_operators = ["=","<>","!=",">","<",">=","<=","!<","!>","<=>"]
|
||||
|
||||
# remove table from responsedict if there is no column to used
|
||||
if length(responsedict[table_name]) == 0
|
||||
delete!(responsedict, table_name)
|
||||
end
|
||||
end
|
||||
# Build new list of hard condition entries
|
||||
hard_conditions = JSON.Object{String, Any}[]
|
||||
vector_search = JSON.Object{String, Any}[]
|
||||
for entry in responsedict["extracted_info"]
|
||||
if entry["operator"] ∈ hard_operators
|
||||
push!(hard_conditions, entry)
|
||||
else
|
||||
push!(vector_search, entry)
|
||||
end
|
||||
end
|
||||
responsedict = hard_conditions
|
||||
|
||||
println("")
|
||||
pprintln(responsedict)
|
||||
@show responsedict
|
||||
@info "predefined_wine_search_sql() " @__LINE__
|
||||
|
||||
#WORKING do vector searched
|
||||
println("")
|
||||
@show vector_search_words
|
||||
|
||||
sql = predefined_wine_search_sql(responsedict)
|
||||
|
||||
return sql
|
||||
return (hard_conditions=hard_conditions, vector_search=vector_search)
|
||||
end
|
||||
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
||||
end
|
||||
|
||||
function predefined_wine_search_sql(conditions::Vector{JSON.Object{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,
|
||||
r.retailer_name,
|
||||
rw.retailer_id
|
||||
FROM wine AS w
|
||||
JOIN retailer_wine AS rw ON w.wine_id = rw.wine_id
|
||||
JOIN retailer AS r ON rw.retailer_id = r.retailer_id
|
||||
"""
|
||||
|
||||
# 2. Dynamic WHERE Clause Builder
|
||||
where_clauses = String[]
|
||||
|
||||
# Iterate over each condition object in the array
|
||||
for cond in conditions
|
||||
table_name = String(cond["table_name"])
|
||||
column_name = String(cond["column_name"])
|
||||
op = String(cond["operator"])
|
||||
raw_val = cond["value"]
|
||||
|
||||
# Determine table alias
|
||||
alias = if table_name == "wine"
|
||||
"w"
|
||||
elseif table_name == "retailer_wine"
|
||||
"rw"
|
||||
else
|
||||
continue
|
||||
end
|
||||
|
||||
# --- Value Type Handling ---
|
||||
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
|
||||
escaped_val = replace(string(final_val), "'" => "''")
|
||||
clause = "$(alias).$(column_name) $(op) '$(escaped_val)'"
|
||||
end
|
||||
|
||||
push!(where_clauses, clause)
|
||||
end
|
||||
|
||||
# 3. Assemble Final Query
|
||||
where_sql = isempty(where_clauses) ? "" : "WHERE " * join(where_clauses, " AND ")
|
||||
|
||||
return string(base_query, where_sql, ";")
|
||||
end
|
||||
|
||||
function SQLexecution(executeSQL::Function, sql::T
|
||||
)::NamedTuple where {T<:AbstractString}
|
||||
|
||||
@@ -1286,92 +1418,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
|
||||
error("extractWineAttributes_2() failed to get a response")
|
||||
end
|
||||
|
||||
function predefined_wine_search_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,
|
||||
r.retailer_name,
|
||||
rw.retailer_id
|
||||
FROM wine AS w
|
||||
JOIN retailer_wine AS rw ON w.wine_id = rw.wine_id
|
||||
JOIN retailer AS r ON rw.retailer_id = r.retailer_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
|
||||
|
||||
|
||||
|
||||
@@ -1467,13 +1514,6 @@ end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function classify_column(pg_conn_str::String, table_name::String, column_name::String;
|
||||
sample_size::Integer=1000)
|
||||
conn = LibPQ.Connection(pg_conn_str)
|
||||
@@ -1575,44 +1615,28 @@ 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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-7
@@ -164,7 +164,6 @@ function sommelier(
|
||||
- You can only recommend wines that are currently in our inventory
|
||||
- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
|
||||
- Ask the user one question at a time.
|
||||
- Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database.
|
||||
- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
|
||||
- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
|
||||
- Spicy foods should be paired only with light red wines.
|
||||
@@ -200,17 +199,12 @@ function sommelier(
|
||||
- Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
|
||||
- Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
|
||||
|
||||
# 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 in JSON format
|
||||
1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
|
||||
2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name
|
||||
3) "action_input", The input to the action you are about to perform according to your plan.
|
||||
After the action is executed you gets "action_result". It is the output from the action you selected.
|
||||
|
||||
# you should only respond in YAML format as described below
|
||||
plan: "..."
|
||||
action_name: "..."
|
||||
action_input: "..."
|
||||
|
||||
# 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 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.
|
||||
|
||||
Reference in New Issue
Block a user