This commit is contained in:
2026-07-16 22:31:40 +07:00
parent 44bb8baf7c
commit 87bc6a46a1
7 changed files with 273 additions and 138 deletions
+101 -44
View File
@@ -75,7 +75,7 @@ OrderedDict{String, Any} with 4 entries:
"""
function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
) where {T<:agent}
@info "YiemAgent decisionMaker() 1 " @__LINE__
# lessonDict = copy(JSON.parsefile("lesson.json"))
# lesson =
@@ -110,7 +110,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
<internal_context_for_assistant>
</internal_context_for_assistant>
"""
@info "YiemAgent decisionMaker() 2 " @__LINE__
# add context to text of the latest message (in the front).
# use for loop because in openai format, each msg may contain both text and image.
for d in a.chathistory[end]["content"]
@@ -123,28 +123,27 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
response = nothing # placeholder for show when error msg show up
for attempt in 1:maxattempt
if attempt > 1
println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
@info "YiemAgent decisionMaker() 3 " @__LINE__
msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
"messages" => a.chathistory,
"temperature" => 0.7
)
@info "YiemAgent decisionMaker() 4 " @__LINE__
response = a.context.text2textInstructLLM(a.id, msg)
@info "YiemAgent decisionMaker() 5 " @__LINE__
response = GeneralUtils.clean_json_response(response)
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)
@info "YiemAgent decisionMaker() 6 " @__LINE__
# dollar sign in Julia means string interpolation
while occursin('$', response)
response = replace(response, '$' => "USD")
end
@info "YiemAgent decisionMaker() 7 " @__LINE__
responsedict = nothing
if occursin(requiredKeys[2], response)
try
@@ -155,14 +154,24 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
continue
end
end
@info "YiemAgent decisionMaker() 8 " @__LINE__
# check whether all answer's key points are in responsedict
println("\n---")
println(responsedict)
println("---\n")
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
@info "YiemAgent decisionMaker() 9 " @__LINE__
if !ispass
errornote = errormsg
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
@info "YiemAgent decisionMaker() 10 " @__LINE__
if responsedict["action_input"] == "CHAT_BOX" &&
occursin("similar", responsedict["action_input"])
@info "illegal CHAT_BOX search term" @__LINE__
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"
@@ -172,7 +181,7 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict)
@info "YiemAgent decisionMaker() 11 " @__LINE__
return responsedict
end
error("DecisionMaker failed to generate a thought ", response)
@@ -380,49 +389,85 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
@info "YiemAgent conversation() 2-3 think count $loopcount " @__LINE__
thoughtdict, result_raw = think(a)
@info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__
if thoughtdict["action_name"] ["CHAT_BOX"]
@info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__
@info "YiemAgent conversation() 2-5 think count $loopcount " @__LINE__
assistant_response = Dict{String, Any}(
"role" => "assistant",
"content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
)
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
else
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,
...
),
]
),
]
)
"""
@info "YiemAgent conversation() 2-6 think count $loopcount " @__LINE__
return response_to_frontend
else # still in action
@info "YiemAgent conversation() 2-7 think count $loopcount " @__LINE__
action_name = thoughtdict["action_name"]
action_input = thoughtdict["action_input"]
@info "YiemAgent conversation() 2-8 think count $loopcount " @__LINE__
action_call = Dict{String, Any}(
"role" => "action_call",
"content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),]
)
@info "YiemAgent conversation() 2-9 think count $loopcount " @__LINE__
addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg)
@info "YiemAgent conversation() 2-10 think count $loopcount " @__LINE__
action_result = thoughtdict["action_result"]
actionresult = Dict{String, Any}(
"role" => "action_result",
"content" => [Dict("type" => "text", "text" => "$action_result"),]
)
@info "YiemAgent conversation() 2-11 think count $loopcount " @__LINE__
addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg)
@info "YiemAgent conversation() 2-12 think count $loopcount " @__LINE__
end
end
end
@@ -442,15 +487,16 @@ julia>
"""
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)
@info "YiemAgent think() 1-1 " @__LINE__
thoughtdict = decisionMaker(a)
@info "YiemAgent think() 1 " @__LINE__
@info "YiemAgent think() 1-2 " @__LINE__
# pprintln(thoughtdict)
result_raw = nothing
if thoughtdict["action_name"] ["CHAT_BOX"]
@info "YiemAgent think() 2 " @__LINE__
@info "YiemAgent think() 2-1 " @__LINE__
thoughtdict, result_raw = generatechat!(a)
@info "YiemAgent think() 2-2 " @__LINE__
elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE"
@info "YiemAgent think() 3 " @__LINE__
thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict)
@@ -462,7 +508,14 @@ function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict,
elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE"
@info "YiemAgent think() 5 " @__LINE__
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
@info "YiemAgent think() 6 " @__LINE__
error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
@@ -629,7 +682,7 @@ function generatechat!(a::T; maxattempt::Integer=10
"action_name": "...",
"action_input": "..."
"""
@info "YiemAgent generatechat!() 1 " @__LINE__
system_msg = Dict(
"role" => "system",
"content" => [
@@ -644,7 +697,7 @@ function generatechat!(a::T; maxattempt::Integer=10
errornote = "N/A"
response = nothing # placeholder for show when error msg show up
@info "YiemAgent generatechat!() 2 " @__LINE__
for attempt in 1:maxattempt
if attempt > 1
println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
@@ -660,10 +713,9 @@ function generatechat!(a::T; maxattempt::Integer=10
response = GeneralUtils.clean_json_response(response)
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)
response = strip(response)
@info "YiemAgent generatechat!() 3 " @__LINE__
responsedict = nothing
if occursin(requiredKeys[2], response)
try
@@ -673,8 +725,12 @@ function generatechat!(a::T; maxattempt::Integer=10
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")
continue
end
@info "YiemAgent generatechat!() 4 " @__LINE__
# check whether all answer's key points are in responsedict
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
if !ispass
@@ -682,7 +738,7 @@ function generatechat!(a::T; maxattempt::Integer=10
println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
continue
end
@info "YiemAgent generatechat!() 5 " @__LINE__
# 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())")
@@ -691,7 +747,8 @@ function generatechat!(a::T; maxattempt::Integer=10
# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# pprintln(responsedict)
responsedict["action_result"] = "Action result is the next user dialogue."
@info "YiemAgent generatechat!() 6 " @__LINE__
return (thoughtdict=responsedict, result_raw=responsedict["action_input"])
end
error("YiemAgent generatechat() failed to generate a thought ", response)
+36 -50
View File
@@ -2,9 +2,10 @@ module llmfunction
export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recommendbox,
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 ..type, ..util
@@ -211,7 +212,7 @@ pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemms
receiverName= "text2textinstruct",
mqttBroker= config["mqttServerInfo"]["broker"],
mqttBrokerPort= config["mqttServerInfo"]["port"],
msgId = string(uuid4()) #CHANGE remove after testing finished
msgId = string(uuid4()) # remove after testing finished
)
outgoingMsg = Dict(
@@ -286,11 +287,11 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"])
# wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"])
wineattributes_2 = extractWineAttributes_2(a, thoughtdict["action_input"])
retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency", "image_url", "retailer_name", "retailer_id"]
_inventoryquery = "$wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)"
_inventoryquery = "$(thoughtdict["action_input"]), $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)"
inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}"
println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
@@ -309,47 +310,33 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
# direct query with possible sql instead of SQLLLM.
sql = generatesql(a, inventoryquery)
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
textresult, result_raw, _, _ = 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)
# )
# ]
# )
textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql)
items = nothing
if sql_result_df !== nothing
result_vec = GeneralUtils.dfToVectorDict(sql_result_df)
# get image
for d in result_vec
image_url_json_str = d["image_url"]
image_url_json_obj = JSON.parse(image_url_json_str)
base_url = "http://192.168.88.106:8080/"
if haskey(image_url_json_obj, "bottle")
url = base_url * image_url_json_obj["bottle"]
image_data = HTTP.get(url) # vector{int} data
image_base64_string = base64encode(image_data.body)
d["image"] = image_base64_string
else
d["image"] = nothing
end
end
items = result_vec # image is added to each item
end
thoughtdict["action_result"] = textresult
end
return (thoughtdict=thoughtdict, result_raw=result_raw)
return (thoughtdict=thoughtdict, result_raw=items)
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.
- 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.
- 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
At each round of conversation, you will be given the following:
@@ -720,7 +707,7 @@ 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.
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
_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 only respond in JSON format as described below>
"wine_name": "...",
@@ -827,15 +814,15 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
responsedict[k] = _v
end
# println("\n--- extractWineAttributes_1-1()")
# @show responsedict
# @info "---\n" @__LINE__
println("\n--- extractWineAttributes_1-1()")
@show responsedict
@info "---\n" @__LINE__
# check each attributes against each column in a database table with BM25
for (k, v) in responsedict
if k ["wine_price_min", "wine_price_max"]
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
end
end
@@ -849,10 +836,9 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
end
result = result[1:end-2] # remove the ending ", "
# println("\n--- extractWineAttributes_1-2()")
# @show responsedict
# @show result
# @info "---\n" @__LINE__
println("\n--- extractWineAttributes_1-2()")
@show result
@info "---\n" @__LINE__
return result
end
error("extractWineAttributes_1() failed to get a response")
+4 -3
View File
@@ -252,6 +252,7 @@ function sommelier(
- Your store carries only wine.
- 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.
- User usually ask for something similar. This means you should use the search term based on the profile they like.
# situation
You are having conversation with a customer.
@@ -285,10 +286,10 @@ function sommelier(
# available actions
"CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan.
"SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is 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.
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."
"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, 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.
"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.
"""