This commit is contained in:
narawat lamaiin
2024-07-31 06:52:44 +07:00
parent 792accc619
commit 70e55f0306
5 changed files with 262 additions and 319 deletions

View File

@@ -214,20 +214,24 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent}
"""
You are a helpful sommelier working for a wine store.
Your task is to help the user choose the best wine that match the user preferences from your inventory.
You are also eager to improve your helpfulness.
At each round of conversation, the user will give you the current situation:
Context: ...
Your earlier conversation with the user: ...
You must follow the following DO guidelines:
You SHOULD follow the following guidelines:
- If the user interrupts, prioritize the user then get back to the guidelines.
- Check your inventory before mentioning any specific wine.
- Get to know how much the user willing to spend
- Get to know type of wine the user is looking for e.g. red, white, sparkling, rose, dessert, fortified
- Get to know what occasion the user is buying wine for
- Get to know what characteristics of wine the user is looking for e.g. tannin, sweetness, intensity, acidity
- Get to know what food will be served with wine
- Search for wines that match the user preferences
- Recommend wine to the user
- Ask the customer if there is anything else you could help. If not, finish the conversation.
You MUST follow the following guidelines:
- Do not mentioning any wine until you've check your inventory.
You should then respond to the user with interleaving Thought, Plan, Action and Observation:
- thought:
@@ -235,7 +239,7 @@ function decisionMaker(a::T)::Dict{Symbol, Any} where {T<:agent}
- plan: Based on the current situation, state a complete plan to complete the task. Be specific.
- action_name (Must be aligned with your plan): Can be one of the following functions:
1) CHATBOX[text], which you can use to talk with the user. "text" is in verbal English.
2) CHECKINVENTORY[query], which you can use to find info about wine in your inventory. "query" is a search term in verbal English.
2) CHECKINVENTORY[query], which you can use to check info about wine in your inventory. "query" is a search term in verbal English.
Good query example: black car with a stereo, 200 mile range and an electric motor.
Good query example: How many car brand are from Asia?
- action_input: input to the action
@@ -926,13 +930,16 @@ julia> response = ChatAgent.conversation(newAgent, "Hi! how are you?")
# TODO
- [] update docstring
- [x] MCTS() for planning
- [] add recap to initialState for earlier completed question
- [WORKING] conversation loop
# Signature
"""
function conversation(a::T, userinput::Dict) where {T<:agent}
# place holder
actionname = nothing
result = nothing
chatresponse = nothing
if userinput[:text] == "newtopic"
clearhistory(a)
@@ -941,10 +948,45 @@ function conversation(a::T, userinput::Dict) where {T<:agent}
# add usermsg to a.chathistory
addNewMessage(a, "user", userinput[:text])
think(a)
actionname, result = think(a)
# -------- use dummy memory to check generatechat() for halucination (checking inventory) -------- #
mem = deepcopy(a.memory)
if actionname == "CHATBOX"
mem[:chatbox] = result
else
push!(mem[:shortmem], Dict(Symbol(actionname)=> result))
end
# thought will be added to chat model via context
chatresponse = generatechat(a)
chatresponse = generatechat(mem, a.chathistory, a.text2textInstructLLM)
# some time LLM said to user that it (checking inventory) but it is not.
# if chatresponse want to check inventory but think() didn't checkinventory then do it
if occursin("(check", chatresponse) && occursin("inventory)", chatresponse) &&
actionname != "checkinventory"
actionname, result = forceInventoryCheck(a)
if actionname == "CHATBOX"
a.memory[:chatbox] = result
else
push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result))
end
# generate chatresponse again because we have force inventory check
chatresponse = generatechat(a.memory, a.chathistory, a.text2textInstructLLM)
else
if actionname == "CHATBOX"
a.memory[:chatbox] = result
else
push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result))
end
# since chatresponse does not halucinate i.e. no (check inventory), it does not need
# to regenerate again and con be use directly
end
addNewMessage(a, "assistant", chatresponse)
return chatresponse
@@ -952,78 +994,6 @@ function conversation(a::T, userinput::Dict) where {T<:agent}
end
# function conversation(a::T, userinput::Dict) where {T<:agent}
# config = deepcopy(a.config)
# pprint(config)
# if userinput[:text] == "newtopic"
# clearhistory(a)
# return "Okay. What shall we talk about?"
# else
# # add usermsg to a.chathistory
# addNewMessage(a, "user", userinput[:text])
# if isempty(a.plan[:currenttrajectory])
# # initial state
# a.plan[:currenttrajectory] = Dict{Symbol, Any}(
# # deepcopy the info to prevent modifying the info unintentionally during MCTS planning
# :customerinfo=> deepcopy(a.keywordinfo[:customerinfo]),
# :storeinfo=> deepcopy(a.keywordinfo[:storeinfo]),
# :userselect=> nothing,
# :reward=> 0,
# :isterminal=> false,
# :evaluation=> nothing,
# :lesson=> nothing,
# :totalTrajectoryReward=> nothing,
# # contain question, thought_1, action_1, observation_1, thought_2, ...
# :thoughtHistory=> OrderedDict{Symbol, Any}(
# #[] :recap=>,
# :question=> userinput[:text],
# ),
# # store conversation for virtual customer because the virtual customer agent is just
# # a function and stateless.
# :virtualCustomerChatHistory=> Vector{Dict{Symbol, Any}}(
# [Dict(:name=> "user", :text=> userinput[:text])]
# ),
# )
# else
# _, a.plan[:currenttrajectory] = makeNewState(a.plan[:currenttrajectory],
# a.plan[:activeplan][:thoughtHistory], userinput[:text], userinput[:select],
# userinput[:reward], userinput[:isterminal])
# end
# end
# while true
# bestNextState, besttrajectory = LLMMCTS.runMCTS(a.plan[:currenttrajectory],
# transition, config, decisionMaker, evaluator, reflector;
# totalsample=2, maxDepth=3, maxiterations=3, explorationweight=1.0)
# a.plan[:activeplan] = bestNextState
# latestActionKey, latestActionIndice =
# GeneralUtils.findHighestIndexKey(bestNextState[:thoughtHistory], "action")
# actionname = bestNextState[:thoughtHistory][latestActionKey][:name]
# actioninput = bestNextState[:thoughtHistory][latestActionKey][:input]
# # transition
# if actionname == "chatbox"
# # add usermsg to a.chathistory
# addNewMessage(a, "assistant", actioninput)
# return actioninput
# elseif actionname == "recommendbox"
# # add usermsg to a.chathistory
# addNewMessage(a, "assistant", actioninput)
# return actioninput
# else
# _, a.plan[:currenttrajectory] = transition(a, a.plan[:currenttrajectory], a.plan[:activeplan])
# end
# end
# end
"""
@@ -1043,7 +1013,7 @@ julia>
# Signature
"""
function think(a::T) where {T<:agent}
function think(a::T)::NamedTuple{(:actionname, :result), Tuple{String, String}} where {T<:agent}
thoughtDict = decisionMaker(a)
actionname = thoughtDict[:action_name]
actioninput = thoughtDict[:action_input]
@@ -1066,11 +1036,133 @@ function think(a::T) where {T<:agent}
errormsg::Union{AbstractString, Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing
success::Bool = haskey(response, :success) ? response[:success] : false
if actionname == "CHATBOX"
a.memory[:chatbox] = result
else
push!(a.memory[:shortmem], Dict(Symbol(actionname)=> result))
return (actionname=actionname, result=result)
end
""" Force to think and check inventory
[WORKING]
"""
function forceInventoryCheck(a::T)::NamedTuple{(:actionname, :result), Tuple{String, String}} where {T<:agent}
thoughtDict = thinkCheckInventory(a)
actionname = thoughtDict[:action_name]
actioninput = thoughtDict[:action_input]
# map action and input() to llm function
response =
if actionname == "CHECKINVENTORY"
checkinventory(a, actioninput)
else
error("undefined LLM function. Requesting $actionname")
end
# this section allow LLM functions above to have different return values.
result = haskey(response, :result) ? response[:result] : nothing
select = haskey(response, :select) ? response[:select] : nothing
reward::Integer = haskey(response, :reward) ? response[:reward] : 0
isterminal::Bool = haskey(response, :isterminal) ? response[:isterminal] : false
errormsg::Union{AbstractString, Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing
success::Bool = haskey(response, :success) ? response[:success] : false
return (actionname=actionname, result=result)
end
"""
[WORKING]
"""
function thinkCheckInventory(a::T)::Dict{Symbol, Any} where {T<:agent}
systemmsg =
"""
You are a helpful sommelier working for a wine store.
Your task is to help the user choose the best wine that match the user preferences from your inventory.
Definitions:
- observation: result of the preceding immediate action.
At each round of conversation, the user will give you the current situation:
Context: ...
Your earlier conversation with the user: ...
You must follow the following guidelines:
- Check inventory immediately based on what you know about the user.
You should then respond to the user with interleaving Thought, Plan, Action and Observation:
- thought:
1) State your reasoning about the current situation.
- plan: Based on the current situation, state a complete plan to complete the task. Be specific.
- action_name (Must be aligned with your plan): Can be one of the following functions:
1) CHECKINVENTORY[query], which you can use to check info about wine in your inventory. "query" is a search term in verbal English.
Good query example: black car with a stereo, 200 mile range and an electric motor.
Good query example: How many car brand are from Asia?
- action_input: input to the action
You should only respond in format as described below:
thought: ...
plan: ...
action_name: ...
action_input: ...
Let's begin!
"""
usermsg =
"""
Context: None
Your earlier conversation with the user: $(vectorOfDictToText(a.chathistory))
"""
_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|>
"""
response = nothing # store for show when error msg show up
for attempt in 1:10
try
response = a.text2textInstructLLM(prompt)
responsedict = GeneralUtils.textToDict(response,
["thought", "plan", "action_name", "action_input"],
rightmarker=":", symbolkey=true)
if responsedict[:action_name] ["CHECKINVENTORY"]
error("decisionMaker didn't use the given functions ", @__LINE__)
end
for i [:thought, :plan, :action_name]
if length(JSON3.write(responsedict[i])) == 0
error("$i is empty ", @__LINE__)
end
end
# check if there are more than 1 key per categories
for i [:thought, :plan, :action_name, :action_input]
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
if length(matchkeys) > 1
error("DecisionMaker has more than one key per categories")
end
end
return responsedict
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("DecisionMaker failed to generate a thought ", response)
end
@@ -1090,14 +1182,14 @@ julia>
# TODO
- [] update docs
- [WORKING] implement the function
- [x] implement the function
# Signature
"""
function generatechat(a::T) where {T<:agent}
function generatechat(memory::Dict, chathistory::Vector, text2textInstructLLM::Function)
systemmsg =
"""
You are a polite sommelier working for a wine store.
You are an website-based polite sommelier working for an online wine store.
Your task is to help the user choose the best wine that match the user preferences from your inventory.
At each round of conversation, the user will give you the current situation:
@@ -1105,9 +1197,11 @@ function generatechat(a::T) where {T<:agent}
Your thoughts: Your current thinking in your mind
Your earlier conversation with the user: ...
You must follow the following DON'T guidelines:
- Don't mention any specific wine until you've checked your inventory.
You must follow the following guidelines:
- Do not mentioning any wine until you've check your inventory.
- Check your inventory before recommending any specific wine.
- Your thoughts matter.
- Do not offer the user to try wine as you are internet-based agent.
You should then respond to the user with:
- chat: what do you want to say to the user based on the current situation
@@ -1118,15 +1212,17 @@ function generatechat(a::T) where {T<:agent}
Let's begin!
"""
context = length(a.memory[:shortmem]) > 0 ? vectorOfDictToText(a.memory[:shortmem], withkey=false) : "None"
context = length(memory[:shortmem]) > 0 ? vectorOfDictToText(memory[:shortmem], withkey=false) : "None"
usermsg =
"""
Context: $context
Your earlier conversation with the user: $(vectorOfDictToText(a.chathistory))
Your thoughts: $(a.memory[:chatbox])
Your earlier conversation with the user: $(vectorOfDictToText(chathistory))
Your thoughts: $(memory[:chatbox])
"""
println("")
println("--> think ", @__FILE__, " ", @__LINE__)
println(memory[:chatbox])
_prompt =
[
Dict(:name=> "system", :text=> systemmsg),
@@ -1142,7 +1238,7 @@ function generatechat(a::T) where {T<:agent}
for attempt in 1:5
try
response = a.text2textInstructLLM(prompt)
response = text2textInstructLLM(prompt)
responsedict = GeneralUtils.textToDict(response,
["chat"],
rightmarker=":", symbolkey=true)
@@ -1175,13 +1271,6 @@ function generatechat(a::T) where {T<:agent}
end
end
error("generatechat failed to generate an evaluation")
end