diff --git a/src/interface.jl b/src/interface.jl
index 8035373..21d19cf 100755
--- a/src/interface.jl
+++ b/src/interface.jl
@@ -1,8 +1,8 @@
module interface
-export agent, addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix,
- generatePrompt_tokenSuffix, conversation
+export agentReact, addNewMessage, clearMessage, removeLatestMsg, generatePrompt_tokenPrefix,
+ generatePrompt_tokenSuffix, conversation, work
using JSON3, DataStructures, Dates, UUIDs
using CommUtils, GeneralUtils
@@ -32,33 +32,101 @@ using CommUtils, GeneralUtils
#------------------------------------------------------------------------------------------------100
-@kwdef mutable struct 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
+abstract type agent end
- """ Dict(Role=> Content) ; Role can be system, user, assistant
- Example:
- messages=[
- Dict(:role=>"system", :content=> "You are a helpful assistant."),
- Dict(:role=>"assistant", :content=> "How may I help you"),
- Dict(:role=>"user", :content=> "Hello, how are you"),
- ]
- """
- # Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
- # messages= [Dict(:role=>"system", :content=> "", :timestamp=> Dates.now()),]
- messages = []
- thougt::String = "" # internal thinking area
- info::String = "" # additional info
+@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{String, Any}}()
+ context::String = "nothing" # internal thinking area
+ tools::Union{Dict, Nothing} = nothing
+ thought::String = "nothing" # contain unfinished thoughts
end
-function agent(
+function agentReact(
agentName::String,
mqttClientSpec::NamedTuple;
- systemMessage::String="You are a helpful assistant.", # system message of an agent
+ role::Symbol=:assistant,
+ roles::Dict=Dict(
+ :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 [Chatbox, Internet, WineStock]
+ 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 [Chatbox, Internet, WineStock]
+ 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 => 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 => "" ,
+ ),
+ :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 => "" ,
+ ),
+ ),
msgMeta::Dict=Dict(
:msgPurpose=> "updateStatus",
:from=> "chatbothub",
@@ -73,37 +141,19 @@ function agent(
),
availableRole::AbstractArray=["system", "user", "assistant"],
maxUserMsg::Int=10,)
-
- newAgent = agent()
+
+ newAgent = agentReact()
newAgent.availableRole = availableRole
newAgent.maxUserMsg = maxUserMsg
- systemMessage_ = "Your name is $agentName. " * systemMessage
- push!(newAgent.messages, Dict(:role=>"system", :content=> systemMessage_, :timestamp=> Dates.now()))
newAgent.mqttClient = CommUtils.mqttClient(mqttClientSpec)
newAgent.msgMeta = msgMeta
+ newAgent.tools = tools
+ newAgent.role = role
+ newAgent.roles = roles
return newAgent
end
-# @kwdef mutable struct agentLangchain
-# availableRole=["system", "user", "assistant"]
-# maxUserMsg::Int= 10
-# llmAIRequestTopic_openblas = "llm/openblas/request"
-# llmAIRequestTopic_gpu = "llm/api/v0.0.1/gpu/request"
-# self_llmReceiveTopic = "chatbothub/llm/respond"
-
-# """ Dict(Role=> Content) ; Role can be system, user, assistant
-# Example:
-# messages=[
-# Dict(:role=>"system", :content=> "You are a helpful assistant."),
-# Dict(:role=>"assistant", :content=> "How may I help you"),
-# Dict(:role=>"user", :content=> "Hello, how are you"),
-# ]
-# """
-# # Ref: https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
-# #
-# messages=[Dict(:role=>"system", :content=> "You are a helpful assistant.", :timestamp=> Dates.now()),]
-# end
"""
add new message to agent
@@ -112,7 +162,7 @@ end
julia> addNewMessage(agent1, "user", "Where should I go to buy snacks")
````
"""
-function addNewMessage(a::agent, role::String, content::String)
+function addNewMessage(a::T, role::String, content::String) where {T<:agent}
if role ∉ a.availableRole # guard against typo
error("role is not in agent.availableRole")
end
@@ -139,8 +189,7 @@ function addNewMessage(a::agent, role::String, content::String)
return messageleft
end
-
-function clearMessage(a::agent)
+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)
@@ -150,27 +199,87 @@ function clearMessage(a::agent)
end
end
-function removeLatestMsg(a::agent)
+function removeLatestMsg(a::T) where {T<:agent}
if length(a.messages) > 1
pop!(a.messages)
end
end
-function generatePrompt_tokenSuffix(a::agent;
- userToken::String="[/INST]", assistantToken="[INST]",
- systemToken="[INST]<> content <>")
- prompt = nothing
- for msg in a.messages
+# function generatePrompt_tokenSuffix(a::agentReact;
+# userToken::String="[/INST]", assistantToken="[INST]",
+# systemToken="[INST]<> content <>")
+# prompt = nothing
+# for msg in a.messages
+# role = msg[:role]
+# content = msg[:content]
+
+# if role == "system"
+# prompt = replace(systemToken, "content" => content) * " "
+# elseif role == "user"
+# prompt *= " " * content * " " * userToken
+# elseif role == "assistant"
+# prompt *= " " * content * " " * assistantToken
+# else
+# error("undefied condition role = $role")
+# end
+# end
+
+# return prompt
+# end
+
+# function generatePrompt_tokenPrefix(a::agentReact;
+# userToken::String="Q:", assistantToken="A:",
+# systemToken="[INST]<> content <>")
+# prompt = nothing
+# for msg in a.messages
+# role = msg[:role]
+# content = msg[:content]
+
+# if role == "system"
+# prompt = replace(systemToken, "content" => content) * " "
+# elseif role == "user"
+# prompt *= userToken * " " * content * " "
+# elseif role == "assistant"
+# prompt *= assistantToken * " " * content * " "
+# else
+# error("undefied condition role = $role")
+# end
+# end
+
+# return prompt
+# end
+
+function 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(systemToken, "content" => content) * " "
+ 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 *= " " * content * " " * userToken
+ prompt *= "<|im_start|>user\n" * content * "\n<|im_end|>\n"
elseif role == "assistant"
- prompt *= " " * content * " " * assistantToken
- else
+ prompt *= "<|im_start|>assistant\n" * content * "\n<|im_end|>\n"
+ else
error("undefied condition role = $role")
end
end
@@ -178,53 +287,217 @@ function generatePrompt_tokenSuffix(a::agent;
return prompt
end
-function generatePrompt_tokenPrefix(a::agent;
- userToken::String="Q:", assistantToken="A:",
- systemToken="[INST]<> content <>")
- prompt = nothing
- for msg in a.messages
- role = msg[:role]
- content = msg[:content]
-
- if role == "system"
- prompt = replace(systemToken, "content" => content) * " "
- elseif role == "user"
- prompt *= userToken * " " * content * " "
- elseif role == "assistant"
- prompt *= assistantToken * " " * content * " "
- else
- error("undefied condition role = $role")
- end
+function generatePrompt_react_mistral_openorca(a::T, usermsg::String) 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])
+
+ toollines = ""
+ for (toolname, v) in a.tools
+ toolline = "$toolname: $(v[:description]) $(v[:input]) $(v[:output])\n"
+ toollines *= toolline
end
+ prompt = replace(prompt, "{tools}" => toollines)
+
+ prompt = replace(prompt, "{context}" => a.context)
+
+ prompt *= "<|im_start|>user\nQTS: " * usermsg * "\n<|im_end|>\n"
+ prompt *= "<|im_start|>assistant\n"
return prompt
end
-
-function conversation(a::agent, usermsg::String)
- addNewMessage(a, "user", usermsg)
- userIntent = identifyUserIntention(a, usermsg)
- @show userIntent
-
- #WORKING 1) add if-else user intention logic. 2) add recursive thinking
- if userIntent == "chat"
- generatePrompt_tokenPrefix(a, userToken="Q:", assistantToken="A:")
- result = sendReceivePrompt(a, usermsg)
- addNewMessage(a, "assistant", result)
-
- return result
- elseif userIntent == "task"
-
+#WORKING
+function conversation(a::T, usermsg::String) where {T<:agent}
+ if a.thought == "nothing"
+ a.context = conversationSummary(a)
+ addNewMessage(a, "user", usermsg)
+ prompt = generatePrompt_react_mistral_openorca(a, usermsg)
+ @show prompt
+ error("conversation done")
else
- error("user intent $userIntent not define $(@__LINE__)")
+
+
end
+
+
+
+
+
+
+
+
+
+end
+
+#WORKING
+function work(a::T, usermsg::String) where {T<:agent}
+
+end
+
+function workContinueThought(a::T, usermsg::String) where {T<:agent}
+
+end
+
+#WORKING
+"""
+ make a conversation summary.
+ ```julia
+ 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()
+ ```
+"""
+function conversationSummary(a::T) where {T<:agent}
+
+ promptTemplate =
+ """
+ <|im_start|>system
+ You are a helpful assistant.
+ <|im_end|>
+
+ <|im_start|>user
+ Please make a detailed bullet summary of the following earlier conversation between you and the user.
+ {conversation}
+ <|im_end|>
+ """
+ conversation = ""
+ summary = ""
+ 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(promptTemplate, "{conversation}" => conversation)
+ result = sendReceivePrompt(a, prompt)
+ summary = result === nothing ? "nothing" : result
+ if summary[1:1] == "\n"
+ summary = summary[2:end]
+ end
+ 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
#TESTING
function identifyUserIntention(a::agent, usermsg::String)
- identify_usermsg =
+ prompt =
"""
- You are to determine intention of the question.
+ <|im_start|>system
+ You are a helpful assistant. Your job is to determine intention of the question.
Your choices are:
chat: normal conversation that you don't need to do something.
task: a request for you to do something.
@@ -274,50 +547,26 @@ function identifyUserIntention(a::agent, usermsg::String)
Begin!
+ Here are the context for the question:
+ {context}
+
+ <|im_end|>
+
+ <|im_start|>user
Question: {input}
+ <|im_end|>
+ <|im_start|>assistant
+
"""
- identify_usermsg = replace(identify_usermsg, "{input}" => usermsg)
-
- result = sendReceivePrompt(a, identify_usermsg)
-
- # msg = Dict(
- # :msgMeta=> a.msgMeta,
- # :txt=> identify_usermsg,
- # )
-
- # payloadChannel = Channel(1)
-
- # # send prompt
- # CommUtils.request(a.mqttClient, msg, pubtopic=a.mqttClient.pubtopic.llmAI)
- # starttime = Dates.now()
- # timeout = 10
- # result = nothing
-
- # while true
- # timepass = (Dates.now() - starttime).value / 1000.0
- # CommUtils.mqttRun(a.mqttClient, payloadChannel)
- # if isready(payloadChannel)
- # topic, payload = take!(payloadChannel)
- # if payload[:msgMeta][:repondToMsgId] == msg[:msgMeta][:msgId]
- # result = payload[:txt]
- # break
- # end
- # elseif timepass <= timeout
- # # skip, within waiting period
- # elseif timepass > timeout
- # result = nothing
- # break
- # else
- # error("undefined condition $(@__LINE__)")
- # end
- # end
+ prompt = replace(prompt, "{input}" => usermsg)
+ result = sendReceivePrompt(a, prompt)
answer = result === nothing ? nothing : GeneralUtils.getStringBetweenCharacters(result, "{", "}")
return answer
end
-function sendReceivePrompt(a::agent, prompt::String; timeout::Int=10)
+function sendReceivePrompt(a::agent, prompt::String; timeout::Int=30)
a.msgMeta[:msgId] = "$(uuid4())" # new msg id for each msg
msg = Dict(
:msgMeta=> a.msgMeta,
@@ -336,7 +585,7 @@ function sendReceivePrompt(a::agent, prompt::String; timeout::Int=10)
if isready(payloadChannel)
topic, payload = take!(payloadChannel)
if payload[:msgMeta][:repondToMsgId] == msg[:msgMeta][:msgId]
- result = payload[:txt]
+ result = haskey(payload, :txt) ? payload[:txt] : nothing
break
end
elseif timepass <= timeout
@@ -352,37 +601,134 @@ function sendReceivePrompt(a::agent, prompt::String; timeout::Int=10)
return result
end
-# function getStringBetweenCurlyBraces(s::AbstractString)
-# m = match(r"\{(.+?)\}", s)
-# m = m == "" ? "" : m.captures[1]
-# return m
-# end
-
-# function getStringBetweenCharacters(text::AbstractString, startChar::String, endChar::String)
-# startIndex= findlast(startChar, text)
-# endIndex= findlast(endChar, text)
-# if startIndex === nothing || endIndex === nothing
-# return nothing
-# else
-# return text[startIndex.stop+1: endIndex.start-1]
-# end
-# end
-
+function toolNameBeingCalled(act::String, tools::Dict)
+ toolNameBeingCalled = nothing
+ for (k, v) in tools
+ toolname = String(k)
+ if contains(act, 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=["", "<>", ],
+ )
+ _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", startInd = 4, endInd = 4)
+ (char = "eat", startInd = 11, endInd = 13)
+ (char = "use", startInd = 26, endInd = 28)
+ (char = "i", startInd = 35, endInd = 35)
+ ```
+"""
+function detectCharacters(text::T, characters::Vector{T}) where {T<: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
+ if text[char_startInd: char_endInd] == char
+ push!(result, (char=char, startInd=char_startInd, endInd=char_endInd))
+ 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", startInd = 4, endInd = 4)
+ (char = "eat", startInd = 11, endInd = 13)
+ (char = "use", startInd = 26, endInd = 28)
+ (char = "i", startInd = 35, endInd = 35) ]
+ julia> findDetectedCharacter(a, "i")
+ [1, 4]
+ ```
+"""
+function findDetectedCharacter(detectedCharacters, character)
+ allchar = [i[1] for i in detectedCharacters]
+ return findall(isequal.(allchar, character))
+end