This commit is contained in:
2023-12-04 04:57:08 +00:00
parent e6c464c96e
commit 45bd332b6f
8 changed files with 3697 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
channels = ["anaconda", "conda-forge", "pytorch"]
[deps]
python = ">=3.8,<3.11"
[pip.deps]
langchain = ""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
name = "ChatAgent"
uuid = "cff63402-b71f-455f-804d-24489fc61e5e"
authors = ["narawat <narawat@gmail.com>"]
version = "0.1.0"
[deps]
CommUtils = "646cbe82-3d4a-47b2-9440-2e80a472ca20"
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

View File

@@ -0,0 +1,101 @@
module ChatAgent
# export agent, addNewMessage, clearMessage
""" Order by dependencies of each file. The 1st included file must not depend on any other
files and each file can only depend on the file included before it.
"""
include("type.jl")
using .type
include("utils.jl")
using .utils
include("llmfunction.jl")
using .llmfunction
include("interface.jl")
using .interface
#------------------------------------------------------------------------------------------------100
""" version 0.0.1
Todo:
[] add chat mechanism
Change from version: 0.0.0
-
"""
end # module ChatAgent

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
module llmfunction
export wikisearch, winestock
using HTTP, JSON3
using GeneralUtils
using ..type, ..utils
#------------------------------------------------------------------------------------------------100
"""
Search wikipedia.
Args:
query (string): The query to search for
Returns:
string: The search result text from wikipedia
```jldoctest
julia> using HTTP, JSON3
julia> result = wikisearch("AMD")
"Advanced Micro Devices, Inc., commonly abbreviated as AMD, is an ..."
```
"""
function wikisearch(a::agentReflex, phrase::T) where {T<:AbstractString}
phrase = phrase[1] == " " ? phrase[2:end] : phrase
# prepare input phrase
if occursin("\"", phrase)
phrase = GeneralUtils.getStringBetweenCharacters(phrase, "\"", "\"")
end
phrase = replace(phrase, "\n"=>"")
url = "https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts&titles=$(replace(phrase, " " => "%20"))&exintro=1&explaintext=1"
@show url
response = HTTP.get(url)
json_data = JSON3.read(String(response.body))
page_id = first(keys(json_data["query"]["pages"]))
if page_id == "-1"
return "Sorry, I couldn't find any Wikipedia page for the given phrase."
end
result = nothing
try
result = json_data["query"]["pages"][page_id]["extract"]
wiki = result
@show wiki
catch
result = "No info available for your search query."
end
# if result == ""
# result = "No info available for your search query."
# else
# result = makeSummary(a, result)
# end
return result
end
function winestock(a::agentReflex, phrase::T) where {T<:AbstractString}
# result = [
# Dict(
# "name" => "Louis Latou - Corton-Charlamagne - Chardonnay",
# "description" => "Corton-Charlemagne 2018 is a powerful, complex wine. Its nose is intense, with notes of white stone fruits such as white peach, fresh hazelnut, vanilla, and almond paste. The wine is full-bodied for the palate, and the vanilla is complemented by aromas of fresh almond and lime blossom. The experience ends with a very fine aromatic aftertaste that has subtle saline notes.",
# "price" => "49",
# "ID" => "ws-114"
# ),
# Dict(
# "name" => "Louis Latou - Corton-Charlamagne - Chardonnay",
# "description" => "Corton-Charlemagne 2018 is a powerful, complex wine. Its nose is intense, with notes of white stone fruits such as white peach, fresh hazelnut, vanilla, and almond paste. The wine is full-bodied for the palate, and the vanilla is complemented by aromas of fresh almond and lime blossom. The experience ends with a very fine aromatic aftertaste that has subtle saline notes.",
# "price" => "49",
# "ID" => "ws-114"
# )
# ]
result =
"""
1. Name: Louis Latou - Corton-Charlamagne - Chardonnay,
Description: Corton-Charlemagne 2018 is a powerful, complex wine. Its nose is intense, with notes of white stone fruits such as white peach, fresh hazelnut, vanilla, and almond paste. The wine is full-bodied for the palate, and the vanilla is complemented by aromas of fresh almond and lime blossom. The experience ends with a very fine aromatic aftertaste that has subtle saline notes.,
Price: 49 dollars,
ID: ws-114
2. Name: Chateau de Beaucastel Hommage Jacques Perrin Chateauneuf-du-Pape,
Year: 2019,
Description: The quintessence of Château de Beaucastel, Hommage à Jacques Perrin delights us every year, and the 2019 vintage is no exception. To the eye it offers a splendid deep red color, verging on black. Full of power and supremely elegant, the nose is of magnificent aromatic complexity with notes of black fruit and spices that offer all the characteristic expression of Mourvèdre. Perfectly balanced by an incredible freshness, the mouth is eminently elegant with intense and complex aromas of great subtlety, a full, refined texture, subtle tannins of great finesse, and infinite length. A great classic Hommage à Jacques Perrin.,
Price: 42,
ID: ed-23
3. Name: M. Chapoutier Ermitage l'Ermite Blanc,
Year: 2017
Description: Brilliant pale yellow. Complex aromas of vanilla, almonds, dried fruits and linden-tree. The mineraliaty is marked (typical of soil). Very round and rich wine. An elegant balance, of very ripe white fruit aromas (peach and apricot) and light notes of minerality. Beautiful length and complexity.,
Price: 13,
ID: wwr-259
"""
return result
end
end # end module

