This commit is contained in:
narawat lamaiin
2025-05-14 21:21:35 +07:00
parent a0152a3c29
commit d0c26e52e8
4 changed files with 659 additions and 465 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -339,7 +339,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
As a helpful sommelier, your task is to extract the user information from the user's query as much as possible to fill out user's preference form.
At each round of conversation, the user will give you the following:
User's query: ...
- The query: the query provided by the user.
You must follow the following guidelines:
- If specific information required in the preference form is not available in the query or there isn't any, mark with "N/A" to indicate this.
@@ -347,121 +347,167 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
- Do not generate other comments.
You should then respond to the user with:
Wine_name: name of the wine
Winery: name of the winery
Vintage: the year of the wine
Region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc
Country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States"
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
Tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc
Wine_price_range: price range of wine. Example: For price 10-20, price range will be "10 to 20". For price 100, price range will be 0 to 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
wine_name: name of the wine
winery: name of the winery
vintage: the year of the wine
region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc
country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States"
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
tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc
wine_price_min: minimum price range of wine. Example: For wine price 20, wine_price_min will be 0. For wine price 10 to 100, wine_price_min will be 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
You should only respond in format as described below:
Wine_name: ...
Winery: ...
Vintage: ...
Region: ...
Country: ...
Wine_type:
Grape_varietal: ...
Tasting_notes: ...
Wine_price_range: ...
Occasion: ...
Food_to_be_paired_with_wine: ...
You should only respond in JSON format as described below:
{
"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": "..."
}
Here are some example:
User's query: red, Chenin Blanc, Riesling, 20 USD from Tuscany, Italy or Napa Valley, USA
Wine_name: N/A. Winery: N/A. Vintage: N/A. Region: Tuscany, Napa Valley. Country: Italy, United States. Wine_type: red, white. Grape_varietal: Chenin Blanc, Riesling. Tasting_notes: citrus. Wine_price_range: 0 to 20. Occasion: N/A. Food_to_be_paired_with_wine: N/A
{
"wine_name": "N/A",
"winery": "N/A",
"vintage": "N/A",
"region": "Tuscany, Napa Valley",
"country": "Italy, United States",
"wine_type": "red, white",
"grape_varietal": "Chenin Blanc, Riesling",
"tasting_notes": "citrus",
"wine_price_min": "0",
"wine_price_max": "20",
"occasion": "N/A",
"food_to_be_paired_with_wine": "N/A"
}
User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot
Winery: Domaine du Collier. Wine_name: Saumur Blanc. Vintage: 2019. Region: Saumur. Country: France. Wine_type: white. Grape_varietal: Merlot. Tasting_notes: plum. Wine_price_range: N/A. Occasion: N/A. Food_to_be_paired_with_wine: N/A.
{
"wine_name": "Saumur Blanc",
"winery": "Domaine du Collier",
"vintage": "2019",
"region": "Saumur",
"country": "France",
"wine_type": "white",
"grape_varietal": "Merlot",
"tasting_notes": "plum",
"wine_price_min": "N/A",
"wine_price_max": "N/A",
"occasion": "N/A",
"food_to_be_paired_with_wine": "N/A"
}
Let's begin!
"""
header = ["Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price_range:", "Occasion:", "Food_to_be_paired_with_wine:"]
dictkey = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_range", "occasion", "food_to_be_paired_with_wine"]
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]
errornote = "N/A"
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.2,
)
for attempt in 1:maxattempt
usermsg =
"""
$input
"""
assistantinfo =
context =
"""
<information>
<context>
P.S. $errornote
</information>
</context>
/no_think
"""
_prompt =
unformatPrompt =
[
Dict(:name=> "system", :text=> systemmsg),
Dict(:name=> "user", :text=> usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)
prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
# add info
prompt = prompt * assistantinfo
prompt = prompt * context
response = a.func[:text2textInstructLLM](prompt;
modelsize="medium", llmkwargs=llmkwargs, senderId=a.id)
response = GeneralUtils.remove_french_accents(response)
response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id)
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response)
# check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response)
kwvalue = [i for i in values(detected_kw)]
zeroind = findall(x -> x == 0, kwvalue)
missingkeys = [header[i] for i in zeroind]
if 0 values(detected_kw)
errornote = "$missingkeys are missing from your previous response"
println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "Your previous attempt has duplicated points"
println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
responsedict = nothing
try
responsedict = copy(JSON3.read(response))
catch
println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
# check whether response has all answer's key points
detected_kw = GeneralUtils.detect_keyword(header, response)
if 0 values(detected_kw)
errornote = "In your previous attempts, the response does not have all answer's key points"
println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# check whether all answer's key points are in responsedict
_responsedictKey = keys(responsedict)
responsedictKey = [i for i in _responsedictKey] # convert into a list
is_requiredKeys_in_responsedictKey = [i responsedictKey for i in requiredKeys]
if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys)
errornote = "Your previous attempt has more key points than answer's required key points."
println("\nERROR YiemAgent extractWineAttributes_1() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "In your previous attempts, the response has duplicated answer's key points"
println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
println(response)
elseif !all(is_requiredKeys_in_responsedictKey)
zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey)
missingkeys = [requiredKeys[i] for i in zeroind]
errornote = "$missingkeys are missing from your previous response"
println("\nERROR YiemAgent extractWineAttributes_1() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
# # check whether response has all header
# detected_kw = GeneralUtils.detect_keyword(header, response)
# kwvalue = [i for i in values(detected_kw)]
# zeroind = findall(x -> x == 0, kwvalue)
# missingkeys = [header[i] for i in zeroind]
# if 0 ∈ values(detected_kw)
# errornote = "$missingkeys are missing from your previous response"
# println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# elseif sum(values(detected_kw)) > length(header)
# errornote = "Your previous attempt has duplicated points"
# println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# end
# # check whether response has all answer's key points
# detected_kw = GeneralUtils.detect_keyword(header, response)
# if 0 ∈ values(detected_kw)
# errornote = "In your previous attempts, the response does not have all answer's key points"
# println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
# elseif sum(values(detected_kw)) > length(header)
# errornote = "In your previous attempts, the response has duplicated answer's key points"
# println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# println(response)
# continue
# end
# responsedict = GeneralUtils.textToDict(response, header;
# dictKey=dictkey, symbolkey=true)
delete!(responsedict, :thought)
delete!(responsedict, :tasting_notes)
delete!(responsedict, :occasion)
delete!(responsedict, :food_to_be_paired_with_wine)
println(@__FILE__, " ", @__LINE__)
pprintln(responsedict)
# check if winery, wine_name, region, country, wine_type, grape_varietal's value are in the query because sometime AI halucinates
checkFlag = false
for i in dictkey
for i in requiredKeys
j = Symbol(i)
if j [:thought, :tasting_notes, :occasion, :food_to_be_paired_with_wine]
# in case j is wine_price it needs to be checked differently because its value is ranged
@@ -759,7 +805,8 @@ function paraphrase(text2textInstructLLM::Function, text::String)
Let's begin!
"""
#[WORKING] use JSON3 the same as extractWineAttributes_1 is better
#[WORKING] change this function to use the same format use decisionMater
header = ["Paraphrase:"]
dictkey = ["paraphrase"]

