11 Commits
v0.2.0 ... main

Author SHA1 Message Date
ton
a7da0b8123 Merge pull request 'v0.3.0' (#5) from v0.3.0 into main
Reviewed-on: #5
2025-05-26 00:07:21 +00:00
narawat lamaiin
e524813021 update 2025-05-26 07:05:14 +07:00
narawat lamaiin
3444f00062 update 2025-05-19 21:10:04 +07:00
narawat lamaiin
919d8ec85e update 2025-05-18 17:21:51 +07:00
narawat lamaiin
3a88e0e7d4 update 2025-05-17 21:36:29 +07:00
narawat lamaiin
68c2c2f12b update 2025-05-17 12:18:25 +07:00
narawat lamaiin
3e79c0bfed update 2025-05-16 10:26:50 +07:00
narawat lamaiin
d0c26e52e8 update 2025-05-14 21:21:35 +07:00
narawat lamaiin
a0152a3c29 update 2025-05-04 20:56:17 +07:00
narawat lamaiin
1fc5dfe820 mark new version 2025-05-02 15:27:29 +07:00
ton
4b2575f4a4 Merge pull request 'v0.2.0' (#4) from v0.2.0 into main
Reviewed-on: #4
2025-05-02 08:21:05 +00:00
5 changed files with 1458 additions and 776 deletions

View File

@@ -1,7 +1,7 @@
name = "YiemAgent" name = "YiemAgent"
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
authors = ["narawat lamaiin <narawat@outlook.com>"] authors = ["narawat lamaiin <narawat@outlook.com>"]
version = "0.2.0" version = "0.3.0"
[deps] [deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
module llmfunction module llmfunction
export virtualWineUserChatbox, jsoncorrection, checkinventory, # recommendbox, export virtualWineUserChatbox, jsoncorrection, checkwine, # recommendbox,
virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1,
extractWineAttributes_2, paraphrase extractWineAttributes_2, paraphrase
using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames
using GeneralUtils, SQLLLM using GeneralUtils, SQLLLM
using ..type, ..util using ..type, ..util
@@ -288,23 +288,46 @@ julia> result = checkinventory(agent, input)
# Signature # Signature
""" """
function checkinventory(a::T1, input::T2 function checkwine(a::T1, input::T2; maxattempt::Int=3
) where {T1<:agent, T2<:AbstractString} ) where {T1<:agent, T2<:AbstractString}
println("\ncheckinventory order: $input ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\ncheckinventory order: $input ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
wineattributes_1 = extractWineAttributes_1(a, input) wineattributes_1 = extractWineAttributes_1(a, input)
wineattributes_2 = extractWineAttributes_2(a, input) wineattributes_2 = extractWineAttributes_2(a, input)
_inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" # placeholder
inventoryquery = "Retrieves winery, wine_name, wine_id, vintage, region, country, wine_type, grape, serving_temperature, sweetness, intensity, tannin, acidity, tasting_notes, price and currency of wines that match the following criteria - {$_inventoryquery}" textresult = nothing
println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") rawresponse = nothing
# add suppport for similarSQLVectorDB
textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL], for i in 1:maxattempt
a.func[:text2textInstructLLM];
insertSQLVectorDB=a.func[:insertSQLVectorDB],
similarSQLVectorDB=a.func[:similarSQLVectorDB],
llmFormatName="qwen3")
#CHANGE if you want to add retailer name
# _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2"
_inventoryquery = "$wineattributes_1, $wineattributes_2"
retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency"]
inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}"
println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# add suppport for similarSQLVectorDB
textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL],
a.func[:text2textInstructLLM];
insertSQLVectorDB=a.func[:insertSQLVectorDB],
similarSQLVectorDB=a.func[:similarSQLVectorDB],
llmFormatName="qwen3")
# check if all of retrieve_attributes appears in textresult
isin = [occursin(x, textresult) for x in retrieve_attributes]
# check if rawresponse type is DataFrame so that I can check for column
if typeof(rawresponse) == DataFrame &&
!occursin("The resulting table has 0 row", textresult) &&
!all(isin)
errornote = "Not all of $retrieve_attributes appear in search result"
println("\nERROR YiemAgent checkwine() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
else
break
end
end
println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
println(textresult) println(textresult)
@@ -339,150 +362,210 @@ 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. 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: 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: 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 "NA" to indicate this. - 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.
Additionally, words like 'any' or 'unlimited' mean no information is available. Additionally, words like 'any' or 'unlimited' mean no information is available.
- Do not generate other comments. - Do not generate other comments.
You should then respond to the user with: You should then respond to the user with:
Thought: state your understanding of the current situation 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
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"
Country: a country where the 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"
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 tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc
Tasting_notes: a brief description of the wine's taste, such as "butter", "oak", "fruity", 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: price range of wine. 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
You should only respond in format as described below: You should only respond in JSON format as described below:
Thought: ... {
Wine_name: ... "wine_name": "...",
Winery: ... "winery": "...",
Vintage: ... "vintage": "...",
Region: ... "region": "...",
Country: ... "country": "...",
Wine_type: "wine_type": "...",
Grape_varietal: ... "grape_varietal": "...",
Tasting_notes: ... "tasting_notes": "...",
Wine_price: ... "wine_price_min": "...",
Occasion: ... "wine_price_max": "...",
Food_to_be_paired_with_wine: ... "occasion": "...",
"food_to_be_paired_with_wine": "..."
}
Here are some example: Here are some example:
User's query: red, Chenin Blanc, Riesling, 20 USD
{"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red, white", "grape_varietal": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "0-20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} 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 or Napa Valley",
"country": "Italy or United States",
"wine_type": "red or white",
"grape_varietal": "Chenin Blanc or 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 User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot
{"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Merlot", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} {
"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! Let's begin!
""" """
header = ["Thought:", "Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price:", "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]
dictkey = ["thought", "wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"]
errornote = "N/A" errornote = "N/A"
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.5,
)
for attempt in 1:maxattempt for attempt in 1:maxattempt
#[PENDING] I should add generatequestion()
if attempt > 1
println("\nYiemAgent extractWineAttributes_1() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
usermsg = usermsg =
""" """
User's query: $input $input
P.S. $errornote """
""" context =
"""
<context>
P.S. $errornote
</context>
/no_think
"""
_prompt = unformatPrompt =
[ [
Dict(:name=> "system", :text=> systemmsg), Dict(:name=> "system", :text=> systemmsg),
Dict(:name=> "user", :text=> usermsg) Dict(:name=> "user", :text=> usermsg)
] ]
# put in model format # put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
response = a.func[:text2textInstructLLM](prompt; # add info
modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) prompt = prompt * context
response = GeneralUtils.remove_french_accents(response)
response = GeneralUtils.deFormatLLMtext(response, "granite3")
# check wheter all attributes are in the response response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id)
checkFlag = false response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
for word in header response = GeneralUtils.remove_french_accents(response)
if !occursin(word, response) think, response = GeneralUtils.extractthink(response)
errornote = "In your previous attempts, the $word attribute is missing. Please try again."
println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") responsedict = nothing
checkFlag = true try
break responsedict = copy(JSON3.read(response))
end catch
println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end end
checkFlag == true ? continue : nothing
# 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 !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
# # 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 # # check whether response has all answer's key points
detected_kw = GeneralUtils.detect_keyword(header, response) # detected_kw = GeneralUtils.detect_keyword(header, response)
if 0 values(detected_kw) # if 0 ∈ values(detected_kw)
errornote = "In your previous attempts, the response does not have all answer's key points" # 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())") # println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue # continue
elseif sum(values(detected_kw)) > length(header) # elseif sum(values(detected_kw)) > length(header)
errornote = "In your previous attempts, the response has duplicated answer's key points" # 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("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue # println(response)
# continue
# end
# 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 end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
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)
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 # 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 dictkey 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] != "NA" if responsedict[:wine_price] != "N/A"
# check whether wine_price is in ranged number # check whether wine_price is in ranged number
if !occursin('-', responsedict[:wine_price]) if !occursin("to", responsedict[:wine_price])
errornote = "In your previous attempt, the 'wine_price' was not set to a ranged number. Please adjust it accordingly." errornote = "In your previous attempt, the 'wine_price' was set to $(responsedict[:wine_price]) which is not a correct format. Please adjust it accordingly."
println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
checkFlag = true checkFlag = true
break break
end end
# check whether max wine_price is in the input # # check whether max wine_price is in the input
pricerange = split(responsedict[:wine_price], '-') # pricerange = split(responsedict[:wine_price], '-')
minprice = pricerange[1] # minprice = pricerange[1]
maxprice = pricerange[end] # maxprice = pricerange[end]
if !occursin(maxprice, input) # if !occursin(maxprice, input)
responsedict[:wine_price] = "NA" # responsedict[:wine_price] = "N/A"
end # end
# price range like 100-100 is not good # # price range like 100-100 is not good
if minprice == maxprice # if minprice == maxprice
errornote = "In your previous attempt, you inputted 'wine_price' with a 'minimum' value equaling the 'maximum', which is not valid." # errornote = "In your previous attempt, you inputted 'wine_price' with a 'minimum' value equaling the 'maximum', which is not valid."
println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
checkFlag = true # checkFlag = true
break # break
end # end
end end
else else
content = responsedict[j] content = responsedict[j]
@@ -517,7 +600,7 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
result = "" result = ""
for (k, v) in responsedict for (k, v) in responsedict
# some time LLM generate text with "(some comment)". this line removes it # some time LLM generate text with "(some comment)". this line removes it
if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v) if !occursin("N/A", v) && v != "" && !occursin("none", v) && !occursin("None", v)
result *= "$k: $v, " result *= "$k: $v, "
end end
end end
@@ -540,7 +623,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.
@@ -565,127 +648,151 @@ 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
You must follow the following guidelines: You must follow the following guidelines:
1) If specific information required in the preference form is not available in the query or there isn't any, mark with 'NA' to indicate this. 1) 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.
Additionally, words like 'any' or 'unlimited' mean no information is available. Additionally, words like 'any' or 'unlimited' mean no information is available.
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: NA {
Sweetness: NA "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: NA {
Sweetness: NA "sweetness_keyword": "N/A",
Acidity_keyword: NA "sweetness": "N/A",
Acidity: NA "acidity_keyword": "N/A",
Tannin_keyword: NA "acidity": "N/A",
Tannin: NA "tannin_keyword": "N/A",
Intensity_keyword: NA "tannin": "N/A",
Intensity: NA "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"]
errornote = "" # 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 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, "granite3") 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, "granite3") response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
response = GeneralUtils.remove_french_accents(response)
# check whether response has all answer's key points think, response = GeneralUtils.extractthink(response)
detected_kw = GeneralUtils.detect_keyword(header, response)
if 0 values(detected_kw) responsedict = nothing
errornote = "In your previous attempt does not have all answer's key points" try
println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") responsedict = copy(JSON3.read(response))
continue catch
elseif sum(values(detected_kw)) > length(header) println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
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"]
keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword
value = responsedict[keyword] value = responsedict[keyword]
if value != "NA" && !occursin(value, input) if value != "N/A" && !occursin(value, input)
errornote = "In your previous attempt, keyword $keyword: $value does not appear in the input. You must use information from the input only" errornote = "In your previous attempt, keyword $keyword: $value does not appear in the input. You must use information from the input only"
println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
end end
# if value == "NA" then responsedict[i] = "NA" # if value == "N/A" then responsedict[i] = "N/A"
# e.g. if sweetness_keyword == "NA" then sweetness = "NA" # e.g. if sweetness_keyword == "N/A" then sweetness = "N/A"
if value == "NA" if value == "N/A"
responsedict[Symbol(i)] = "NA" responsedict[Symbol(i)] = "N/A"
end end
end end
# some time LLM not put integer range # some time LLM not put integer range
for (k, v) in responsedict for (k, v) in responsedict
if !occursin("keyword", string(k)) if !occursin("keyword", string(k))
if v !== "NA" && (!occursin('-', v) || length(v) > 5) if v !== "N/A" && (!occursin('-', v) || length(v) > 5)
errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, i.e. min-max." errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, i.e. min-max."
println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue continue
@@ -693,18 +800,25 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
end end
end end
# some time LLM says NA-2. Need to convert NA to 1 # some time LLM says N/A-2. Need to convert N/A to 1
for (k, v) in responsedict for (k, v) in responsedict
if occursin("NA", v) && occursin("-", v) if occursin("N/A", v) && occursin("-", v)
new_v = replace(v, "NA"=>"1") new_v = replace(v, "N/A"=>"1")
responsedict[k] = new_v responsedict[k] = new_v
end end
end end
# delete some key words from responsedict
for (k, v) in responsedict
if k [:sweetness_keyword, :acidity_keyword, :tannin_keyword, :intensity_keyword]
delete!(responsedict, k)
end
end
result = "" result = ""
for (k, v) in responsedict for (k, v) in responsedict
# some time LLM generate text with "(some comment)". this line removes it # some time LLM generate text with "(some comment)". this line removes it
if !occursin("NA", v) if !occursin("N/A", v)
result *= "$k: $v, " result *= "$k: $v, "
end end
end end
@@ -752,11 +866,12 @@ function paraphrase(text2textInstructLLM::Function, text::String)
Let's begin! 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:"] header = ["Paraphrase:"]
dictkey = ["paraphrase"] dictkey = ["paraphrase"]
errornote = "" errornote = "N/A"
response = nothing # placeholder for show when error msg show up response = nothing # placeholder for show when error msg show up
@@ -773,11 +888,12 @@ function paraphrase(text2textInstructLLM::Function, text::String)
] ]
# put in model format # put in model format
prompt = GeneralUtils.formatLLMtext(_prompt, "granite3") prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)
try try
response = text2textInstructLLM(prompt) response = text2textInstructLLM(prompt)
response = GeneralUtils.deFormatLLMtext(response, "granite3") response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
think, response = GeneralUtils.extractthink(response)
# sometime the model response like this "here's how I would respond: ..." # sometime the model response like this "here's how I would respond: ..."
if occursin("respond:", response) if occursin("respond:", response)
errornote = "You don't need to intro your response" errornote = "You don't need to intro your response"

