This commit is contained in:
2023-11-24 07:21:35 +00:00
parent 27ae99afeb
commit d040e46950
5 changed files with 647 additions and 587 deletions

568
src/utils.jl Normal file
View File

@@ -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=["</response>", "<<END>>", ],
)
_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