View File

@@ -193,8 +193,7 @@ function sommelier(
"""
memory = Dict{Symbol, Any}(
:shortmem=> OrderedDict{Symbol, Any}(
:available_wine=> [],
:found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines
:db_search_result=> Any[],
),
:events=> Vector{Dict{Symbol, Any}}(),
:state=> Dict{Symbol, Any}(

View File

@@ -1,7 +1,7 @@
module util
export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText, createEventsLog
availableWineToText, createEventsLog, createChatLog
using UUIDs, Dates, DataStructures, HTTP, JSON3
using GeneralUtils
@@ -301,10 +301,10 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin
for (i, event) in zip(ind, events)
# If no outcome exists, format without outcome
if event[:outcome] === nothing
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput])\n"
timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n"
# If outcome exists, include it in formatting
else
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n"
end
end
@@ -313,15 +313,15 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin
end
function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty log array
log = Dict{Symbol, String}[]
# Determine which indices to use - either provided range or full length
ind =
if eventindex !== nothing
[eventindex...]
if index !== nothing
[index...]
else
1:length(events)
end
@@ -338,7 +338,7 @@ function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothi
subject = event[:subject]
actioninput = event[:actioninput]
outcome = event[:outcome]
str = "$subject: $actioninput $outcome"
str = "Action: $actioninput Outcome: $outcome"
d = Dict{Symbol, String}(:name=>subject, :text=>str)
push!(log, d)
end
@@ -348,6 +348,31 @@ function createEventsLog(events::T1; eventindex::Union{UnitRange, Nothing}=nothi
end
function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty log array
log = Dict{Symbol, String}[]
# Determine which indices to use - either provided range or full length
ind =
if index !== nothing
[index...]
else
1:length(chatdict)
end
# Iterate through events and format each one
for (i, event) in zip(ind, chatdict)
subject = event[:name]
text = event[:text]
d = Dict{Symbol, String}(:name=>subject, :text=>text)
push!(log, d)
end
return log
end
@@ -382,9 +407,6 @@ end