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
"""
function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10
function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
) where {T<:agent}
# lessonDict = copy(JSON3.read("lesson.json"))
@@ -133,13 +133,8 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10
includelatest=true)
recentchat = createChatLog(a.chathistory; index=recentchat_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
# # query similar result from vectorDB
# recapkeys = keys(a.memory[:recap])
@@ -215,7 +210,7 @@ function decisionMaker(a::T; recentevents::Integer=10, maxattempt=10
- Vintage 0 means non-vintage.
</Store guidelines>
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:
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>
<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.
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.
- 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>
$(a.memory[:shortmem][:scratchpad])
Remark: $errornote
Database search result: $database_search_result
</context>
"""
@@ -1123,22 +1118,22 @@ julia>
"""
function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent}
# a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0)
thoughtDict = decisionMaker(a; recentevents=5)
thoughtDict = decisionMaker(a)
actionname = thoughtDict[:action_name]
actioninput = thoughtDict[:action_input]
# map action and input() to llm function
response =
if actionname == "CHATBOX"
if actionname == "CHATBOX" || actionname == "ENDCONVERSATION"
(result=thoughtDict[:plan], errormsg=nothing, success=true)
elseif actionname == "CHECKINVENTORY"
checkinventory(a, actioninput)
elseif actionname == "PRESENTBOX"
(result=actioninput, errormsg=nothing, success=true)
elseif actionname == "ENDCONVERSATION"
x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time."
(result=actioninput, errormsg=nothing, success=true)
# elseif actionname == "ENDCONVERSATION"
# x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time."
# (result=actioninput, errormsg=nothing, success=true)
else
error("undefined LLM function. Requesting $actionname")
end
@@ -1166,7 +1161,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
# )
# )
# result = chatresponse
if actionname ["CHATBOX"]
if actionname ["CHATBOX", "ENDCONVERSATION"]
push!(a.memory[:events],
eventdict(;
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
elseif actionname ["ENDCONVERSATION"]
chatresponse = generatechat(a, thoughtDict)
push!(a.memory[:events],
eventdict(;
event_description="the assistant talks to the user.",
timestamp=Dates.now(),
subject="assistant",
thought=thoughtDict,
actionname=actionname,
actioninput=chatresponse,
)
)
result = chatresponse
# elseif actionname ∈ ["ENDCONVERSATION"]
# chatresponse = generatechat(a, thoughtDict)
# push!(a.memory[:events],
# eventdict(;
# event_description="the assistant talks to the user.",
# timestamp=Dates.now(),
# subject="assistant",
# thought=thoughtDict,
# actionname=actionname,
# actioninput=chatresponse,
# )
# )
# result = chatresponse
elseif actionname ["PRESENTBOX"]
chatresponse = presentbox(a, thoughtDict)
push!(a.memory[:events],
@@ -1215,6 +1210,21 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
else
a.memory[:shortmem][:db_search_result] = vd
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
push!(a.memory[:events],
@@ -1224,7 +1234,7 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
subject= "assistant",
thought=thoughtDict,
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"
)
)
@@ -1296,7 +1306,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev
"""
<context>
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
</context>
"""

View File

@@ -500,16 +500,24 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
# responsedict = GeneralUtils.textToDict(response, header;
# 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, :tasting_notes)
delete!(responsedict, :occasion)
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
checkFlag = false
for i in requiredKeys
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
if j == :wine_price
if responsedict[:wine_price] != "N/A"
@@ -592,7 +600,7 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
conversiontable =
"""
<Conversion Table>
<conversion_table>
Intensity level:
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.
@@ -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.
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.
</Conversion Table>
</conversion_table>
"""
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.
At each round of conversation, the user will give you the current situation:
Conversion Table: ...
User's query: ...
At each round of conversation, you will be given the following information:
conversion_table: a conversion table that maps descriptive words to their corresponding integer levels
query: the words from the user's query that describe their preferences
The preference form requires the following information:
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.
3) Do not generate other comments.
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: ( 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: ( 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: ( 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: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4
You should only respond in format as described below:
Sweetness_keyword: ...
Sweetness: ...
Acidity_keyword: ...
Acidity: ...
Tannin_keyword: ...
Tannin: ...
Intensity_keyword: ...
Intensity: ...
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
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
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
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
You should only respond in JSON format as described below:
{
"sweetness_keyword": "...",
"sweetness": "...",
"acidity_keyword": "...",
"acidity": "...",
"tannin_keyword": "...",
"tannin": "...",
"intensity_keyword": "...",
"intensity": "..."
}
Here are some examples:
User's query: I want a wine with a medium-bodied, low acidity, medium tannin.
Sweetness_keyword: N/A
Sweetness: N/A
Acidity_keyword: low acidity
Acidity: 1-2
Tannin_keyword: medium tannin
Tannin: 3-4
Intensity_keyword: medium-bodied
Intensity: 3-4
{
"sweetness_keyword": "N/A",
"sweetness": "N/A",
"acidity_keyword": "low acidity",
"acidity": 1-2,
"tannin_keyword": "medium tannin",
"tannin": 3-4,
"intensity_keyword": "medium-bodied",
"intensity": 3-4
}
User's query: German red wine, under 100, pairs with spicy food
Sweetness_keyword: N/A
Sweetness: N/A
Acidity_keyword: N/A
Acidity: N/A
Tannin_keyword: N/A
Tannin: N/A
Intensity_keyword: N/A
Intensity: N/A
{
"sweetness_keyword": "N/A",
"sweetness": "N/A",
"acidity_keyword": "N/A",
"acidity": "N/A",
"tannin_keyword": "N/A",
"tannin": "N/A",
"intensity_keyword": "N/A",
"intensity": "N/A"
}
Let's begin!
"""
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"]
requiredKeys = [: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"
for attempt in 1:10
usermsg =
context =
"""
$conversiontable
User's query: $input
<query>
$input
</query>
P.S. $errornote
/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 * context
response = a.func[:text2textInstructLLM](prompt)
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 answer's key points
detected_kw = GeneralUtils.detect_keyword(header, response)
if 0 values(detected_kw)
errornote = "In your previous attempt does not have all answer's key points"
println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__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())")
responsedict = nothing
try
responsedict = copy(JSON3.read(response))
catch
println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
# 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_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
for i in ["sweetness", "acidity", "tannin", "intensity"]

View File

@@ -194,6 +194,7 @@ function sommelier(
memory = Dict{Symbol, Any}(
:shortmem=> OrderedDict{Symbol, 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}}(),
:state=> Dict{Symbol, Any}(