View File

@@ -0,0 +1,376 @@
module type
export agent, agentReflex
using Dates, UUIDs, DataStructures
using CommUtils
#------------------------------------------------------------------------------------------------100
abstract type agent end
@kwdef mutable struct agentReflex <: agent
availableRole::AbstractVector = ["system", "user", "assistant"]
agentName::String = "Jene" # ex. Jene
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}}()
tools::Union{Dict, Nothing} = nothing
newplan::Bool = false # if true, new plan will be generated
attemptlimit::Int = 5 # thinking round limit
attempt::Int = 1 # attempted number
step::Int = 1 # step number
thinkingmode::Symbol = :no_thinking
thinkingFormat::Union{Dict, Nothing} = nothing
memory::Dict = Dict(
:shortterm=> OrderedDict{String, Any}(),
:longterm=> OrderedDict{String, Any}(),
:log=> OrderedDict{String, Any}(), # span from user stimulus -> multiples attempts -> final respond
)
end
function agentReflex(
agentName::String;
mqttClientSpec::NamedTuple=(
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,),
role::Symbol=:assistant,
roles::Dict=Dict(
:assistant =>
"""
You are a helpful assistant who respond to a stimulus as best you can.
""",
:sommelier =>
"""
You are a sommelier at an online wine reseller who always help users choosing their wine from your inventory.
You don't know other people personal info previously.
""",
# :sommelier =>
# """
# You are a sommelier at an online wine reseller who always ask user for wine relevant info before you could help them choosing wine.
# You provide a personalized recommendation of up to two wines based on the user's preference, and you describe the benefits of each wine in detail.
# You don't know other people personal info previously.
# Info used to select wine:
# - type of food
# - occasion
# - user's personal taste of wine
# - wine price range
# - temperature at the serving location
# - wine we have in stock
# """,
),
thinkingFormat::Dict=Dict(
:react=>
"""
Use the following format:
Question: the input question your user is asking and you must answer
Plan: first you should always think about the question and the info you have thoroughly then extract and devise a complete plan to find the answer (pay attention to variables and their corresponding numerals).
Thought: ask yourself do you have all the info you need? And what to do according to the plan (pay attention to correct numeral calculation and commonsense).
Act: the tool that match your thought, should be one of {toolnames}
ActInput: the input to the action (pay attention to the tool's input)
Obs: the result of the action
... (this Plan/Thought/Act/ActInput/Obs can repeat N times until you know the answer.)
Thought: I think I know the answer
Answer: Answer of the original question
Begin!""",
:planner=>
"""
Use the following format:
Stimulus: the input user gives to you and you must respond
Plan: first you should always think about the stimulus, the info you need and the info you have thoroughly then extract and devise a step by step plan (pay attention to correct numeral calculation and commonsense).
p.s.1 each step should be a single question/action.
p.s.2 Do not respond yet.
""",
:actor=>
"""
Use the following format:
Thought: ask yourself do you have what you need and what to do according to step {step} of the plan (pay attention to correct numeral calculation and commonsense).
Act: the action to take 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
Check: check whether you are ready for the next step of the plan
""",
),
tools::Dict=Dict(
:chatbox=>Dict(
:name => "chatbox",
:description => "Useful for when you need to communicate with the user.",
:input => "Input should be a conversation to the user.",
:output => "" ,
:func => nothing,
),
# :wikisearch=>Dict(
# :name => "wikisearch",
# :description => "Useful for when you need to search an encyclopedia",
# :input => "Input is keywords and not a question.",
# :output => "",
# :func => wikisearch, # put function here
# ),
# :wineStock=>Dict(
# :name => "wineStock",
# :description => "useful for when you need to search for wine by your description, price, name or ID.",
# :input => "Input should be a search query with as much details as possible.",
# :output => "" ,
# :func => nothing,
# ),
# :NTHING=>Dict(
# :name => "NTHING",
# :description => "useful for when you don't need to use tools or actions",
# :input => "No input is needed",
# :output => "" ,
# :func => nothing,
# ),
),
msgMeta::Dict=Dict(
:msgPurpose=> "updateStatus",
:from=> "chatbothub",
:to=> "llmAI",
:requestrespond=> "request",
:sendto=> "", # destination topic
:replyTo=> "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())",
),
availableRole::AbstractArray=["system", "user", "assistant"],
maxUserMsg::Int=10,)
newAgent = agentReflex()
newAgent.availableRole = availableRole
newAgent.maxUserMsg = maxUserMsg
newAgent.mqttClient = CommUtils.mqttClient(mqttClientSpec)
newAgent.msgMeta = msgMeta
newAgent.tools = tools
newAgent.role = role
newAgent.roles = roles
newAgent.thinkingFormat = thinkingFormat
newAgent.agentName = agentName
return newAgent
end
@kwdef mutable struct agentReact <: agent
availableRole::AbstractVector = ["system", "user", "assistant"]
agentName::String = "assistant"
maxUserMsg::Int = 10
earlierConversation::String = "" # summary of earlier conversation
mqttClient::Union{mqttClient, Nothing} = nothing
msgMeta::Union{Dict, Nothing} = nothing
""" Dict(Role=> Content) ; Role can be system, user, assistant
Example:
messages=[
Dict(:role=>"system", :content=> "You are a helpful assistant."),
Dict(:role=>"assistant", :content=> "How may I help you"),
Dict(:role=>"user", :content=> "Hello, how are you"),
]
"""
role::Symbol = :assistant
roles::Dict = Dict(:assistant => "You are a helpful assistant.",)
# Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
# messages= [Dict(:role=>"system", :content=> "", :timestamp=> Dates.now()),]
messages = Vector{Dict{Symbol, Any}}()
context::String = "nothing" # internal thinking area
tools::Union{Dict, Nothing} = nothing
thought::String = "nothing" # contain unfinished thoughts for ReAct agent only
thinkinground::Int = 0 # no. of thinking round
thinkingroundlimit::Int = 5 # thinking round limit
thinkingMode::Union{Dict, Nothing} = nothing
end
function agentReact(
agentName::String,
mqttClientSpec::NamedTuple;
role::Symbol=:assistant,
roles::Dict=Dict(
:assistant =>
"""
You are a helpful assistant who answer the user's questions as best you can.
""",
:sommelier =>
"""
You are a helpful sommelier at an online wine reseller who always ask user for wine relevant info before you could help them choosing wine.
You provide a personalized recommendation of up to two wines based on the user's preference, and you describe the benefits of each wine in detail.
You don't know other people personal info previously.
Info used to select wine:
- type of food
- occasion
- user's personal taste of wine
- wine price range
- temperature at the serving location
- wine we have in stock
""",
),
thinkingMode::Dict=Dict(
:no_thinking=> "",
:thinking=>
"""Use the following format:
Question: the input question your user is asking and you must answer
Plan: first you should always think about the question and the info you have thoroughly then extract and devise a complete plan to find the answer (pay attention to variables and their corresponding numerals).
Thought: ask yourself do you have all the info you need? And what to do according to the plan (pay attention to correct numeral calculation and commonsense).
Act: the tool that match your thought, should be one of {toolnames}
ActInput: the input to the action (pay attention to the tool's input)
Obs: the result of the action
... (this Plan/Thought/Act/ActInput/Obs can repeat N times until you know the answer.)
Thought: I think I know the answer
Answer: Answer of the original question
Begin!""",
),
tools::Dict=Dict(
:wikisearch=>Dict(
:name => "wikisearch",
:description => "Useful for when you need to search an encyclopedia",
:input => "Input is keywords and not a question.",
:output => "",
:func => wikisearch, # put function here
),
:chatbox=>Dict(
:name => "chatbox",
:description => "Useful for when you need to ask a customer for more context.",
:input => "Input should be a conversation to customer.",
:output => "" ,
:func => nothing,
),
# :wineStock=>Dict(
# :name => "wineStock",
# :description => "useful for when you need to search for wine by your description, price, name or ID.",
# :input => "Input should be a search query with as much details as possible.",
# :output => "" ,
# :func => nothing,
# ),
# :NTHING=>Dict(
# :name => "NTHING",
# :description => "useful for when you don't need to use tools or actions",
# :input => "No input is needed",
# :output => "" ,
# :func => nothing,
# ),
),
msgMeta::Dict=Dict(
:msgPurpose=> "updateStatus",
:from=> "chatbothub",
:to=> "llmAI",
:requestrespond=> "request",
:sendto=> "", # destination topic
:replyTo=> "chatbothub/llm/respond", # requester ask responder to send reply to this topic
:repondToMsgId=> "", # responder is responding to this msg id
:taskstatus=> "", # "complete", "fail", "waiting" or other status
:timestamp=> Dates.now(),
:msgId=> "$(uuid4())",
),
availableRole::AbstractArray=["system", "user", "assistant"],
maxUserMsg::Int=10,)
newAgent = agentReact()
newAgent.availableRole = availableRole
newAgent.maxUserMsg = maxUserMsg
newAgent.mqttClient = CommUtils.mqttClient(mqttClientSpec)
newAgent.msgMeta = msgMeta
newAgent.tools = tools
newAgent.role = role
newAgent.roles = roles
newAgent.thinkingMode = thinkingMode
return newAgent
end
end # end module

