This commit is contained in:
narawat lamaiin
2025-05-17 12:18:25 +07:00
parent 3e79c0bfed
commit 68c2c2f12b
3 changed files with 133 additions and 91 deletions

View File

@@ -97,7 +97,7 @@ julia> output_thoughtDict = Dict(
# Signature # Signature
""" """
function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10 function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
) where {T<:agent} ) where {T<:agent}
# lessonDict = copy(JSON3.read("lesson.json")) # lessonDict = copy(JSON3.read("lesson.json"))
@@ -134,11 +134,6 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10
recentchat = createChatLog(a.chathistory; index=recentchat_ind) recentchat = createChatLog(a.chathistory; index=recentchat_ind)
# recentEventsDict = createEventsLog(recentevents; index=recent_ind) # recentEventsDict = createEventsLog(recentevents; index=recent_ind)
#BUG timeline only cover event 1-9 out of 10 events while recentchat cover 1-9 because
# recent_ind is based on chathistory. i should ind based on events. The reason is events always
# have more than chathistory due to CHECKINVENTORY() function which store in events BUT NOT in
# chathistory
# # recap as caching # # recap as caching
# # query similar result from vectorDB # # query similar result from vectorDB
@@ -215,7 +210,7 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10
- Vintage 0 means non-vintage. - Vintage 0 means non-vintage.
</Store guidelines> </Store guidelines>
At each round of conversation, you will be given the following information: At each round of conversation, you will be given the following information:
Database search result: the result of a database search using SQL commands you have found so far context: additional information about the current situation
You should then respond to the user with interleaving Plan, Action_name, Action_input: You should then respond to the user with interleaving Plan, Action_name, Action_input:
1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific. 1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific.
@@ -253,14 +248,14 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10
""" """
<context> <context>
<Available tools> <Available tools>
- CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. - CHATBOX which you can use to talk with the user. Be specific.
- CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - CHECKINVENTORY allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity.
Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD." Example query: "Dry, full-bodied red wine from Burgundy, France or Tuscany, Italy. Merlot varietal. price 100 to 1000 USD."
- PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present.
- ENDCONVERSATION which you can use to properly end the conversation with the user. Input is "NA". - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is a dialogue where you wrap up the conversation, thank the user, and invite them to return next time.
</Available tools> </Available tools>
$(a.memory[:shortmem][:scratchpad])
Remark: $errornote Remark: $errornote
Database search result: $database_search_result
</context> </context>
""" """
@@ -1123,22 +1118,22 @@ julia>
""" """
function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent}
# a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) # a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0)
thoughtDict = decisionMaker(a; recentevents=5) thoughtDict = decisionMaker(a)
actionname = thoughtDict[:action_name] actionname = thoughtDict[:action_name]
actioninput = thoughtDict[:action_input] actioninput = thoughtDict[:action_input]
# map action and input() to llm function # map action and input() to llm function
response = response =
if actionname == "CHATBOX" if actionname == "CHATBOX" || actionname == "ENDCONVERSATION"
(result=thoughtDict[:plan], errormsg=nothing, success=true) (result=thoughtDict[:plan], errormsg=nothing, success=true)
elseif actionname == "CHECKINVENTORY" elseif actionname == "CHECKINVENTORY"
checkinventory(a, actioninput) checkinventory(a, actioninput)
elseif actionname == "PRESENTBOX" elseif actionname == "PRESENTBOX"
(result=actioninput, errormsg=nothing, success=true) (result=actioninput, errormsg=nothing, success=true)
elseif actionname == "ENDCONVERSATION" # elseif actionname == "ENDCONVERSATION"
x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time." # x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time."
(result=actioninput, errormsg=nothing, success=true) # (result=actioninput, errormsg=nothing, success=true)
else else
error("undefined LLM function. Requesting $actionname") error("undefined LLM function. Requesting $actionname")
end end
@@ -1166,7 +1161,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
# ) # )
# ) # )
# result = chatresponse # result = chatresponse
if actionname ["CHATBOX"] if actionname ["CHATBOX", "ENDCONVERSATION"]
push!(a.memory[:events], push!(a.memory[:events],
eventdict(; eventdict(;
event_description="the assistant talks to the user.", event_description="the assistant talks to the user.",
@@ -1178,19 +1173,19 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
) )
) )
result = actioninput result = actioninput
elseif actionname ["ENDCONVERSATION"] # elseif actionname ∈ ["ENDCONVERSATION"]
chatresponse = generatechat(a, thoughtDict) # chatresponse = generatechat(a, thoughtDict)
push!(a.memory[:events], # push!(a.memory[:events],
eventdict(; # eventdict(;
event_description="the assistant talks to the user.", # event_description="the assistant talks to the user.",
timestamp=Dates.now(), # timestamp=Dates.now(),
subject="assistant", # subject="assistant",
thought=thoughtDict, # thought=thoughtDict,
actionname=actionname, # actionname=actionname,
actioninput=chatresponse, # actioninput=chatresponse,
) # )
) # )
result = chatresponse # result = chatresponse
elseif actionname ["PRESENTBOX"] elseif actionname ["PRESENTBOX"]
chatresponse = presentbox(a, thoughtDict) chatresponse = presentbox(a, thoughtDict)
push!(a.memory[:events], push!(a.memory[:events],
@@ -1215,6 +1210,21 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
else else
a.memory[:shortmem][:db_search_result] = vd a.memory[:shortmem][:db_search_result] = vd
end end
# add to scratchpad
a.memory[:shortmem][:scratchpad] *=
"""
<database_search_result>
I searched the database with this search term: $actioninput This is what I found: $result
</database_search_result>
"""
else
a.memory[:shortmem][:scratchpad] *=
"""
<database_search_result>
I searched the database with this search term: $actioninput This is what I found: $result
</database_search_result>
"""
end end
push!(a.memory[:events], push!(a.memory[:events],
@@ -1224,7 +1234,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
subject= "assistant", subject= "assistant",
thought=thoughtDict, thought=thoughtDict,
actionname=actionname, actionname=actionname,
actioninput= "I found something in the database using this SQL: $actioninput", actioninput= "I search the database with this search term: $actioninput",
outcome= "This is what I found:, $result" outcome= "This is what I found:, $result"
) )
) )
@@ -1296,7 +1306,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev
""" """
<context> <context>
Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) Name of the wines that needs to be introduced: $(thoughtDict[:action_input])
Database search result: $database_search_result $(a.memory[:shortmem][:scratchpad])
P.S. $errornote P.S. $errornote
</context> </context>
""" """

