Compare commits

..

17 Commits

Author SHA1 Message Date
ton b349c3a8b6 update 2026-07-17 07:01:55 +07:00
ton 0148e03d6a update 2026-07-16 23:31:36 +07:00
ton e718cc4a5c update 2026-07-16 23:13:18 +07:00
ton 87bc6a46a1 update 2026-07-16 22:31:40 +07:00
ton 44bb8baf7c Merge pull request 'update' (#23) from v0.6.0-output_text_image into main
Reviewed-on: #23
2026-07-15 11:51:43 +00:00
ton b3c3bb9b75 Merge pull request 'update' (#22) from v0.6.0-output_text_image into main
Reviewed-on: #22
2026-07-15 11:50:09 +00:00
ton 8898226825 Merge pull request 'v0.6.0-output_text_image' (#21) from v0.6.0-output_text_image into main
Reviewed-on: #21
2026-07-15 11:48:20 +00:00
ton 5c7caf0b49 Merge pull request 'update' (#20) from v0.6.0-output_text_image into main
Reviewed-on: #20
2026-07-15 07:01:48 +00:00
ton edeef4ed2a Merge pull request 'v0.6.0-output_text_image' (#19) from v0.6.0-output_text_image into main
Reviewed-on: #19
2026-07-15 06:59:56 +00:00
ton 4610137f04 Merge pull request 'update' (#18) from v0.6.0-output_text_image into main
Reviewed-on: #18
2026-07-15 05:20:58 +00:00
ton aedc53bf86 Merge pull request 'update' (#17) from v0.6.0-output_text_image into main
Reviewed-on: #17
2026-07-15 05:17:56 +00:00
ton 7fa988313d Merge pull request 'update' (#16) from v0.6.0-output_text_image into main
Reviewed-on: #16
2026-07-15 05:15:31 +00:00
ton 0df4159261 Merge pull request 'update' (#15) from v0.6.0-output_text_image into main
Reviewed-on: #15
2026-07-15 05:11:05 +00:00
ton 9167ece0c0 Merge pull request 'v0.6.0' (#14) from v0.6.0 into main
Reviewed-on: #14
2026-07-15 04:57:47 +00:00
ton a6a9395ecc Merge pull request 'update' (#13) from v0.6.0-output_text_image into v0.6.0
Reviewed-on: #13
2026-07-15 04:57:11 +00:00
ton f45a036971 Merge pull request 'v0.6.0' (#12) from v0.6.0 into main
Reviewed-on: #12
2026-07-15 04:38:48 +00:00
ton 24b85be58b Merge pull request 'updatet' (#11) from v0.6.0-output_text_image into v0.6.0
Reviewed-on: #11
2026-07-15 04:38:18 +00:00
7 changed files with 325 additions and 160 deletions
+19 -13
View File
@@ -2,7 +2,7 @@
julia_version = "1.12.6" julia_version = "1.12.6"
manifest_format = "2.0" manifest_format = "2.0"
project_hash = "a5128932115e2d3aa9658af105ac712db061d523" project_hash = "1e317787f914f6d857feb7c23bb910d1185caed9"
[[deps.Accessors]] [[deps.Accessors]]
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
@@ -97,9 +97,9 @@ uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.8" version = "0.7.8"
[[deps.CommonSolve]] [[deps.CommonSolve]]
git-tree-sha1 = "99ee296f88c12485402e37c2fd025f95ae097637" git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662"
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
version = "0.2.9" version = "0.2.11"
[[deps.Compat]] [[deps.Compat]]
deps = ["TOML", "UUIDs"] deps = ["TOML", "UUIDs"]
@@ -246,9 +246,13 @@ version = "0.1.10"
[[deps.FileIO]] [[deps.FileIO]]
deps = ["Pkg", "Requires", "UUIDs"] deps = ["Pkg", "Requires", "UUIDs"]
git-tree-sha1 = "91e0e5c68d02bcdaae76d3c8ceb4361e8f28d2e9" git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819"
uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
version = "1.16.5" version = "1.20.0"
weakdeps = ["HTTP"]
[deps.FileIO.extensions]
HTTPExt = "HTTP"
[[deps.FilePathsBase]] [[deps.FilePathsBase]]
deps = ["Compat", "Dates"] deps = ["Compat", "Dates"]
@@ -311,9 +315,9 @@ version = "1.14.0"
[[deps.HTTP]] [[deps.HTTP]]
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
git-tree-sha1 = "eda1d37cb55d90a17d0957c75841138c88b361a1" git-tree-sha1 = "c2c808326222b6dc4bec295a83b55f79aeec98e0"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "2.5.4" version = "2.5.5"
[[deps.HashArrayMappedTries]] [[deps.HashArrayMappedTries]]
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
@@ -678,15 +682,17 @@ version = "0.4.2"
[[deps.PrettyTables]] [[deps.PrettyTables]]
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
git-tree-sha1 = "624de6279ab7d94fc9f672f0068107eb6619732c" git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325"
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
version = "3.3.2" version = "3.4.0"
[deps.PrettyTables.extensions] [deps.PrettyTables.extensions]
PrettyTablesExcelExt = "XLSX"
PrettyTablesTypstryExt = "Typstry" PrettyTablesTypstryExt = "Typstry"
[deps.PrettyTables.weakdeps] [deps.PrettyTables.weakdeps]
Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e"
XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"
[[deps.Printf]] [[deps.Printf]]
deps = ["Unicode"] deps = ["Unicode"]
@@ -767,9 +773,9 @@ version = "0.5.1+0"
[[deps.Roots]] [[deps.Roots]]
deps = ["Accessors", "CommonSolve", "Printf"] deps = ["Accessors", "CommonSolve", "Printf"]
git-tree-sha1 = "ed45bcc7cf3c8887595b973f2b1efbe91dcc50ec" git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455"
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
version = "3.0.1" version = "3.0.5"
[deps.Roots.extensions] [deps.Roots.extensions]
RootsChainRulesCoreExt = "ChainRulesCore" RootsChainRulesCoreExt = "ChainRulesCore"
@@ -1047,10 +1053,10 @@ uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
version = "1.6.1" version = "1.6.1"
[[deps.YiemAgent]] [[deps.YiemAgent]]
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"] deps = ["CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"]
path = "." path = "."
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
version = "0.6.4" version = "0.6.5"
[[deps.Zlib_jll]] [[deps.Zlib_jll]]
deps = ["Libdl"] deps = ["Libdl"]
+3 -1
View File
@@ -1,9 +1,10 @@
name = "YiemAgent" name = "YiemAgent"
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
version = "0.6.4" version = "0.7.0"
authors = ["narawat lamaiin <narawat@outlook.com>"] authors = ["narawat lamaiin <narawat@outlook.com>"]
[deps] [deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
@@ -23,6 +24,7 @@ URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat] [compat]
Base64 = "1.11.0"
CSV = "0.10.15" CSV = "0.10.15"
DataFrames = "1.7.0" DataFrames = "1.7.0"
GeneralUtils = "0.5.1" GeneralUtils = "0.5.1"
+110 -27
View File
@@ -1,28 +1,111 @@
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures
using GeneralUtils, SQLLLM, YiemAgent
config = JSON.parsefile("./appconfig.json")
host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':')
port = parse(Int, _port)
dbname = "winedb"
user = config["externalservice"]["sommpanion_db"]["user"]
password = config["externalservice"]["sommpanion_db"]["password"]
pg_conn_str = "host=$host_url port=$port dbname=$dbname user=$user password=$password"
function execute_sql_winedb(sql::T) where {T<:AbstractString}
host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':')
port = parse(Int, _port)
dbname = "winedb"
user = config["externalservice"]["sommpanion_db"]["user"]
password = config["externalservice"]["sommpanion_db"]["password"]
db_connection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password")
result = nothing
try
result = LibPQ.execute(db_connection, sql)
catch e
LibPQ.close(db_connection)
end
LibPQ.close(db_connection)
return result
end
sql =
""" """
CREATE TABLE "public"."wine" ( 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';
"wine_id" uuid DEFAULT gen_random_uuid() NOT NULL, """
"seo_name" character varying(128) NOT NULL,
"wine_name" character varying(128) NOT NULL, textresult, sql_result_raw, _, _ = YiemAgent.SQLexecution(execute_sql_winedb, sql)
"winery" character varying(128) NOT NULL, result_vec = GeneralUtils.dfToVectorDict(sql_result_raw)
"vintage" integer NOT NULL,
"region" character varying(128) NOT NULL, for d in result_vec
"country" character varying(128) NOT NULL, wine_name = d["wine_name"]
"wine_type" character varying(128) NOT NULL, image_url_json_str = d["image_url"]
"grape" character varying(128) NOT NULL, image_url_json_obj = JSON.parse(image_url_json)
"serving_temperature" character varying(128) NOT NULL, base_url = "http://192.168.88.106:8080/"
"intensity" integer NULL, image_base64 =
"sweetness" integer NULL, if haskey(image_url_json_obj, "bottle")
"tannin" integer NULL, url = base_url * image_url_json_obj["bottle"]
"acidity" integer NULL, image_data = HTTP.get(url) # vector{int} data
"fizziness" integer NULL, image_base64_string = base64encode(image_data)
"tasting_notes" text NULL, else
"image_url" jsonb NULL, nothing
"manufacturer_sku" text NULL, end
"note" text NULL, d["image"] = image_base64
"other_attributes" jsonb NULL, end
"created_time" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NULL,
"updated_time" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NULL,
"description" text NULL,
PRIMARY KEY (wine_id)
);
"""
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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

+146 -56
View File
@@ -75,7 +75,7 @@ OrderedDict{String, Any} with 4 entries:
""" """
function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
) where {T<:agent} ) where {T<:agent}
@info "YiemAgent decisionMaker() start " @__LINE__
# lessonDict = copy(JSON.parsefile("lesson.json")) # lessonDict = copy(JSON.parsefile("lesson.json"))
# lesson = # lesson =
@@ -123,9 +123,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
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 for attempt in 1:maxattempt
if attempt > 1
println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
msg = Dict( msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL", "model" => "gemma-4-E4B-it-UD-Q4_K_XL",
@@ -134,12 +132,13 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
) )
response = a.context.text2textInstructLLM(a.id, msg) response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response) 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 = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to
response = strip(response) response = strip(response)
# dollar sign in Julia means string interpolation # dollar sign in Julia means string interpolation
while occursin('$', response) while occursin('$', response)
response = replace(response, '$' => "USD") response = replace(response, '$' => "USD")
@@ -154,13 +153,26 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
else
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end end
# check whether all answer's key points are in responsedict # check whether all answer's key points are in responsedict
println("\n---")
println(responsedict)
println("---\n")
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass if !ispass
errornote = errormsg errornote = errormsg
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") 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 continue
end end
@@ -172,7 +184,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict) # pprintln(responsedict)
@info "YiemAgent decisionMaker() end " @__LINE__
return responsedict return responsedict
end end
error("DecisionMaker failed to generate a thought ", response) error("DecisionMaker failed to generate a thought ", response)
@@ -304,7 +316,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext
ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass if !ispass
errornote = errormsg errornote = errormsg
println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue continue
end end
@@ -347,7 +359,7 @@ message => Dict(
function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}}, function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}},
maximumMsg=50, max_think_loop::Integer=3) maximumMsg=50, max_think_loop::Integer=3)
@info "YiemAgent conversation() 1 " @__LINE__ @info "YiemAgent conversation() start " @__LINE__
userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"]) userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"])
# find text in usermsg # find text in usermsg
@@ -363,7 +375,7 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
clearhistory(a) clearhistory(a)
return "Okay. What shall we talk about?" return "Okay. What shall we talk about?"
else else
@info "YiemAgent conversation() 2 " @__LINE__
# add usermsg to a.chathistory but how do I handle images? # add usermsg to a.chathistory but how do I handle images?
addNewMessage(a, "user", userinput; maximumMsg=maximumMsg) addNewMessage(a, "user", userinput; maximumMsg=maximumMsg)
@@ -372,49 +384,111 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
while true while true
loopcount += 1 loopcount += 1
if loopcount > max_think_loop if loopcount > max_think_loop
@info "YiemAgent conversation() 2-1 think count $loopcount " @__LINE__
r = generatechat!(a)
@info "YiemAgent conversation() 2-2 think count $loopcount " @__LINE__
return r
end
@info "YiemAgent conversation() 2-3 think count $loopcount " @__LINE__ thoughtdict, result_raw = generatechat!(a)
thoughtdict, result_raw = think(a)
if thoughtdict["action_name"] ["CHAT_BOX"]
@info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__
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"]),]
) )
addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)
@info "YiemAgent conversation() 2-5 think count $loopcount " @__LINE__
return thoughtdict["action_input"]
# elseif thoughtdict["action_name"] ∈ ["CHAT_BOX"]
# @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__
# assistant_response = Dict{String, Any}(
# "role" => "assistant",
# "content" => [
# Dict("type" => "text", "text" => thoughtdict["action_input"]),
# Dict( #WORKING put 1st image here
# "type" => "image_url",
# "image_url" => Dict("url" => image1_data_uri)
# ),
# Dict( #WORKING put 2nd image here
# "type" => "image_url",
# "image_url" => Dict("url" => image2_data_uri)
# ),
# ]
# )
# addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)
# return thoughtdict["action_input"] #XXX change output from string to dict items_info = []
else if haskey(a.memory["shortmem"], "items_info")
for (i, item) in enumerate(a.memory["shortmem"]["items_info"])
if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"])
push!(items_info, item)
deleteat!(a.memory["shortmem"]["items_info"], i)
end
end
end
response_to_frontend = Dict{String, Any}(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => thoughtdict["action_input"]),
Dict(
"type" => "items_info",
"items_info" => items_info
),
]
)
return response_to_frontend
end
thoughtdict, result_raw = think(a)
if thoughtdict["action_name"] ["CHAT_BOX"]
assistant_response = Dict{String, Any}(
"role" => "assistant",
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
)
addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)
items_info = []
if haskey(a.memory["shortmem"], "items_info")
for (i, item) in enumerate(a.memory["shortmem"]["items_info"])
if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"])
push!(items_info, item)
deleteat!(a.memory["shortmem"]["items_info"], i)
end
end
end
response_to_frontend = Dict{String, Any}(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => thoughtdict["action_input"]),
Dict(
"type" => "items_info",
"items_info" => items_info
),
]
)
""" intended message to send to frontend should have the following format.
response_to_frontend = Dict{String, Any}(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => "assistant_text_response"),
Dict(
"type" => "items_info",
"items_info" => [
Dict(
"wine_name"=> "wine name 1",
"wine_id"=> "...",
"image"=> base64 encoded image,
...
),
Dict(
"wine_name"=> "wine name 2",
"wine_id"=> "...",
"image"=> base64 encoded image,
...
),
]
),
]
)
"""
return response_to_frontend
else # still in action
action_name = thoughtdict["action_name"] action_name = thoughtdict["action_name"]
action_input = thoughtdict["action_input"] action_input = thoughtdict["action_input"]
action_call = Dict{String, Any}( action_call = Dict{String, Any}(
"role" => "action_call", "role" => "action_call",
"content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),] "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),]
) )
addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg) addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg)
action_result = thoughtdict["action_result"] action_result = thoughtdict["action_result"]
@@ -422,7 +496,9 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
"role" => "action_result", "role" => "action_result",
"content" => [Dict("type" => "text", "text" => "$action_result"),] "content" => [Dict("type" => "text", "text" => "$action_result"),]
) )
addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg) addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg)
@info "YiemAgent conversation() end think count $loopcount " @__LINE__
end end
end end
end end
@@ -442,34 +518,44 @@ julia>
""" """
function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
# a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0) # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0)
@info "YiemAgent think() start " @__LINE__
thoughtdict = decisionMaker(a) thoughtdict = decisionMaker(a)
@info "YiemAgent think() 1 " @__LINE__ @info "YiemAgent think() 1 " @__LINE__
# pprintln(thoughtdict) @show thoughtdict
println("---\n")
result_raw = nothing result_raw = nothing
if thoughtdict["action_name"] ["CHAT_BOX"] if thoughtdict["action_name"] ["CHAT_BOX"]
@info "YiemAgent think() 2 " @__LINE__
thoughtdict, result_raw = generatechat!(a) thoughtdict, result_raw = generatechat!(a)
elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE" elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE"
@info "YiemAgent think() 3 " @__LINE__
thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict) thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict)
elseif thoughtdict["action_name"] ["WINE_PRESENTATION_GUIDELINE"] elseif thoughtdict["action_name"] ["WINE_PRESENTATION_GUIDELINE"]
@info "YiemAgent think() 4 " @__LINE__
thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict) thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict)
elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE" elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE"
@info "YiemAgent think() 5 " @__LINE__
thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false) thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false)
#WORKING result_raw will be a df. i need to get images so i can send to frontend if result_raw !== nothing && result_raw isa Vector
if haskey(a.memory["shortmem"], "items_info")
append!(a.memory["shortmem"]["items_info"], result_raw)
else
a.memory["shortmem"]["items_info"] = result_raw
end
end
else else
@info "YiemAgent think() 6 " @__LINE__
error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end end
println("\n")
@info "YiemAgent think() end " @__LINE__
@show thoughtdict @show thoughtdict
@info "YiemAgent think() 7 " @__LINE__ println("---\n")
return (thoughtdict=thoughtdict, result_raw=result_raw) return (thoughtdict=thoughtdict, result_raw=result_raw)
end end
@@ -546,7 +632,7 @@ end
#PENDING #PENDING
function generatechat!(a::T; maxattempt::Integer=10 function generatechat!(a::T; maxattempt::Integer=10
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
@info "YiemAgent generatechat!() start " @__LINE__
# lessonDict = copy(JSON.parsefile("lesson.json")) # lessonDict = copy(JSON.parsefile("lesson.json"))
# lesson = # lesson =
@@ -629,7 +715,7 @@ function generatechat!(a::T; maxattempt::Integer=10
"action_name": "...", "action_name": "...",
"action_input": "..." "action_input": "..."
""" """
system_msg = Dict( system_msg = Dict(
"role" => "system", "role" => "system",
"content" => [ "content" => [
@@ -660,8 +746,7 @@ function generatechat!(a::T; maxattempt::Integer=10
response = GeneralUtils.clean_json_response(response) 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) response = strip(response)
responsedict = nothing responsedict = nothing
@@ -673,13 +758,17 @@ function generatechat!(a::T; maxattempt::Integer=10
println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
else
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end end
# check whether all answer's key points are in responsedict # check whether all answer's key points are in responsedict
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass if !ispass
errornote = errormsg errornote = errormsg
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue continue
end end
@@ -691,7 +780,8 @@ function generatechat!(a::T; maxattempt::Integer=10
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict) # 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"]) return (thoughtdict=responsedict, result_raw=responsedict["action_input"])
end end
error("YiemAgent generatechat() failed to generate a thought ", response) error("YiemAgent generatechat() failed to generate a thought ", response)
+43 -60
View File
@@ -2,9 +2,10 @@ module llmfunction
export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recommendbox, export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recommendbox,
virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1,
extractWineAttributes_2, paraphrase extractWineAttributes_2, paraphrase, SQLexecution
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures,
Base64
using GeneralUtils, SQLLLM using GeneralUtils, SQLLLM
using ..type, ..util using ..type, ..util
@@ -211,7 +212,7 @@ pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemms
receiverName= "text2textinstruct", receiverName= "text2textinstruct",
mqttBroker= config["mqttServerInfo"]["broker"], mqttBroker= config["mqttServerInfo"]["broker"],
mqttBrokerPort= config["mqttServerInfo"]["port"], mqttBrokerPort= config["mqttServerInfo"]["port"],
msgId = string(uuid4()) #CHANGE remove after testing finished msgId = string(uuid4()) # remove after testing finished
) )
outgoingMsg = Dict( outgoingMsg = Dict(
@@ -290,7 +291,7 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
wineattributes_2 = extractWineAttributes_2(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"] retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency", "image_url", "retailer_name", "retailer_id"]
_inventoryquery = "$wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)" _inventoryquery = "$(thoughtdict["action_input"]), $wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)"
inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}"
println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
@@ -309,47 +310,33 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
# direct query with possible sql instead of SQLLLM. # direct query with possible sql instead of SQLLLM.
sql = generatesql(a, inventoryquery) sql = generatesql(a, inventoryquery)
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n") println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
textresult, result_raw, _, _ = SQLexecution(a.context.executeSQL, sql) textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql)
#WORKING if result_raw != nothing, get image from image_url column of a df.
# then store in a.memory["shortmem]["image"] = OrderedDict(
# Dict(
# "wine_id"=> "wine_id,
# "name"=> "wine name",
# "image_url" => Dict("url" => data1_uri)
# )
# )
# # 1. Read local file and encode to base64 string
# image2_path = "test/small_image.png"
# image2_bytes = read(image2_path)
# image2_base64_string = base64encode(image2_bytes)
# mime_type = "image/png"
# data2_uri = "data:$(mime_type);base64,$(image2_base64_string)"
# # 3. Construct payload with the Data URI
# message = Dict(
# "role" => "user",
# "content" => [
# Dict("type" => "text", "text" => "Do you know type of wine in the image?"),
# Dict(
# "type" => "image_url",
# "image_url" => Dict("url" => data1_uri)
# )
# ]
# )
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 thoughtdict["action_result"] = textresult
end end
return (thoughtdict=thoughtdict, result_raw=result_raw) return (thoughtdict=thoughtdict, result_raw=items)
end end
@@ -366,7 +353,7 @@ function generatesql(a::T, searchterm::String,
- A junction table can be used to link tables together. Another use case is for filtering data. - A junction table can be used to link tables together. Another use case is for filtering data.
- If you can't find a single table that can be used to answer the user's search term, try joining multiple tables to see if you can obtain the answer. - If you can't find a single table that can be used to answer the user's search term, try joining multiple tables to see if you can obtain the answer.
- Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search. - Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search.
- If there is no search result from the database, remove the restrictive criteria until a search result is available, and proceed from there. - Overly strict condition usually yields empth result
# situation # situation
At each round of conversation, you will be given the following: At each round of conversation, you will be given the following:
@@ -711,7 +698,6 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
wine_name: name of the wine wine_name: name of the wine
winery: name of the winery winery: name of the winery
vintage: the year of the wine vintage: the year of the wine
region: a region, such as Burgundy, Bordeaux, Champagne, Napa Valley, Tuscany, California, Oregon, etc. Use "or" if there are multiple regions.
country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States". Use "or" if there are multiple countries. country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States". Use "or" if there are multiple countries.
wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified"
grape_varietal: the name of the primary grape used to make the wine grape_varietal: the name of the primary grape used to make the wine
@@ -720,13 +706,12 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100. wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100.
occasion: the occasion the user is having the wine for occasion: the occasion the user is having the wine for
food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc
_keyword suffice is the related keyword that appears in user's query. each keyword can not be used twice. _keyword suffice is the related keyword that appears in user's query.
</you should then respond to the user with> </you should then respond to the user with>
<you should only respond in JSON format as described below> <you should only respond in JSON format as described below>
"wine_name": "...", "wine_name": "...",
"winery": "...", "winery": "...",
"vintage": "...", "vintage": "...",
"region": "...",
"country": "...", "country": "...",
"wine_type": "...", "wine_type": "...",
"grape_varietal": "...", "grape_varietal": "...",
@@ -741,7 +726,6 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
"wine_name": "N/A", "wine_name": "N/A",
"winery": "N/A", "winery": "N/A",
"vintage": "N/A", "vintage": "N/A",
"region": "Tuscany or Napa Valley",
"country": "Italy or United States", "country": "Italy or United States",
"wine_type": "red or white", "wine_type": "red or white",
"grape_varietal": "Chenin Blanc or Riesling", "grape_varietal": "Chenin Blanc or Riesling",
@@ -755,7 +739,6 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
"wine_name": "Saumur Blanc", "wine_name": "Saumur Blanc",
"winery": "Domaine du Collier", "winery": "Domaine du Collier",
"vintage": "2019", "vintage": "2019",
"region": "N/A",
"country": "France", "country": "France",
"wine_type": "white", "wine_type": "white",
"grape_varietal": "Merlot", "grape_varietal": "Merlot",
@@ -766,7 +749,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
"food_to_be_paired_with_wine": "N/A" "food_to_be_paired_with_wine": "N/A"
</here are some examples> </here are some examples>
""" """
requiredKeys = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"] requiredKeys = ["wine_name", "winery", "vintage", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"]
errornote = "" errornote = ""
context = context =
""" """
@@ -827,15 +810,15 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
responsedict[k] = _v responsedict[k] = _v
end end
# println("\n--- extractWineAttributes_1-1()") @info "YiemAgent extractWineAttributes_1() " @__LINE__
# @show responsedict @show responsedict
# @info "---\n" @__LINE__ @info "---\n" @__LINE__
# check each attributes against each column in a database table with BM25 # check each attributes against each column in a database table with BM25
for (k, v) in responsedict for (k, v) in responsedict
if k ["wine_price_min", "wine_price_max"] if k ["wine_price_min", "wine_price_max"]
words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k) words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k)
resolved_word = GeneralUtils.resolve_entity(v, words_catalog;threshold=0.9) resolved_word = GeneralUtils.resolve_entity(v, words_catalog; threshold=0.9)
responsedict[k] = resolved_word responsedict[k] = resolved_word
end end
end end
@@ -849,10 +832,11 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
end end
result = result[1:end-2] # remove the ending ", " result = result[1:end-2] # remove the ending ", "
# println("\n--- extractWineAttributes_1-2()")
# @show responsedict @info "YiemAgent extractWineAttributes_1() " @__LINE__
# @show result @show result
# @info "---\n" @__LINE__ @info "---\n" @__LINE__
return result return result
end end
error("extractWineAttributes_1() failed to get a response") error("extractWineAttributes_1() failed to get a response")
@@ -1005,9 +989,6 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
for attempt in 1:10 for attempt in 1:10
response = a.context.text2textInstructLLM(a.id, msg) response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response) response = GeneralUtils.clean_json_response(response)
println("\n--- extractWineAttributes_2-1()")
println(response)
println("--- \n")
response = GeneralUtils.remove_french_accents(response) response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response) think, response = GeneralUtils.extractthink(response)
@@ -1045,9 +1026,11 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
end end
end end
result = result[1:end-2] # remove the ending ", " result = result[1:end-2] # remove the ending ", "
println("\n--- extractWineAttributes_2-2()")
println(result) @info "YiemAgent extractWineAttributes_2() " @__LINE__
println("--- \n") @show result
@info "---\n" @__LINE__
return result return result
end end
error("extractWineAttributes_2() failed to get a response") error("extractWineAttributes_2() failed to get a response")
+4 -3
View File
@@ -252,6 +252,7 @@ function sommelier(
- Your store carries only wine. - Your store carries only wine.
- Vintage 0 means non-vintage. - Vintage 0 means non-vintage.
- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.
- User usually ask for something similar. This means you should use the search term based on the profile they like.
# situation # situation
You are having conversation with a customer. You are having conversation with a customer.
@@ -285,10 +286,10 @@ function sommelier(
# 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.
"SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is supported search criteria including: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. "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 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." 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 2: "Red or white wine, medium tannin, price under 700 USD"
Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France Example query 3: "white wine from Tuscany, Italy or Bordeaux, France
"WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. "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. "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.
""" """