module interface export agent, addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix, generatePrompt_tokenSuffix, conversation using JSON3, DataStructures, Dates, UUIDs using CommUtils # ---------------------------------------------------------------------------- # # 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 @kwdef mutable struct agent availableRole = ["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"), ] """ # Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 # messages= [Dict(:role=>"system", :content=> "", :timestamp=> Dates.now()),] messages = [] thougt::String = "" # internal thinking area info::String = "" # additional info end function agent( agentName::String, mqttClientSpec::NamedTuple; systemMessage::String="You are a helpful assistant.", # system message of an agent 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 = agent() newAgent.availableRole = availableRole newAgent.maxUserMsg = maxUserMsg systemMessage_ = "Your name is $agentName. " * systemMessage push!(newAgent.messages, Dict(:role=>"system", :content=> systemMessage_, :timestamp=> Dates.now()))[,] newAgent.mqttClient = CommUtils.mqttClient(mqttClientSpec) newAgent.msgMeta = msgMeta return newAgent end # @kwdef mutable struct agentLangchain # availableRole=["system", "user", "assistant"] # maxUserMsg::Int= 10 # llmAIRequestTopic_openblas = "llm/openblas/request" # llmAIRequestTopic_gpu = "llm/api/v0.0.1/gpu/request" # self_llmReceiveTopic = "chatbothub/llm/respond" # """ 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"), # ] # """ # # Ref: https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 # # # messages=[Dict(:role=>"system", :content=> "You are a helpful assistant.", :timestamp=> Dates.now()),] # end """ add new message to agent # Example ```jldoctest julia> addNewMessage(agent1, "user", "Where should I go to buy snacks") ```` """ function addNewMessage(a::agent, role::String, content::String) 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::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::agent) if length(a.messages) > 1 pop!(a.messages) end end function generatePrompt_tokenSuffix(a::agent; 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::agent; 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 conversation(a::agent, usermsg::String) addNewMessage(a, "user", usermsg) userIntent = identifyUserIntention(a, usermsg) @show userIntent #WORKING 1) add if-else user intention logic. 2) add recursive thinking if userIntent == "chat" generatePrompt_tokenPrefix(a, userToken="Q:", assistantToken="A:") elseif userIntent == "task" else error("user intent $userIntent not define $(@__LINE__)") end end #TESTING function identifyUserIntention(a::agent, usermsg::String) identify_usermsg = """ You are 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! Question: {input} """ identify_usermsg = replace(identify_usermsg, "{input}" => usermsg) result = sendPrompt(a, identify_usermsg, ) # msg = Dict( # :msgMeta=> a.msgMeta, # :txt=> identify_usermsg, # ) # payloadChannel = Channel(1) # # send prompt # CommUtils.request(a.mqttClient, msg, pubtopic=a.mqttClient.pubtopic.llmAI) # starttime = Dates.now() # timeout = 10 # 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 = payload[:txt] # break # end # elseif timepass <= timeout # # skip, within waiting period # elseif timepass > timeout # result = nothing # break # else # error("undefined condition $(@__LINE__)") # end # end answer = result === nothing ? nothing : GeneralUtils.getStringBetweenCharacters(result, "{", "}") return answer end function sendPrompt(a::agent, prompt::String; timeout::Int=10) a.msgMeta[:msgId] = "$(uuid4())" msg = Dict( :msgMeta=> a.msgMeta, :txt=> prompt, ) payloadChannel = Channel(1) #WORKING 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 = payload[:txt] break end elseif timepass <= timeout # skip, within waiting period elseif timepass > timeout result = nothing break else error("undefined condition $(@__LINE__)") end end # function getStringBetweenCurlyBraces(s::AbstractString) # m = match(r"\{(.+?)\}", s) # m = m == "" ? "" : m.captures[1] # return m # end # function getStringBetweenCharacters(text::AbstractString, startChar::String, endChar::String) # startIndex= findlast(startChar, text) # endIndex= findlast(endChar, text) # if startIndex === nothing || endIndex === nothing # return nothing # else # return text[startIndex.stop+1: endIndex.start-1] # end # end end # module