diff --git a/src/ChatAgent.jl b/src/ChatAgent.jl index 39b073c..6c4cea2 100755 --- a/src/ChatAgent.jl +++ b/src/ChatAgent.jl @@ -10,6 +10,9 @@ module ChatAgent include("type.jl") using .type + include("utils.jl") + using .utils + include("llmfunction.jl") using .llmfunction diff --git a/src/interface.jl b/src/interface.jl index 8a29ed4..a1c098f 100755 --- a/src/interface.jl +++ b/src/interface.jl @@ -2,13 +2,11 @@ module interface export agentReact, agentReflex, - addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix, - generatePrompt_tokenSuffix, conversation, work, detectCharacters, chunktext, - findDetectedCharacter, wikisearch, sendReceivePrompt, extractStepFromPlan + addNewMessage, clearMessage, removeLatestMsg, conversation using JSON3, DataStructures, Dates, UUIDs, HTTP using CommUtils, GeneralUtils -using ..type +using ..type, ..utils # ---------------------------------------------------------------------------- # # pythoncall setting # @@ -575,8 +573,8 @@ function work(a::T, prompt::String, maxround::Int=3) where {T<:agent} return respond end -function conversation(a::agentReflex, usermsg::String; thinkingroundlimit::Int=3) - a.thinkingroundlimit = thinkingroundlimit +function conversation(a::agentReflex, usermsg::String; attemptlimit::Int=3) + a.attemptlimit = attemptlimit respond = nothing # determine thinking mode @@ -600,7 +598,7 @@ function conversation(a::agentReflex, usermsg::String; thinkingroundlimit::Int=3 return respond end -#WORKING + function work(a::agentReflex, usermsg::String) if a.thinkingmode == :new_thinking a.earlierConversation = conversationSummary(a) @@ -613,9 +611,14 @@ function work(a::agentReflex, usermsg::String) error("undefined condition thinkingmode = $thinkingmode") end - while true + while true # Work loop # plan a.attempt += 1 + if a.attempt <= a.attemptlimit + + else # attempt limit reached + + end @show a.attempt @show usermsg logmsg = "<|im_start|>user:\nStimulus: $usermsg\n<|im_end|>\n" @@ -628,47 +631,12 @@ function work(a::agentReflex, usermsg::String) respond = sendReceivePrompt(a, prompt) plan = split(respond, "<|im_end|>")[1] plan = split(plan, "Response:")[1] - - @show plan - totalsteps = checkTotalStepInPlan(a, plan) - logmsg = "<|im_start|>assistant:\n$plan\n" + _plan = replace(plan, "Plan:"=>"Plan $(a.attempt):") + logmsg = "<|im_start|>assistant:\n$_plan\n" a.memory[:shortterm] *= logmsg + result = actor(a, plan) + #WORKING - a.step = 0 - while true # execute step by step - a.step += 1 - if a.step <= totalsteps - stepdetail = extractStepFromPlan(a, plan, a.step) - @show stepdetail - prompt = actor_mistral_openorca(a, stepdetail) - respond = sendReceivePrompt(a, prompt) - respond = split(respond, "<|im_end|>")[1] - @show respond - #WORKING extract Act/ActInput - headerToDetect = ["Question:", "Plan:", "Thought:", "Act:", "ActInput:", "Obs:", "...", "Answer:", - "Conclusion:", "Summary:"] - headers = detectCharacters(respond, headerToDetect) - @show headers - chunkedtext = chunktext(respond, headers) - @show chunkedtext - toolname = toolNameBeingCalled(chunkedtext["Act:"], a.tools) - @show toolname - toolinput = chunkedtext["ActInput:"] - - @show toolinput - error("work done 1") - else #TODO finish all steps - - - - - error("work done 2") - break - end - - - - end # evaluate @@ -677,552 +645,69 @@ function work(a::agentReflex, usermsg::String) end -function actor(a::agentReflex, usermsg::String) +function actor(a::agentReflex, plan::T) where {T<:AbstractString} + actorState = nothing + @show plan + totalsteps = checkTotalStepInPlan(a, plan) -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. - Use "I" when refers to yourself in the summary. - - 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) - 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 - println("conversation summary: $summary") - return summary -end - -function makeSummary(a::T1, input::T2) where {T1<:agent, T2<:AbstractString} - summary = "Nothing." - prompt = - """ - <|im_start|>system - Your need to determine now whether you can make a summary of user's text. - - You have the following choices: - If you cannot make a summary say, "{no}". - If you can make a summary say, "{yes}". - <|im_end|> - - <|im_start|>user - {input} - <|im_end|> - <|im_start|>assistant - - """ - prompt = replace(prompt, "{input}" => usermsg) - result = sendReceivePrompt(a, prompt) - result = GeneralUtils.getStringBetweenCharacters(result, "{", "}") - if result == "yes" - prompt = - """ - <|im_start|>system - You are a helpful assistant. - Your job is to make a concise summary of user's text. - <|im_end|> - - <|im_start|>user - {input} - <|im_end|> - <|im_start|>assistant - - """ - prompt = replace(prompt, "{input}" => input) - result = sendReceivePrompt(a, prompt) - summary = replace(result, "<|im_end|>" => "") - if summary[1:1] == "\n" - summary = summary[2:end] - end - end - - return summary -end - -function chooseThinkingMode(a::T, usermsg::String) where {T<:agent} - thinkingMode = nothing - if a.thought != "nothing" - thinkingMode = :continue_thinking - else - 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 assignment. - - You have the following choices: - If you don't need tools or actions to fininsh the assignment say, "{no}". - If you need tools or actions to finish the assignment 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" ? :thinking : :no_thinking - end - - return thinkingMode -end - -function chooseThinkingMode(a::agentReflex, usermsg::String) - thinkingmode = nothing - if length(a.thoughtlog) != 0 - thinkingmode = :continue_thinking - else - prompt = - """ - <|im_start|>system - {systemMsg} - You always use tools if there is a chance to impove your respond. - You have access to the following tools: - {tools} - Your job is to determine whether you will use tools or actions to respond. - - Choose one of the following choices: - If you don't need tools or actions to respond to the stimulus say, "{no}". - If you need tools or actions to respond to the stimulus 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" ? :new_thinking : :no_thinking - end - - 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 + a.step = 0 + while true # Actor loop + a.step += 1 + @show a.step + if a.step <= totalsteps + stepdetail = extractStepFromPlan(a, plan, a.step) + prompt = actor_mistral_openorca(a, stepdetail) + @show prompt + respond = sendReceivePrompt(a, prompt) + respond = split(respond, "<|im_end|>")[1] + @show respond + headerToDetect = ["Question:", "Plan:", "Thought:", "Act:", "ActInput:", "Obs:", "...", "Answer:", + "Conclusion:", "Summary:"] + headers = detectCharacters(respond, headerToDetect) - 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 + # add to memory + _respond = addStepNumber(respond, headers, a.step) + a.memory[:shortterm] *= _respond + + chunkedtext = chunktext(respond, headers) + toolname = toolNameBeingCalled(chunkedtext["Act:"], a.tools) + toolinput = chunkedtext["ActInput:"] + @show toolname + @show toolinput + + #WORKING + if toolname == "chatbox" # chat with user + # a.memory[:shortterm] *= toolinput + respond = toolinput + _ = addNewMessage(a, "assistant", respond) + result = respond + error("actor done 0") + actorState = "chatbox" break + else # function call + f = a.tools[Symbol(toolname)][:func] + result = f(toolinput) + result = "Obs $(a.step): $result\n" + a.memory[:shortterm] *= result + error("actor done 1") end - elseif timepass <= timeout - # skip, within waiting period - elseif timepass > timeout - println("sendReceivePrompt timeout $timepass/$timeout") - result = nothing - break - else - error("undefined condition. timepass=$timepass timeout=$timeout $(@__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 + else #TODO finish all steps + + + + actorState = "all steps done" + error("actor done 2") break end + + + end - return toolNameBeingCalled + error("actor done 3") + return actorState, result 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 assignments. - If the user assignment can be answered given the tools available say, "This is a reasonable assignment". - If the user assignment cannot be answered then provide some feedback to the user that may improve - their assignment. - - Here is the context for the assignment: - {context} - - <|im_end|> - - <|im_start|>user - {assignment} - <|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, "{assignment}" => 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 = Dict() - - 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)) - result[v[:char]] = body - else - body = text[v[:stop]+1: end] - # push!(result, (header=v[:char], body=body)) - result[v[:char]] = body - end - end - - return result -end -# 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 extractStepFromPlan(a::agent, plan::T, step::Int) where {T<:AbstractString} - prompt = - """ - <|im_start|>system - You are a helpful assistant. - Your job is to extract step $step in the user plan. - - Use the following format only: - {copy the step and put it here} - - <|im_end|> - - <|im_start|>user - $plan - <|im_end|> - <|im_start|>assistant - - """ - - respond = sendReceivePrompt(a, prompt) - - return respond -end - -function checkTotalStepInPlan(a::agent, plan::T) where {T<:AbstractString} - prompt = - """ - <|im_start|>system - You are a helpful assistant. - Your job is to determine how many steps in a user plan. - - Use the following format to answer: - Total step number is {} - <|im_end|> - - <|im_start|>user - $plan - <|im_end|> - <|im_start|>assistant - - """ - respond = sendReceivePrompt(a, prompt) - result = extract_number(respond) - - return result -end - - - - -function extract_number(text::T) where {T<:AbstractString} - regex = r"\d+" # regular expression to match one or more digits - match = Base.match(regex, text) # find the first match in the text - if match !== nothing - number = parse(Int, match.match) - return number - else - error("No number found in the text") - end -end diff --git a/src/llmfunction.jl b/src/llmfunction.jl index de9d775..6651224 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -3,7 +3,8 @@ module llmfunction export wikisearch using HTTP, JSON3 - +using GeneralUtils +using ..utils #------------------------------------------------------------------------------------------------100 """ @@ -24,7 +25,7 @@ function wikisearch(phrase::T) where {T<:AbstractString} phrase = phrase[1] == " " ? phrase[2:end] : phrase # prepare input phrase if occursin("\"", phrase) - phrase = GeneralUtils.getStringBetweenCharacters(toolinput, "\"", "\"") + phrase = GeneralUtils.getStringBetweenCharacters(phrase, "\"", "\"") end phrase = replace(phrase, "\n"=>"") @@ -45,6 +46,10 @@ function wikisearch(phrase::T) where {T<:AbstractString} if result == "" result = "No info available." end + + if result != "No info available." #TODO for use with wikisearch(). Not good for other tools + result = makeSummary(a, result) + end return result end @@ -99,7 +104,6 @@ end - diff --git a/src/type.jl b/src/type.jl index e3cc5d5..89c5f2b 100644 --- a/src/type.jl +++ b/src/type.jl @@ -37,7 +37,7 @@ abstract type agent end thoughtlog::String = "" # logs unfinished thoughts attempt::Int = 0 # attempted number step::Int = 0 # step number - thinkingroundlimit::Int = 5 # thinking round limit + attemptlimit::Int = 5 # thinking round limit thinkingmode::Symbol = :no_thinking thinkingFormat::Union{Dict, Nothing} = nothing memory::Dict = Dict( diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..6bbb155 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,568 @@ +module utils + +export makeSummary, sendReceivePrompt, chunktext, extractStepFromPlan, checkTotalStepInPlan, + detectCharacters, findDetectedCharacter, extract_number, toolNameBeingCalled, + chooseThinkingMode, conversationSummary, checkReasonableness, addStepNumber + +using UUIDs, Dates +using CommUtils, GeneralUtils +using ..type + +#------------------------------------------------------------------------------------------------100 + +function makeSummary(a::T1, input::T2) where {T1<:agent, T2<:AbstractString} + summary = "Nothing." + prompt = + """ + <|im_start|>system + Your need to determine now whether you can make a summary of user's text. + + You have the following choices: + If you cannot make a summary say, "{no}". + If you can make a summary say, "{yes}". + <|im_end|> + + <|im_start|>user + {input} + <|im_end|> + <|im_start|>assistant + + """ + prompt = replace(prompt, "{input}" => usermsg) + result = sendReceivePrompt(a, prompt) + result = GeneralUtils.getStringBetweenCharacters(result, "{", "}") + if result == "yes" + prompt = + """ + <|im_start|>system + You are a helpful assistant. + Your job is to make a concise summary of user's text. + <|im_end|> + + <|im_start|>user + {input} + <|im_end|> + <|im_start|>assistant + + """ + prompt = replace(prompt, "{input}" => input) + result = sendReceivePrompt(a, prompt) + summary = replace(result, "<|im_end|>" => "") + if summary[1:1] == "\n" + summary = summary[2:end] + end + end + + return summary +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. timepass=$timepass timeout=$timeout $(@__LINE__)") + end + end + + return result +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 = Dict() + + 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)) + result[v[:char]] = body + else + body = text[v[:stop]+1: end] + # push!(result, (header=v[:char], body=body)) + result[v[:char]] = body + end + end + + return result +end + + +function extractStepFromPlan(a::agent, plan::T, step::Int) where {T<:AbstractString} + prompt = + """ + <|im_start|>system + You are a helpful assistant. + Your job is to extract step $step in the user plan. + + Use the following format only: + {copy the step and put it here} + + <|im_end|> + + <|im_start|>user + $plan + <|im_end|> + <|im_start|>assistant + + """ + + respond = sendReceivePrompt(a, prompt) + + return respond +end + +function checkTotalStepInPlan(a::agent, plan::T) where {T<:AbstractString} + prompt = + """ + <|im_start|>system + You are a helpful assistant. + Your job is to determine how many steps in a user plan. + + Use the following format to answer: + Total step number is {} + <|im_end|> + + <|im_start|>user + $plan + <|im_end|> + <|im_start|>assistant + + """ + respond = sendReceivePrompt(a, prompt) + result = extract_number(respond) + + return result +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 + + + + + + +function extract_number(text::T) where {T<:AbstractString} + regex = r"\d+" # regular expression to match one or more digits + match = Base.match(regex, text) # find the first match in the text + if match !== nothing + number = parse(Int, match.match) + return number + else + error("No number found in the text") + end +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 chooseThinkingMode(a::agentReflex, usermsg::String) + thinkingmode = nothing + if length(a.thoughtlog) != 0 + thinkingmode = :continue_thinking + else + prompt = + """ + <|im_start|>system + {systemMsg} + You always use tools if there is a chance to impove your respond. + You have access to the following tools: + {tools} + Your job is to determine whether you will use tools or actions to respond. + + Choose one of the following choices: + If you don't need tools or actions to respond to the stimulus say, "{no}". + If you need tools or actions to respond to the stimulus 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" ? :new_thinking : :no_thinking + end + + return thinkingmode +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. + You must refers to yourself by "I" in the summary. + + 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) + 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 + println("conversation summary: $summary") + return summary +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 assignments. + If the user assignment can be answered given the tools available say, "This is a reasonable assignment". + If the user assignment cannot be answered then provide some feedback to the user that may improve + their assignment. + + Here is the context for the assignment: + {context} + + <|im_end|> + + <|im_start|>user + {assignment} + <|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, "{assignment}" => 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 + +""" + Add step number to header in a text +""" +function addStepNumber(text::T, headers, step::Int) where {T<:AbstractString} + newtext = text + for i in headers + if occursin(i[:char], newtext) + new = replace(i[:char], ":"=> " $step:") + newtext = replace(newtext, i[:char]=>new ) + end + end + return newtext +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +end # end module \ No newline at end of file