Files
ChatAgent_v2/src/interface.jl
2023-11-17 22:29:30 +00:00

980 lines
32 KiB
Julia
Executable File

module interface
export agentReact, addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix,
generatePrompt_tokenSuffix, conversation, work, detectCharacters, chunktext,
findDetectedCharacter
using JSON3, DataStructures, Dates, UUIDs
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
end
function agentReact(
agentName::String,
mqttClientSpec::NamedTuple;
role::Symbol=:assistant,
roles::Dict=Dict(
:assistant =>
"""
You are a helpful assistant.
""",
:assistant_react =>
"""
You are a helpful assistant. You don't know other people personal info previously.
Use the following format:
QTS: 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: you should always think about the info you need and what to do (pay attention to correct numeral calculation and commonsense).
Act: the action tool related to what you intend to do, 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 loop can repeat N times.)
Thought: I think I know the answer
ANS: Answer of the original question and the rationale behind your answer
""",
: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 usually recommend atmost 2 wines for customers.
You don't know other people personal info previously.
Use the following format:
QTS: 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 (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 loop can repeat N times until you know the answer.)
ANS: Answer of the original question. You describe detailed benefits of each answer to user's preference.
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
""",
),
tools::Dict=Dict(
:internetsearch=>Dict(
:name => "internetsearch",
:description => "Useful for when you need to search the Internet",
:input => "Input should be a search query.",
:output => "",
:func => nothing # 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,
),
:nothing=>Dict(
:name => "nothing",
: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
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_tokenSuffix(a::agentReact;
# userToken::String="[/INST]", assistantToken="[INST]",
# systemToken="[INST]<<SYS>> content <</SYS>>")
# 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::agentReact;
# userToken::String="Q:", assistantToken="A:",
# systemToken="[INST]<<SYS>> content <</SYS>>")
# 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 generatePrompt_react_mistral_openorca(messages::Dict, systemMsg::String, context::String="",
tools::Union{Dict, Nothing}=nothing)
promptTemplate =
"""
<|im_start|>system
{systemMsg}
You have access to the following tools:
{tools}
Begin!
<|im_end|>
Here are the context for the question:
{context}
"""
for msg in messages
role = msg[:role]
content = msg[:content]
if role == "system"
prompt = replace(promptTemplate, "{systemMsg}" => systemMsg)
toollines = ""
for tool in tools
toolline = "$(tool[:name]): $(tool[:description]) $(tool[:input]) $(tool[:output])\n"
toollines *= toolline
end
prompt = replace(promptTemplate, "{tools}" => toollines)
prompt = replace(promptTemplate, "{context}" => context)
elseif role == "user"
prompt *= "<|im_start|>user\n" * content * "\n<|im_end|>\n"
elseif role == "assistant"
prompt *= "<|im_start|>assistant\n" * content * "\n<|im_end|>\n"
else
error("undefied condition role = $role")
end
end
return prompt
end
function generatePrompt_mistral_openorca(a::T, usermsg::String) where {T<:agent}
prompt =
"""
<|im_start|>system
{systemMsg}
<|im_end|>
Here are the context for the question:
{context}
"""
prompt = replace(prompt, "{systemMsg}" => a.roles[:assistant])
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_react_mistral_openorca(a::T, usermsg::String,
continuethought::Bool=false) where {T<:agent}
prompt =
"""
<|im_start|>system
{systemMsg}
You have access to the following tools:
{tools}
Begin!
<|im_end|>
Here are the context for the question:
{context}
"""
prompt = replace(prompt, "{systemMsg}" => a.roles[a.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)
if continuethought == false
prompt *= "<|im_start|>user\nQTS: " * usermsg * "\n<|im_end|>\n"
prompt *= "<|im_start|>assistant\n"
else
prompt *= "Obs: $usermsg\n"
end
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}
userintend = identifyUserIntention(a, usermsg)
@show userintend
respond = nothing
# AI thinking mode
if userintend == "chat"
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)
@show prompt
respond = sendReceivePrompt(a, prompt)
respond = split(respond, "<|im_end|>")[1]
respond = replace(respond, "\n" => "")
_ = addNewMessage(a, "assistant", respond)
@show respond
elseif userintend == "wine" #WORKING
if a.thought == "nothing" # new thought
a.context = conversationSummary(a)
_ = addNewMessage(a, "user", usermsg)
prompt = generatePrompt_react_mistral_openorca(a, usermsg)
respond = work(a, prompt)
else # continue thought
error("wine done")
end
elseif userintend == "thought"
else
error("undefined condition userintend = $userintend")
end
# error("conversation done")
return respond
end
"""
Continuously run llm functions except when llm is getting ANS: or chatbox.
"""
function work(a::T, prompt::String) where {T<:agent}
respond = nothing
while true
@show prompt
toolname = nothing
toolinput = nothing
respond = sendReceivePrompt(a, prompt)
@show respond
try
respond = split(respond, "Obs:")[1]
catch
end
headers = detectCharacters(respond,
["QTS:", "Plan:", "Thought:", "Act:", "ActInput:", "Obs:", ".....", "ANS:"])
@show headers
chunkedtext = chunktext(respond, headers)
@show chunkedtext
if headers[1][:char] == "ANS:"
a.thought = "nothing" # question finished, no more thought
respond = chunkedtext[1][:body]
_ = addNewMessage(a, "assistant", respond)
break
else
# check for tool being called
ActInd = findDetectedCharacter(headers, "Act:")[1]
toolname = toolNameBeingCalled(chunkedtext[ActInd][:body], a.tools)
toolinput = chunkedtext[ActInd+1][:body]
if toolname == "chatbox" # chat with user
a.thought *= toolinput
respond = toolinput
_ = addNewMessage(a, "assistant", respond)
break
else # function call
error("function call")
f = a.tools[Symbol(toolname)][:func]
_result = f(toolinput)
result = "Obs: $_result\n"
a.thought *= result
prompt = a.thought
end
end
end
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)
println("<<<<<")
@show prompt
result = sendReceivePrompt(a, prompt)
summary = result === nothing ? "nothing" : result
summary = replace(summary, "<|im_end|>" => "")
if summary[1:1] == "\n"
summary = summary[2:end]
end
@show summary
println(">>>>>")
end
return summary
end
# function work2(a::agentReact, usermsg::String)
# addNewMessage(a, "user", usermsg)
# userIntent = identifyUserIntention(a, usermsg)
# @show userIntent
# # checkReasonableness()
# if userIntent == "chat"
# prompt = generatePrompt_tokenPrefix(a, userToken="Q:", assistantToken="A:")
# result = sendReceivePrompt(a, prompt)
# addNewMessage(a, "assistant", result)
# return result
# elseif userIntent == "task"
# while true
# if thought == "nothing" # no unfinished thought
# prompt = generatePrompt_react_mistral_openorca(
# a.messages, a.roles[a.role], a.context, a.tools)
# output = sendReceivePrompt(a, prompt)
# obscount = count(output["text"], "Obs:")
# a.thought = prompt * out
# if contains(output["text"], "ANS:") # know the answer
# a.thought = "nothing"
# return output["text"]
# else
# out = split(output["text"], "Obs:")[1] # LLM may generate long respond with multiple Obs: but I do only 1 Obs: at a time(1st).
# act = react_act(out, "first")
# actinput = react_actinput(out, "first")
# toolname = toolNameBeingCalled(act, a.tools)
# if toolname == "chatbox"
# return actinput
# else # function call
# toolresult = a.tools[toolname][:func](actinput)
# Obs = "Obs: $toolresult\n" # observe in ReAct agent
# work(a, Obs)
# end
# end
# else # continue thought
# usermsg = "Obs: $usermsg"
# prompt = a.thought * usermsg
# output = sendReceivePrompt(a, prompt)
# obs = count(output["text"], "Obs:")
# out = split(output["text"], "Obs:")[1]
# a.thought = prompt * out
# if obs == 0 # llm config has too short characters generation
# error("No Obs: detected. Probably LLM config has too short max_tokens generation")
# elseif obs == 1 # first conversation
# act = react_act(out, "first")
# actinput = react_actinput(out, "first")
# toolname = toolNameBeingCalled(act, a.tools)
# if toolname == "chatbox"
# return actinput
# else # function call
# toolresult = a.tools[toolname][:func](actinput)
# Obs = "Obs: $toolresult\n" # observe in ReAct agent
# work(a, Obs)
# end
# else # later conversation
# act = react_act(out, "last")
# actinput = react_actinput(out, "last")
# toolname = toolNameBeingCalled(act, a.tools)
# if toolname == "chatbox"
# return actinput
# else # function call
# toolresult = a.tools[toolname][:func](actinput)
# Obs = "Obs: $toolresult\n" # observe in ReAct agent
# work(a, Obs)
# end
# end
# end
# else
# error("user intent $userIntent not define $(@__LINE__)")
# end
# end
function workContinue(a::agent)
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=30) 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
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
#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
function react_plan(text::String, firstlast="first")
"Plan" * GeneralUtils.getStringBetweenCharacters(text, "Plan", "Thought", firstlast=firstlast)
end
function react_thought(text::String, firstlast="first")
"Thought" * GeneralUtils.getStringBetweenCharacters(text, "Thought", "Act", firstlast=firstlast)
end
function react_act(text::String, firstlast="first")
"Act" * GeneralUtils.getStringBetweenCharacters(text, "Act", "ActInput", firstlast=firstlast)
end
function react_actinput(text::String, firstlast="first")
"ActInput" * GeneralUtils.getStringBetweenCharacters(text, "ActInput", "Obs", firstlast=firstlast)
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> text = "Plan: First, we need to find out what kind of wine the user wants."
julia> headers = 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 = chunktext(text, headers)
```
"""
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
end # module