View File

@@ -1,6 +1,6 @@
module type module type
export agent, sommelier, companion export agent, sommelier, companion, virtualcustomer
using Dates, UUIDs, DataStructures, JSON3 using Dates, UUIDs, DataStructures, JSON3
using GeneralUtils using GeneralUtils
@@ -24,17 +24,13 @@ end
function companion( function companion(
func::NamedTuple # NamedTuple of functions func::NamedTuple # NamedTuple of functions
; ;
systemmsg::Union{String, Nothing}= nothing,
name::String= "Assistant", name::String= "Assistant",
id::String= GeneralUtils.uuid4snakecase(), id::String= GeneralUtils.uuid4snakecase(),
maxHistoryMsg::Integer= 20, maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(),
llmFormatName::String= "granite3" llmFormatName::String= "granite3",
) systemmsg::String=
"""
if systemmsg === nothing
systemmsg =
"""
Your name: $name Your name: $name
Your sex: Female Your sex: Female
Your role: You are a helpful assistant. Your role: You are a helpful assistant.
@@ -43,8 +39,8 @@ function companion(
- Your like to be short and concise. - Your like to be short and concise.
Let's begin! Let's begin!
""" """,
end )
tools = Dict( # update input format tools = Dict( # update input format
"CHATBOX"=> Dict( "CHATBOX"=> Dict(
@@ -197,8 +193,83 @@ function sommelier(
""" """
memory = Dict{Symbol, Any}( memory = Dict{Symbol, Any}(
:shortmem=> OrderedDict{Symbol, Any}( :shortmem=> OrderedDict{Symbol, Any}(
:available_wine=> [], :db_search_result=> Any[],
:found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines :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}(
),
:recap=> OrderedDict{Symbol, Any}(),
)
newAgent = sommelier(
name,
id,
retailername,
tools,
maxHistoryMsg,
chathistory,
memory,
func,
llmFormatName
)
return newAgent
end
mutable struct virtualcustomer <: agent
name::String # agent name
id::String # agent id
systemmsg::String # system message
tools::Dict
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
chathistory::Vector{Dict{Symbol, Any}}
memory::Dict{Symbol, Any}
func # NamedTuple of functions
llmFormatName::String
end
function virtualcustomer(
func, # NamedTuple of functions
;
name::String= "Assistant",
id::String= string(uuid4()),
maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(),
llmFormatName::String= "granite3",
systemmsg::String=
"""
Your name: $name
Your sex: Female
Your role: You are a helpful assistant.
You should follow the following guidelines:
- Focus on the latest conversation.
- Your like to be short and concise.
Let's begin!
""",
)
tools = Dict( # update input format
"chatbox"=> Dict(
:description => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
:input => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
:output => "" ,
),
)
""" Memory
Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
NO "system" message in chathistory because I want to add it at the inference time
chathistory= [
Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()),
Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()),
]
"""
memory = Dict{Symbol, Any}(
:shortmem=> OrderedDict{Symbol, Any}(
), ),
:events=> Vector{Dict{Symbol, Any}}(), :events=> Vector{Dict{Symbol, Any}}(),
:state=> Dict{Symbol, Any}( :state=> Dict{Symbol, Any}(
@@ -206,10 +277,10 @@ function sommelier(
:recap=> OrderedDict{Symbol, Any}(), :recap=> OrderedDict{Symbol, Any}(),
) )
newAgent = sommelier( newAgent = virtualcustomer(
name, name,
id, id,
retailername, systemmsg,
tools, tools,
maxHistoryMsg, maxHistoryMsg,
chathistory, chathistory,

View File

@@ -1,7 +1,7 @@
module util module util
export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText availableWineToText, createEventsLog, createChatLog
using UUIDs, Dates, DataStructures, HTTP, JSON3 using UUIDs, Dates, DataStructures, HTTP, JSON3
using GeneralUtils using GeneralUtils
@@ -297,22 +297,25 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin
1:length(events) 1:length(events)
end end
# Iterate through events and format each one #[WORKING] Iterate through events and format each one
for (i, event) in zip(ind, events) for i in ind
event = events[i]
# If no outcome exists, format without outcome # If no outcome exists, format without outcome
if event[:outcome] === nothing # if event[:actionname] == "CHATBOX"
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput])\n" # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n"
# elseif event[:actionname] == "CHECKINVENTORY" && event[:outcome] === nothing
# 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 # If outcome exists, include it in formatting
if event[:actionname] == "CHECKWINE"
timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n"
else else
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n" timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n"
end end
end end
# Return formatted timeline string # Return formatted timeline string
return timeline return timeline
end end
# function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing # function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
# ) where {T1<:AbstractVector} # ) where {T1<:AbstractVector}
# # Initialize empty timeline string # # Initialize empty timeline string
@@ -327,15 +330,14 @@ end
# end # end
# # Iterate through events and format each one # # Iterate through events and format each one
# for (i, event) in zip(ind, events) # for i in ind
# event = events[i]
# # If no outcome exists, format without outcome # # If no outcome exists, format without outcome
# subject = titlecase(event[:subject])
# if event[:outcome] === nothing # if event[:outcome] === nothing
# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n"
# timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput])\n"
# # If outcome exists, include it in formatting # # If outcome exists, include it in formatting
# else # else
# timeline *= "Event_$i) Who: $subject Action_name: $(event[:actionname]) Action_input: $(event[:actioninput]) Action output: $(event[:outcome])\n" # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n"
# end # end
# end # end
@@ -344,6 +346,73 @@ end
# end # end
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 index !== nothing
[index...]
else
1:length(events)
end
# Iterate through events and format each one
for i in ind
event = events[i]
# If no outcome exists, format without outcome
if event[:outcome] === nothing
subject = event[:subject]
actioninput = event[:actioninput]
d = Dict{Symbol, String}(:name=>subject, :text=>actioninput)
push!(log, d)
else
subject = event[:subject]
actioninput = event[:actioninput]
outcome = event[:outcome]
str = "Action: $actioninput Outcome: $outcome"
d = Dict{Symbol, String}(:name=>subject, :text=>str)
push!(log, d)
end
end
return log
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 in ind
event = chatdict[i]
subject = event[:name]
text = event[:text]
d = Dict{Symbol, String}(:name=>subject, :text=>text)
push!(log, d)
end
return log
end