module interface
export agentReact, addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix,
generatePrompt_tokenSuffix, conversation, work
using JSON3, DataStructures, Dates, UUIDs
using CommUtils, GeneralUtils
# ---------------------------------------------------------------------------- #
# pythoncall setting #
# ---------------------------------------------------------------------------- #
# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252
# by setting the following variables, PythonCall will use system python or conda python and
# packages installed by system or conda
# if these setting are not set (comment out), PythonCall will use its own python and package that
# installed by CondaPkg (from env_preparation.jl)
# ENV["JULIA_CONDAPKG_BACKEND"] = "Null"
# systemPython = split(read(`which python`, String), "\n")[1]
# ENV["JULIA_PYTHONCALL_EXE"] = systemPython # find python location with $> which python ex. raw"/root/conda/bin/python"
# using PythonCall
# const py_agents = PythonCall.pynew()
# const py_llms = PythonCall.pynew()
# function __init__()
# # PythonCall.pycopy!(py_cv2, pyimport("cv2"))
# # equivalent to from urllib.request import urlopen in python
# PythonCall.pycopy!(py_agents, pyimport("langchain.agents"))
# PythonCall.pycopy!(py_llms, pyimport("langchain.llms"))
# end
#------------------------------------------------------------------------------------------------100
abstract type agent end
@kwdef mutable struct agentReact <: agent
availableRole::AbstractVector = ["system", "user", "assistant"]
agentName::String = "assistant"
maxUserMsg::Int = 10
earlierConversation::String = "" # summary of earlier conversation
mqttClient::Union{mqttClient, Nothing} = nothing
msgMeta::Union{Dict, Nothing} = nothing
""" Dict(Role=> Content) ; Role can be system, user, assistant
Example:
messages=[
Dict(:role=>"system", :content=> "You are a helpful assistant."),
Dict(:role=>"assistant", :content=> "How may I help you"),
Dict(:role=>"user", :content=> "Hello, how are you"),
]
"""
role::Symbol = :assistant
roles::Dict = Dict(:assistant => "You are a helpful assistant.",)
# Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
# messages= [Dict(:role=>"system", :content=> "", :timestamp=> Dates.now()),]
messages = Vector{Dict{Symbol, Any}}()
context::String = "nothing" # internal thinking area
tools::Union{Dict, Nothing} = nothing
thought::String = "nothing" # contain unfinished thoughts
end
function agentReact(
agentName::String,
mqttClientSpec::NamedTuple;
role::Symbol=:assistant,
roles::Dict=Dict(
:assistant_react =>
"""
You are a helpful assistant. You don't know other people personal info previously.
Use the following format:
QTS: the input question your user is asking and you must answer
Plan: first you should always think about the question and the info you have thoroughly then extract and devise a complete plan to find the answer (pay attention to variables and their corresponding numerals).
Thought: you should always think about the info you need and what to do (pay attention to correct numeral calculation and commonsense).
Act: the action tool related to what you intend to do, should be one of [Chatbox, Internet, WineStock]
ActInput: the input to the action (pay attention to the tool's input)
Obs: the result of the action
... (this Plan/Thought/Act/ActInput/Obs loop can repeat N times.)
Thought: I think I know the answer
ANS: Answer of the original question and the rationale behind your answer
""",
:sommelier =>
"""
You are a sommelier at an online wine reseller who always ask user for wine relevant info before you could help them choosing wine.
You usually recommend atmost 2 wines for customers.
You don't know other people personal info previously.
Use the following format:
QTS: the input question your user is asking and you must answer
Plan: first you should always think about the question and the info you have thoroughly then extract and devise a complete plan to find the answer (pay attention to variables and their corresponding numerals).
Thought: ask yourself do you have all the info you need? And what to do (pay attention to correct numeral calculation and commonsense).
Act: the tool that match your thought, should be one of [Chatbox, Internet, WineStock]
ActInput: the input to the action (pay attention to the tool's input)
Obs: the result of the action
... (this Plan/Thought/Act/ActInput/Obs loop can repeat N times until you know the answer.)
ANS: Answer of the original question. You describe detailed benefits of each answer to user's preference.
Info used to select wine:
- type of food
- occasion
- user's personal taste of wine
- wine price range
- temperature at the serving location
- wine we have in stock
""",
),
tools::Dict=Dict(
:internetsearch=>Dict(
:name => "internetsearch",
:description => "Useful for when you need to search the Internet",
:input => "Input should be a search query.",
:output => "",
# :func => internetsearch # function
),
:chatbox=>Dict(
:name => "chatbox",
:description => "Useful for when you need to ask a customer what you need to know or to talk with them.",
:input => "Input should be a conversation to customer.",
:output => "" ,
),
:wineStock=>Dict(
:name => "wineStock",
:description => "useful for when you need to search for wine by your description, price, name or ID.",
:input => "Input should be a search query with as much details as possible.",
:output => "" ,
),
),
msgMeta::Dict=Dict(
:msgPurpose=> "updateStatus",
:from=> "chatbothub",
:to=> "llmAI",
:requestrespond=> "request",
:sendto=> "", # destination topic
:replyTo=> "chatbothub/llm/respond", # requester ask responder to send reply to this topic
:repondToMsgId=> "", # responder is responding to this msg id
:taskstatus=> "", # "complete", "fail", "waiting" or other status
:timestamp=> Dates.now(),
:msgId=> "$(uuid4())",
),
availableRole::AbstractArray=["system", "user", "assistant"],
maxUserMsg::Int=10,)
newAgent = agentReact()
newAgent.availableRole = availableRole
newAgent.maxUserMsg = maxUserMsg
newAgent.mqttClient = CommUtils.mqttClient(mqttClientSpec)
newAgent.msgMeta = msgMeta
newAgent.tools = tools
newAgent.role = role
newAgent.roles = roles
return newAgent
end
"""
add new message to agent
# Example
```jldoctest
julia> addNewMessage(agent1, "user", "Where should I go to buy snacks")
````
"""
function addNewMessage(a::T, role::String, content::String) where {T<:agent}
if role ∉ a.availableRole # guard against typo
error("role is not in agent.availableRole")
end
# check whether user messages exceed limit
userMsg = 0
for i in a.messages
if i[:role] == "user"
userMsg += 1
end
end
messageleft = 0
if userMsg > a.maxUserMsg # delete all conversation
clearMessage(a)
messageleft = a.maxUserMsg
else
userMsg += 1
d = Dict(:role=> role, :content=> content, :timestamp=> Dates.now())
push!(a.messages, d)
messageleft = a.maxUserMsg - userMsg
end
return messageleft
end
function clearMessage(a::T) where {T<:agent}
for i in eachindex(a.messages)
if length(a.messages) > 1 # system instruction will NOT be deleted
pop!(a.messages)
else
break
end
end
end
function removeLatestMsg(a::T) where {T<:agent}
if length(a.messages) > 1
pop!(a.messages)
end
end
# function generatePrompt_tokenSuffix(a::agentReact;
# userToken::String="[/INST]", assistantToken="[INST]",
# systemToken="[INST]<> content <>")
# prompt = nothing
# for msg in a.messages
# role = msg[:role]
# content = msg[:content]
# if role == "system"
# prompt = replace(systemToken, "content" => content) * " "
# elseif role == "user"
# prompt *= " " * content * " " * userToken
# elseif role == "assistant"
# prompt *= " " * content * " " * assistantToken
# else
# error("undefied condition role = $role")
# end
# end
# return prompt
# end
# function generatePrompt_tokenPrefix(a::agentReact;
# userToken::String="Q:", assistantToken="A:",
# systemToken="[INST]<> content <>")
# prompt = nothing
# for msg in a.messages
# role = msg[:role]
# content = msg[:content]
# if role == "system"
# prompt = replace(systemToken, "content" => content) * " "
# elseif role == "user"
# prompt *= userToken * " " * content * " "
# elseif role == "assistant"
# prompt *= assistantToken * " " * content * " "
# else
# error("undefied condition role = $role")
# end
# end
# return prompt
# end
function generatePrompt_react_mistral_openorca(messages::Dict, systemMsg::String, context::String="",
tools::Union{Dict, Nothing}=nothing)
promptTemplate =
"""
<|im_start|>system
{systemMsg}
You have access to the following tools:
{tools}
Begin!
<|im_end|>
Here are the context for the question:
{context}
"""
for msg in messages
role = msg[:role]
content = msg[:content]
if role == "system"
prompt = replace(promptTemplate, "{systemMsg}" => systemMsg)
toollines = ""
for tool in tools
toolline = "$(tool[:name]): $(tool[:description]) $(tool[:input]) $(tool[:output])\n"
toollines *= toolline
end
prompt = replace(promptTemplate, "{tools}" => toollines)
prompt = replace(promptTemplate, "{context}" => context)
elseif role == "user"
prompt *= "<|im_start|>user\n" * content * "\n<|im_end|>\n"
elseif role == "assistant"
prompt *= "<|im_start|>assistant\n" * content * "\n<|im_end|>\n"
else
error("undefied condition role = $role")
end
end
return prompt
end
function generatePrompt_react_mistral_openorca(a::T, usermsg::String) where {T<:agent}
prompt =
"""
<|im_start|>system
{systemMsg}
You have access to the following tools:
{tools}
Begin!
<|im_end|>
Here are the context for the question:
{context}
"""
prompt = replace(prompt, "{systemMsg}" => a.roles[a.role])
toollines = ""
for (toolname, v) in a.tools
toolline = "$toolname: $(v[:description]) $(v[:input]) $(v[:output])\n"
toollines *= toolline
end
prompt = replace(prompt, "{tools}" => toollines)
prompt = replace(prompt, "{context}" => a.context)
prompt *= "<|im_start|>user\nQTS: " * usermsg * "\n<|im_end|>\n"
prompt *= "<|im_start|>assistant\n"
return prompt
end
#WORKING
function conversation(a::T, usermsg::String) where {T<:agent}
if a.thought == "nothing"
a.context = conversationSummary(a)
addNewMessage(a, "user", usermsg)
prompt = generatePrompt_react_mistral_openorca(a, usermsg)
@show prompt
error("conversation done")
else
end
end
#WORKING
function work(a::T, usermsg::String) where {T<:agent}
end
function workContinueThought(a::T, usermsg::String) where {T<:agent}
end
#WORKING
"""
make a conversation summary.
```julia
julia> conversation = [
Dict(:role=> "user", :content=> "I would like to get a bottle of wine", :timestamp=> Dates.now()),
Dict(:role=> "assistant", :content=> "What kind of Thai dishes are you having?", :timestamp=> Dates.now()),
Dict(:role=> "user", :content=> "It a pad thai.", :timestamp=> Dates.now()),
Dict(:role=> "assistant", :content=> "Is there any special occasion for this event?", :timestamp=> Dates.now()),
Dict(:role=> "user", :content=> "We'll hold a wedding party at the beach.", :timestamp=> Dates.now()),
Dict(:role=> "assistant", :content=> "What is your preferred type of wine?", :timestamp=> Dates.now()),
Dict(:role=> "user", :content=> "I like dry white wine with medium tanins.", :timestamp=> Dates.now()),
Dict(:role=> "assistant", :content=> "What is your preferred price range for this bottle of wine?", :timestamp=> Dates.now()),
Dict(:role=> "user", :content=> "lower than 50 dollars.", :timestamp=> Dates.now()),
Dict(:role=> "assistant", :content=> "Based on your preferences and our stock, I recommend the following two wines for you:
1. Pierre Girardin \"Murgers des Dents de Chien\" - Saint-Aubin 1er Cru (17 USD)
2. Etienne Sauzet'Les Perrieres' - Puligny Montrachet Premier Cru (22 USD)
The first wine, Pierre Girardin \"Murgers des Dents de Chien\" - Saint-Aubin 1er Cru, is a great choice for its affordable price and refreshing taste.
It pairs well with Thai dishes and will be perfect for your beach wedding party.
The second wine, Etienne Sauzet'Les Perrieres' - Puligny Montrachet Premier Cru, offers a more complex flavor profile and slightly higher price point, but still remains within your budget.
Both wines are suitable for serving at 22 C temperature.", :timestamp=> Dates.now()),
]
julia> summary = conversationSummary()
```
"""
function conversationSummary(a::T) where {T<:agent}
promptTemplate =
"""
<|im_start|>system
You are a helpful assistant.
<|im_end|>
<|im_start|>user
Please make a detailed bullet summary of the following earlier conversation between you and the user.
{conversation}
<|im_end|>
"""
conversation = ""
summary = ""
if length(a.messages)!= 0
for msg in a.messages
role = msg[:role]
content = msg[:content]
if role == "user"
conversation *= "$role: $content\n"
elseif role == "assistant"
conversation *= "I: $content\n"
else
error("undefied condition role = $role")
end
end
prompt = replace(promptTemplate, "{conversation}" => conversation)
result = sendReceivePrompt(a, prompt)
summary = result === nothing ? "nothing" : result
if summary[1:1] == "\n"
summary = summary[2:end]
end
end
return summary
end
# function work2(a::agentReact, usermsg::String)
# addNewMessage(a, "user", usermsg)
# userIntent = identifyUserIntention(a, usermsg)
# @show userIntent
# # checkReasonableness()
# if userIntent == "chat"
# prompt = generatePrompt_tokenPrefix(a, userToken="Q:", assistantToken="A:")
# result = sendReceivePrompt(a, prompt)
# addNewMessage(a, "assistant", result)
# return result
# elseif userIntent == "task"
# while true
# if thought == "nothing" # no unfinished thought
# prompt = generatePrompt_react_mistral_openorca(
# a.messages, a.roles[a.role], a.context, a.tools)
# output = sendReceivePrompt(a, prompt)
# obscount = count(output["text"], "Obs:")
# a.thought = prompt * out
# if contains(output["text"], "ANS:") # know the answer
# a.thought = "nothing"
# return output["text"]
# else
# out = split(output["text"], "Obs:")[1] # LLM may generate long respond with multiple Obs: but I do only 1 Obs: at a time(1st).
# act = react_act(out, "first")
# actinput = react_actinput(out, "first")
# toolname = toolNameBeingCalled(act, a.tools)
# if toolname == "chatbox"
# return actinput
# else # function call
# toolresult = a.tools[toolname][:func](actinput)
# Obs = "Obs: $toolresult\n" # observe in ReAct agent
# work(a, Obs)
# end
# end
# else # continue thought
# usermsg = "Obs: $usermsg"
# prompt = a.thought * usermsg
# output = sendReceivePrompt(a, prompt)
# obs = count(output["text"], "Obs:")
# out = split(output["text"], "Obs:")[1]
# a.thought = prompt * out
# if obs == 0 # llm config has too short characters generation
# error("No Obs: detected. Probably LLM config has too short max_tokens generation")
# elseif obs == 1 # first conversation
# act = react_act(out, "first")
# actinput = react_actinput(out, "first")
# toolname = toolNameBeingCalled(act, a.tools)
# if toolname == "chatbox"
# return actinput
# else # function call
# toolresult = a.tools[toolname][:func](actinput)
# Obs = "Obs: $toolresult\n" # observe in ReAct agent
# work(a, Obs)
# end
# else # later conversation
# act = react_act(out, "last")
# actinput = react_actinput(out, "last")
# toolname = toolNameBeingCalled(act, a.tools)
# if toolname == "chatbox"
# return actinput
# else # function call
# toolresult = a.tools[toolname][:func](actinput)
# Obs = "Obs: $toolresult\n" # observe in ReAct agent
# work(a, Obs)
# end
# end
# end
# else
# error("user intent $userIntent not define $(@__LINE__)")
# end
# end
function workContinue(a::agent)
end
#TESTING
function identifyUserIntention(a::agent, usermsg::String)
prompt =
"""
<|im_start|>system
You are a helpful assistant. Your job is to determine intention of the question.
Your choices are:
chat: normal conversation that you don't need to do something.
task: a request for you to do something.
Here are examples of how to determine intention of the question:
Question: How are you?
Answer: {chat}, this question intention is about chat.
Question: Ummm.
Answer: {chat}, this question is about chat.
Question: Search for the stock prices of 'BJC' and 'IOU'on June 10th.
Answer: {task}, this question is about asking you to do something.
Question: Hello
Answer: {chat}, this question is about chat.
Question: 'BJC' and 'IOU'on June 10th.
Answer: {task}, this question is about asking you to do something.
Question: What time is it?
Answer: {task}, this question is about asking you to do something.
Question: What is the price of 'ABC'
Answer: {task}, this question is about asking you to do something.
Question: Do you like coconut?
Answer: {task}, this question is about asking you to do something.
Question: What is the weather tomorrow?
Answer: {task}, this question is about asking you to do something.
Question: I'm fine.
Answer: {chat}, this question is about chat.
Question: How to make a cake?
Answer: {task}, this question is about asking you to do something.
Question: Did you turn off the light?
Answer: {task}, this question is about asking you to do something.
Question: What is stock price of Tesla on June 6th?
Answer: {task}, this question is about asking you to do something.
Question: Tell me some jokes.
Answer: {chat}, this question is about chat.
Begin!
Here are the context for the question:
{context}
<|im_end|>
<|im_start|>user
Question: {input}
<|im_end|>
<|im_start|>assistant
"""
prompt = replace(prompt, "{input}" => usermsg)
result = sendReceivePrompt(a, prompt)
answer = result === nothing ? nothing : GeneralUtils.getStringBetweenCharacters(result, "{", "}")
return answer
end
function sendReceivePrompt(a::agent, prompt::String; timeout::Int=30)
a.msgMeta[:msgId] = "$(uuid4())" # new msg id for each msg
msg = Dict(
:msgMeta=> a.msgMeta,
:txt=> prompt,
)
payloadChannel = Channel(1)
# send prompt
CommUtils.request(a.mqttClient, msg)
starttime = Dates.now()
result = nothing
while true
timepass = (Dates.now() - starttime).value / 1000.0
CommUtils.mqttRun(a.mqttClient, payloadChannel)
if isready(payloadChannel)
topic, payload = take!(payloadChannel)
if payload[:msgMeta][:repondToMsgId] == msg[:msgMeta][:msgId]
result = haskey(payload, :txt) ? payload[:txt] : nothing
break
end
elseif timepass <= timeout
# skip, within waiting period
elseif timepass > timeout
result = nothing
break
else
error("undefined condition $(@__LINE__)")
end
end
return result
end
function toolNameBeingCalled(act::String, tools::Dict)
toolNameBeingCalled = nothing
for (k, v) in tools
toolname = String(k)
if contains(act, toolname)
toolNameBeingCalled = toolname
break
end
end
return toolNameBeingCalled
end
#TODO
function checkReasonableness(userMsg::String, context::String, tools)
# Ref: https://www.youtube.com/watch?v=XV4IBaZqbps
prompt =
"""
<|im_start|>system
You are a helpful assistant. Your job is to check the reasonableness of user questions.
If the user question can be answered given the tools available say, "This is a reasonable question".
If the user question cannot be answered then provide some feedback to the user that may improve
their question.
Here is the context for the question:
{context}
<|im_end|>
<|im_start|>user
{question}
<|im_end|>
<|im_start|>assistant
"""
context = "You have access to the following tools:
WineStock: useful for when you need to find info about wine by matching your description, price, name or ID. Input should be a search query with as much details as possible."
prompt = replace(prompt, "{question}" => userMsg)
prompt = replace(prompt, "{context}" => context)
output_py = llm(
prompt,
max_tokens=512,
temperature=0.1,
# top_p=top_p,
echo=false,
stop=["", "<>", ],
)
_output_jl = pyconvert(Dict, output_py);
output = pyconvert(Dict, _output_jl["choices"][1]);
output["text"]
end
function react_plan(text::String, firstlast="first")
"Plan" * GeneralUtils.getStringBetweenCharacters(text, "Plan", "Thought", firstlast=firstlast)
end
function react_thought(text::String, firstlast="first")
"Thought" * GeneralUtils.getStringBetweenCharacters(text, "Thought", "Act", firstlast=firstlast)
end
function react_act(text::String, firstlast="first")
"Act" * GeneralUtils.getStringBetweenCharacters(text, "Act", "ActInput", firstlast=firstlast)
end
function react_actinput(text::String, firstlast="first")
"ActInput" * GeneralUtils.getStringBetweenCharacters(text, "ActInput", "Obs", firstlast=firstlast)
end
"""
Detect given characters. Output is a list of named tuple of detected char.
```jldoctest
julia> text = "I like to eat apples and use utensils."
julia> characters = ["eat", "use", "i"]
julia> result = detectCharacters(text, characters)
4-element Vector{Any}:
(char = "i", startInd = 4, endInd = 4)
(char = "eat", startInd = 11, endInd = 13)
(char = "use", startInd = 26, endInd = 28)
(char = "i", startInd = 35, endInd = 35)
```
"""
function detectCharacters(text::T, characters::Vector{T}) where {T<:AbstractString}
result = []
for i in eachindex(text)
for char in characters
l = length(char)
char_startInd = i
char_endInd = i+l-1 # -1 because Julia use inclusive index
if char_endInd > length(text)
# skip
else
if text[char_startInd: char_endInd] == char
push!(result, (char=char, startInd=char_startInd, endInd=char_endInd))
end
end
end
end
return result
end
"""
Find a given character from a vector of named tuple.
Output is character location index inside detectedCharacters
```jldoctest
julia a = [ (char = "i", startInd = 4, endInd = 4)
(char = "eat", startInd = 11, endInd = 13)
(char = "use", startInd = 26, endInd = 28)
(char = "i", startInd = 35, endInd = 35) ]
julia> findDetectedCharacter(a, "i")
[1, 4]
```
"""
function findDetectedCharacter(detectedCharacters, character)
allchar = [i[1] for i in detectedCharacters]
return findall(isequal.(allchar, character))
end
end # module