View File

@@ -0,0 +1,666 @@
module utils
export makeSummary, sendReceivePrompt, chunktext, extractStepFromPlan, checkTotalStepInPlan,
detectCharacters, findDetectedCharacter, extract_number, toolNameBeingCalled,
chooseThinkingMode, conversationSummary, checkReasonableness, replaceHeaders,
addShortMem!, splittext
using UUIDs, Dates, DataStructures
using CommUtils, GeneralUtils
using ..type
#------------------------------------------------------------------------------------------------100
function makeSummary(a::T1, input::T2) where {T1<:agent, T2<:AbstractString}
summary = "Nothing."
prompt =
"""
<|im_start|>system
Input text:
$input
Your job is to determine now whether you can make a summary of the input text by choosing one of following choices:
If you cannot make a summary say, "{No}".
If you can make a summary say, "{Yes}".
<|im_end|>
"""
prompt = replace(prompt, "{input}" => input)
result = sendReceivePrompt(a, prompt)
result = GeneralUtils.getStringBetweenCharacters(result, "{", "}")
if result == "Yes" # seperate summary part
prompt =
"""
<|im_start|>system
Input text:
$input
Your job is to make a concise summary of the input text.
<|im_end|>
"""
result = sendReceivePrompt(a, prompt)
if result[1:1] == "\n"
summary = result[2:end]
else
summary = result
end
end
input_summary = input
@show input_summary
@show summary
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; max_tokens=256, timeout::Int=120) where {T<:agent}
a.msgMeta[:msgId] = "$(uuid4())" # new msg id for each msg
msg = Dict(
:msgMeta=> a.msgMeta,
:txt=> prompt,
:max_tokens=>max_tokens
)
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
"""
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
"""
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)
OrderedDict{String, String} with 3 entries:
"Act 1:" => " wikisearch"
"ActInput 1:" => " latest AMD GPU"
"Thought 1:" => " I should always think about..."
```
"""
function chunktext(text::T1, headers::T2) where {T1<:AbstractString, T2<:AbstractVector}
result = OrderedDict{String, Any}()
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)
headers = []
for (k, v) in a.memory[:shortterm]
push!(headers, k)
end
# Plan will have number e.g. Plan 3: so I need a way to detect latest Plan
header = nothing
for i in reverse(headers)
if occursin("Plan", i)
header = i
break
end
end
p = a.memory[:shortterm][header]
plan = "Plan: $p"
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
"""
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 $(@__LINE__)")
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.memory[:log]) != 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 to use tools or actions to respond to the stimulus say, "{no}".
If you need tools or actions to respond to the stimulus say, "{yes}".
If the user want to get wine 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 $(@__LINE__)")
end
end
prompt = replace(prompt, "{conversation}" => conversation)
result = sendReceivePrompt(a, prompt)
summary = result === nothing ? "nothing" : result
summary = split(summary, "<|im_end|>")[1]
if summary[1:1] == "\n"
summary = summary[2:end]
end
end
@show 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 chunked text to a short term memory of a chat agent
Args:
shortMem = short memory of a chat agent,
chunkedtext = a dict contains text
Return: no return
# Example
```jldoctest
julia> chunkedtext = OrderedDict{String, String}(
"Thought 1:" => " I should always think about...",
"Act 1:" => " wikisearch",
"ActInput 1:" => " latest AMD GPU",)
julia> shortMem = OrderedDict{String, Any}()
julia> addShortMem!(shortMem, chunkedtext)
OrderedDict{String, Any} with 3 entries:
"Thought 1:" => " I should always think about..."
"Act 1:" => " wikisearch"
"ActInput 1:" => " latest AMD GPU"
```
"""
function addShortMem!(shortMem::OrderedDict{String, Any}, chunkedtext::T) where {T<:AbstractDict}
for (k, v) in chunkedtext
shortMem[k] = v
end
return shortMem
end
""" Split text using all keywords in a list. Start spliting from rightmost of the text.
Args:
text = a text you want to split
list = a list of keywords you want to split
Return:
a leftmost text after split
# Example
```jldoctest
julia> text = "Consider the type of food, occasion and temperature at the serving location."
julia> list = ["at", "and"]
"Consider the type of food, occasion "
```
"""
function splittext(text, list)
newtext = text
for i in list
newtext = split(newtext, i)[1]
end
return newtext
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
function addStepNumber(text::T, headers, step::Int, substep::Int) where {T<:AbstractString}
newtext = text
for i in headers
if occursin(i[:char], newtext)
new = replace(i[:char], ":"=> " $step-$substep:")
newtext = replace(newtext, i[:char]=>new )
end
end
return newtext
end
""" Add step number to header in a text
Args:
text = a text you want to split
headers = a list of keywords you want to add step and substep to
Return:
a leftmost text after split
# Example
```jldoctest
julia> text = "Consider the type of food, occasion and temperature at the serving location."
julia> headers = ["Thought", "Act"]
```
"""
function replaceHeaders(text::T, headers, step::Int) where {T<:AbstractString}
newtext = text
for i in headers
header = i[1:end-1] # not include ":"
if occursin(header, newtext)
startind = findfirst(header, newtext)[1]
stopind = findnext(":", newtext, startind+1)[end]
word = newtext[startind: stopind]
newword = "$header $step:"
newtext = replace(newtext, word=> newword)
end
end
return newtext
end
end # end module