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=["", "<>", ], ) _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