31 Commits

Author SHA1 Message Date
narawat lamaiin
c0edf7dadf update 2025-04-04 15:04:02 +07:00
narawat lamaiin
c21f943b12 update 2025-04-01 21:17:15 +07:00
narawat lamaiin
b8fd772a28 update 2025-03-31 21:30:14 +07:00
narawat lamaiin
883f581b2a update 2025-03-22 15:34:00 +07:00
narawat lamaiin
5a890860a6 update 2025-03-22 09:42:51 +07:00
7d5bc14a09 mark new version 2025-03-21 10:13:53 +07:00
ton
37ba3a9d31 Merge pull request 'v0.1.3-dev' (#2) from v0.1.3-dev into main
Reviewed-on: #2
2025-03-21 03:09:16 +00:00
bfadd53033 update 2025-03-21 10:03:08 +07:00
8fc3afe348 update 2025-03-20 16:15:38 +07:00
c60037226a update 2025-03-13 19:11:20 +07:00
narawat lamaiin
db6c9c5f2b update 2025-03-07 13:34:15 +07:00
narawat lamaiin
6504099959 update 2025-01-31 09:50:44 +07:00
724b092bdb update 2025-01-30 21:28:49 +07:00
c56c3d02b0 update 2025-01-29 12:16:01 +07:00
ton
a7f3e29e9c Merge pull request 'WIP v0.1.2-dev' (#1) from v0.1.2-dev into main
Reviewed-on: #1
2025-01-25 07:30:18 +00:00
narawat lamaiin
b8fc23b41e update 2025-01-25 14:21:37 +07:00
narawat lamaiin
cf4cd13b14 update 2025-01-25 13:31:23 +07:00
narawat lamaiin
29adc077d5 update 2025-01-23 19:34:13 +07:00
narawat lamaiin
d89d425885 update 2025-01-21 08:28:26 +07:00
narawat lamaiin
bb81b973d3 update 2025-01-20 18:19:38 +07:00
narawat lamaiin
4197625e57 update 2025-01-17 22:09:48 +07:00
narawat lamaiin
3fdc0adf99 update 2025-01-16 07:40:39 +07:00
narawat lamaiin
c7000f66b8 update 2025-01-15 08:35:25 +07:00
narawat lamaiin
2206831bab update 2025-01-15 06:13:18 +07:00
narawat lamaiin
a29e8049a7 update 2025-01-11 16:57:57 +07:00
narawat lamaiin
944d9eaf2b update 2025-01-10 18:08:21 +07:00
narawat lamaiin
616c159336 update 2025-01-10 08:06:01 +07:00
narawat lamaiin
022cb5caf0 update 2025-01-05 17:41:21 +07:00
cff0d31ae6 update 2025-01-04 16:10:23 +07:00
82167fe006 update 2025-01-04 16:07:18 +07:00
814a0ecc6a update 2024-12-27 20:53:15 +07:00
11 changed files with 2158 additions and 1599 deletions

View File

@@ -1,7 +1,7 @@
name = "YiemAgent"
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
authors = ["narawat lamaiin <narawat@outlook.com>"]
version = "0.1.1"
version = "0.1.4"
[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
@@ -22,6 +22,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat]
DataFrames = "1.7.0"
GeneralUtils = "0.1.0"
GeneralUtils = "0.1, 0.2"
LLMMCTS = "0.1.2"
SQLLLM = "0.2.0"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@ abstract type agent end
mutable struct companion <: agent
name::String # agent name
id::String # agent id
systemmsg::Union{String, Nothing}
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
""" Memory
@@ -34,8 +34,8 @@ end
function companion(
text2textInstructLLM::Function
;
name::String= "Assistant",
id::String= string(uuid4()),
systemmsg::Union{String, Nothing}= nothing,
maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(),
)
@@ -48,13 +48,13 @@ function companion(
)
newAgent = companion(
name,
id,
maxHistoryMsg,
chathistory,
memory,
text2textInstructLLM
)
id,
systemmsg,
maxHistoryMsg,
chathistory,
memory,
text2textInstructLLM
)
return newAgent
end
@@ -146,7 +146,6 @@ mutable struct sommelier <: agent
"""
chathistory::Vector{Dict{Symbol, Any}}
memory::Dict{Symbol, Any}
func # NamedTuple of functions
end
@@ -179,14 +178,17 @@ function sommelier(
# ),
)
memory = Dict{Symbol, Any}(
:chatbox=> "",
:shortmem=> OrderedDict{Symbol, Any}(),
:events=> Vector{Dict{Symbol, Any}}(),
:state=> Dict{Symbol, Any}(
:wine_presented_to_user=> "None",
),
)
memory = Dict{Symbol, Any}(
:chatbox=> "",
:shortmem=> OrderedDict{Symbol, Any}(
:available_wine=> [],
:found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines
),
:events=> Vector{Dict{Symbol, Any}}(),
:state=> Dict{Symbol, Any}(
),
:recap=> OrderedDict{Symbol, Any}(),
)
newAgent = sommelier(
name,

View File

@@ -1,6 +1,7 @@
module util
export clearhistory, addNewMessage, vectorOfDictToText, eventdict, noises
export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText
using UUIDs, Dates, DataStructures, HTTP, JSON3
using GeneralUtils
@@ -106,7 +107,7 @@ function addNewMessage(a::T1, name::String, text::T2;
error("name is not in agent.availableRole $(@__LINE__)")
end
#[] summarize the oldest 10 message
#[PENDING] summarize the oldest 10 message
if length(a.chathistory) > maximumMsg
summarize(a.chathistory)
else
@@ -121,47 +122,53 @@ This function takes in a vector of dictionaries and outputs a single string wher
# Arguments
- `vecd::Vector`
a vector of dictionaries
A vector of dictionaries containing chat messages
- `withkey::Bool`
whether to include the key in the output text. Default is true
Whether to include the name as a prefix in the output text. Default is true
- `range::Union{Nothing,UnitRange,Int}`
Optional range of messages to include. If nothing, includes all messages
# Return
a string with the formatted dictionaries
# Returns
A formatted string where each line contains either:
- If withkey=true: "name> message\n"
- If withkey=false: "message\n"
# Example
```jldoctest
julia> using Revise
julia> using GeneralUtils
julia> vecd = [Dict(:name => "John", :text => "Hello"), Dict(:name => "Jane", :text => "Goodbye")]
julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true)
"John> Hello\nJane> Goodbye\n"
```
# Signature
"""
function vectorOfDictToText(vecd::Vector; withkey=true)::String
function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String
# Initialize an empty string to hold the final text
text = ""
# Get the elements within the specified range, or all elements if no range provided
elements = isnothing(range) ? vecd : vecd[range]
# Determine whether to include the key in the output text or not
if withkey
# Loop through each dictionary in the input vector
for d in vecd
# Extract the 'name' and 'text' keys from the dictionary
name = d[:name]
_text = d[:text]
# Append the formatted string to the text variable
text *= "$name> $_text \n"
# Loop through each dictionary in the input vector
for d in elements
# Extract the 'name' and 'text' keys from the dictionary
name = d[:name]
_text = d[:text]
# Append the formatted string to the text variable
text *= "$name:> $_text \n"
end
else
# Loop through each dictionary in the input vector
for d in vecd
# Iterate over all key-value pairs in the dictionary
for (k, v) in d
# Append the formatted string to the text variable
text *= "$v \n"
end
end
# Loop through each dictionary in the input vector
for d in elements
# Iterate over all key-value pairs in the dictionary
for (k, v) in d
# Append the formatted string to the text variable
text *= "$v \n"
end
end
end
# Return the final text
@@ -169,11 +176,63 @@ function vectorOfDictToText(vecd::Vector; withkey=true)::String
end
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
""" Create a dictionary representing an event with optional details.
# Arguments
- `event_description::Union{String, Nothing}`
A description of the event
- `timestamp::Union{DateTime, Nothing}`
The time when the event occurred
- `subject::Union{String, Nothing}`
The subject or entity associated with the event
- `thought::Union{AbstractDict, Nothing}`
Any associated thoughts or metadata
- `actionname::Union{String, Nothing}`
The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
- `actioninput::Union{String, Nothing}`
Input or parameters for the action
- `location::Union{String, Nothing}`
Where the event took place
- `equipment_used::Union{String, Nothing}`
Equipment involved in the event
- `material_used::Union{String, Nothing}`
Materials used during the event
- `outcome::Union{String, Nothing}`
The result or consequence of the event after action execution
- `note::Union{String, Nothing}`
Additional notes or comments
# Returns
A dictionary with event details as symbol-keyed key-value pairs
"""
function eventdict(;
event_description::Union{String, Nothing}=nothing,
timestamp::Union{DateTime, Nothing}=nothing,
subject::Union{String, Nothing}=nothing,
action_or_dialogue::Union{String, Nothing}=nothing,
thought::Union{AbstractDict, Nothing}=nothing,
actionname::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENTBOX", etc
actioninput::Union{String, Nothing}=nothing,
location::Union{String, Nothing}=nothing,
equipment_used::Union{String, Nothing}=nothing,
material_used::Union{String, Nothing}=nothing,
@@ -184,7 +243,9 @@ function eventdict(;
:event_description=> event_description,
:timestamp=> timestamp,
:subject=> subject,
:action_or_dialogue=> action_or_dialogue,
:thought=> thought,
:actionname=> actionname,
:actioninput=> actioninput,
:location=> location,
:equipment_used=> equipment_used,
:material_used=> material_used,
@@ -194,6 +255,61 @@ function eventdict(;
end
""" Create a formatted timeline string from a sequence of events.
# Arguments
- `events::T1`
Vector of event dictionaries containing subject, actioninput and optional outcome fields
Each event dictionary should have the following keys:
- :subject - The subject or entity performing the action
- :actioninput - The action or input performed by the subject
- :outcome - (Optional) The result or outcome of the action
# Returns
- `timeline::String`
A formatted string representing the events with their subjects, actions, and optional outcomes
Format: "{index}) {subject}> {actioninput} {outcome}\n" for each event
# Example
events = [
Dict(:subject => "User", :actioninput => "Hello", :outcome => nothing),
Dict(:subject => "Assistant", :actioninput => "Hi there!", :outcome => "with a smile")
]
timeline = createTimeline(events)
# 1) User> Hello
# 2) Assistant> Hi there! with a smile
"""
function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty timeline string
timeline = ""
# Determine which indices to use - either provided range or full length
ind =
if eventindex !== nothing
[eventindex...]
else
1:length(events)
end
# Iterate through events and format each one
for (i, event) in zip(ind, events)
# If no outcome exists, format without outcome
if event[:outcome] === nothing
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput])\n"
# If outcome exists, include it in formatting
else
timeline *= "Event_$i $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
end
end
# Return formatted timeline string
return timeline
end
# """ Convert a single chat dictionary into LLM model instruct format.

41
test/Manifest.toml Normal file
View File

@@ -0,0 +1,41 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.11.4"
manifest_format = "2.0"
project_hash = "71d91126b5a1fb1020e1098d9d492de2a4438fd2"
[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
version = "1.11.0"
[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
version = "1.11.0"
[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
version = "1.11.0"
[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
version = "1.11.0"
[[deps.Random]]
deps = ["SHA"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
version = "1.11.0"
[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"
[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
version = "1.11.0"
[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
version = "1.11.0"

2
test/Project.toml Normal file
View File

@@ -0,0 +1,2 @@
[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

View File

@@ -27,30 +27,50 @@
"description": "agent role"
},
"organization": {
"value": "yiem_hq",
"value": "yiem_branch_1",
"description": "organization name"
},
"externalservice": {
"text2textinstruct": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"virtualWineCustomer_1": {
"mqtttopic": "/virtualenvironment/winecustomer",
"description": "text to text service with instruct LLM that act as wine customer",
"llminfo": {
"name": "llama3instruct"
}
},
"text2textchat": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
}
"loadbalancer": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM"
},
"text2textinstruct": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"virtualWineCustomer_1": {
"mqtttopic": "/virtualenvironment/winecustomer",
"description": "text to text service with instruct LLM that act as wine customer",
"llminfo": {
"name": "llama3instruct"
}
},
"text2textchat": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"wineDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10201,
"dbname": "wineDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
},
"SQLVectorDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10203,
"dbname": "SQLVectorDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
}
}
}

View File

@@ -1,9 +0,0 @@
using GeneralUtils
response = "trajectory_evaluation:\nThe trajectory is correct so far. The thought accurately reflects the user's question, and the action taken is a valid attempt to retrieve data from the database that matches the specified criteria.\n\nanswer_evaluation:\nThe observation provides information about two red wines from Bordeaux rive droite in France, which partially answers the question. However, it does not provide a complete answer as it only lists the wine names and characteristics, but does not explicitly state whether there are any other wines that match the criteria.\n\naccepted_as_answer: No\n\nscore: 6\nThe trajectory is mostly correct, but the observation does not fully address the question.\n\nsuggestion: Consider adding more filters or parameters to the database query to retrieve a complete list of wines that match the specified criteria."
responsedict = GeneralUtils.textToDict(response,
["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"],
rightmarker=":", symbolkey=true)

0
test/runtests.jl Normal file
View File

View File

@@ -1,272 +1,292 @@
using Revise
using JSON, JSON3, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames
using YiemAgent, GeneralUtils
using Base.Threads
# ---------------------------------------------- 100 --------------------------------------------- #
# load config
config = JSON3.read("./test/config.json")
# config = copy(JSON3.read("../mountvolume/config.json"))
function executeSQL(sql::T) where {T<:AbstractString}
DBconnection = LibPQ.Connection("host=192.168.88.12 port=10201 dbname=wineDB user=yiemtechnologies password=yiemtechnologies@Postgres_0.0")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function executeSQLVectorDB(sql)
DBconnection = LibPQ.Connection("host=192.168.88.12 port=10203 dbname=SQLVectorDB user=yiemtechnologies password=yiemtechnologies@Postgres_0.0")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function text2textInstructLLM(prompt::String)
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:text2textinstruct][:mqtttopic];
msgPurpose="inference",
senderName="yiemagent",
senderId=string(uuid4()),
receiverName="text2textinstruct",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => prompt,
:kwargs => Dict(
:num_ctx => 16384,
:temperature => 0.2,
)
)
)
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=6000)
response = _response[:response][:text]
return response
end
# get text embedding from a LLM service
function getEmbedding(text::T) where {T<:AbstractString}
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:text2textinstruct][:mqtttopic];
msgPurpose="embedding",
senderName="yiemagent",
senderId=string(uuid4()),
receiverName="text2textinstruct",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => [text] # must be a vector of string
)
)
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=6000)
embedding = response[:response][:embeddings]
return embedding
end
function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3,
vectorDB::Function; limit::Integer=1
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
# get embedding from LLM service
embedding = getEmbedding(text)[1]
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
sql = """
SELECT *, $embeddingColumnName <-> '$embedding' as distance
FROM $tablename
ORDER BY distance LIMIT $limit;
"""
response = vectorDB(sql)
df = DataFrame(response)
return df
end
function similarSQLVectorDB(query; maxdistance::Integer=100)
tablename = "sqlllm_decision_repository"
# get embedding of the query
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable SQL, return it.
output_b64 = df[1, :function_output_base64] # pick the closest match
output_str = String(base64decode(output_b64))
rowid = df[1, :id]
println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
return (dict=output_str, distance=distance)
else
println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return (dict=nothing, distance=nothing)
end
end
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=1) where {T1<:AbstractString, T2<:AbstractString}
tablename = "sqlllm_decision_repository"
# get embedding of the query
# query = state[:thoughtHistory][:question]
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
query_embedding = getEmbedding(query)[1]
query = replace(query, "'" => "")
sql_base64 = base64encode(SQL)
sql_ = replace(SQL, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
end
end
function similarSommelierDecision(recentevents::T1; maxdistance::Integer=5
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
tablename = "sommelier_decision_repository"
# find similar
println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__)
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable decision, return it.
rowid = df[1, :id]
println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
output_b64 = df[1, :function_output_base64] # pick the closest match
_output_str = String(base64decode(output_b64))
output = copy(JSON3.read(_output_str))
return output
else
println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return nothing
end
end
function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5
) where {T1<:AbstractString, T2<:AbstractDict}
tablename = "sommelier_decision_repository"
# find similar
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
recentevents_embedding = a.func[:getEmbedding](recentevents)[1]
recentevents = replace(recentevents, "'" => "")
decision_json = JSON3.write(decision)
decision_base64 = base64encode(decision_json)
decision = replace(decision_json, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
else
println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
end
end
sessionId = "12345"
externalFunction = (
getEmbedding=getEmbedding,
text2textInstructLLM=text2textInstructLLM,
executeSQL=executeSQL,
similarSQLVectorDB=similarSQLVectorDB,
insertSQLVectorDB=insertSQLVectorDB,
similarSommelierDecision=similarSommelierDecision,
insertSommelierDecision=insertSommelierDecision,
)
a = YiemAgent.sommelier(
externalFunction;
name="Ton",
id=sessionId, # agent instance id
retailername="Yiem",
)
while true
println("your respond: ")
user_answer = readline()
response = YiemAgent.conversation(a, Dict(:text=> user_answer))
println("\n$response")
end
# response = YiemAgent.conversation(a, Dict(:text=> "I want to get a French red wine under 100."))
using Revise
using JSON, JSON3, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames
using YiemAgent, GeneralUtils
using Base.Threads
# ---------------------------------------------- 100 --------------------------------------------- #
# load config
config = JSON3.read("/appfolder/app/dev/YiemAgent/test/config.json")
# config = copy(JSON3.read("../mountvolume/config.json"))
function executeSQL(sql::T) where {T<:AbstractString}
host = config[:externalservice][:wineDB][:host]
port = config[:externalservice][:wineDB][:port]
dbname = config[:externalservice][:wineDB][:dbname]
user = config[:externalservice][:wineDB][:user]
password = config[:externalservice][:wineDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function executeSQLVectorDB(sql)
host = config[:externalservice][:SQLVectorDB][:host]
port = config[:externalservice][:SQLVectorDB][:port]
dbname = config[:externalservice][:SQLVectorDB][:dbname]
user = config[:externalservice][:SQLVectorDB][:user]
password = config[:externalservice][:SQLVectorDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function text2textInstructLLM(prompt::String; maxattempt::Integer=2, modelsize::String="medium")
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="inference",
senderName="yiemagent",
senderId=sessionId,
receiverName="text2textinstruct_$modelsize",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => prompt,
:kwargs => Dict(
:num_ctx => 16384,
:temperature => 0.2,
)
)
)
response = nothing
for attempts in 1:maxattempt
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=180, maxattempt=maxattempt)
payload = _response[:response]
if _response[:success] && payload[:text] !== nothing
response = _response[:response][:text]
break
else
println("\n<text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(outgoingMsg)
println("</text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
sleep(3)
end
end
return response
end
# get text embedding from a LLM service
function getEmbedding(text::T) where {T<:AbstractString}
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="embedding",
senderName="yiemagent",
senderId=sessionId,
receiverName="textembedding",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => [text] # must be a vector of string
)
)
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120, maxattempt=3)
embedding = response[:response][:embeddings]
return embedding
end
function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3,
vectorDB::Function; limit::Integer=1
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
# get embedding from LLM service
embedding = getEmbedding(text)[1]
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
sql = """
SELECT *, $embeddingColumnName <-> '$embedding' as distance
FROM $tablename
ORDER BY distance LIMIT $limit;
"""
response = vectorDB(sql)
df = DataFrame(response)
return df
end
function similarSQLVectorDB(query; maxdistance::Integer=100)
tablename = "sqlllm_decision_repository"
# get embedding of the query
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
# println(df[1, [:id, :function_output]])
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
# distance = 100 # CHANGE this is for testing only
if row != 0 && distance < maxdistance
# if there is usable SQL, return it.
output_b64 = df[1, :function_output_base64] # pick the closest match
output_str = String(base64decode(output_b64))
rowid = df[1, :id]
println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=output_str, distance=distance)
else
println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=nothing, distance=nothing)
end
end
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString}
tablename = "sqlllm_decision_repository"
# get embedding of the query
# query = state[:thoughtHistory][:question]
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
query_embedding = getEmbedding(query)[1]
query = replace(query, "'" => "")
sql_base64 = base64encode(SQL)
sql_ = replace(SQL, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
"""
# println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# println(sql)
_ = executeSQLVectorDB(sql)
end
end
function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
tablename = "sommelier_decision_repository"
# find similar
println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__)
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable decision, return it.
rowid = df[1, :id]
println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
output_b64 = df[1, :function_output_base64] # pick the closest match
_output_str = String(base64decode(output_b64))
output = copy(JSON3.read(_output_str))
return output
else
println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return nothing
end
end
function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5
) where {T1<:AbstractString, T2<:AbstractDict}
tablename = "sommelier_decision_repository"
# find similar
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
recentevents_embedding = a.func[:getEmbedding](recentevents)[1]
recentevents = replace(recentevents, "'" => "")
decision_json = JSON3.write(decision)
decision_base64 = base64encode(decision_json)
decision = replace(decision_json, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
else
println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
end
end
sessionId = "12345"
externalFunction = (
getEmbedding=getEmbedding,
text2textInstructLLM=text2textInstructLLM,
executeSQL=executeSQL,
similarSQLVectorDB=similarSQLVectorDB,
insertSQLVectorDB=insertSQLVectorDB,
similarSommelierDecision=similarSommelierDecision,
insertSommelierDecision=insertSommelierDecision,
)
a = YiemAgent.sommelier(
externalFunction;
name="Ton",
id=sessionId, # agent instance id
retailername="Yiem",
)
while true
print("\nyour respond: ")
user_answer = readline()
response = YiemAgent.conversation(a, Dict(:text=> user_answer))
println("\n$response")
end
# response = YiemAgent.conversation(a, Dict(:text=> "I want to get a French red wine under 100."))
"""
hello I want to get a bottle of red wine for my boss. I have a budget around 50 dollars. Show me some options.
I have no idea about his wine taste but he likes spicy food.
"""