new version

This commit is contained in:
Your Name
2024-02-19 09:51:57 +07:00
parent 5965784c7e
commit 51381abc73
9 changed files with 5219 additions and 1 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,17 @@
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"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
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.5
Todo:
[WORKING] add formulateUserRespond to AI tools
Change from version: 0.0.4
-
"""
end # module ChatAgent

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,368 @@
module llmfunction
export wikisearch, winestock, askbox
using HTTP, JSON3, URIs, Random
using GeneralUtils
using ..type, ..utils
#------------------------------------------------------------------------------------------------100
""" Search wikipedia.
Arguments\n
query {string} : The query to search for
Returns\n
result {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
""" Search wine in stock.
Arguments\n
a : one of ChatAgent's agent.
Return\n
A JSON string of available wine
Example\n
```jldoctest
julia> using ChatAgent, CommUtils
julia> agent = ChatAgent.agentReflex("Jene")
julia> input = "{\"food\": \"pizza\", \"occasion\": \"anniversary\"}"
julia> result = winestock(agent, input)
"{"wine 1": {\"Winery\": \"Pichon Baron\", \"wine name\": \"Pauillac (Grand Cru Classé)\", \"grape variety\": \"Cabernet Sauvignon\", \"year\": 2010, \"price\": \"125 USD\", \"stock ID\": \"ar-17\"}, }"
```
"""
function winestock(a::agentReflex, input::NamedTuple)
println("")
@show input
wineSearchCriteria = GeneralUtils.JSON3read_stringKey(input[:toolinput])
newDict = Dict{String,Any}()
for (k,v) in wineSearchCriteria
println("k $k v $v")
newDict[string(k)] = v
end
#TODO temporary delete key "food pairing from a dict
newDict = deepcopy(a.memory[:keyword])
delete!(newDict, "food pairing")
query = JSON3.write(newDict)
println("")
@show query
# prompt =
# """
# <|system|>
# <About yourself>
# Your are a helpful assistant.
# </About yourself>
# <You have the following conversion table>
# Database table name by wine type:
# Red = table for wine type "red"
# White = table for wine type "white"
# Sparkling = table for wine type "sparkling"
# Rose = table for wine type "rose"
# Dessert = table for wine type "dessert"
# Fortified = table for wine type "fortified"
# Intensity level:
# intensity = 1, light bodied
# intensity = 2, semi-light bodied
# intensity = 3, medium bodied
# intensity = 4, semi-full bodied
# intensity = 5, full bodied
# Sweetness level:
# sweetness = 1, dry
# sweetness = 2, off-dry
# sweetness = 3, semi-sweet
# sweetness = 4, sweet
# sweetness = 5, very sweet
# Tannin level:
# tannin = 1, low tannin
# tannin = 2, semi-low tannin
# tannin = 3, medium tannin
# tannin = 4, semi-high tannin
# tannin = 5, high tannin
# Acidity level:
# acidity = 1, low acidity
# acidity = 2, semi-low acidity
# acidity = 3, medium acidity
# acidity = 4, semi-high acidity
# acidity = 5, high acidity
# </You have the following conversion table>
# <Your job>
# Consult the conversion table then write a specific SQL command using only available info from a JSON-format query.
# List of keywords not allowed in SQL: ["BETWEEN", "--", "WHEN", "IN"]
# Use the following format:
# Info map: based on conversion table, map the info in query to appropriate variables
# SQL: write a specific SQL command
# </Your job>
# <Example 1>
# query: {\"wine type\": \"white\", \"wine characteristics\": \"full-bodied | off-dry | low acidity | low to medium tannin\", \"price\": {\"max\": \"50\"}}
# Think: 1) low to medium tannin is not explicitly stated, but assuming it falls within the range of low-medium tannin.
# Info map: {\"wine type\": \"white\", \"intensity\": 5, \"sweetness\": 2, \"tannin\": 2, \"acidity\": 1, \"price\": 50}
# SQL: SELECT * FROM White WHERE intensity = 5 AND sweetness = 2 AND acidity = 1 AND tannin = 2 AND price <= 50;
# </Example 1>
# <Example 2>
# query: {\"wine characteristics\":\"low-bodied | a little sweet | low-medium tannin\",\"price\":\"22 USD\",\"occasion\":\"anniversary\",\"wine type\":\"Rose\",\"food\":\"American dishes\"}
# Think: 1) medium sweet is not explicitly stated, but assuming it falls within the range of dry and off-dry.
# Info map: {\"wine type\": \"Rose\", \"intensity\": 1, \"sweetness\": 3, \"tannin\": 2, \"acidity\": 3, \"price\": 22, \"food\":\"American dishes\"}
# SQL: SELECT * FROM Rose WHERE intensity = 1 AND tannin = 2 AND (sweetness = 1 OR sweetness = 2) AND price <= 22 AND food = American;
# </Example 2>
# </s>
# <|query|>
# $query
# </s>
# <|assistant|>
# """
prompt =
"""
<s>
<|system|>
<About yourself>
Your are a helpful assistant.
</About yourself>
<You have the following conversion table>
Database table name by wine type:
Red = table for wine type "red"
White = table for wine type "white"
Sparkling = table for wine type "sparkling"
Rose = table for wine type "rose"
Dessert = table for wine type "dessert"
Fortified = table for wine type "fortified"
Intensity level:
light bodied = 1
semi-light bodied = 2
medium bodied = 3
semi-full bodied = 4
full bodied = 5
Sweetness level:
dry = 1
off-dry = 2
semi-sweet = 3
sweet = 4
very sweet = 5
Tannin level:
low tannin = 1
semi-low tannin = 2
medium tannin = 3
semi-high tannin = 4
high tannin = 4
Acidity level:
low acidity = 1
semi-low acidity = 2
medium acidity = 3
semi-high acidity = 4
high acidity = 5
</You have the following conversion table>
<Your job>
Write a specific SQL command from a query using a conversion table
List of keywords not allowed the command: ["BETWEEN", "--", "WHEN", "IN"]
Use the following format:
Info map: based on conversion table, map the info in query to appropriate variables
SQL: write a specific SQL command
</Your job>
</|system|>
<Example 1>
<query>
{\"wine type\": \"white\", \"wine characteristics\": \"full-bodied | off-dry | low acidity | low to medium tannin\", \"price\": {\"max\": \"50\"}}
</query>
<|assistant|>
Think: 1) low to medium tannin is not explicitly stated, but assuming it falls within the range of low-medium tannin.
Info map: {\"wine type\": \"white\", \"intensity\": 5, \"sweetness\": 2, \"tannin\": 2, \"acidity\": 1, \"price\": 50}
SQL: SELECT * FROM White WHERE intensity = 5 AND sweetness = 2 AND acidity = 1 AND tannin = 2 AND price <= 50;
</|assistant|>
</Example 1>
<Example 2>
<query>
{\"wine characteristics\":\"low-bodied | a little sweet | low-medium tannin\",\"price\":\"22 USD\",\"occasion\":\"anniversary\",\"wine type\":\"Rose\",\"food\":\"American dishes\"}
</query>
<|assistant|>
Think: 1) medium sweet is not explicitly stated, but assuming it falls within the range of dry and off-dry.
Info map: {\"wine type\": \"Rose\", \"intensity\": 1, \"sweetness\": 3, \"tannin\": 2, \"acidity\": 3, \"price\": 22, \"food\":\"American dishes\"}
SQL: SELECT * FROM Rose WHERE intensity = 1 AND tannin = 2 AND (sweetness = 1 OR sweetness = 2) AND price <= 22 AND food = American;
</|assistant|>
</Example 2>
</s>
<query>
$query
</query>
<|assistant|>
"""
println("")
@show db_prompt = prompt
_sql = nothing
while true
_sql = sendReceivePrompt(a, prompt, max_tokens=256, temperature=0.4,
stopword=["/n/n", "END", "End", "Obs", "<|", "</"])
_sql = split(_sql, ";")[1] * ";"
@show _sql
# check for valid SQL command
check_1 = occursin("BETWEEN", _sql)
check_2 = occursin("--", _sql)
check_3 = occursin("IN", _sql)
if check_1 == false && check_2 == false && check_3 == false
break
end
println("invalid SQL command")
end
_sql = split(_sql, "SQL:")[end]
println("")
@show db_sql = replace(_sql, '\n'=>"")
# remove any blank character in front of a string
newsql = nothing
for i in eachindex(_sql)
if _sql[i] != ' '
newsql = _sql[i:end]
break
end
end
body = newsql
uri = URI(scheme="http", host="192.168.88.12", port="9010", path="/sql", userinfo="root:root")
r = HTTP.request("POST", uri, ["Accept" => "application/json", "NS"=>"yiem", "DB"=>"Blossom_wines"], body)
a.memory[:r] = r
result = copy(JSON3.read(r.body))
wines = shuffle(result[1][:result]) # shuffle in case there are more than 1 result
# choose only 2 wines
if length(wines) > 2
println("$(length(wines)) wines found")
wines = wines[1:2]
end
println("")
@show wines
result = nothing
if length(wines) == 0
result =
"""
No wine match my search query.
"""
else
# write wines dictionary in to string
wines_str = ""
for (i, wine) in enumerate(wines)
winename = wine[:wine_name]
wines_str *= "$i: $(JSON3.write(wines[i])),"
end
result =
"""
I found the following wines in our stock:
{
$wines_str
}
"""
end
@show result
return result
end
""" Get the first JSON string questions.
Arguments\n
a : one of ChatAgent's agent.
input {JSON string} : message to the user
Return\n
a single message to the user
Example\n
```jldoctest
julia> input = "{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}"
julia> askbox(input)
"How are you doing?"
```
"""
function askbox(input::String)
dict = GeneralUtils.JSON3read_stringKey(input)
_keylist = keys(dict)
keylist = [key for key in _keylist]
return dict[keylist[1]]
end
end # end module

View File

@@ -0,0 +1,342 @@
module type
export agent, agentReflex, newAgentMemory
using Dates, UUIDs, DataStructures
using CommUtils
#------------------------------------------------------------------------------------------------100
function newAgentMemory()
memory::Dict{Any, Any} = Dict(
:shortterm=> OrderedDict{String, Any}(),
:longterm=> OrderedDict{String, Any}(),
:log=> OrderedDict{String, Any}(), # span from user stimulus -> multiples attempts -> final respond
:keyword=> Dict{String, Any}(),
)
return memory
end
abstract type agent end
""" A LLM agent with self reflect capabilities.
Example:
```jldoctest
julia> using 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"), # this is where LLM server located
subtopic= (imgAI="agent/api/v0.1.0/img/respond",
txtAI="agent/api/v0.1.0/txt/respond"), # this is where this agent located
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> tools=Dict(
:chatbox=>Dict(
:name => "chatbox",
:description => "Useful only for when you need to ask the user for more info or context. Do not ask the user their own question.",
:input => "Input should be a text.",
: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 => ChatAgent.wikisearch, # put function here
),
:winestock=>Dict(
:name => "wineStock",
:description => "useful for when you need to search your wine stock by wine description, price, name or ID.",
:input => "Input is a search query.",
:output => "Output are Wine name, description, price and ID" ,
:func => ChatAgent.winestock,
),
)
julia> agent = ChatAgent.agentReflex(
"Jene",
role=:assistant,
mqttClientSpec=mqttClientSpec,
msgMeta=msgMeta,
tools=tools
)
```
"""
@kwdef mutable struct agentReflex <: agent
availableRole::AbstractVector = ["system", "user", "assistant"]
agentName::String = "Jene" # ex. Jene
maxUserMsg::Int = 30
earlierConversation::String = "N/A" # 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
task::Int = 1 # task number
env::AbstractString = "N/A"
thinkingFormat::Union{Dict, Nothing} = nothing
roleSpecificInstruction::Union{Dict, Nothing} = nothing
memory = newAgentMemory()
# LLM function related
winestockResult = ""
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.
""",
:sommelier =>
"""
You are a helpful sommelier at a wine retailer.
You helps users by searching wine that match the user preferences from your stock.
""",
),
roleSpecificInstruction::Dict=Dict(
:assistant => "",
# :sommelier =>
# """
# Required info you need for wine recommendation:
# - occasion: ask the user
# - type of food that will be served with wine: ask the user
# - type of wine (we have Rose, White, Red, Rose and Sparkling): ask the user
# - wine sweetness level (dry to very sweet)
# - wine intensity level (light to full bodied)
# - wine tannin level (low to high)
# - wine acidity level (low to high)
# - wine price range: ask the user
# - wines we have in stock: use winestock tool
# """
:sommelier =>
"""
<Required wine info>
You need to gather all of the following info sequentially before you can recommend wine:
1. "wine budget"
2. "wine type" (rose, white, red, sparkling, dessert)
3. "food pairing" that will be served with wine
4. "wine sweetness level" (dry to very sweet)
5. "wine intensity level" (light to full bodied)
6. "wine tannin level" (low to high)
7. "wine acidity level" (low to high)
8. wine you have in stock that match the user preferences
</Required wine info>
"""
),
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 action.
""",
:actor=>
"""
<Your job>
Use the following format:
Thought: based on the plan and the recap of the plan, what to do? (pay attention to correct numeral calculation and commonsense).
Act: an action to take based on your thought, must be one of [{toolnames}]
Actinput: your input to the action based on your thought (pay attention to the tool's input)
Obs: observed result of the action
P.S.1 ask the user one by one question.
P.S.2 ask the user what you want to know if you didn't ask yet.
</Your job>
""",
:actorOriginal=>
"""
Use the following format:
Thought: you should always think about do you have all the required info and what to do according to step {step} of the plan and the info you have (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
""",
),
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.roleSpecificInstruction = roleSpecificInstruction
newAgent.agentName = agentName
return newAgent
end
end # end module

File diff suppressed because it is too large Load Diff