Compare commits

...

22 Commits

Author SHA1 Message Date
ton 6e53f57707 Merge pull request 'v0.7.4' (#39) from v0.7.4 into main
Reviewed-on: #39
2026-07-28 00:16:48 +00:00
ton 1eea6c66b6 Merge pull request 'update' (#38) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #38
2026-07-27 10:43:41 +00:00
ton 37ff68c963 update 2026-07-27 17:43:11 +07:00
ton e5fc800c83 Merge pull request 'update' (#37) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #37
2026-07-27 09:55:11 +00:00
ton 4397ec5fb3 update 2026-07-27 16:54:36 +07:00
ton 9e3f5a0967 Merge pull request 'v0.7.4' (#36) from v0.7.4 into main
Reviewed-on: #36
2026-07-27 02:48:28 +00:00
ton 30b79a3b34 Merge pull request 'fix tool name' (#35) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #35
2026-07-27 02:47:43 +00:00
ton e5b8518c00 fix tool name 2026-07-27 09:47:07 +07:00
ton d115e60ddf Merge pull request 'update' (#34) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #34
2026-07-26 16:02:35 +00:00
ton e0e6aced33 update 2026-07-26 23:01:51 +07:00
ton c803238f86 Merge pull request 'v0.7.4-add_vector_search' (#33) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #33
2026-07-26 15:42:34 +00:00
ton 4d6cdbcf4a update 2026-07-26 22:42:09 +07:00
ton 206c0d2c62 Merge pull request 'v0.7.4-add_vector_search' (#32) from v0.7.4-add_vector_search into main
Reviewed-on: #32
2026-07-26 15:21:53 +00:00
ton 597319a147 update 2026-07-26 22:21:10 +07:00
ton abfe6f45fb update 2026-07-25 10:13:02 +07:00
ton f8b3150c17 update 2026-07-24 16:44:49 +07:00
ton caed2a82d6 update 2026-07-24 15:51:32 +07:00
ton 7ec3edfd77 update 2026-07-24 08:31:36 +07:00
ton d7adfaffa8 update 2026-07-23 21:14:45 +07:00
ton 5bde0ca1f2 Merge pull request 'update' (#31) from v0.7.4-use_yaml_format into v0.7.4
Reviewed-on: #31
2026-07-22 03:20:13 +00:00
ton a3ab288cfe update 2026-07-22 10:18:28 +07:00
ton 17b0974d82 Merge pull request 'v0.7.4-predefine_wine_search' (#30) from v0.7.4-predefine_wine_search into v0.7.4
Reviewed-on: #30
2026-07-21 13:17:25 +00:00
6 changed files with 595 additions and 707 deletions
+5 -5
View File
@@ -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
View File
@@ -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"
+10 -200
View File
@@ -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)
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
# 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
+150 -118
View File
@@ -4,7 +4,7 @@ export addNewMessage, conversation, decisionMaker, reflector, generatechat,
generalconversation, detectWineryName, generateSituationReport
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, CSV
DataFrames, Serde
using GeneralUtils
using ..type, ..util, ..llmfunction
@@ -76,35 +76,7 @@ OrderedDict{String, Any} with 4 entries:
function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3
) where {T<:agent}
@info "YiemAgent decisionMaker() start " @__LINE__
# lessonDict = copy(JSON.parsefile("lesson.json"))
# lesson =
# if isempty(lessonDict)
# ""
# else
# lessons = Dict{String, Any}()
# for (k, v) in lessonDict
# lessons[k] = lessonDict[k][:lesson]
# end
# """
# You have attempted to help the user before and failed, either because your reasoning for the
# recommendation was incorrect or your response did not exactly match the user expectation.
# The following lesson(s) give a plan to avoid failing to help the user in the same way you
# did previously. Use them to improve your strategy to help the user.
# Here are some lessons in JSON format:
# $(JSON.json(lessons))
# When providing the thought and action for the current trial, that into account these failed
# trajectories and make sure not to repeat the same mistakes and incorrect answers.
# """
# end
# recentevents_ind = GeneralUtils.recentElementsIndex(
# length(a.memory["events"]), recentevents; includelatest=true)
requiredKeys = ["plan", "action_name", "action_input"]
context =
"""
<internal_context_for_assistant>
@@ -122,75 +94,125 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3
errornote = "N/A"
response = nothing # placeholder for show when error msg show up
for attempt in 1:maxattempt
"""
{
"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 action 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
}
}
}
}
"""
msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
"messages" => a.chathistory,
"temperature" => 0.7
# strict output format
response_format = Dict(
"type"=> "json_schema",
"json_schema"=> Dict(
"name"=> "user_profile",
"strict"=> true,
"schema"=> Dict(
"type"=> "object",
"properties"=> Dict(
"think"=> Dict(
"type"=> "string",
"description"=> "Your step-by-step reasoning process. Explain why you are choosing this action."
),
"action_name"=> Dict(
"type"=> "string",
"enum"=> ["CHAT_BOX", "SEARCH_WINE_DATABASE", "WINE_PRESENTATION_GUIDELINE", "END_CONVER_GUIDELINE"],
"description"=> "one of the available actions"
),
"action_input"=> Dict(
"type"=> "object",
"properties"=> Dict(
"dialogue"=> Dict("type"=> "string", "description"=> "for CHAT_BOX"),
"query"=> Dict("type"=> "string", "description"=> "for SEARCH_WINE_DATABASE"),
"present_guide"=> Dict("type"=> "null", "description"=> "for WINE_PRESENTATION_GUIDELINE"),
"endconv_guide"=> Dict("type"=> "null", "description"=> "for END_CONVER_GUIDELINE"),
)
),
),
"required"=> ["think", "action_name", "action_input"],
"additionalProperties"=> false
)
)
)
response = a.context.text2textInstructLLM(a.id, msg)
msg = Dict(
"model"=> "gemma-4-E4B-it-UD-Q4_K_XL",
"messages"=> a.chathistory,
"temperature"=> 0.7,
"response_format"=> response_format,
)
response = GeneralUtils.clean_json_response(response)
for attempt in 1:maxattempt
response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response)
response = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to
response = strip(response)
# dollar sign in Julia means string interpolation
while occursin('$', response)
response = replace(response, '$' => "USD")
end
responsedict = nothing
if occursin(requiredKeys[2], response)
try
_responsedict = JSON.parse(response)
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
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
end
# check whether all answer's key points are in responsedict
println("\n---")
println(responsedict)
println("---\n")
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass
errornote = errormsg
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
if responsedict["action_input"] == "CHAT_BOX" &&
occursin("similar", responsedict["action_input"])
continue
end
# if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"]
# errornote = "Your previous attempt didn't use the given functions"
# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# end
responsedict = JSON.parse(response)
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict)
# for decision that use a single action. make it simpler
for (k, v) in responsedict["action_input"]
responsedict["action_input"] = v
end
if occursin("CHAT_BOX", responsedict["action_input"])
println("\nERROR YiemAgent decisionMaker() --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
if responsedict["action_name"] ["WINE_PRESENTATION_GUIDELINE", "END_CONVER_GUIDELINE"] &&
length(responsedict["action_input"]) < 20
println("\nERROR YiemAgent decisionMaker() --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(responsedict)
@info "YiemAgent decisionMaker() end " @__LINE__
return responsedict
end
# in case decisionMaker failed, force to use generatechat!()
responsedict = OrderedDict(
"plan"=> "N/A",
"think"=> "N/A",
"action_name"=> "CHAT_BOX",
"action_input"=> "N/A"
)
@@ -576,7 +598,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
@@ -726,16 +748,14 @@ function generatechat!(a::T; maxattempt::Integer=10
- 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
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 "CHAT_BOX
# you should then respond to the user with interleaving think, action_name, action_input in JSON format
1) "think", Your step-by-step reasoning process. Explain why you are choosing this action.
2) "action_name", Can be one of the available_actions name.
3) "action_input", Dialogue you want to chat with the user according to your plan.
After the action is executed you gets "action_result". It is the output from the action you selected.
# you should only respond in JSON format as described below
"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.
"""
system_msg = Dict(
@@ -758,50 +778,62 @@ function generatechat!(a::T; maxattempt::Integer=10
println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
response_format = Dict(
"type"=> "json_schema",
"json_schema"=> Dict(
"name"=> "user_profile",
"strict"=> true,
"schema"=> Dict(
"type"=> "object",
"properties"=> Dict(
"think"=> Dict(
"type"=> "string",
"description" => "Your step-by-step reasoning process. Explain why you are choosing this action.",
),
"action_name"=> Dict(
"type"=> "string",
"enum"=> ["CHAT_BOX"],
"description" => "one of the available actions",
),
"action_input"=> Dict(
"type"=> "string",
"description" => "Dialogue you want to chat with the user according to your plan.",
),
),
"required"=> ["think", "action_name", "action_input"],
"additionalProperties"=> false
)
)
)
msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
"messages" => chathistory,
"temperature" => 0.7
"temperature" => 0.7,
"response_format"=> response_format,
)
response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response)
response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response)
response = strip(response)
# dollar sign in Julia means string interpolation
while occursin('$', response)
response = replace(response, '$' => "USD")
end
responsedict = nothing
if occursin(requiredKeys[2], response)
try
_responsedict = JSON.parse(response)
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
catch
println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
else
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
responsedict = JSON.parse(response)
if occursin("CHAT_BOX", responsedict["action_input"]) ||
length(responsedict["action_input"]) < 20
println("\nERROR YiemAgent generatechat() --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
println("\nYiem generatechat!() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(responsedict)
responsedict["action_result"] = "Action result is the next user dialogue."
@info "YiemAgent generatechat!() end " @__LINE__
# check whether all answer's key points are in responsedict
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass
errornote = errormsg
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
# if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"]
# errornote = "Your previous attempt didn't use the given functions"
# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# end
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict)
responsedict["action_result"] = "Action result is the next user dialogue."
@info "YiemAgent generatechat!() end " @__LINE__
return (thoughtdict=responsedict, result_raw=responsedict["action_input"])
+409 -357
View File
@@ -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
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>
"""
@@ -683,7 +723,9 @@ function predefined_wine_search_sql(a::T, searchterm::String,
related_tables = a.context.find_related_tables_for_user_question(searchterm)
table_schema = ""
for table in related_tables
_table_schema_str = GeneralUtils.get_db_table_schema_simple(a.context.pg_conn_str, table)
_table_schema_str = 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
@@ -698,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" => [
@@ -714,68 +798,141 @@ 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 = Serde.parse_yaml(response)
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)
#
do_not_resolve_BM25_column = ["tasting_notes", "seo_name", "vintage", "grape", "price"]
if column_name do_not_resolve_BM25_column
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
else
delete!(responsedict[table_name], column_name)
if length(responsedict[table_name]) == 0
delete!(responsedict, table_name)
end
end
if bucket == "fuzzy_correction"
words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, table_name, column_name)
resolved_word = GeneralUtils.resolve_entity(entry["value"], words_catalog; threshold=0.9)
entry["value"] = resolved_word
end
end
println("\n", responsedict)
@info "after BM25 " @__LINE__
# filter for column that will be used for hard condition (SQL where clause)
# column with non-standard operator will be used in vector search
vector_search_words = ""
hard_operators = ["=","<>","!=",">","<",">=","<=","!<","!>","<=>"]
sql = predefined_wine_search_sql(responsedict)
# 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
return sql
println("")
@show responsedict
@info "predefined_wine_search_sql() " @__LINE__
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}
@@ -1261,300 +1418,198 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
end
function paraphrase(text2textInstructLLM::Function, text::String)
systemmsg =
"""
Your name: N/A
Your vision:
- You are a helpful assistant who help the user to paraphrase their text.
Your mission:
- To help paraphrase the user's text
Mission's objective includes:
- To help paraphrase the user's text
Your responsibility includes:
1) To help paraphrase the user's text
Your responsibility does NOT includes:
1) N/A
Your profile:
- N/A
Additional information:
- N/A
At each round of conversation, you will be given the following information:
Text: The user's given text
You MUST follow the following guidelines:
- N/A
You should follow the following guidelines:
- 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)
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
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
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())")
df = DataFrame(LibPQ.execute(conn, meta_query))
if !isempty(df)
pg_type = df[1, :data_type]
udt_name = df[1, :udt_name]
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())")
@error "Failed to fetch metadata for $table_name.$column_name" exception=e
return "error"
end
end
error("paraphrase() failed to generate a response")
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
""" Attemp to correct LLM response's incorrect JSON response.
# 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;
"""
# 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)
correctjson = nothing
for attempt in 1:maxattempt
try
d = copy(JSON.parsefile(incorrectjson))
correctjson = incorrectjson
return correctjson
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 "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
Let's begin!
Given JSON string: $incorrectjson
The given JSON string failed to load previously because: $e
Corrected JSON string:
"""
# apply LLM specific instruct format
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
msgMeta = GeneralUtils.generate_msgMeta(
externalService["mqtttopic"],
senderName= "jsoncorrection",
senderId= string(uuid4()),
receiverName= "text2textinstruct",
mqttBroker= config["mqttServerInfo"]["broker"],
mqttBrokerPort= config["mqttServerInfo"]["port"],
)
outgoingMsg = Dict(
"msgMeta"=> msgMeta,
"payload"=> Dict(
"text"=> prompt,
"kwargs"=> Dict(
"max_tokens"=> 512,
"stop"=> ["<|eot_id|>"],
)
)
)
result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
incorrectjson = result[:response][:text]
@warn "Failed to sample column_name $table_name.$column_name" exception=e
return "unknown"
end
end
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,
NULL AS retailer_name,66
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
# 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
# 3. Assemble Final Query
where_sql = isempty(where_clauses) ? "" : "WHERE " * join(where_clauses, " AND ")
return string(base_query, where_sql, ";")
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
@@ -1584,9 +1639,6 @@ end
+7 -13
View File
@@ -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,25 +199,20 @@ 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
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.
# you should then respond to the user with interleaving think, action_name, action_input in JSON format
1) "think", Your step-by-step reasoning process. Explain why you are choosing this action.
2) "action_name", Can be one of the available actions. Typically corresponds to the execution of the first step in your thought
3) "action_input", The input to the action you are about to perform.
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)
"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.
"CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to say with the user.
"SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity.
Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD."
Example query 2: "Red or white wine, medium tannin, price under 700 USD"
Example query 3: "white wine from Tuscany, Italy or Bordeaux, France
"WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
"END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
"WINE_PRESENTATION_GUIDELINE", store guidelines about how to present wines to the user appropriately. The input is "null" keyword.
"END_CONVER_GUIDELINE", store guidelines about how to end the conversation with the user appropriately. The input is "null" keyword.
"""
system_msg = Dict(