View File

@@ -500,16 +500,24 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
# responsedict = GeneralUtils.textToDict(response, header; # responsedict = GeneralUtils.textToDict(response, header;
# dictKey=dictkey, symbolkey=true) # dictKey=dictkey, symbolkey=true)
removekeys = [:thought, :tasting_notes, :occasion, :food_to_be_paired_with_wine, :vintage]
for i in removekeys
delete!(responsedict, i)
end
delete!(responsedict, :thought) delete!(responsedict, :thought)
delete!(responsedict, :tasting_notes) delete!(responsedict, :tasting_notes)
delete!(responsedict, :occasion) delete!(responsedict, :occasion)
delete!(responsedict, :food_to_be_paired_with_wine) delete!(responsedict, :food_to_be_paired_with_wine)
delete!(responsedict, :vintage)
# check if winery, wine_name, region, country, wine_type, grape_varietal's value are in the query because sometime AI halucinates # check if winery, wine_name, region, country, wine_type, grape_varietal's value are in the query because sometime AI halucinates
checkFlag = false checkFlag = false
for i in requiredKeys for i in requiredKeys
j = Symbol(i) j = Symbol(i)
if j [:thought, :tasting_notes, :occasion, :food_to_be_paired_with_wine] if j removekeys
# in case j is wine_price it needs to be checked differently because its value is ranged # in case j is wine_price it needs to be checked differently because its value is ranged
if j == :wine_price if j == :wine_price
if responsedict[:wine_price] != "N/A" if responsedict[:wine_price] != "N/A"
@@ -592,7 +600,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
conversiontable = conversiontable =
""" """
<Conversion Table> <conversion_table>
Intensity level: Intensity level:
1 to 2: May correspond to "light-bodied" or a similar description. 1 to 2: May correspond to "light-bodied" or a similar description.
2 to 3: May correspond to "med light bodied", "medium light" or a similar description. 2 to 3: May correspond to "med light bodied", "medium light" or a similar description.
@@ -617,16 +625,16 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
3 to 4: May correspond to "medium acidity" or a similar description. 3 to 4: May correspond to "medium acidity" or a similar description.
4 to 5: May correspond to "semi high acidity" or a similar description. 4 to 5: May correspond to "semi high acidity" or a similar description.
4 to 5: May correspond to "high acidity" or a similar description. 4 to 5: May correspond to "high acidity" or a similar description.
</Conversion Table> </conversion_table>
""" """
systemmsg = systemmsg =
""" """
As an helpful sommelier, your task is to fill out the user's preference form based on the corresponding words from the user's query. As an helpful sommelier, your task is to fill out the user's preference form based on the corresponding words from the user's query.
At each round of conversation, the user will give you the current situation: At each round of conversation, you will be given the following information:
Conversion Table: ... conversion_table: a conversion table that maps descriptive words to their corresponding integer levels
User's query: ... query: the words from the user's query that describe their preferences
The preference form requires the following information: The preference form requires the following information:
sweetness, acidity, tannin, intensity sweetness, acidity, tannin, intensity
@@ -637,86 +645,109 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. 2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer.
3) Do not generate other comments. 3) Do not generate other comments.
You should then respond to the user with: You should then respond to the user with:
Sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine. sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine.
Sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2 sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2
Acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine. acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine.
Acidity: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5 acidity: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5
Tannin_keyword: The exact keywords in the user's query describing the tannin level of the wine. tannin_keyword: The exact keywords in the user's query describing the tannin level of the wine.
Tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3 tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3
Intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine. intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine.
Intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4 intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4
You should only respond in format as described below: You should only respond in JSON format as described below:
Sweetness_keyword: ... {
Sweetness: ... "sweetness_keyword": "...",
Acidity_keyword: ... "sweetness": "...",
Acidity: ... "acidity_keyword": "...",
Tannin_keyword: ... "acidity": "...",
Tannin: ... "tannin_keyword": "...",
Intensity_keyword: ... "tannin": "...",
Intensity: ... "intensity_keyword": "...",
"intensity": "..."
}
Here are some examples: Here are some examples:
User's query: I want a wine with a medium-bodied, low acidity, medium tannin. User's query: I want a wine with a medium-bodied, low acidity, medium tannin.
Sweetness_keyword: N/A {
Sweetness: N/A "sweetness_keyword": "N/A",
Acidity_keyword: low acidity "sweetness": "N/A",
Acidity: 1-2 "acidity_keyword": "low acidity",
Tannin_keyword: medium tannin "acidity": 1-2,
Tannin: 3-4 "tannin_keyword": "medium tannin",
Intensity_keyword: medium-bodied "tannin": 3-4,
Intensity: 3-4 "intensity_keyword": "medium-bodied",
"intensity": 3-4
}
User's query: German red wine, under 100, pairs with spicy food User's query: German red wine, under 100, pairs with spicy food
Sweetness_keyword: N/A {
Sweetness: N/A "sweetness_keyword": "N/A",
Acidity_keyword: N/A "sweetness": "N/A",
Acidity: N/A "acidity_keyword": "N/A",
Tannin_keyword: N/A "acidity": "N/A",
Tannin: N/A "tannin_keyword": "N/A",
Intensity_keyword: N/A "tannin": "N/A",
Intensity: N/A "intensity_keyword": "N/A",
"intensity": "N/A"
}
Let's begin! Let's begin!
""" """
header = ["Sweetness_keyword:", "Sweetness:", "Acidity_keyword:", "Acidity:", "Tannin_keyword:", "Tannin:", "Intensity_keyword:", "Intensity:"] requiredKeys = [:sweetness_keyword, :sweetness, :acidity_keyword, :acidity, :tannin_keyword, :tannin, :intensity_keyword, :intensity]
dictkey = ["sweetness_keyword", "sweetness", "acidity_keyword", "acidity", "tannin_keyword", "tannin", "intensity_keyword", "intensity"]
# header = ["Sweetness_keyword:", "Sweetness:", "Acidity_keyword:", "Acidity:", "Tannin_keyword:", "Tannin:", "Intensity_keyword:", "Intensity:"]
# dictkey = ["sweetness_keyword", "sweetness", "acidity_keyword", "acidity", "tannin_keyword", "tannin", "intensity_keyword", "intensity"]
errornote = "N/A" errornote = "N/A"
for attempt in 1:10 for attempt in 1:10
usermsg = context =
""" """
$conversiontable $conversiontable
User's query: $input <query>
$input
</query>
P.S. $errornote P.S. $errornote
/no_think
""" """
_prompt = unformatPrompt =
[ [
Dict(:name=> "system", :text=> systemmsg), Dict(:name=> "system", :text=> systemmsg),
Dict(:name=> "user", :text=> usermsg)
] ]
# put in model format # put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
# add info
prompt = prompt * context
response = a.func[:text2textInstructLLM](prompt) response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id)
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
response = GeneralUtils.remove_french_accents(response)
think, response = GeneralUtils.extractthink(response) think, response = GeneralUtils.extractthink(response)
# check whether response has all answer's key points responsedict = nothing
detected_kw = GeneralUtils.detect_keyword(header, response) try
if 0 values(detected_kw) responsedict = copy(JSON3.read(response))
errornote = "In your previous attempt does not have all answer's key points" catch
println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "In your previous attempt has duplicated answer's key points"
println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
responsedict = GeneralUtils.textToDict(response, header; # check whether all answer's key points are in responsedict
dictKey=dictkey, symbolkey=true) _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_2() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
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_2() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
# check whether each describing keyword is in the input to prevent halucination # check whether each describing keyword is in the input to prevent halucination
for i in ["sweetness", "acidity", "tannin", "intensity"] for i in ["sweetness", "acidity", "tannin", "intensity"]

View File

@@ -194,6 +194,7 @@ function sommelier(
memory = Dict{Symbol, Any}( memory = Dict{Symbol, Any}(
:shortmem=> OrderedDict{Symbol, Any}( :shortmem=> OrderedDict{Symbol, Any}(
:db_search_result=> Any[], :db_search_result=> Any[],
:scratchpad=> "", #[PENDING] should be a dict e.g. Dict(:database_search_result=>Dict(:wines=> "", :search_query=> ""))
), ),
:events=> Vector{Dict{Symbol, Any}}(), :events=> Vector{Dict{Symbol, Any}}(),
:state=> Dict{Symbol, Any}( :state=> Dict{Symbol, Any}(