24 Commits

Author SHA1 Message Date
bfadd53033 update 2025-03-21 10:03:08 +07:00
8fc3afe348 update 2025-03-20 16:15:38 +07:00
c60037226a update 2025-03-13 19:11:20 +07:00
narawat lamaiin
db6c9c5f2b update 2025-03-07 13:34:15 +07:00
narawat lamaiin
6504099959 update 2025-01-31 09:50:44 +07:00
724b092bdb update 2025-01-30 21:28:49 +07:00
c56c3d02b0 update 2025-01-29 12:16:01 +07:00
ton
a7f3e29e9c Merge pull request 'WIP v0.1.2-dev' (#1) from v0.1.2-dev into main
Reviewed-on: #1
2025-01-25 07:30:18 +00:00
narawat lamaiin
b8fc23b41e update 2025-01-25 14:21:37 +07:00
narawat lamaiin
cf4cd13b14 update 2025-01-25 13:31:23 +07:00
narawat lamaiin
29adc077d5 update 2025-01-23 19:34:13 +07:00
narawat lamaiin
d89d425885 update 2025-01-21 08:28:26 +07:00
narawat lamaiin
bb81b973d3 update 2025-01-20 18:19:38 +07:00
narawat lamaiin
4197625e57 update 2025-01-17 22:09:48 +07:00
narawat lamaiin
3fdc0adf99 update 2025-01-16 07:40:39 +07:00
narawat lamaiin
c7000f66b8 update 2025-01-15 08:35:25 +07:00
narawat lamaiin
2206831bab update 2025-01-15 06:13:18 +07:00
narawat lamaiin
a29e8049a7 update 2025-01-11 16:57:57 +07:00
narawat lamaiin
944d9eaf2b update 2025-01-10 18:08:21 +07:00
narawat lamaiin
616c159336 update 2025-01-10 08:06:01 +07:00
narawat lamaiin
022cb5caf0 update 2025-01-05 17:41:21 +07:00
cff0d31ae6 update 2025-01-04 16:10:23 +07:00
82167fe006 update 2025-01-04 16:07:18 +07:00
814a0ecc6a update 2024-12-27 20:53:15 +07:00
9 changed files with 1367 additions and 1321 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.1.1" version = "0.1.3"
[deps] [deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
@@ -22,6 +22,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat] [compat]
DataFrames = "1.7.0" DataFrames = "1.7.0"
GeneralUtils = "0.1.0" GeneralUtils = "0.1, 0.2"
LLMMCTS = "0.1.2" LLMMCTS = "0.1.2"
SQLLLM = "0.2.0" SQLLLM = "0.2.0"

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
module llmfunction module llmfunction
export virtualWineUserChatbox, jsoncorrection, checkinventory, # recommendbox, export virtualWineUserChatbox, jsoncorrection, checkinventory, # recommendbox,
virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1 virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1,
extractWineAttributes_2, paraphrase
using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates
using GeneralUtils, SQLLLM using GeneralUtils, SQLLLM
@@ -290,20 +291,20 @@ julia> result = checkinventory(agent, input)
function checkinventory(a::T1, input::T2 function checkinventory(a::T1, input::T2
) where {T1<:agent, T2<:AbstractString} ) where {T1<:agent, T2<:AbstractString}
println("\n~~~ checkinventory order: $input ", @__FILE__, " ", @__LINE__) println("\n~~~ checkinventory order: $input ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
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" _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2"
inventoryquery = "Retrieves winery, wine_name, 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}" inventoryquery = "Retrieves winery, wine_name, 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}"
println("~~~ checkinventory input: $inventoryquery ", @__FILE__, " ", @__LINE__) println("~~~ checkinventory input: $inventoryquery ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
# add suppport for similarSQLVectorDB # add suppport for similarSQLVectorDB
textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL], textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL],
a.func[:text2textInstructLLM], a.func[:text2textInstructLLM],
insertSQLVectorDB=a.func[:insertSQLVectorDB], insertSQLVectorDB=a.func[:insertSQLVectorDB],
similarSQLVectorDB=a.func[:similarSQLVectorDB]) similarSQLVectorDB=a.func[:similarSQLVectorDB])
println("\n~~~ checkinventory result ", @__FILE__, " ", @__LINE__) println("\n~~~ checkinventory result ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
println(textresult) println(textresult)
return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing)
@@ -325,234 +326,67 @@ julia>
# TODO # TODO
- [] update docstring - [] update docstring
- [x] implement the function - implement the function
# Signature # Signature
""" """
# function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
# systemmsg =
# """
# 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 current situation:
# User's query: ...
# 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.
# Additionally, words like 'any' or 'unlimited' mean no information is available.
# 2) Do not generate other comments.
# You should then respond to the user with the following points:
# - reasoning: state your understanding of the current situation
# - wine_name: name of the wine
# - winery: name of the winery
# - vintage: the year of the wine
# - region: a region in a country where the wine is produced, such as Burgundy, Napa Valley, etc
# - 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"
# - grape_variety: the name of the primary grape used to make the wine
# - tasting_notes: a brief description of the wine's taste, such as "butter", "oak", "fruity", etc
# - wine_price: price of wine. For example, up to 100, less than 100, 20 to 100, 30-79.95
# - 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 the user's preference form as described below:
# reasoning: ...
# winery: ...
# wine_name: ...
# vintage: ...
# region: ...
# country: ...
# wine_type: ...
# grape_variety: ...
# tasting_notes: ...
# wine_price: ...
# occasion: ...
# food_to_be_paired_with_wine: ...
# Here are some example:
# User's query: red, Chenin Blanc, Riesling, under 20
# reasoning: ...
# winery: NA
# wine_name: NA
# vintage: NA
# region: NA
# country: NA
# wine_type: red
# grape_variety: Chenin Blanc, Riesling
# tasting_notes: NA
# wine_price: under 20
# occasion: NA
# food_to_be_paired_with_wine: NA
# User's query: Domaine du Collier Saumur Blanc 2019, France, white, Chenin Blanc
# reasoning: ...
# winery: Domaine du Collier
# wine_name: Saumur Blanc
# vintage: 2019
# region: Saumur
# country: France
# wine_type: white
# grape_variety: Chenin Blanc
# tasting_notes: NA
# wine_price: 109
# occasion: NA
# food_to_be_paired_with_wine: NA
# Let's begin!
# """
# attributes = ["reasoning", "winery", "wine_name", "vintage", "region", "country", "wine_type", "grape_variety", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"]
# errornote = ""
# maxattempt = 5
# for attempt in 1:maxattempt
# usermsg =
# """
# User's query: $input
# $errornote
# """
# _prompt =
# [
# Dict(:name=> "system", :text=> systemmsg),
# Dict(:name=> "user", :text=> usermsg)
# ]
# # put in model format
# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct")
# prompt *=
# """
# <|start_header_id|>assistant<|end_header_id|>
# """
# try
# response = a.func[:text2textInstructLLM](prompt)
# response = GeneralUtils.remove_french_accents(response)
# # check wheter all attributes are in the response
# for word in attributes
# if !occursin(word, response)
# error("$word attribute is missing")
# end
# end
# responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true)
# for i ∈ attributes
# if length(JSON3.write(responsedict[Symbol(i)])) == 0
# error("$i is empty ", @__LINE__)
# end
# end
# #check if the following attributes has more than 1 name
# # responsedict[:grape_variety] = split(responsedict[:grape_variety], ',')[1]
# # responsedict[:grape_variety] = split(responsedict[:grape_variety], '/')[1]
# responsedict[:country] = split(responsedict[:country], ',')[1]
# responsedict[:country] = split(responsedict[:country], '/')[1]
# responsedict[:region] = split(responsedict[:region], ',')[1]
# responsedict[:region] = split(responsedict[:region], '/')[1]
# delete!(responsedict, :reasoning)
# delete!(responsedict, :tasting_notes)
# delete!(responsedict, :occasion)
# delete!(responsedict, :food_to_be_paired_with_wine)
# # check if winery, wine_name, region, country, wine_type, grape_variety are in the query because sometime AI halucinates
# for i in [:grape_variety, :winery, :wine_name, :region]
# result = check_key_in_input(input, responsedict, attempt, maxattempt, i)
# if result === nothing
# # nothing wrong
# elseif result == "NA"
# responsedict[i] = "NA"
# else
# errornote = result
# error(errornote)
# end
# end
# # remove (some text)
# for (k, v) in responsedict
# _v = replace(v, r"\(.*?\)" => "")
# responsedict[k] = _v
# end
# result = ""
# for (k, v) in responsedict
# # some time LLM generate text with "(some comment)". this line removes it
# if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v)
# result *= "$k: $v, "
# end
# end
# #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral"
# result = result[1:end-2] # remove the ending ", "
# return result
# catch e
# io = IOBuffer()
# showerror(io, e)
# errorMsg = String(take!(io))
# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
# println("")
# println("Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__)
# println("")
# end
# end
# error("wineattributes_wordToNumber() failed to get a response")
# end
function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
systemmsg = systemmsg =
""" """
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 current situation: At each round of conversation, the user will give you the following:
User's query: ... User's query: ...
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. - 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.
Additionally, words like 'any' or 'unlimited' mean no information is available. Additionally, words like 'any' or 'unlimited' mean no information is available.
2) Do not generate other comments. - Do not generate other comments.
You should then respond to the user with the following points: You should then respond to the user with:
- reasoning: state your understanding of the current situation Comprehension: 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 (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc Region: a region (NOT a country) where the wine is produced, such as Burgundy, Napa Valley, etc
- country: a country where the 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_variety: 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 brief description of the wine's taste, such as "butter", "oak", "fruity", etc Tasting_notes: a brief description of the wine's taste, such as "butter", "oak", "fruity", etc
- wine_price: price of wine. For example, up to 100, less than 100, 20 to 100, 30-79.95 Wine_price: price range of wine.
- 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 the user's preference form (JSON) as described below: Comprehension: ...
{"reasoning": ..., "winery": ..., "wine_name": ..., "vintage": ..., "region": ..., "country": ..., "wine_type": ..., "grape_variety": ..., "tasting_notes": ..., "wine_price": ..., "occasion": ..., "food_to_be_paired_with_wine": ...} Wine_name: ...
Winery: ...
Vintage: ...
Region: ...
Country: ...
Wine_type:
Grape_varietal: ...
Tasting_notes: ...
Wine_price: ...
Occasion: ...
Food_to_be_paired_with_wine: ...
Here are some example: Here are some example:
User's query: red, Chenin Blanc, Riesling, under 20 User's query: red, Chenin Blanc, Riesling, 20 USD
{"reasoning": ..., "winery": "NA", "wine_name": "NA", "vintage": "NA", "region": "NA", "country": "NA", "wine_type": "red", "grape_variety": "Chenin Blanc, Riesling", "tasting_notes": "NA", "wine_price": "under 20", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} {"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: Domaine du Collier Saumur Blanc 2019, France, white, Chenin Blanc User's query: Domaine du Collier Saumur Blanc 2019, France, white, Chenin Blanc
{"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_variety": "Chenin Blanc", "tasting_notes": "NA", "wine_price": "109", "occasion": "NA", "food_to_be_paired_with_wine": "NA"} {"reasoning": ..., "winery": "Domaine du Collier", "wine_name": "Saumur Blanc", "vintage": "2019", "region": "Saumur", "country": "France", "wine_type": "white", "grape_varietal": "Chenin Blanc", "tasting_notes": "NA", "wine_price": "NA", "occasion": "NA", "food_to_be_paired_with_wine": "NA"}
Let's begin! Let's begin!
""" """
attributes = ["reasoning", "winery", "wine_name", "vintage", "region", "country", "wine_type", "grape_variety", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"] header = ["Comprehension:", "Wine_name:", "Winery:", "Vintage:", "Region:", "Country:", "Wine_type:", "Grape_varietal:", "Tasting_notes:", "Wine_price:", "Occasion:", "Food_to_be_paired_with_wine:"]
dictkey = ["comprehension", "wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price", "occasion", "food_to_be_paired_with_wine"]
errornote = "" errornote = ""
maxattempt = 5
for attempt in 1:maxattempt
for attempt in 1:5
usermsg = usermsg =
""" """
User's query: $input User's query: $input
@@ -566,76 +400,116 @@ function extractWineAttributes_1(a::T1, input::T2)::String where {T1<:agent, T2<
] ]
# put in model format # put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct") prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen")
prompt *= response = a.func[:text2textInstructLLM](prompt)
""" response = GeneralUtils.remove_french_accents(response)
<|start_header_id|>assistant<|end_header_id|>
"""
try # check wheter all attributes are in the response
response = a.func[:text2textInstructLLM](prompt) checkFlag = false
response = GeneralUtils.remove_french_accents(response) for word in header
if !occursin(word, response)
# check wheter all attributes are in the response errornote = "$word attribute is missing in previous attempts"
for word in attributes println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
if !occursin(word, response) checkFlag = true
error("$word attribute is missing") break
end
end end
end
checkFlag == true ? continue : nothing
responsedict = copy(JSON3.read(response)) # check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response)
if sum(values(detected_kw)) < length(header)
errornote = "\nYiemAgent extractWineAttributes_1() response does not have all header"
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "\nYiemAgent extractWineAttributes_1() response has duplicated header"
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
delete!(responsedict, :reasoning) delete!(responsedict, :comprehension)
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)
# check if winery, wine_name, region, country, wine_type, grape_variety are in the query because sometime AI halucinates println(@__FILE__, " ", @__LINE__)
for i in [:grape_variety, :winery, :wine_name, :region] pprintln(responsedict)
content = responsedict[i]
if occursin(",", content) # check if winery, wine_name, region, country, wine_type, grape_varietal's value are in the query because sometime AI halucinates
content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" checkFlag = false
content = strip.(content) for i in dictkey
j = Symbol(i)
if j [:comprehension, :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
if j == :wine_price
if responsedict[:wine_price] != "NA"
# check whether wine_price is in ranged number
if !occursin('-', responsedict[:wine_price])
errornote = "wine_price must be a range number"
println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
checkFlag = true
break
end
# check whether max wine_price is in the input
pricerange = split(responsedict[:wine_price], '-')
minprice = pricerange[1]
maxprice = pricerange[end]
if !occursin(maxprice, input)
responsedict[:wine_price] = "NA"
end
# price range like 100-100 is not good
if minprice == maxprice
errornote = "wine_price with minimum equals to maximum is not valid"
println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
checkFlag = true
break
end
end
else else
content = [content] content = responsedict[j]
end if typeof(content) <: AbstractVector
content = strip.(content)
elseif occursin(',', content)
content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling"
content = strip.(content)
else
content = [content]
end
for x in content for x in content #check whether price are mentioned in the input
if !occursin("NA", responsedict[i]) && !occursin(x, input) if !occursin("NA", responsedict[j]) && !occursin(x, input)
errornote = "$x is not mentioned in the user query, you must only use the info from the query." errornote = "$x is not mentioned in the user query, you must only use the info from the query."
error(errornote) println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
checkFlag == true
break
end
end end
end end
end end
# remove (some text)
for (k, v) in responsedict
_v = replace(v, r"\(.*?\)" => "")
responsedict[k] = _v
end
result = ""
for (k, v) in responsedict
# some time LLM generate text with "(some comment)". this line removes it
if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v)
result *= "$k: $v, "
end
end
#[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral"
result = result[1:end-2] # remove the ending ", "
return result
catch e
io = IOBuffer()
showerror(io, e)
errorMsg = String(take!(io))
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
println("")
println("Attempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, " ", @__LINE__)
println("")
end end
checkFlag == true ? continue : nothing # skip the rest code if true
# remove (some text)
for (k, v) in responsedict
_v = replace(v, r"\(.*?\)" => "")
responsedict[k] = _v
end
result = ""
for (k, v) in responsedict
# some time LLM generate text with "(some comment)". this line removes it
if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v)
result *= "$k: $v, "
end
end
#[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_varietal: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral"
result = result[1:end-2] # remove the ending ", "
return result
end end
error("wineattributes_wordToNumber() failed to get a response") error("wineattributes_wordToNumber() failed to get a response")
end end
@@ -643,12 +517,13 @@ end
""" """
# TODO # TODO
- [PENDING] "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve. - [PENDING] "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve.
- [PENDING] French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why?
""" """
function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
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.
@@ -673,10 +548,9 @@ 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>
""" """
# chathistory = vectorOfDictToText(a.chathistory)
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.
@@ -688,106 +562,241 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
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 'NA' 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 must follow the following guidelines>
You should then respond to the user with the following points: <You should then respond to the user with>
- reasoning: State your understanding of the current situation 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: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5 Acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine.
- tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3 Acidity: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5
- intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4 Tannin_keyword: The exact keywords in the user's query describing the tannin level of the wine.
- notes: Anything you want to add 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 then respond to the user with>
You should only respond in the form as described below: <You should only respond in format as described below>
reasoning: ... Sweetness_keyword: ...
sweetness: ... Sweetness: ...
acidity: ... Acidity_keyword: ...
tannin: ... Acidity: ...
intensity: ... Tannin_keyword: ...
notes: ... Tannin: ...
Intensity_keyword: ...
Intensity: ...
</You should only respond in format as described below>
<Here are some examples>
User's query: I want a wine with a medium-bodied, low acidity, medium tannin.
Sweetness_keyword: NA
Sweetness: NA
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: NA
Sweetness: NA
Acidity_keyword: NA
Acidity: NA
Tannin_keyword: NA
Tannin: NA
Intensity_keyword: NA
Intensity: NA
</Here are some examples>
Let's begin! 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"]
errornote = ""
# chathistory = vectorOfDictToText(a.chathistory) for attempt in 1:10
usermsg =
usermsg =
"""
$conversiontable
User's query: $input
"""
_prompt =
[
Dict(:name=> "system", :text=> systemmsg),
Dict(:name=> "user", :text=> usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct")
prompt *=
""" """
<|start_header_id|>assistant<|end_header_id|> $conversiontable
User's query: $input
$errornote
""" """
attributes = ["reasoning", "sweetness", "acidity", "tannin", "intensity", "notes"] _prompt =
[
Dict(:name=> "system", :text=> systemmsg),
Dict(:name=> "user", :text=> usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="qwen")
response = a.func[:text2textInstructLLM](prompt)
# check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response)
if sum(values(detected_kw)) < length(header)
errornote = "\nYiemAgent extractWineAttributes_2() response does not have all header"
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "\nYiemAgent extractWineAttributes_2() response has duplicated header"
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
# check whether each describing keyword is in the input to prevent halucination
for i in ["sweetness", "acidity", "tannin", "intensity"]
keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword
value = responsedict[keyword]
if value != "NA" && !occursin(value, input)
errornote = "WARNING. Keyword $keyword: $value does not appear in the input. You must use information from the input only"
println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
continue
end
# if value == "NA" then responsedict[i] = "NA"
# e.g. if sweetness_keyword == "NA" then sweetness = "NA"
if value == "NA"
responsedict[Symbol(i)] = "NA"
end
end
# some time LLM not put integer range
for (k, v) in responsedict
if !occursin("keyword", string(k))
if v !== "NA" && (!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."
println("Attempt $attempt $errornote ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
continue
end
end
end
# some time LLM says NA-2. Need to convert NA to 1
for (k, v) in responsedict
if occursin("NA", v) && occursin("-", v)
new_v = replace(v, "NA"=>"1")
responsedict[k] = new_v
end
end
result = ""
for (k, v) in responsedict
# some time LLM generate text with "(some comment)". this line removes it
if !occursin("NA", v)
result *= "$k: $v, "
end
end
result = result[1:end-2] # remove the ending ", "
return result
end
error("wineattributes_wordToNumber() failed to get a response")
end
function paraphrase(text2textInstructLLM::Function, text::String)
systemmsg =
"""
Your name: N/A
Your vision:
- You are a helpful assistant who help the user to paraphrase their text.
Your mission:
- To help paraphrase the user's text
Mission's objective includes:
- To help paraphrase the user's text
Your responsibility includes:
1) To help paraphrase the user's text
Your responsibility does NOT includes:
1) N/A
Your profile:
- N/A
Additional information:
- N/A
At each round of conversation, you will be given the following information:
Text: The user's given text
You MUST follow the following guidelines:
- N/A
You should follow the following guidelines:
- N/A
You should then respond to the user with:
1) Paraphrase: Paraphrased text
You should only respond in format as described below:
Paraphrase: ...
Let's begin!
"""
errornote = ""
response = nothing # placeholder for show when error msg show up
for attempt in 1:10
usermsg = """
Text: $text
$errornote
"""
_prompt =
[
Dict(:name => "system", :text => systemmsg),
Dict(:name => "user", :text => usermsg)
]
# put in model format
prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct")
prompt *= """
<|start_header_id|>assistant<|end_header_id|>
"""
for attempt in 1:5
try try
response = a.func[:text2textInstructLLM](prompt) response = text2textInstructLLM(prompt)
responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true) # sometime the model response like this "here's how I would respond: ..."
if occursin("respond:", response)
errornote = "You don't need to intro your response"
error("\n~~~ paraphrase() response contain : ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
end
response = GeneralUtils.remove_french_accents(response)
response = replace(response, '*'=>"")
response = replace(response, '$' => "USD")
response = replace(response, '`' => "")
response = GeneralUtils.remove_french_accents(response)
for i attributes header = ["Paraphrase:"]
if length(JSON3.write(responsedict[Symbol(i)])) == 0 dictkey = ["paraphrase"]
error("$i is empty ", @__LINE__) responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
for i [:paraphrase]
if length(JSON3.write(responsedict[i])) == 0
error("$i is empty ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
end end
end end
delete!(responsedict, :reasoning) # check if there are more than 1 key per categories
delete!(responsedict, :notes) # LLM traps. so it can add useless info here like comments. for i [:paraphrase]
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
# some time LLM think the user mentioning acidity and tannin but actually didn't if length(matchkeys) > 1
for (k, v) in responsedict error("paraphrase() has more than one key per categories")
if k [:acidity, :tannin] && !occursin(string(k), input)
responsedict[k] = "NA"
end end
end end
# remove (some text) println("\n~~~ paraphrase() ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
for (k, v) in responsedict pprintln(Dict(responsedict))
_v = replace(v, r"\(.*?\)" => "")
responsedict[k] = _v
end
# some time LLM not put integer range result = responsedict[:paraphrase]
for (k, v) in responsedict
responsedict[k] = v
if length(v) > 5
error("non-range is not allowed. $k $v")
end
end
# some time LLM says NA-2. Need to convert NA to 1
for (k, v) in responsedict
if occursin("NA", v) && occursin("-", v)
new_v = replace(v, "NA"=>"1")
responsedict[k] = new_v
end
end
result = ""
for (k, v) in responsedict
# some time LLM generate text with "(some comment)". this line removes it
if !occursin("NA", v)
result *= "$k: $v, "
end
end
result = result[1:end-2] # remove the ending ", "
return result return result
catch e catch e
@@ -795,153 +804,13 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<
showerror(io, e) showerror(io, e)
errorMsg = String(take!(io)) errorMsg = String(take!(io))
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
println("") println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
println("Attempt $attempt. Error occurred: $errorMsg\n$st")
println("")
end end
end end
error("wineattributes_wordToNumber() failed to get a response") error("generatechat failed to generate a response")
end end
# function recommendbox(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
# error("recommendbox")
# 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:
# User's query: ...
# The preference form requires the following information:
# wine_type, price, occasion, food_to_be_paired_with_wine, country, grape_variety, flavors, aromas.
# 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.
# 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.
# 3) Do not generate other comments.
# You should then respond to the user with the following points:
# - reasoning: State your understanding of the current situation
# - wine_type: Can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified"
# - price: Must be an integer representing the cost of the wine.
# - occasion: ...
# - food_to_be_paired_with_wine: food that the user will be served with wine
# - country: wine's country of origin
# - region: wine's region of origin such as Burgundy, Napa Valley
# - grape variety: a single name of grape used to make wine.
# - flavors: Names of items that the wine tastes like.
# - aromas: wine's aroma
# You should only respond in the form as described below:
# reasoning: ...
# wine_type: ...
# price: ...
# occasion: ...
# food_to_be_paired_with_wine: ...
# country: ...
# region: ...
# grape_variety: ...
# flavors: ...
# aromas: ...
# Let's begin!
# """
# attributes = ["reasoning", "wine_type", "price", "occasion", "food_to_be_paired_with_wine", "country", "region", "grape_variety", "flavors", "aromas"]
# errornote = ""
# for attempt in 1:5
# usermsg =
# """
# User's query: $input
# $errornote
# """
# _prompt =
# [
# Dict(:name=> "system", :text=> systemmsg),
# Dict(:name=> "user", :text=> usermsg)
# ]
# # put in model format
# prompt = GeneralUtils.formatLLMtext(_prompt; formatname="llama3instruct")
# prompt *=
# """
# <|start_header_id|>assistant<|end_header_id|>
# """
# try
# response = a.func[:text2textInstructLLM](prompt)
# responsedict = GeneralUtils.textToDict(response, attributes, rightmarker=":", symbolkey=true)
# for i ∈ attributes
# if length(JSON3.write(responsedict[Symbol(i)])) == 0
# error("$i is empty ", @__LINE__)
# end
# end
# #[PENDING] check if the following attributes has more than 1 name
# x = length(split(responsedict[:grape_variety], ",")) * length(split(responsedict[:grape_variety], "/"))
# if x > 1
# errornote = "only a single name in grape_variety is allowed"
# error("only a single grape_variety name is allowed")
# end
# x = length(split(responsedict[:country], ",")) * length(split(responsedict[:country], "/"))
# if x > 1
# errornote = "only a single name in country is allowed"
# error("only a single country name is allowed")
# end
# x = length(split(responsedict[:region], ",")) * length(split(responsedict[:region], "/"))
# if x > 1
# errornote = "only a single name in region is allowed"
# error("only a single region name is allowed")
# end
# # check if grape_variety is mentioned in the input
# if responsedict[:grape_variety] != "NA" && !occursin(responsedict[:grape_variety], input)
# error("$(responsedict[:grape_variety]) is not mentioned in the input")
# end
# responsedict[:flavors] = replace(responsedict[:flavors], "notes"=>"")
# delete!(responsedict, :reasoning)
# delete!(responsedict, :tasting_notes)
# delete!(responsedict, :flavors)
# delete!(responsedict, :aromas)
# # remove (some text)
# for (k, v) in responsedict
# _v = replace(v, r"\(.*?\)" => "")
# responsedict[k] = _v
# end
# result = ""
# for (k, v) in responsedict
# # some time LLM generate text with "(some comment)". this line removes it
# if !occursin("NA", v) && v != "" && !occursin("none", v) && !occursin("None", v)
# result *= "$k: $v, "
# end
# end
# #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_variety: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral"
# result = result[1:end-2] # remove the ending ", "
# return result
# catch e
# io = IOBuffer()
# showerror(io, e)
# errorMsg = String(take!(io))
# st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
# println("")
# println("Attempt $attempt. Error occurred: $errorMsg\n$st")
# println("")
# end
# end
# error("wineattributes_wordToNumber() failed to get a response")
# end
""" Attemp to correct LLM response's incorrect JSON response. """ Attemp to correct LLM response's incorrect JSON response.
@@ -1133,7 +1002,7 @@ end
# state[:isterminal] = true # state[:isterminal] = true
# state[:reward] = 1 # state[:reward] = 1
# end # end
# println("--> 5 Evaluator ", @__FILE__, " ", @__LINE__) # println("--> 5 Evaluator ", Dates.now(), " ", @__FILE__, " ", @__LINE__)
# pprintln(Dict(responsedict)) # pprintln(Dict(responsedict))
# return responsedict[:score] # return responsedict[:score]
# catch e # catch e

View File

@@ -11,8 +11,8 @@ abstract type agent end
mutable struct companion <: agent mutable struct companion <: agent
name::String # agent name
id::String # agent id id::String # agent id
systemmsg::Union{String, Nothing}
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
""" Memory """ Memory
@@ -34,8 +34,8 @@ end
function companion( function companion(
text2textInstructLLM::Function text2textInstructLLM::Function
; ;
name::String= "Assistant",
id::String= string(uuid4()), id::String= string(uuid4()),
systemmsg::Union{String, Nothing}= nothing,
maxHistoryMsg::Integer= 20, maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(),
) )
@@ -48,13 +48,13 @@ function companion(
) )
newAgent = companion( newAgent = companion(
name, id,
id, systemmsg,
maxHistoryMsg, maxHistoryMsg,
chathistory, chathistory,
memory, memory,
text2textInstructLLM text2textInstructLLM
) )
return newAgent return newAgent
end end
@@ -146,7 +146,6 @@ mutable struct sommelier <: agent
""" """
chathistory::Vector{Dict{Symbol, Any}} chathistory::Vector{Dict{Symbol, Any}}
memory::Dict{Symbol, Any} memory::Dict{Symbol, Any}
func # NamedTuple of functions func # NamedTuple of functions
end end
@@ -179,14 +178,17 @@ function sommelier(
# ), # ),
) )
memory = Dict{Symbol, Any}( memory = Dict{Symbol, Any}(
:chatbox=> "", :chatbox=> "",
:shortmem=> OrderedDict{Symbol, Any}(), :shortmem=> OrderedDict{Symbol, Any}(
:events=> Vector{Dict{Symbol, Any}}(), :available_wine=> [],
:state=> Dict{Symbol, Any}( :found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines
:wine_presented_to_user=> "None", ),
), :events=> Vector{Dict{Symbol, Any}}(),
) :state=> Dict{Symbol, Any}(
),
:recap=> OrderedDict{Symbol, Any}(),
)
newAgent = sommelier( newAgent = sommelier(
name, name,

View File

@@ -1,6 +1,7 @@
module util module util
export clearhistory, addNewMessage, vectorOfDictToText, eventdict, noises export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText
using UUIDs, Dates, DataStructures, HTTP, JSON3 using UUIDs, Dates, DataStructures, HTTP, JSON3
using GeneralUtils using GeneralUtils
@@ -106,7 +107,7 @@ function addNewMessage(a::T1, name::String, text::T2;
error("name is not in agent.availableRole $(@__LINE__)") error("name is not in agent.availableRole $(@__LINE__)")
end end
#[] summarize the oldest 10 message #[PENDING] summarize the oldest 10 message
if length(a.chathistory) > maximumMsg if length(a.chathistory) > maximumMsg
summarize(a.chathistory) summarize(a.chathistory)
else else
@@ -138,7 +139,7 @@ julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true)
``` ```
# Signature # Signature
""" """
function vectorOfDictToText(vecd::Vector; withkey=true)::String function chatHistoryToText(vecd::Vector; withkey=true)::String
# Initialize an empty string to hold the final text # Initialize an empty string to hold the final text
text = "" text = ""
@@ -169,11 +170,34 @@ function vectorOfDictToText(vecd::Vector; withkey=true)::String
end end
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
function eventdict(; function eventdict(;
event_description::Union{String, Nothing}=nothing, event_description::Union{String, Nothing}=nothing,
timestamp::Union{DateTime, Nothing}=nothing, timestamp::Union{DateTime, Nothing}=nothing,
subject::Union{String, Nothing}=nothing, subject::Union{String, Nothing}=nothing,
action_or_dialogue::Union{String, Nothing}=nothing, thought::Union{AbstractDict, Nothing}=nothing,
actionname::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENTBOX", etc
actioninput::Union{String, Nothing}=nothing,
location::Union{String, Nothing}=nothing, location::Union{String, Nothing}=nothing,
equipment_used::Union{String, Nothing}=nothing, equipment_used::Union{String, Nothing}=nothing,
material_used::Union{String, Nothing}=nothing, material_used::Union{String, Nothing}=nothing,
@@ -184,7 +208,9 @@ function eventdict(;
:event_description=> event_description, :event_description=> event_description,
:timestamp=> timestamp, :timestamp=> timestamp,
:subject=> subject, :subject=> subject,
:action_or_dialogue=> action_or_dialogue, :thought=> thought,
:actionname=> actionname,
:actioninput=> actioninput,
:location=> location, :location=> location,
:equipment_used=> equipment_used, :equipment_used=> equipment_used,
:material_used=> material_used, :material_used=> material_used,
@@ -194,6 +220,23 @@ function eventdict(;
end end
function createTimeline(memory::T1; skiprecent::Integer=0) where {T1<:AbstractVector}
events = memory[1:end-skiprecent]
timeline = ""
for (i, event) in enumerate(events)
if event[:outcome] === nothing
timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n"
else
timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
end
end
return timeline
end
# """ Convert a single chat dictionary into LLM model instruct format. # """ Convert a single chat dictionary into LLM model instruct format.

View File

@@ -31,26 +31,46 @@
"description": "organization name" "description": "organization name"
}, },
"externalservice": { "externalservice": {
"text2textinstruct": { "loadbalancer": {
"mqtttopic": "/loadbalancer/requestingservice", "mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM", "description": "text to text service with instruct LLM"
"llminfo": { },
"name": "llama3instruct" "text2textinstruct": {
} "mqtttopic": "/loadbalancer/requestingservice",
}, "description": "text to text service with instruct LLM",
"virtualWineCustomer_1": { "llminfo": {
"mqtttopic": "/virtualenvironment/winecustomer", "name": "llama3instruct"
"description": "text to text service with instruct LLM that act as wine customer", }
"llminfo": { },
"name": "llama3instruct" "virtualWineCustomer_1": {
} "mqtttopic": "/virtualenvironment/winecustomer",
}, "description": "text to text service with instruct LLM that act as wine customer",
"text2textchat": { "llminfo": {
"mqtttopic": "/loadbalancer/requestingservice", "name": "llama3instruct"
"description": "text to text service with instruct LLM", }
"llminfo": { },
"name": "llama3instruct" "text2textchat": {
} "mqtttopic": "/loadbalancer/requestingservice",
} "description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"wineDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10201,
"dbname": "wineDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
},
"SQLVectorDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10203,
"dbname": "SQLVectorDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
}
} }
} }

View File

@@ -1,9 +0,0 @@
using GeneralUtils
response = "trajectory_evaluation:\nThe trajectory is correct so far. The thought accurately reflects the user's question, and the action taken is a valid attempt to retrieve data from the database that matches the specified criteria.\n\nanswer_evaluation:\nThe observation provides information about two red wines from Bordeaux rive droite in France, which partially answers the question. However, it does not provide a complete answer as it only lists the wine names and characteristics, but does not explicitly state whether there are any other wines that match the criteria.\n\naccepted_as_answer: No\n\nscore: 6\nThe trajectory is mostly correct, but the observation does not fully address the question.\n\nsuggestion: Consider adding more filters or parameters to the database query to retrieve a complete list of wines that match the specified criteria."
responsedict = GeneralUtils.textToDict(response,
["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"],
rightmarker=":", symbolkey=true)

0
test/runtests.jl Normal file
View File

View File

@@ -8,31 +8,41 @@ using Base.Threads
# load config # load config
config = JSON3.read("./test/config.json") config = JSON3.read("/appfolder/app/dev/YiemAgent/test/config.json")
# config = copy(JSON3.read("../mountvolume/config.json")) # config = copy(JSON3.read("../mountvolume/config.json"))
function executeSQL(sql::T) where {T<:AbstractString} function executeSQL(sql::T) where {T<:AbstractString}
DBconnection = LibPQ.Connection("host=192.168.88.12 port=10201 dbname=wineDB user=yiemtechnologies password=yiemtechnologies@Postgres_0.0") host = config[:externalservice][:wineDB][:host]
port = config[:externalservice][:wineDB][:port]
dbname = config[:externalservice][:wineDB][:dbname]
user = config[:externalservice][:wineDB][:user]
password = config[:externalservice][:wineDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql) result = LibPQ.execute(DBconnection, sql)
close(DBconnection) close(DBconnection)
return result return result
end end
function executeSQLVectorDB(sql) function executeSQLVectorDB(sql)
DBconnection = LibPQ.Connection("host=192.168.88.12 port=10203 dbname=SQLVectorDB user=yiemtechnologies password=yiemtechnologies@Postgres_0.0") host = config[:externalservice][:SQLVectorDB][:host]
port = config[:externalservice][:SQLVectorDB][:port]
dbname = config[:externalservice][:SQLVectorDB][:dbname]
user = config[:externalservice][:SQLVectorDB][:user]
password = config[:externalservice][:SQLVectorDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql) result = LibPQ.execute(DBconnection, sql)
close(DBconnection) close(DBconnection)
return result return result
end end
function text2textInstructLLM(prompt::String) function text2textInstructLLM(prompt::String; maxattempt=3)
msgMeta = GeneralUtils.generate_msgMeta( msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:text2textinstruct][:mqtttopic]; config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="inference", msgPurpose="inference",
senderName="yiemagent", senderName="yiemagent",
senderId=string(uuid4()), senderId=sessionId,
receiverName="text2textinstruct", receiverName="text2textinstruct_small",
mqttBrokerAddress=config[:mqttServerInfo][:broker], mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port], mqttBrokerPort=config[:mqttServerInfo][:port],
) )
@@ -48,8 +58,20 @@ function text2textInstructLLM(prompt::String)
) )
) )
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=6000) response = nothing
response = _response[:response][:text] for attempts in 1:maxattempt
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=300, maxattempt=maxattempt)
payload = _response[:response]
if _response[:success] && payload[:text] !== nothing
response = _response[:response][:text]
break
else
println("\n<text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(outgoingMsg)
println("</text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
sleep(3)
end
end
return response return response
end end
@@ -57,11 +79,11 @@ end
# get text embedding from a LLM service # get text embedding from a LLM service
function getEmbedding(text::T) where {T<:AbstractString} function getEmbedding(text::T) where {T<:AbstractString}
msgMeta = GeneralUtils.generate_msgMeta( msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:text2textinstruct][:mqtttopic]; config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="embedding", msgPurpose="embedding",
senderName="yiemagent", senderName="yiemagent",
senderId=string(uuid4()), senderId=sessionId,
receiverName="text2textinstruct", receiverName="text2textinstruct_small",
mqttBrokerAddress=config[:mqttServerInfo][:broker], mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port], mqttBrokerPort=config[:mqttServerInfo][:port],
) )
@@ -72,18 +94,16 @@ function getEmbedding(text::T) where {T<:AbstractString}
:text => [text] # must be a vector of string :text => [text] # must be a vector of string
) )
) )
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=6000) response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
embedding = response[:response][:embeddings] embedding = response[:response][:embeddings]
return embedding return embedding
end end
function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3, function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3,
vectorDB::Function; limit::Integer=1 vectorDB::Function; limit::Integer=1
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
# get embedding from LLM service # get embedding from LLM service
embedding = getEmbedding(text)[1] embedding = getEmbedding(text)[1]
# check whether there is close enough vector already store in vectorDB. if no, add, else skip # check whether there is close enough vector already store in vectorDB. if no, add, else skip
sql = """ sql = """
SELECT *, $embeddingColumnName <-> '$embedding' as distance SELECT *, $embeddingColumnName <-> '$embedding' as distance
@@ -95,29 +115,29 @@ function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnNam
return df return df
end end
function similarSQLVectorDB(query; maxdistance::Integer=100) function similarSQLVectorDB(query; maxdistance::Integer=100)
tablename = "sqlllm_decision_repository" tablename = "sqlllm_decision_repository"
# get embedding of the query # get embedding of the query
df = findSimilarTextFromVectorDB(query, tablename, df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB) "function_input_embedding", executeSQLVectorDB)
# println(df[1, [:id, :function_output]])
row, col = size(df) row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance] distance = row == 0 ? Inf : df[1, :distance]
# distance = 100 # CHANGE this is for testing only
if row != 0 && distance < maxdistance if row != 0 && distance < maxdistance
# if there is usable SQL, return it. # if there is usable SQL, return it.
output_b64 = df[1, :function_output_base64] # pick the closest match output_b64 = df[1, :function_output_base64] # pick the closest match
output_str = String(base64decode(output_b64)) output_str = String(base64decode(output_b64))
rowid = df[1, :id] rowid = df[1, :id]
println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=output_str, distance=distance) return (dict=output_str, distance=distance)
else else
println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=nothing, distance=nothing) return (dict=nothing, distance=nothing)
end end
end end
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString}
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=1) where {T1<:AbstractString, T2<:AbstractString}
tablename = "sqlllm_decision_repository" tablename = "sqlllm_decision_repository"
# get embedding of the query # get embedding of the query
# query = state[:thoughtHistory][:question] # query = state[:thoughtHistory][:question]
@@ -134,8 +154,8 @@ function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=1) where {T1
sql = """ sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
""" """
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
println(sql) # println(sql)
_ = executeSQLVectorDB(sql) _ = executeSQLVectorDB(sql)
end end
end end