1017 lines
34 KiB
Julia
Executable File
1017 lines
34 KiB
Julia
Executable File
module interface
|
|
|
|
|
|
export agentReact, addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix,
|
|
generatePrompt_tokenSuffix, conversation, work, detectCharacters, chunktext,
|
|
findDetectedCharacter, wikisearch
|
|
|
|
using JSON3, DataStructures, Dates, UUIDs, HTTP
|
|
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 for ReAct agent only
|
|
thoughtround::Int = 0 # no. of thinking round
|
|
thoughtlimit::Int = 5 # thinking round limit
|
|
thinkingMode::Union{Dict, Nothing} = nothing
|
|
end
|
|
|
|
function agentReact(
|
|
agentName::String,
|
|
mqttClientSpec::NamedTuple;
|
|
role::Symbol=:assistant,
|
|
roles::Dict=Dict(
|
|
:assistant =>
|
|
"""
|
|
You are a helpful assistant. You don't know other people personal info previously.
|
|
""",
|
|
: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 provide a personalized recommendation of up to two wines based on the user's preference, and you describe the benefits of each wine in detail.
|
|
You don't know other people personal info previously.
|
|
|
|
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
|
|
""",
|
|
),
|
|
thinkingMode::Dict=Dict(
|
|
:nothinking=> "",
|
|
:react=>
|
|
"""Use the following format:
|
|
Question: 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 according to the plan (pay attention to correct numeral calculation and commonsense).
|
|
Act: the tool that match your thought, should be one of {toolnames}
|
|
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 can repeat N times until you know the answer.)
|
|
Thought: I think I know the answer
|
|
Answer: Answer of the original question
|
|
|
|
Begin!""",
|
|
),
|
|
tools::Dict=Dict(
|
|
:wikisearch=>Dict(
|
|
:name => "wikisearch",
|
|
:description => "Useful for when you need to search the Internet",
|
|
:input => "Input should be keywords not a question.",
|
|
:output => "",
|
|
:func => wikisearch, # put function here
|
|
),
|
|
: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 => "" ,
|
|
:func => nothing,
|
|
),
|
|
: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 => "" ,
|
|
:func => nothing,
|
|
),
|
|
# :NTHING=>Dict(
|
|
# :name => "NTHING",
|
|
# :description => "useful for when you don't need to use tools or actions",
|
|
# :input => "No input is needed",
|
|
# :output => "" ,
|
|
# :func => nothing,
|
|
# ),
|
|
),
|
|
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
|
|
newAgent.thinkingMode = thinkingMode
|
|
|
|
return newAgent
|
|
end
|
|
|
|
|
|
"""
|
|
add new message to agent
|
|
# Example
|
|
```jldoctest
|
|
julia> addNewMessage(agent1, "user", "Where should I go to buy snacks")
|
|
````
|
|
"""
|
|
function addNewMessage(a::T1, role::String, content::T2) where {T1<:agent, T2<:AbstractString}
|
|
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_mistral_openorca(a::T, usermsg::String, role::Symbol) where {T<:agent}
|
|
# prompt =
|
|
# """
|
|
# <|im_start|>system
|
|
# {systemMsg}
|
|
# <|im_end|>
|
|
# Here are the context for the question:
|
|
# {context}
|
|
# """
|
|
# prompt = replace(prompt, "{systemMsg}" => a.roles[role])
|
|
|
|
# toolnames = ""
|
|
# toollines = ""
|
|
# for (toolname, v) in a.tools
|
|
# toolline = "$toolname: $(v[:description]) $(v[:input]) $(v[:output])\n"
|
|
# toollines *= toolline
|
|
# toolnames *= "$toolname,"
|
|
# end
|
|
# prompt = replace(prompt, "{toolnames}" => toolnames)
|
|
# prompt = replace(prompt, "{tools}" => toollines)
|
|
|
|
# prompt = replace(prompt, "{context}" => a.context)
|
|
|
|
# prompt *= "<|im_start|>user\n" * usermsg * "\n<|im_end|>\n"
|
|
# prompt *= "<|im_start|>assistant\n"
|
|
|
|
# return prompt
|
|
# end
|
|
|
|
function generatePrompt_mistral_openorca(a::T, usermsg::String,
|
|
thinkingMode::Symbol=:nothinking) where {T<:agent}
|
|
|
|
prompt =
|
|
"""
|
|
<|im_start|>system
|
|
{systemMsg}
|
|
You have access to the following tools:
|
|
{tools}
|
|
{thinkingMode}
|
|
<|im_end|>
|
|
Here are the context for the question:
|
|
{context}
|
|
"""
|
|
prompt = replace(prompt, "{systemMsg}" => a.roles[a.role])
|
|
prompt = replace(prompt, "{thinkingMode}" => a.thinkingMode[thinkingMode])
|
|
toolnames = ""
|
|
toollines = ""
|
|
for (toolname, v) in a.tools
|
|
toolline = "$toolname: $(v[:description]) $(v[:input]) $(v[:output])\n"
|
|
toollines *= toolline
|
|
toolnames *= "$toolname,"
|
|
end
|
|
prompt = replace(prompt, "{toolnames}" => toolnames)
|
|
prompt = replace(prompt, "{tools}" => toollines)
|
|
|
|
prompt = replace(prompt, "{context}" => a.context)
|
|
|
|
prompt *= "<|im_start|>user\nQuestion: " * usermsg * "\n<|im_end|>\n"
|
|
prompt *= "<|im_start|>assistant\n"
|
|
|
|
return prompt
|
|
end
|
|
|
|
"""
|
|
Chat with llm.
|
|
|
|
```jldoctest
|
|
julia> using JSON3, UUIDs, Dates, FileIO, CommUtils, ChatAgent
|
|
julia> mqttClientSpec = (
|
|
clientName= "someclient", # name of this client
|
|
clientID= "$(uuid4())",
|
|
broker= "mqtt.yiem.ai",
|
|
pubtopic= (imgAI="img/api/v0.0.1/gpu/request",
|
|
txtAI="txt/api/v0.1.0/gpu/request"),
|
|
subtopic= (imgAI="agent/api/v0.1.0/img/respond",
|
|
txtAI="agent/api/v0.1.0/txt/respond"),
|
|
keepalive= 30,
|
|
)
|
|
julia> msgMeta = Dict(
|
|
:msgPurpose=> "updateStatus",
|
|
:from=> "agent",
|
|
:to=> "llmAI",
|
|
:requestrespond=> "request",
|
|
:sendto=> "", # destination topic
|
|
:replyTo=> "agent/api/v0.1.0/txt/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())",
|
|
)
|
|
julia> newAgent = ChatAgent.agentReact(
|
|
"Jene",
|
|
mqttClientSpec,
|
|
role=:assistant_react,
|
|
msgMeta=msgMeta
|
|
)
|
|
julia> respond = ChatAgent.conversation(newAgent, "Hi! how are you?")
|
|
```
|
|
"""
|
|
function conversation(a::T, usermsg::String) where {T<:agent}
|
|
respond = nothing
|
|
|
|
if a.thought != "nothing" # continue thought
|
|
_ = addNewMessage(a, "user", usermsg)
|
|
a.thought *= "Obs $(a.thoughtround): $usermsg\n"
|
|
prompt = a.thought
|
|
respond = work(a, prompt)
|
|
else # new thought
|
|
thinkingmode = chooseThinkingMode(a, usermsg)
|
|
@show thinkingmode
|
|
if thinkingmode == :nothinking
|
|
a.context = conversationSummary(a) #TODO should be long conversation before use summary because it leaves out details
|
|
_ = addNewMessage(a, "user", usermsg)
|
|
prompt = generatePrompt_mistral_openorca(a, usermsg, thinkingmode)
|
|
@show prompt
|
|
respond = sendReceivePrompt(a, prompt)
|
|
respond = split(respond, "<|im_end|>")[1]
|
|
respond = replace(respond, "\n" => "")
|
|
_ = addNewMessage(a, "assistant", respond)
|
|
@show respond
|
|
elseif thinkingmode == :react
|
|
a.context = conversationSummary(a)
|
|
_ = addNewMessage(a, "user", usermsg)
|
|
prompt = generatePrompt_mistral_openorca(a, usermsg, thinkingmode)
|
|
respond = work(a, prompt)
|
|
else
|
|
error("undefined condition thinkingmode = $thinkingmode")
|
|
end
|
|
end
|
|
|
|
return respond
|
|
end
|
|
|
|
"""
|
|
Continuously run llm functions except when llm is getting Answer: or chatbox.
|
|
"""
|
|
function work(a::T, prompt::String, maxround::Int=3) where {T<:agent}
|
|
respond = nothing
|
|
while true
|
|
a.thoughtround += 1
|
|
@show a.thoughtround
|
|
toolname = nothing
|
|
toolinput = nothing
|
|
|
|
#WORKING force answer if thoughtround exceed limit
|
|
if a.thoughtround > a.thoughtlimit
|
|
a.thought *= "Thought $(a.thoughtround): I think I know the answer."
|
|
prompt = a.thought
|
|
end
|
|
@show prompt
|
|
respond = sendReceivePrompt(a, prompt)
|
|
|
|
headerToDetect = nothing
|
|
if a.thoughtround == 1
|
|
try
|
|
respond = split(respond, "Obs:")[1]
|
|
headerToDetect = ["Question:", "Plan:", "Thought:", "Act:", "ActInput:", "Obs:", "...", "Answer:",
|
|
"Conclusion:", "Summary:"]
|
|
catch
|
|
end
|
|
else
|
|
try
|
|
respond = split(respond, "Obs $(a.thoughtround):")[1]
|
|
headerToDetect = ["Question $(a.thoughtround):", "Plan $(a.thoughtround):",
|
|
"Thought $(a.thoughtround):", "Act $(a.thoughtround):",
|
|
"ActInput $(a.thoughtround):", "Obs $(a.thoughtround):",
|
|
"...", "Answer:",
|
|
"Conclusion:", "Summary:"]
|
|
catch
|
|
end
|
|
end
|
|
@show respond
|
|
headers = detectCharacters(respond, headerToDetect)
|
|
chunkedtext = chunktext(respond, headers)
|
|
|
|
Answer = findDetectedCharacter(headers, "Answer:")
|
|
AnswerInd = length(Answer) != 0 ? Answer[1] : nothing
|
|
Act = findDetectedCharacter(headers, "Act $(a.thoughtround):")
|
|
if length(Answer) == 1 && length(Act) == 0
|
|
a.thought = "nothing" # question finished, no more thought
|
|
a.thoughtround = 0
|
|
respond = chunkedtext[AnswerInd][:body]
|
|
_ = addNewMessage(a, "assistant", respond)
|
|
break
|
|
else
|
|
|
|
# check for tool being called
|
|
ActHeader = a.thoughtround == 1 ? "Act:" : "Act $(a.thoughtround):"
|
|
if length(findDetectedCharacter(headers, ActHeader)) != 0 # check whether there is Act: in a respond
|
|
ActInd = findDetectedCharacter(headers, ActHeader)[1]
|
|
toolname = toolNameBeingCalled(chunkedtext[ActInd][:body], a.tools)
|
|
end
|
|
ActInputHeader = a.thoughtround == 1 ? "ActInput:" : "ActInput $(a.thoughtround):"
|
|
if length(findDetectedCharacter(headers, ActInputHeader)) != 0 # check whether there is ActInput: in a respond
|
|
ActInputInd = findDetectedCharacter(headers, ActInputHeader)[1]
|
|
toolinput = chunkedtext[ActInputInd][:body]
|
|
end
|
|
|
|
# clean up
|
|
if occursin(" \"", toolinput)
|
|
toolinput = GeneralUtils.getStringBetweenCharacters(toolinput, " \"", "\"\n")
|
|
else
|
|
toolinput = GeneralUtils.getStringBetweenCharacters(toolinput, " ", "\n")
|
|
end
|
|
@show toolname #BUG llm not specify tools
|
|
@show toolinput
|
|
if toolname === nothing || toolinput === nothing
|
|
println("toolname $toolname toolinput $toolinput retry thinking")
|
|
a.thoughtround -= 1
|
|
continue
|
|
end
|
|
|
|
if a.thought == "nothing"
|
|
thought = ""
|
|
for i in chunkedtext
|
|
header = i[:header]
|
|
header = replace(header, ":"=>" $(a.thoughtround):") # add number so that llm not confused
|
|
body = i[:body]
|
|
thought *= "$header $body"
|
|
end
|
|
a.thought = prompt * thought #BUG should be prompt + thought
|
|
else
|
|
a.thought *= respond
|
|
end
|
|
|
|
|
|
if toolname == "chatbox" # chat with user
|
|
a.thought *= toolinput
|
|
respond = toolinput
|
|
_ = addNewMessage(a, "assistant", respond)
|
|
break
|
|
else # function call
|
|
println("//////////// $(a.thoughtround)")
|
|
f = a.tools[Symbol(toolname)][:func]
|
|
_result = f(toolinput)
|
|
if _result != "No info available." #TODO for use with wikisearch(). Not good for other tools
|
|
_result = makeSummary(a, _result)
|
|
end
|
|
result = "Obs $(a.thoughtround): $_result\n"
|
|
a.thought *= result
|
|
prompt = a.thought
|
|
end
|
|
end
|
|
end
|
|
@show respond
|
|
return respond
|
|
end
|
|
|
|
# function work(a::T, prompt::String, maxround::Int=3) where {T<:agent}
|
|
# respond = nothing
|
|
# while true
|
|
# a.thoughtround += 1
|
|
# toolname = nothing
|
|
# toolinput = nothing
|
|
|
|
# if a.thoughtround > a.thoughtlimit
|
|
# a.thought *= "Thought $(a.thoughtround): I think I know the answer."
|
|
# prompt = a.thought
|
|
# end
|
|
# @show prompt
|
|
# respond = sendReceivePrompt(a, prompt)
|
|
|
|
# headerToDetect = nothing
|
|
# if a.thoughtround == 1
|
|
# try
|
|
# respond = split(respond, "Obs:")[1]
|
|
# @show respond
|
|
# headerToDetect = ["Question:", "Plan:", "Thought:", "Act:", "ActInput:", "Obs:", "...", "Answer:",
|
|
# "Conclusion:", "Summary:"]
|
|
# catch
|
|
# end
|
|
# else
|
|
# try
|
|
# respond = split(respond, "Obs $(a.thoughtround):")[1]
|
|
# @show respond
|
|
# headerToDetect = ["Question $(a.thoughtround):", "Plan $(a.thoughtround):",
|
|
# "Thought $(a.thoughtround):", "Act $(a.thoughtround):",
|
|
# "ActInput $(a.thoughtround):", "Obs $(a.thoughtround):",
|
|
# "...", "Answer:",
|
|
# "Conclusion:", "Summary:"]
|
|
# catch
|
|
# end
|
|
# end
|
|
|
|
# headers = detectCharacters(respond, headerToDetect)
|
|
# chunkedtext = chunktext(respond, headers)
|
|
# @show chunkedtext
|
|
|
|
# if a.thought == "nothing"
|
|
# thought = ""
|
|
# for i in chunkedtext
|
|
# header = i[:header]
|
|
# header = replace(header, ":"=>" $(a.thoughtround):") # add number so that llm not confused
|
|
# body = i[:body]
|
|
# thought *= "$header $body"
|
|
# end
|
|
# a.thought = prompt * thought
|
|
# else
|
|
# a.thought *= respond
|
|
# end
|
|
|
|
# Answer = findDetectedCharacter(headers, "Answer:")
|
|
# AnswerInd = length(Answer) != 0 ? Answer[1] : nothing
|
|
# Act = findDetectedCharacter(headers, "Act $(a.thoughtround):")
|
|
# if length(Answer) == 1 && length(Act) == 0
|
|
# a.thought = "nothing" # question finished, no more thought
|
|
# a.thoughtround = 0
|
|
# respond = chunkedtext[AnswerInd][:body]
|
|
# _ = addNewMessage(a, "assistant", respond)
|
|
# break
|
|
# else
|
|
# # check for tool being called
|
|
# ActHeader = a.thoughtround == 1 ? "Act:" : "Act $(a.thoughtround):"
|
|
# ActInd = findDetectedCharacter(headers, ActHeader)[1]
|
|
# toolname = toolNameBeingCalled(chunkedtext[ActInd][:body], a.tools)
|
|
# toolinput = chunkedtext[ActInd+1][:body]
|
|
# if occursin(" \"", toolinput)
|
|
# toolinput = GeneralUtils.getStringBetweenCharacters(toolinput, " \"", "\"\n")
|
|
# else
|
|
# toolinput = GeneralUtils.getStringBetweenCharacters(toolinput, " ", "\n")
|
|
# end
|
|
# @show toolname #BUG llm not specify tools
|
|
# @show toolinput
|
|
|
|
|
|
# if toolname == "chatbox" # chat with user
|
|
# a.thought *= toolinput
|
|
# respond = toolinput
|
|
# _ = addNewMessage(a, "assistant", respond)
|
|
# break
|
|
# else # function call
|
|
# println("//////////// $(a.thoughtround)")
|
|
# f = a.tools[Symbol(toolname)][:func]
|
|
# _result = f(toolinput)
|
|
# if _result != "No info available." #TODO for use with wikisearch(). Not good for other tools
|
|
# _result = makeSummary(a, _result)
|
|
# end
|
|
# result = "Obs $(a.thoughtround): $_result\n"
|
|
# a.thought *= result
|
|
# prompt = a.thought
|
|
# end
|
|
# end
|
|
# end
|
|
# @show respond
|
|
# return respond
|
|
# end
|
|
|
|
|
|
"""
|
|
make a conversation summary.
|
|
```jldoctest
|
|
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(conversation)
|
|
```
|
|
"""
|
|
function conversationSummary(a::T) where {T<:agent}
|
|
prompt =
|
|
"""
|
|
<|im_start|>system
|
|
You talked with a user earlier.
|
|
Now you make a detailed bullet summary of the conversation from your perspective.
|
|
|
|
Here are the context:
|
|
{context}
|
|
Here are the conversation:
|
|
{conversation}
|
|
<|im_end|>
|
|
|
|
"""
|
|
conversation = ""
|
|
summary = "nothing"
|
|
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(prompt, "{conversation}" => conversation)
|
|
prompt = replace(prompt, "{context}" => a.context)
|
|
result = sendReceivePrompt(a, prompt)
|
|
summary = result === nothing ? "nothing" : result
|
|
summary = replace(summary, "<|im_end|>" => "")
|
|
if summary[1:1] == "\n"
|
|
summary = summary[2:end]
|
|
end
|
|
end
|
|
|
|
return summary
|
|
end
|
|
|
|
function makeSummary(a::T1, input::T2) where {T1<:agent, T2<:AbstractString}
|
|
prompt =
|
|
"""
|
|
<|im_start|>system
|
|
You are a helpful assistant.
|
|
Your job is to make a concise summary of a given text.
|
|
If you can't summarize say, "No info available.".
|
|
<|im_end|>
|
|
|
|
<|im_start|>user
|
|
{input}
|
|
<|im_end|>
|
|
<|im_start|>assistant
|
|
|
|
"""
|
|
prompt = replace(prompt, "{input}" => input)
|
|
result = sendReceivePrompt(a, prompt)
|
|
summary = result === nothing ? "nothing" : result
|
|
summary = replace(summary, "<|im_end|>" => "")
|
|
if summary[1:1] == "\n"
|
|
summary = summary[2:end]
|
|
end
|
|
|
|
return summary
|
|
end
|
|
|
|
function chooseThinkingMode(a::T, usermsg::String) where {T<:agent}
|
|
prompt =
|
|
"""
|
|
<|im_start|>system
|
|
{systemMsg}
|
|
You have access to the following tools:
|
|
{tools}
|
|
Your need to determine now whether you will use tools or actions to answer the question.
|
|
|
|
You have the following choices:
|
|
If you don't need tools or actions to answer the question say, "{no}".
|
|
If you need tools or actions to answer the question say, "{yes}".
|
|
<|im_end|>
|
|
|
|
<|im_start|>user
|
|
{input}
|
|
<|im_end|>
|
|
<|im_start|>assistant
|
|
|
|
"""
|
|
toollines = ""
|
|
for (toolname, v) in a.tools
|
|
if toolname ∉ ["chatbox", "nothing"]
|
|
toolline = "$toolname: $(v[:description]) $(v[:input]) $(v[:output])\n"
|
|
toollines *= toolline
|
|
end
|
|
end
|
|
prompt = replace(prompt, "{systemMsg}" => a.roles[a.role])
|
|
prompt = replace(prompt, "{tools}" => toollines)
|
|
prompt = replace(prompt, "{input}" => usermsg)
|
|
result = sendReceivePrompt(a, prompt)
|
|
willusetools = GeneralUtils.getStringBetweenCharacters(result, "{", "}")
|
|
thinkingMode = willusetools == "yes" ? :react : :nothinking
|
|
|
|
return thinkingMode
|
|
end
|
|
|
|
|
|
function identifyUserIntention(a::T, usermsg::String) where {T<:agent}
|
|
prompt =
|
|
"""
|
|
<|im_start|>system
|
|
You are a helpful assistant. Your job is to determine intention of the question.
|
|
|
|
You have the following choices:
|
|
If the user question is about general conversation say, "{chat}".
|
|
If the user question is about getting wine say, "{wine}".
|
|
<|im_end|>
|
|
|
|
Here are the context for the question:
|
|
{context}
|
|
|
|
<|im_start|>user
|
|
{input}
|
|
<|im_end|>
|
|
<|im_start|>assistant
|
|
|
|
"""
|
|
prompt = replace(prompt, "{context}" => "")
|
|
prompt = replace(prompt, "{input}" => usermsg)
|
|
|
|
result = sendReceivePrompt(a, prompt)
|
|
answer = result === nothing ? nothing : GeneralUtils.getStringBetweenCharacters(result, "{", "}")
|
|
|
|
return answer
|
|
end
|
|
|
|
"""
|
|
Send a msg to registered mqtt topic within mqttClient.
|
|
|
|
```jldoctest
|
|
julia> using JSON3, UUIDs, Dates, FileIO, CommUtils, ChatAgent
|
|
julia> mqttClientSpec = (
|
|
clientName= "someclient", # name of this client
|
|
clientID= "$(uuid4())",
|
|
broker= "mqtt.yiem.ai",
|
|
pubtopic= (imgAI="img/api/v0.0.1/gpu/request",
|
|
txtAI="txt/api/v0.1.0/gpu/request"),
|
|
subtopic= (imgAI="agent/api/v0.1.0/img/respond",
|
|
txtAI="agent/api/v0.1.0/txt/respond"),
|
|
keepalive= 30,
|
|
)
|
|
julia> msgMeta = Dict(
|
|
:msgPurpose=> "updateStatus",
|
|
:from=> "agent",
|
|
:to=> "llmAI",
|
|
:requestrespond=> "request",
|
|
:sendto=> "", # destination topic
|
|
:replyTo=> "agent/api/v0.1.0/txt/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())",
|
|
)
|
|
julia> newAgent = ChatAgent.agentReact(
|
|
"Jene",
|
|
mqttClientSpec,
|
|
role=:assistant_react,
|
|
msgMeta=msgMeta
|
|
)
|
|
```
|
|
"""
|
|
function sendReceivePrompt(a::T, prompt::String; timeout::Int=120) where {T<:agent}
|
|
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
|
|
println("sendReceivePrompt timeout $timepass/$timeout")
|
|
result = nothing
|
|
break
|
|
else
|
|
error("undefined condition $(@__LINE__)")
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
"""
|
|
Extract toolname from text.
|
|
```jldoctest
|
|
julia> text = " internetsearch\n"
|
|
julia> tools = 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 => "" ,
|
|
),
|
|
)
|
|
julia> toolname = toolNameBeingCalled(text, tools)
|
|
```
|
|
"""
|
|
function toolNameBeingCalled(text::T, tools::Dict) where {T<:AbstractString}
|
|
toolNameBeingCalled = nothing
|
|
for (k, v) in tools
|
|
toolname = String(k)
|
|
if contains(text, toolname)
|
|
toolNameBeingCalled = toolname
|
|
break
|
|
end
|
|
end
|
|
return toolNameBeingCalled
|
|
end
|
|
|
|
|
|
function answerNow(a::T) where {T<:agent}
|
|
prompt =
|
|
"""
|
|
<|im_start|>system
|
|
{systemMsg}
|
|
Your need to determine now whether you will use tools or actions to answer the question.
|
|
|
|
You have the following choices:
|
|
If you don't need tools or actions to answer the question say, "{no}".
|
|
If you need tools or actions to answer the question say, "{yes}".
|
|
<|im_end|>
|
|
"""
|
|
prompt = replace(prompt, "{systemMsg}" => a.thought)
|
|
|
|
error("answerNow done")
|
|
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=["</response>", "<<END>>", ],
|
|
)
|
|
_output_jl = pyconvert(Dict, output_py);
|
|
output = pyconvert(Dict, _output_jl["choices"][1]);
|
|
output["text"]
|
|
|
|
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", start = 4, stop = 4)
|
|
(char = "eat", start = 11, stop = 13)
|
|
(char = "use", start = 26, stop = 28)
|
|
(char = "i", start = 35, stop = 35)
|
|
```
|
|
"""
|
|
function detectCharacters(text::T1, characters::Vector{T2}) where {T1<:AbstractString, T2<: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
|
|
try # some time StringIndexError: invalid index [535], valid nearby indices [534]=>'é', [536]=>' '
|
|
if text[char_startInd: char_endInd] == char
|
|
push!(result, (char=char, start=char_startInd, stop=char_endInd))
|
|
end
|
|
catch
|
|
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", start = 4, stop = 4)
|
|
(char = "eat", start = 11, stop = 13)
|
|
(char = "use", start = 26, stop = 28)
|
|
(char = "i", start = 35, stop = 35) ]
|
|
julia> findDetectedCharacter(a, "i")
|
|
[1, 4]
|
|
```
|
|
"""
|
|
function findDetectedCharacter(detectedCharacters, character)
|
|
allchar = [i[1] for i in detectedCharacters]
|
|
return findall(isequal.(allchar, character))
|
|
end
|
|
|
|
"""
|
|
Chunk a text into smaller pieces by header.
|
|
```jldoctest
|
|
julia> using ChatAgent
|
|
julia> text = "Plan: First, we need to find out what kind of wine the user wants."
|
|
julia> headers = ChatAgent.detectCharacters(text, ["Nope", "sick", "First", "user", "Then", ])
|
|
3-element Vector{Any}:
|
|
(char = "First", start = 7, stop = 11)
|
|
(char = "user", start = 56, stop = 59)
|
|
(char = "Then", start = 102, stop = 105)
|
|
julia> chunkedtext = ChatAgent.chunktext(text, headers)
|
|
2-element Vector{Any}:
|
|
(header = "First", body = ", we need to find out what kind of wine the ")
|
|
(header = "user", body = " wants.")
|
|
```
|
|
"""
|
|
function chunktext(text::T, headers) where {T<:AbstractString}
|
|
result = []
|
|
|
|
for (i, v) in enumerate(headers)
|
|
if i < length(headers)
|
|
nextheader = headers[i+1]
|
|
body = text[v[:stop]+1: nextheader[:start]-1]
|
|
push!(result, (header=v[:char], body=body))
|
|
else
|
|
body = text[v[:stop]+1: end]
|
|
push!(result, (header=v[:char], body=body))
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
function wikisearch(phrase::T) where {T<:AbstractString}
|
|
url = "https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts&titles=$(replace(phrase, " " => "%20"))&exintro=1&explaintext=1"
|
|
response = HTTP.get(url)
|
|
json_data = JSON3.read(String(response.body))
|
|
page_id = first(keys(json_data["query"]["pages"]))
|
|
if page_id == "-1"
|
|
return "Sorry, I couldn't find any Wikipedia page for the given phrase."
|
|
end
|
|
|
|
result = nothing
|
|
try
|
|
result = json_data["query"]["pages"][page_id]["extract"]
|
|
catch
|
|
result = "No info available."
|
|
end
|
|
if result == ""
|
|
result = "No info available."
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end # module |