655 lines
18 KiB
Julia
655 lines
18 KiB
Julia
module llmfunction
|
||
|
||
export virtualWineUserChatbox, jsoncorrection, winestock,
|
||
virtualWineUserRecommendbox, userChatbox, userRecommendbox
|
||
|
||
using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs
|
||
using GeneralUtils, SQLLLM
|
||
using ..type, ..util
|
||
|
||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||
|
||
"""
|
||
|
||
# Arguments
|
||
|
||
# Return
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docstring
|
||
- [WORKING] implement the function
|
||
|
||
# Signature
|
||
"""
|
||
function userChatbox(a::T1, input::T2) where {T1<:agent, T2<:AbstractString}
|
||
error("--> userChatbox")
|
||
|
||
# put in model format
|
||
virtualWineCustomer = a.config[:externalservice][:virtualWineCustomer_1]
|
||
llminfo = virtualWineCustomer[:llminfo]
|
||
formattedinput =
|
||
if llminfo[:name] == "llama3instruct"
|
||
formatLLMtext_llama3instruct("assistant", input)
|
||
else
|
||
error("llm model name is not defied yet $(@__LINE__)")
|
||
end
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
|
||
|
||
# return response
|
||
|
||
end
|
||
|
||
|
||
"""
|
||
|
||
# Arguments
|
||
|
||
# Return
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docstring
|
||
- [PENDING] implement the function
|
||
|
||
# Signature
|
||
"""
|
||
function userRecommendbox(a::T1, input::T2) where {T1<:agent, T2<:AbstractString}
|
||
error("--> userRecommendbox")
|
||
|
||
# put in model format
|
||
virtualWineCustomer = a.config[:externalservice][:virtualWineCustomer_1]
|
||
llminfo = virtualWineCustomer[:llminfo]
|
||
formattedinput =
|
||
if llminfo[:name] == "llama3instruct"
|
||
formatLLMtext_llama3instruct("assistant", input)
|
||
else
|
||
error("llm model name is not defied yet $(@__LINE__)")
|
||
end
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
|
||
|
||
# return response
|
||
|
||
end
|
||
|
||
|
||
""" Chatbox for chatting with virtual wine customer.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of Yiem's agent
|
||
- `input::T2`
|
||
text to be send to virtual wine customer
|
||
|
||
# Return
|
||
- `response::String`
|
||
response of virtual wine customer
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docstring
|
||
- [] add reccommend() to compare wine
|
||
|
||
# Signature
|
||
"""
|
||
function virtualWineUserRecommendbox(a::T1, input
|
||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent}
|
||
|
||
# put in model format
|
||
virtualWineCustomer = a.config[:externalservice][:virtualWineCustomer_1]
|
||
llminfo = virtualWineCustomer[:llminfo]
|
||
prompt =
|
||
if llminfo[:name] == "llama3instruct"
|
||
formatLLMtext_llama3instruct("assistant", input)
|
||
else
|
||
error("llm model name is not defied yet $(@__LINE__)")
|
||
end
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
msgMeta = GeneralUtils.generate_msgMeta(
|
||
virtualWineCustomer[:mqtttopic],
|
||
senderName= "virtualWineUserRecommendbox",
|
||
senderId= a.id,
|
||
receiverName= "virtualWineCustomer",
|
||
mqttBroker= a.config[:mqttServerInfo][:broker],
|
||
mqttBrokerPort= a.config[:mqttServerInfo][:port],
|
||
msgId = "dummyid" #CHANGE remove after testing finished
|
||
)
|
||
|
||
outgoingMsg = Dict(
|
||
:msgMeta=> msgMeta,
|
||
:payload=> Dict(
|
||
:text=> prompt,
|
||
)
|
||
)
|
||
|
||
result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
||
response = result[:response]
|
||
|
||
return (response[:text], response[:select], response[:reward], response[:isterminal])
|
||
end
|
||
|
||
|
||
|
||
""" Chatbox for chatting with virtual wine customer.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of Yiem's agent
|
||
- `input::T2`
|
||
text to be send to virtual wine customer
|
||
|
||
# Return
|
||
- `response::String`
|
||
response of virtual wine customer
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docs
|
||
- [x] write a prompt for virtual customer
|
||
|
||
# Signature
|
||
"""
|
||
function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistory
|
||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:AbstractDict, T2<:AbstractString}
|
||
|
||
previouswines =
|
||
"""
|
||
You have the following wines previously:
|
||
|
||
"""
|
||
|
||
systemmsg =
|
||
"""
|
||
You find yourself in a well-stocked wine store, engaged in a conversation with the store's knowledgeable sommelier.
|
||
You're on a quest to find a bottle of wine that aligns with your specific preferences and requirements.
|
||
|
||
The ideal wine you're seeking should meet the following criteria:
|
||
1. It should fit within your budget.
|
||
2. It should be suitable for the occasion you're planning.
|
||
3. It should pair well with the food you intend to serve.
|
||
4. It should be of a particular type of wine you prefer.
|
||
5. It should possess certain characteristics, including:
|
||
- The level of sweetness.
|
||
- The intensity of its flavor.
|
||
- The amount of tannin it contains.
|
||
- Its acidity level.
|
||
|
||
Here's the criteria details:
|
||
{
|
||
"budget": 50,
|
||
"occasion": "graduation ceremony",
|
||
"food pairing": "Thai food",
|
||
"type of wine": "red",
|
||
"wine sweetness level": "dry",
|
||
"wine intensity level": "full-bodied",
|
||
"wine tannin level": "low",
|
||
"wine acidity level": "medium",
|
||
}
|
||
|
||
You should only respond with "text", "select", "reward", "isterminal" steps.
|
||
"text" is your conversation.
|
||
"select" is an integer. Choose an option when presented with choices, or leave it null if none of the options satisfy you or if no choices are available.
|
||
"reward" is an integer, it can be three number:
|
||
1) 1 if you find the right wine.
|
||
2) 0 if you don’t find the ideal wine.
|
||
3) -1 if you’re dissatisfied with the sommelier’s response.
|
||
"isterminal" can be false if you still want to talk with the sommelier, true otherwise.
|
||
|
||
You should only respond in JSON format as describe below:
|
||
{
|
||
"text": "your conversation",
|
||
"select": null,
|
||
"reward": 0,
|
||
"isterminal": false
|
||
}
|
||
|
||
Here are some examples:
|
||
|
||
sommelier: "What's your budget?
|
||
you:
|
||
{
|
||
"text": "My budget is 30 USD.",
|
||
"select": null,
|
||
"reward": 0,
|
||
"isterminal": false
|
||
}
|
||
|
||
sommelier: "The first option is Zena Crown and the second one is Buano Red."
|
||
you:
|
||
{
|
||
"text": "I like the 2nd option.",
|
||
"select": 2,
|
||
"reward": 1,
|
||
"isterminal": true
|
||
}
|
||
|
||
Let's begin!
|
||
"""
|
||
|
||
pushfirst!(virtualCustomerChatHistory, Dict(:name=> "system", :text=> systemmsg))
|
||
|
||
# replace the :user key in chathistory to allow the virtual wine customer AI roleplay
|
||
chathistory::Vector{Dict{Symbol, Any}} = Vector{Dict{Symbol, Any}}()
|
||
for i in virtualCustomerChatHistory
|
||
newdict = Dict()
|
||
newdict[:name] =
|
||
if i[:name] == "user"
|
||
"you"
|
||
elseif i[:name] == "assistant"
|
||
"sommelier"
|
||
else
|
||
i[:name]
|
||
end
|
||
|
||
newdict[:text] = i[:text]
|
||
push!(chathistory, newdict)
|
||
end
|
||
|
||
push!(chathistory, Dict(:name=> "assistant", :text=> input))
|
||
|
||
# put in model format
|
||
prompt = formatLLMtext(chathistory, "llama3instruct")
|
||
prompt *=
|
||
"""
|
||
<|start_header_id|>you<|end_header_id|>
|
||
{"text"
|
||
"""
|
||
|
||
pprint(prompt)
|
||
externalService = config[:externalservice][:text2textinstruct]
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
msgMeta = GeneralUtils.generate_msgMeta(
|
||
externalService[:mqtttopic],
|
||
senderName= "virtualWineUserChatbox",
|
||
senderId= string(uuid4()),
|
||
receiverName= "text2textinstruct",
|
||
mqttBroker= config[:mqttServerInfo][:broker],
|
||
mqttBrokerPort= config[:mqttServerInfo][:port],
|
||
msgId = string(uuid4()) #CHANGE remove after testing finished
|
||
)
|
||
|
||
outgoingMsg = Dict(
|
||
:msgMeta=> msgMeta,
|
||
:payload=> Dict(
|
||
:text=> prompt,
|
||
)
|
||
)
|
||
|
||
attempt = 0
|
||
for attempt in 1:5
|
||
try
|
||
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
||
_responseJsonStr = response[:response][:text]
|
||
expectedJsonExample =
|
||
"""
|
||
Here is an expected JSON format:
|
||
{
|
||
"text": "...",
|
||
"select": "...",
|
||
"reward": "...",
|
||
"isterminal": "..."
|
||
}
|
||
"""
|
||
responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample)
|
||
responseDict = copy(JSON3.read(responseJsonStr))
|
||
|
||
text::AbstractString = responseDict[:text]
|
||
select::Union{Nothing, Number} = responseDict[:select] == "null" ? nothing : responseDict[:select]
|
||
reward::Number = responseDict[:reward]
|
||
isterminal::Bool = responseDict[:isterminal]
|
||
|
||
if text != ""
|
||
# pass test
|
||
else
|
||
error("virtual customer not answer correctly")
|
||
end
|
||
|
||
return (text, select, reward, isterminal)
|
||
catch e
|
||
io = IOBuffer()
|
||
showerror(io, e)
|
||
errorMsg = String(take!(io))
|
||
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
|
||
println("")
|
||
@warn "Error occurred: $errorMsg\n$st"
|
||
println("")
|
||
end
|
||
end
|
||
error("virtualWineUserChatbox failed to get a response")
|
||
end
|
||
|
||
""" Search wine in stock.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of ChatAgent's agent.
|
||
- `input::T2`
|
||
# Return
|
||
A JSON string of available wine
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia> using ChatAgent
|
||
julia> agent = ChatAgent.agentReflex("Jene")
|
||
julia> input = "{\"food\": \"pizza\", \"occasion\": \"anniversary\"}"
|
||
julia> result = winestock(agent, input)
|
||
"{"wine 1": {\"Winery\": \"Pichon Baron\", \"wine name\": \"Pauillac (Grand Cru Classé)\", \"grape variety\": \"Cabernet Sauvignon\", \"year\": 2010, \"price\": \"125 USD\", \"stock ID\": \"ar-17\"}, }"
|
||
```
|
||
|
||
# TODO
|
||
- [] update docs
|
||
- [WORKING] implement the function
|
||
|
||
# Signature
|
||
"""
|
||
function winestock(a::T1, input::T2
|
||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent, T2<:AbstractString}
|
||
|
||
wineattributes = extractWineAttributes(a, input)
|
||
result = SQLLLM.query(Dict(:text=> wineattributes), a.executeSQL, a.text2textInstructLLM)
|
||
return result
|
||
end
|
||
|
||
|
||
"""
|
||
|
||
# Arguments
|
||
- `v::Integer`
|
||
dummy variable
|
||
|
||
# Return
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docstring
|
||
- [x] implement the function
|
||
|
||
# Signature
|
||
"""
|
||
function extractWineAttributes(a::T1, input::T2
|
||
)::String where {T1<:agent, T2<:AbstractString}
|
||
|
||
converstiontable =
|
||
"""
|
||
Conversion Table:
|
||
Intensity level:
|
||
Level 1: May correspond to "light-bodied" or a similar description.
|
||
Level 2: May correspond to "med light", "medium light" or a similar description.
|
||
Level 3: May correspond to "medium" or a similar description.
|
||
Level 4: May correspond to "med full", "medium full" or a similar description.
|
||
Level 5: May correspond to "full" or a similar description.
|
||
Sweetness level:
|
||
Level 1: May correspond to "dry", "no sweet" or a similar description.
|
||
Level 2: May correspond to "off dry", "less sweet" or a similar description.
|
||
Level 3: May correspond to "semi sweet" or a similar description.
|
||
Level 4: May correspond to "sweet" or a similar description.
|
||
Level 5: May correspond to "very sweet" or a similar description.
|
||
Tannin level:
|
||
Level 1: May correspond to "low tannin" or a similar description.
|
||
Level 2: May correspond to "semi low tannin" or a similar description.
|
||
Level 3: May correspond to "medium tannin" or a similar description.
|
||
Level 4: May correspond to "semi high tannin" or a similar description.
|
||
Level 5: May correspond to "high tannin" or a similar description.
|
||
Acidity level:
|
||
Level 1: May correspond to "low acidity" or a similar description.
|
||
Level 2: May correspond to "semi low acidity" or a similar description.
|
||
Level 3: May correspond to "medium acidity" or a similar description.
|
||
Level 4: May correspond to "semi high acidity" or a similar description.
|
||
Level 5: May correspond to "high acidity" or a similar description.
|
||
"""
|
||
|
||
systemmsg =
|
||
"""
|
||
As an attentive sommelier, your task is to determine the user's wine preferred levels of sweetness, intensity, tannin, acidity and other criteria based on the user query.
|
||
|
||
At each round of conversation, the user will give you the current situation:
|
||
Conversion Table: ...
|
||
Query: ...
|
||
|
||
You should follow the following guidelines:
|
||
- If specific information is unavailable, please use "NA" to indicate this.
|
||
- Use converstion table to convert sweetness, acidity, tannin, intensity describing word into an integer.
|
||
- Do not generate other comments.
|
||
|
||
You should then respond to the user with the following points:
|
||
- wine_type: Can be one of: red, white, sparkling, rose, dessert or fortified
|
||
- price: ...
|
||
- occasion: ...
|
||
- food_paired: food that will be served with wine
|
||
- country: wine's country of origin
|
||
- grape_variety: ...
|
||
- tasting_notes: wine's flavors
|
||
- sweetness: S where S is an integer indicating sweetness level
|
||
- acidity: A where A is an integer indicating acidity level
|
||
- tannin: T where T is an integer indicating tannin level
|
||
- intensity: I where I is an integer indicating intensity level
|
||
|
||
You should only respond in format as described below:
|
||
repeat: repeat the user's query
|
||
wine_type: ...
|
||
price: ...
|
||
occasion: ...
|
||
food_paired: ...
|
||
country: ...
|
||
grape_variety: ...
|
||
tasting_notes: ...
|
||
sweetness:
|
||
acidity: ...
|
||
tannin: ...
|
||
intensity: ...
|
||
|
||
Here are some examples:
|
||
|
||
user: "price < 25, for wedding party, full-bodied white wine with sweetness level 2, apple and honey notes, low tannin level and medium acidity level, Pizza, France, Riesling"
|
||
assistant: repeat: ... \n wine_type: white\n price: less than 25\n occasion: wedding party\n food_pairing: Pizza\n country: France\n grape_variety: Riesling\n tasting_notes: apple, honey\n sweetness: 2\n acidity: 3\n tannin: 1\n intensity: 5
|
||
|
||
user: body=full-bodied, dry, acidity=medium and low tannin
|
||
assistant: repeat: ... \n wine_type: NA\n price: NA\n occasion: NA\n food_pairing: NA\n country: NA\n grape_variety: NA\n tasting_notes: NA\n sweetness: 1\n acidity: 3\n tannin: 1\n intensity: 5
|
||
|
||
Let's begin!
|
||
"""
|
||
|
||
usermsg =
|
||
"""
|
||
Conversion Table: $converstiontable
|
||
Query: $input
|
||
"""
|
||
|
||
_prompt =
|
||
[
|
||
Dict(:name=> "system", :text=> systemmsg),
|
||
Dict(:name=> "user", :text=> usermsg)
|
||
]
|
||
|
||
# put in model format
|
||
prompt = GeneralUtils.formatLLMtext(_prompt, "llama3instruct")
|
||
prompt *=
|
||
"""
|
||
<|start_header_id|>assistant<|end_header_id|>
|
||
"""
|
||
|
||
attributes = ["repeat", "wine_type", "price", "occasion", "food_paired", "country", "grape_variety", "sweetness", "acidity", "tannin", "intensity"]
|
||
|
||
for attempt in 1:5
|
||
try
|
||
response = a.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
|
||
|
||
# check if there are more than 1 key per categories
|
||
for i ∈ attributes
|
||
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
|
||
if length(matchkeys) > 1
|
||
error("generatechat has more than one key per categories")
|
||
end
|
||
end
|
||
|
||
result = ""
|
||
for (k, v) in responsedict
|
||
if k != :repeat && !occursin("NA", v)
|
||
result *= "$k: $v, "
|
||
end
|
||
end
|
||
|
||
result = result[1:end-2] # remove the ending ", "
|
||
|
||
# replace because SQLLLM didn't know what food_paired means
|
||
result = replace(result, "food_paired" => "food_to_be_paired_with_wine")
|
||
|
||
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.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of Yiem's agent
|
||
- `input::T2`
|
||
text to be send to virtual wine customer
|
||
|
||
# Return
|
||
- `correctjson::String`
|
||
corrected json string
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# Signature
|
||
"""
|
||
function jsoncorrection(config::T1, input::T2, correctJsonExample::T3;
|
||
maxattempt::Integer=3
|
||
) where {T1<:AbstractDict, T2<:AbstractString, T3<:AbstractString}
|
||
|
||
incorrectjson = deepcopy(input)
|
||
correctjson = nothing
|
||
|
||
for attempt in 1:maxattempt
|
||
try
|
||
d = copy(JSON3.read(incorrectjson))
|
||
correctjson = incorrectjson
|
||
return correctjson
|
||
catch e
|
||
@warn "Attempting to correct JSON string. Attempt $attempt"
|
||
e = """$e"""
|
||
if occursin("EOF", e)
|
||
e = split(e, "EOF")[1] * "EOF"
|
||
end
|
||
incorrectjson = deepcopy(input)
|
||
_prompt =
|
||
"""
|
||
Your goal are:
|
||
1) Use the expected JSON format as a guideline to check why the given JSON string failed to load and provide a corrected version that can be loaded by Python's json.load function.
|
||
2) Provide Corrected JSON string only. Do not provide any other info.
|
||
|
||
$correctJsonExample
|
||
|
||
Let's begin!
|
||
Given JSON string: $incorrectjson
|
||
The given JSON string failed to load previously because: $e
|
||
Corrected JSON string:
|
||
"""
|
||
|
||
# apply LLM specific instruct format
|
||
externalService = config[:externalservice][:text2textinstruct]
|
||
llminfo = externalService[:llminfo]
|
||
prompt =
|
||
if llminfo[:name] == "llama3instruct"
|
||
formatLLMtext_llama3instruct("system", _prompt)
|
||
else
|
||
error("llm model name is not defied yet $(@__LINE__)")
|
||
end
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
msgMeta = GeneralUtils.generate_msgMeta(
|
||
externalService[:mqtttopic],
|
||
senderName= "jsoncorrection",
|
||
senderId= string(uuid4()),
|
||
receiverName= "text2textinstruct",
|
||
mqttBroker= config[:mqttServerInfo][:broker],
|
||
mqttBrokerPort= config[:mqttServerInfo][:port],
|
||
)
|
||
|
||
outgoingMsg = Dict(
|
||
:msgMeta=> msgMeta,
|
||
:payload=> Dict(
|
||
:text=> prompt,
|
||
:kwargs=> Dict(
|
||
:max_tokens=> 512,
|
||
:stop=> ["<|eot_id|>"],
|
||
)
|
||
)
|
||
)
|
||
result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
||
incorrectjson = result[:response][:text]
|
||
end
|
||
end
|
||
end
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
end # module llmfunction |