1303 lines
44 KiB
Julia
1303 lines
44 KiB
Julia
module llmfunction
|
||
|
||
export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recommendbox,
|
||
virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1,
|
||
extractWineAttributes_2, paraphrase
|
||
|
||
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures
|
||
using GeneralUtils, SQLLLM
|
||
using ..type, ..util
|
||
|
||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||
|
||
|
||
""" Chatbox for chatting with virtual wine customer.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of Yiem's agent
|
||
- `input::T2`
|
||
text to be send to virtual wine customer
|
||
|
||
# Return
|
||
- `response::String`
|
||
response of virtual wine customer
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docstring
|
||
- [] add reccommend() to compare wine
|
||
|
||
# Signature
|
||
"""
|
||
function virtualWineUserRecommendbox(a::T1, input
|
||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent}
|
||
|
||
# put in model format
|
||
virtualWineCustomer = a.config["externalservice"]["virtualWineCustomer_1"]
|
||
llminfo = virtualWineCustomer["llminfo"]
|
||
prompt =
|
||
if llminfo["name"] == "llama3instruct"
|
||
formatLLMtext_llama3instruct("assistant", input)
|
||
else
|
||
error("llm model name is not defied yet $(@__LINE__)")
|
||
end
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
msgMeta = GeneralUtils.generate_msgMeta(
|
||
virtualWineCustomer["mqtttopic"],
|
||
senderName= "virtualWineUserRecommendbox",
|
||
senderId= a.id,
|
||
receiverName= "virtualWineCustomer",
|
||
mqttBroker= a.config["mqttServerInfo"]["broker"],
|
||
mqttBrokerPort= a.config["mqttServerInfo"]["port"],
|
||
msgId = "dummyid" #CHANGE remove after testing finished
|
||
)
|
||
|
||
outgoingMsg = Dict(
|
||
"msgMeta"=> msgMeta,
|
||
"payload"=> Dict(
|
||
"text"=> prompt,
|
||
)
|
||
)
|
||
|
||
result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
||
response = result["response"]
|
||
|
||
return (response["text"], response["select"], response["reward"], response["isterminal"])
|
||
end
|
||
|
||
|
||
|
||
""" Chatbox for chatting with virtual wine customer.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of Yiem's agent
|
||
- `input::T2`
|
||
text to be send to virtual wine customer
|
||
|
||
# Return
|
||
- `response::String`
|
||
response of virtual wine customer
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# TODO
|
||
- [] update docs
|
||
- [x] write a prompt for virtual customer
|
||
|
||
# Signature
|
||
"""
|
||
function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistory
|
||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:AbstractDict, T2<:AbstractString}
|
||
|
||
previouswines =
|
||
"""
|
||
You have the following wines previously:
|
||
|
||
"""
|
||
|
||
systemmsg =
|
||
"""
|
||
You find yourself in a well-stocked wine store, engaged in a conversation with the store's knowledgeable sommelier.
|
||
You're on a quest to find a bottle of wine that aligns with your specific preferences and requirements.
|
||
|
||
The ideal wine you're seeking should meet the following criteria:
|
||
1. It should fit within your budget.
|
||
2. It should be suitable for the occasion you're planning.
|
||
3. It should pair well with the food you intend to serve.
|
||
4. It should be of a particular type of wine you prefer.
|
||
5. It should possess certain characteristics, including:
|
||
- The level of sweetness.
|
||
- The intensity of its flavor.
|
||
- The amount of tannin it contains.
|
||
- Its acidity level.
|
||
|
||
Here's the criteria details:
|
||
{
|
||
"budget": 50,
|
||
"occasion": "graduation ceremony",
|
||
"food pairing": "Thai food",
|
||
"type of wine": "red",
|
||
"wine sweetness level": "dry",
|
||
"wine intensity level": "full-bodied",
|
||
"wine tannin level": "low",
|
||
"wine acidity level": "medium",
|
||
}
|
||
|
||
You should only respond with "text", "select", "reward", "isterminal" steps.
|
||
"text" is your conversation.
|
||
"select" is an integer. Choose an option when presented with choices, or leave it null if none of the options satisfy you or if no choices are available.
|
||
"reward" is an integer, it can be three number:
|
||
1) 1 if you find the right wine.
|
||
2) 0 if you don’t find the ideal wine.
|
||
3) -1 if you’re dissatisfied with the sommelier’s response.
|
||
"isterminal" can be false if you still want to talk with the sommelier, true otherwise.
|
||
|
||
You should only respond in JSON format as describe below:
|
||
{
|
||
"text": "your conversation",
|
||
"select": null,
|
||
"reward": 0,
|
||
"isterminal": false
|
||
}
|
||
|
||
Here are some examples:
|
||
|
||
sommelier: "What's your budget?
|
||
you:
|
||
{
|
||
"text": "My budget is 30 USD.",
|
||
"select": null,
|
||
"reward": 0,
|
||
"isterminal": false
|
||
}
|
||
|
||
sommelier: "The first option is Zena Crown and the second one is Buano Red."
|
||
you:
|
||
{
|
||
"text": "I like the 2nd option.",
|
||
"select": 2,
|
||
"reward": 1,
|
||
"isterminal": true
|
||
}
|
||
|
||
Let's begin!
|
||
"""
|
||
|
||
pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemmsg))
|
||
|
||
# replace the :user key in chathistory to allow the virtual wine customer AI roleplay
|
||
chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}()
|
||
for i in virtualCustomerChatHistory
|
||
newdict = Dict()
|
||
newdict["name"] =
|
||
if i["name"] == "user"
|
||
"you"
|
||
elseif i["name"] == "assistant"
|
||
"sommelier"
|
||
else
|
||
i["name"]
|
||
end
|
||
|
||
newdict["text"] = i["text"]
|
||
push!(chathistory, newdict)
|
||
end
|
||
|
||
push!(chathistory, Dict("name"=> "assistant", "text"=> input))
|
||
|
||
# put in model format
|
||
prompt = formatLLMtext(chathistory, "llama3instruct")
|
||
prompt *=
|
||
"""
|
||
<|start_header_id|>you<|end_header_id|>
|
||
{"text"
|
||
"""
|
||
|
||
pprint(prompt)
|
||
externalService = config["externalservice"]["text2textinstruct"]
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
msgMeta = GeneralUtils.generate_msgMeta(
|
||
externalService["mqtttopic"],
|
||
senderName= "virtualWineUserChatbox",
|
||
senderId= string(uuid4()),
|
||
receiverName= "text2textinstruct",
|
||
mqttBroker= config["mqttServerInfo"]["broker"],
|
||
mqttBrokerPort= config["mqttServerInfo"]["port"],
|
||
msgId = string(uuid4()) #CHANGE remove after testing finished
|
||
)
|
||
|
||
outgoingMsg = Dict(
|
||
"msgMeta"=> msgMeta,
|
||
"payload"=> Dict(
|
||
"text"=> prompt,
|
||
)
|
||
)
|
||
|
||
attempt = 0
|
||
for attempt in 1:5
|
||
try
|
||
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
||
_responseJsonStr = response["response"]["text"]
|
||
expectedJsonExample =
|
||
"""
|
||
Here is an expected JSON format:
|
||
{
|
||
"text": "...",
|
||
"select": "...",
|
||
"reward": "...",
|
||
"isterminal": "..."
|
||
}
|
||
"""
|
||
responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample)
|
||
responseDict = copy(JSON.parsefile(responseJsonStr))
|
||
|
||
text::AbstractString = responseDict["text"]
|
||
select::Union{Nothing, Number} = responseDict["select"] == "null" ? nothing : responseDict["select"]
|
||
reward::Number = responseDict["reward"]
|
||
isterminal::Bool = responseDict["isterminal"]
|
||
|
||
if text != ""
|
||
# pass test
|
||
else
|
||
error("virtual customer not answer correctly")
|
||
end
|
||
|
||
return (text, select, reward, isterminal)
|
||
catch e
|
||
io = IOBuffer()
|
||
showerror(io, e)
|
||
errorMsg = String(take!(io))
|
||
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
|
||
println("")
|
||
@warn "Error occurred: $errorMsg\n$st"
|
||
println("")
|
||
end
|
||
end
|
||
error("virtualWineUserChatbox failed to get a response")
|
||
end
|
||
|
||
""" Search wine in stock.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of ChatAgent's agent.
|
||
- `thoughtdict::AbstractDict`
|
||
# Return
|
||
A JSON string of available wine
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia> using ChatAgent
|
||
julia> agent = ChatAgent.agentReflex("Jene")
|
||
julia> input = "{\"food\": \"pizza\", \"occasion\": \"anniversary\"}"
|
||
julia> result = checkinventory(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 search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
||
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
|
||
|
||
println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"])
|
||
wineattributes_2 = extractWineAttributes_2(a, thoughtdict["action_input"])
|
||
|
||
retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency", "image_url", "retailer_name", "retailer_id"]
|
||
_inventoryquery = "$wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)"
|
||
inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}"
|
||
println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
|
||
if useSQLLLM
|
||
# add suppport for similarSQLVectorDB
|
||
textresult, result_raw = SQLLLM.query(
|
||
inventoryquery,
|
||
a.context.executeSQL,
|
||
a.context.text2textInstructLLM;
|
||
insertSQLVectorDB=a.context.insertSQLVectorDB,
|
||
similarSQLVectorDB=a.context.similarSQLVectorDB,
|
||
llmFormatName="qwen3")
|
||
thoughtdict["action_result"] = textresult
|
||
else
|
||
|
||
# direct query with possible sql instead of SQLLLM.
|
||
sql = generatesql(a, inventoryquery)
|
||
println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n")
|
||
textresult, result_raw, _, _ = SQLexecution(a.context.executeSQL, sql)
|
||
#WORKING if result_raw != nothing, get image from image_url column of a df.
|
||
# then store in a.memory["shortmem]["image"] = OrderedDict(
|
||
# Dict(
|
||
# "wine_id"=> "wine_id,
|
||
# "name"=> "wine name",
|
||
# "image_url" => Dict("url" => data1_uri)
|
||
# )
|
||
|
||
# )
|
||
|
||
|
||
# # 1. Read local file and encode to base64 string
|
||
# image2_path = "test/small_image.png"
|
||
# image2_bytes = read(image2_path)
|
||
# image2_base64_string = base64encode(image2_bytes)
|
||
# mime_type = "image/png"
|
||
# data2_uri = "data:$(mime_type);base64,$(image2_base64_string)"
|
||
|
||
# # 3. Construct payload with the Data URI
|
||
# message = Dict(
|
||
# "role" => "user",
|
||
# "content" => [
|
||
# Dict("type" => "text", "text" => "Do you know type of wine in the image?"),
|
||
# Dict(
|
||
# "type" => "image_url",
|
||
# "image_url" => Dict("url" => data1_uri)
|
||
# )
|
||
# ]
|
||
# )
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
thoughtdict["action_result"] = textresult
|
||
end
|
||
|
||
return (thoughtdict=thoughtdict, result_raw=result_raw)
|
||
end
|
||
|
||
|
||
function generatesql(a::T, searchterm::String,
|
||
; maxattempt=10
|
||
)::String where {T<:agent}
|
||
|
||
systemmsg =
|
||
"""
|
||
# database_search_guidelines
|
||
- Keep SQL queries focused only on the provided information.
|
||
- Use wildcard character (%) to search more effectively.
|
||
- Do not create any table in the database.
|
||
- A junction table can be used to link tables together. Another use case is for filtering data.
|
||
- If you can't find a single table that can be used to answer the user's search term, try joining multiple tables to see if you can obtain the answer.
|
||
- Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search.
|
||
- If there is no search result from the database, remove the restrictive criteria until a search result is available, and proceed from there.
|
||
|
||
# situation
|
||
At each round of conversation, you will be given the following:
|
||
- user search term
|
||
|
||
# objective
|
||
Consult the database_search_guidelines. Then find the data from a database to satisfy the user's search term.
|
||
|
||
# your responsibility includes
|
||
Fulfill the objective.
|
||
|
||
# you should then respond to the user with interleaving plan, action_name, action_input
|
||
1) "plan, Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
|
||
2) "action_name, Must be "RUNSQL"
|
||
3) "action_input, The input to the action you are about to perform according to your plan.
|
||
After the action is executed you gets "action_result". It is the output from the action you selected.
|
||
|
||
# you should only respond in JSON format as described below
|
||
"plan": "...",
|
||
"action_name": "...",
|
||
"action_input": "..."
|
||
|
||
# available_actions
|
||
"RUNSQL", which you can use to execute SQL against the database.
|
||
The input must be a single SQL query to be executed against the database.
|
||
For more effective text search, it's necessary to use case-insensitivity and the ILIKE operator.
|
||
Do not wrap the SQL as it will be executed against the database directly and SQL must be ended with ';'.
|
||
"""
|
||
|
||
# table_schema =
|
||
# """
|
||
# create table customer (
|
||
# customer_id uuid primary key default gen_random_uuid (),
|
||
# customer_firstname varchar(128),
|
||
# customer_lastname varchar(128),
|
||
# customer_displayname varchar(128) not null,
|
||
# customer_username varchar(128),
|
||
# customer_password varchar(128),
|
||
# customer_gender varchar(128),
|
||
# country varchar(128),
|
||
# telephone varchar(128),
|
||
# email varchar(128) not null,
|
||
# customer_birthdate varchar(128),
|
||
# note text,
|
||
|
||
# other_attributes jsonb,
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp,
|
||
# description text
|
||
# );
|
||
|
||
# create table retailer (
|
||
# retailer_id uuid primary key default gen_random_uuid (),
|
||
# retailer_name varchar(128) not null,
|
||
# retailer_username varchar(128) not null,
|
||
# retailer_password varchar(128) not null,
|
||
# retailer_address text not null,
|
||
# country varchar(128) not null,
|
||
# contact_person varchar(128) not null,
|
||
# telephone varchar(128) not null,
|
||
# email varchar(128) not null,
|
||
# note text,
|
||
|
||
# other_attributes jsonb,
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp,
|
||
# description text
|
||
# );
|
||
|
||
# create table food (
|
||
# food_id uuid primary key default gen_random_uuid (),
|
||
# food_name varchar(128) not null,
|
||
# country varchar(128),
|
||
# spiciness integer,
|
||
# sweetness integer,
|
||
# sourness integer,
|
||
# savoriness integer,
|
||
# bitterness integer,
|
||
# serving_temperature integer,
|
||
# image_url jsonb,
|
||
# note text,
|
||
# other_attributes jsonb,
|
||
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp,
|
||
# description text
|
||
# );
|
||
|
||
# create table wine (
|
||
# wine_id uuid primary key default gen_random_uuid (),
|
||
# seo_name varchar(128) not null,
|
||
# wine_name varchar(128) not null,
|
||
# winery varchar(128) not null,
|
||
# vintage integer not null,
|
||
# region varchar(128) not null,
|
||
# country varchar(128) not null,
|
||
# wine_type varchar(128) not null,
|
||
# grape varchar(128) not null,
|
||
# serving_temperature varchar(128) not null,
|
||
# intensity integer,
|
||
# sweetness integer,
|
||
# tannin integer,
|
||
# acidity integer,
|
||
# fizziness integer,
|
||
# tasting_notes text,
|
||
# image_url jsonb,
|
||
# manufacturer_sku text,
|
||
# note text,
|
||
# other_attributes jsonb,
|
||
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp,
|
||
# description text
|
||
# );
|
||
|
||
# create table wine_food (
|
||
# wine_id uuid references wine(wine_id),
|
||
# food_id uuid references food(food_id),
|
||
# constraint wine_food_id primary key (wine_id, food_id),
|
||
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp
|
||
# );
|
||
|
||
# CREATE TABLE retailer_wine (
|
||
# retailer_id uuid references retailer(retailer_id),
|
||
# wine_id uuid references wine(wine_id),
|
||
# constraint retailer_wine_id primary key (retailer_id, wine_id),
|
||
# price NUMERIC(10, 2),
|
||
# currency varchar(3) not null,
|
||
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp
|
||
# );
|
||
|
||
# CREATE TABLE retailer_food (
|
||
# retailer_id uuid references retailer(retailer_id),
|
||
# food_id uuid references food(food_id),
|
||
# constraint retailer_food_id primary key (retailer_id, food_id),
|
||
# price NUMERIC(10, 2),
|
||
# currency varchar(3) not null,
|
||
|
||
# created_time timestamptz default current_timestamp,
|
||
# updated_time timestamptz default current_timestamp
|
||
# );
|
||
# """
|
||
|
||
requiredKeys = ["plan", "action_name", "action_input"]
|
||
errornote = ""
|
||
# provide similar sql only for the first attempt
|
||
# sql, distance = a.context.similarSQLVectorDB(searchterm)
|
||
|
||
# similarSQL_ = sql !== nothing ? sql : "None"
|
||
# # if sql is really close, just use it
|
||
# if similarSQL_ != "None" && distance <= 0.1
|
||
# return similarSQL_
|
||
# end
|
||
|
||
#CHANGE use find_related_tables_for_user_question and inject only related table schema instead
|
||
# of hard code table schema. CPU embedding is too slow. use embedding service on GPU.
|
||
related_tables = a.context.find_related_tables_for_user_question(searchterm)
|
||
table_schema = ""
|
||
for table in related_tables
|
||
_table_schema_df = GeneralUtils.get_db_table_schema(a.context.pg_conn_str, table)
|
||
table_schema_df = _table_schema_df[:, [:column_name, :data_type, :constraint_type]]
|
||
table_schema_str = sprint(show, table_schema_df) * "\n"
|
||
table_schema = table_schema * table_schema_str
|
||
end
|
||
|
||
context =
|
||
"""
|
||
<internal_context_for_assistant>
|
||
<database_table_schema>
|
||
$table_schema
|
||
</database_table_schema>
|
||
</internal_context_for_assistant>
|
||
"""
|
||
input = context * searchterm
|
||
|
||
msg = Dict(
|
||
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
|
||
"messages" => [
|
||
Dict(
|
||
"role" => "system",
|
||
"content" => [
|
||
Dict("type" => "text", "text" => systemmsg),
|
||
]
|
||
),
|
||
Dict(
|
||
"role" => "user",
|
||
"content" => [
|
||
Dict("type" => "text", "text" => input),
|
||
]
|
||
),
|
||
],
|
||
"temperature" => 0.7
|
||
)
|
||
|
||
for attempt in 1:maxattempt
|
||
response = a.context.text2textInstructLLM("random_id", msg)
|
||
|
||
response = GeneralUtils.clean_json_response(response)
|
||
|
||
think, response = GeneralUtils.extractthink(response)
|
||
responsedict = nothing
|
||
try
|
||
_responsedict = JSON.parse(response)
|
||
responsedict = GeneralUtils.dictify(_responsedict, keytype=String, sort_order=requiredKeys)
|
||
catch
|
||
println("\nERROR decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
continue
|
||
end
|
||
|
||
# check whether all answer's key points are in responsedict
|
||
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
|
||
if !ispass
|
||
errornote = errormsg
|
||
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||
continue
|
||
end
|
||
|
||
# remove backticks Error occurred: MethodError: no method matching occursin(::String, ::Vector{String})
|
||
if occursin("```", responsedict["action_input"])
|
||
sql = GeneralUtils.extract_triple_backtick_text(responsedict["action_input"])[1]
|
||
if sql[1:4] == "sql\n"
|
||
sql = sql[5:end]
|
||
end
|
||
sql = split(sql, ';') # some time there are comments in the sql
|
||
sql = sql[1] * ';'
|
||
|
||
responsedict["action_input"] = sql
|
||
end
|
||
|
||
toollist = ["RUNSQL"]
|
||
if responsedict["action_name"] ∉ toollist
|
||
errornote = "Your previous attempt has action_name that is not in the tool list"
|
||
println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> $(responsedict["action_name"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
continue
|
||
end
|
||
|
||
for i in toollist
|
||
if occursin(i, responsedict["action_input"])
|
||
errornote = "Your previous attempt has action_name in action_input which is not allowed"
|
||
println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> $(responsedict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
continue
|
||
end
|
||
end
|
||
|
||
# println("\nSQLLLM decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
# pprintln(responsedict)
|
||
# println("---")
|
||
|
||
return responsedict["action_input"]
|
||
end
|
||
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
||
end
|
||
|
||
function SQLexecution(executeSQL::Function, sql::T
|
||
)::NamedTuple where {T<:AbstractString}
|
||
|
||
try
|
||
# add LIMIT to the SQL to prevent loading large data
|
||
sql = strip(sql)
|
||
|
||
# remove DISTINCT keyword because it is incompatible with RANDOM()
|
||
sql = replace(sql, "DISTINCT" => "")
|
||
|
||
if sql[end] == ';'
|
||
if !occursin("LIMIT", sql)
|
||
sql = sql[1:end-1] * " ORDER BY RANDOM() LIMIT 2;"
|
||
end
|
||
else
|
||
sql = sql * ";"
|
||
end
|
||
result = executeSQL(sql)
|
||
df = DataFrame(result)
|
||
tablesize = size(df)
|
||
row, column = tablesize
|
||
if row == 0
|
||
return (result_str="No records found. Try loosening your search criteria.", result_raw=nothing, success=true, errormsg=nothing)
|
||
elseif column > 30
|
||
return (result_str="There are more than 30 columns. Please be more specific.", result_raw=df, success=true, errormsg=nothing)
|
||
else
|
||
df1 =
|
||
if row > 2
|
||
# ramdom row to pick
|
||
df[sample(1:nrow(df), 2, replace=false), :] # random select 2 rows from df
|
||
else
|
||
df
|
||
end
|
||
result = GeneralUtils.dfToString(df1)
|
||
# println("\n~~~ SQLexecution() result: ", @__FILE__, " ", @__LINE__)
|
||
# println(sql)
|
||
# println(df1)
|
||
# println("\n")
|
||
return (result_str=result, result_raw=df1, success=true, errormsg=nothing)
|
||
end
|
||
catch e
|
||
io = IOBuffer()
|
||
showerror(io, e)
|
||
errorMsg = String(take!(io))
|
||
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
|
||
println(errorMsg)
|
||
return (result_str=nothing, result_raw=nothing, success=false, errormsg=errorMsg)
|
||
end
|
||
end
|
||
|
||
|
||
"""
|
||
|
||
# Arguments
|
||
- `v::Integer`
|
||
dummy variable
|
||
|
||
# Return
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
"""
|
||
function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
|
||
)::String where {T1<:agent, T2<:AbstractString}
|
||
|
||
systemmsg =
|
||
"""
|
||
<situation>
|
||
At each round of conversation, the user provides the following:
|
||
- The query: the query provided by the user.
|
||
</situation>
|
||
<objective>
|
||
Extract information from the user's query as much as possible according to wine attributes extraction guidelines to fill out user's preference form.
|
||
</objective>
|
||
<your responsibility includes>
|
||
Fulfill the objective.
|
||
</your responsibility includes>
|
||
<wine attributes extraction guidelines>
|
||
- If specific information required in the preference form is not available in the query or there isn't any, mark with "N/A" to indicate this.
|
||
Additionally, words like 'any' or 'unlimited' mean no information is available.
|
||
- Do not generate other comments.
|
||
</wine attributes extraction guidelines>
|
||
<you should then respond to the user with>
|
||
wine_name: name of the wine
|
||
winery: name of the winery
|
||
vintage: the year of the wine
|
||
region: a region, such as Burgundy, Bordeaux, Champagne, Napa Valley, Tuscany, California, Oregon, etc. Use "or" if there are multiple regions.
|
||
country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States". Use "or" if there are multiple countries.
|
||
wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified"
|
||
grape_varietal: the name of the primary grape used to make the wine
|
||
tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc
|
||
wine_price_min: minimum price range of wine. Example: For wine price 20, wine_price_min will be 0. For wine price 10 to 100, wine_price_min will be 10.
|
||
wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100.
|
||
occasion: the occasion the user is having the wine for
|
||
food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc
|
||
_keyword suffice is the related keyword that appears in user's query. each keyword can not be used twice.
|
||
</you should then respond to the user with>
|
||
<you should only respond in JSON format as described below>
|
||
"wine_name": "...",
|
||
"winery": "...",
|
||
"vintage": "...",
|
||
"region": "...",
|
||
"country": "...",
|
||
"wine_type": "...",
|
||
"grape_varietal": "...",
|
||
"tasting_notes": "...",
|
||
"wine_price_min": "...",
|
||
"wine_price_max": "...",
|
||
"occasion": "...",
|
||
"food_to_be_paired_with_wine": "..."
|
||
</you should only respond in JSON format as described below>
|
||
<here are some examples>
|
||
User's query: red, Chenin Blanc, Riesling, 20 USD from Tuscany, Italy or Napa Valley, USA
|
||
"wine_name": "N/A",
|
||
"winery": "N/A",
|
||
"vintage": "N/A",
|
||
"region": "Tuscany or Napa Valley",
|
||
"country": "Italy or United States",
|
||
"wine_type": "red or white",
|
||
"grape_varietal": "Chenin Blanc or Riesling",
|
||
"tasting_notes": "citrus",
|
||
"wine_price_min": "0",
|
||
"wine_price_max": "20",
|
||
"occasion": "N/A",
|
||
"food_to_be_paired_with_wine": "N/A"
|
||
|
||
User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot
|
||
"wine_name": "Saumur Blanc",
|
||
"winery": "Domaine du Collier",
|
||
"vintage": "2019",
|
||
"region": "Saumur",
|
||
"country": "France",
|
||
"wine_type": "white",
|
||
"grape_varietal": "Merlot",
|
||
"tasting_notes": "plum",
|
||
"wine_price_min": "N/A",
|
||
"wine_price_max": "N/A",
|
||
"occasion": "N/A",
|
||
"food_to_be_paired_with_wine": "N/A"
|
||
</here are some examples>
|
||
"""
|
||
requiredKeys = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"]
|
||
errornote = ""
|
||
context =
|
||
"""
|
||
<internal_context_for_assistant>
|
||
$errornote
|
||
</internal_context_for_assistant>
|
||
"""
|
||
|
||
input = context * input
|
||
|
||
msg = Dict(
|
||
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
|
||
"messages" => [
|
||
Dict(
|
||
"role" => "system",
|
||
"content" => [
|
||
Dict("type" => "text", "text" => systemmsg),
|
||
]
|
||
),
|
||
Dict(
|
||
"role" => "user",
|
||
"content" => [
|
||
Dict("type" => "text", "text" => input),
|
||
]
|
||
),
|
||
],
|
||
"temperature" => 0.7
|
||
)
|
||
|
||
for attempt in 1:maxattempt
|
||
response = a.context.text2textInstructLLM(a.id, msg)
|
||
response = GeneralUtils.clean_json_response(response)
|
||
|
||
response = GeneralUtils.remove_french_accents(response)
|
||
think, response = GeneralUtils.extractthink(response)
|
||
responsedict = nothing
|
||
try
|
||
_responsedict = JSON.parse(response)
|
||
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
|
||
catch
|
||
println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
continue
|
||
end
|
||
# check whether all answer's key points are in responsedict
|
||
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
|
||
if !ispass
|
||
errornote = errormsg
|
||
println("\nERROR YiemAgent extractWineAttributes_1() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||
continue
|
||
end
|
||
removekeys = ["thought", "tasting_notes", "occasion", "food_to_be_paired_with_wine", "vintage"]
|
||
for i in removekeys
|
||
delete!(responsedict, i)
|
||
end
|
||
# remove (some text)
|
||
for (k, v) in responsedict
|
||
_v = replace(v, r"\(.*?\)" => "")
|
||
responsedict[k] = _v
|
||
end
|
||
|
||
# println("\n--- extractWineAttributes_1-1()")
|
||
# @show responsedict
|
||
# @info "---\n" @__LINE__
|
||
|
||
# check each attributes against each column in a database table with BM25
|
||
for (k, v) in responsedict
|
||
if k ∉ ["wine_price_min", "wine_price_max"]
|
||
words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k)
|
||
resolved_word = GeneralUtils.resolve_entity(v, words_catalog;threshold=0.9)
|
||
responsedict[k] = resolved_word
|
||
end
|
||
end
|
||
|
||
result = ""
|
||
for (k, v) in responsedict
|
||
# some time LLM generate text with "(some comment)". this line removes it
|
||
if !occursin("N/A", v) && v != "" && !occursin("none", v) && !occursin("None", v)
|
||
result *= "$k: $v, "
|
||
end
|
||
end
|
||
|
||
result = result[1:end-2] # remove the ending ", "
|
||
# println("\n--- extractWineAttributes_1-2()")
|
||
# @show responsedict
|
||
# @show result
|
||
# @info "---\n" @__LINE__
|
||
return result
|
||
end
|
||
error("extractWineAttributes_1() failed to get a response")
|
||
end
|
||
|
||
"""
|
||
- TODO "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve.
|
||
- TODO French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why?
|
||
"""
|
||
function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
|
||
|
||
conversiontable =
|
||
"""
|
||
<conversion_table>
|
||
Intensity level:
|
||
1 to 2: May correspond to "light-bodied" or a similar description.
|
||
2 to 3: May correspond to "med light bodied", "medium light" or a similar description.
|
||
3 to 4: May correspond to "medium bodied" or a similar description.
|
||
4 to 5: May correspond to "med full bodied", "medium full" or a similar description.
|
||
4 to 5: May correspond to "full bodied" or a similar description.
|
||
Sweetness level:
|
||
1 to 2: May correspond to "dry", "no sweet" or a similar description.
|
||
2 to 3: May correspond to "off dry", "less sweet" or a similar description.
|
||
3 to 4: May correspond to "semi sweet" or a similar description.
|
||
4 to 5: May correspond to "sweet" or a similar description.
|
||
4 to 5: May correspond to "very sweet" or a similar description.
|
||
Tannin level:
|
||
1 to 2: May correspond to "low tannin" or a similar description.
|
||
2 to 3: May correspond to "semi low tannin" or a similar description.
|
||
3 to 4: May correspond to "medium tannin" or a similar description.
|
||
4 to 5: May correspond to "semi high tannin" or a similar description.
|
||
4 to 5: May correspond to "high tannin" or a similar description.
|
||
Acidity level:
|
||
1 to 2: May correspond to "low acidity" or a similar description.
|
||
2 to 3: May correspond to "semi low acidity" or a similar description.
|
||
3 to 4: May correspond to "medium acidity" or a similar description.
|
||
4 to 5: May correspond to "semi high acidity" or a similar description.
|
||
4 to 5: May correspond to "high acidity" or a similar description.
|
||
</conversion_table>
|
||
"""
|
||
|
||
systemmsg =
|
||
"""
|
||
<situation>
|
||
At each round of conversation, you will be given the following information:
|
||
conversion_table: a conversion table that maps descriptive words to their corresponding integer levels
|
||
query: the words from the user's query that describe their preferences
|
||
</situation>
|
||
<objective>
|
||
Fill out the user's preference form based on the corresponding words from the user's query according to the guidelines.
|
||
</objective>
|
||
<your responsibility includes>
|
||
Fulfill the objective
|
||
</your responsibility includes>
|
||
<guidelines>
|
||
- The preference form requires sweetness, acidity, tannin, intensity infomation
|
||
- If specific information required in the preference form is not available in the query or there isn't any, mark with 'N/A' to indicate this.
|
||
Additionally, words like 'any' or 'unlimited' mean no information is available.
|
||
- Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer.
|
||
- Do not generate other comments.
|
||
</guidelines>
|
||
<you should then respond to the user with>
|
||
sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine.
|
||
sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2
|
||
acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine.
|
||
acidity: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5
|
||
tannin_keyword: The exact keywords in the user's query describing the tannin level of the wine.
|
||
tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3
|
||
intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine.
|
||
intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4
|
||
</you should then respond to the user with>
|
||
<you should only respond in JSON format as described below>
|
||
"sweetness_keyword": "...",
|
||
"sweetness_min": "...",
|
||
"sweetness_max": "...",
|
||
"acidity_keyword": "...",
|
||
"acidity_min": "...",
|
||
"acidity_max": "...",
|
||
"tannin_keyword": "...",
|
||
"tannin_min": "...",
|
||
"tannin_max": "...",
|
||
"intensity_keyword": "...",
|
||
"intensity_min": "...",
|
||
"intensity_max": "..."
|
||
</you should only respond in JSON format as described below>
|
||
<here are some examples>
|
||
User's query: I want a wine with a medium-bodied, low acidity, medium tannin.
|
||
"sweetness_keyword": "N/A",
|
||
"sweetness_min": "N/A",
|
||
"sweetness_max": "N/A",
|
||
"acidity_keyword": "low acidity",
|
||
"acidity_min": 1,
|
||
"acidity_max": 2,
|
||
"tannin_keyword": "medium tannin",
|
||
"tannin_min": 3,
|
||
"tannin_max": 4,
|
||
"intensity_keyword": "medium-bodied",
|
||
"intensity_min": 3,
|
||
"intensity_max": 4
|
||
|
||
User's query: German red wine, under 100, pairs with spicy food.
|
||
"sweetness_keyword": "N/A",
|
||
"sweetness_min": "N/A",
|
||
"sweetness_max": "N/A",
|
||
"acidity_keyword": "N/A",
|
||
"acidity_min": "N/A",
|
||
"acidity_max": "N/A",
|
||
"tannin_keyword": "N/A",
|
||
"tannin_min": "N/A",
|
||
"tannin_max": "N/A",
|
||
"intensity_keyword": "N/A",
|
||
"intensity_min": "N/A",
|
||
"intensity_max": "N/A"
|
||
<here are some examples>
|
||
"""
|
||
requiredKeys = ["sweetness_keyword", "sweetness_min", "sweetness_max",
|
||
"acidity_keyword", "acidity_min", "acidity_max",
|
||
"tannin_keyword", "tannin_min", "tannin_max",
|
||
"intensity_keyword", "intensity_min", "intensity_max"]
|
||
errornote = ""
|
||
context =
|
||
"""
|
||
<internal_context_for_assistant>
|
||
$conversiontable
|
||
$errornote
|
||
</internal_context_for_assistant>
|
||
"""
|
||
|
||
input = context * input
|
||
|
||
msg = Dict(
|
||
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
|
||
"messages" => [
|
||
Dict(
|
||
"role" => "system",
|
||
"content" => [
|
||
Dict("type" => "text", "text" => systemmsg),
|
||
]
|
||
),
|
||
Dict(
|
||
"role" => "user",
|
||
"content" => [
|
||
Dict("type" => "text", "text" => input),
|
||
]
|
||
),
|
||
],
|
||
"temperature" => 0.7
|
||
)
|
||
|
||
for attempt in 1:10
|
||
response = a.context.text2textInstructLLM(a.id, msg)
|
||
response = GeneralUtils.clean_json_response(response)
|
||
println("\n--- extractWineAttributes_2-1()")
|
||
println(response)
|
||
println("--- \n")
|
||
|
||
response = GeneralUtils.remove_french_accents(response)
|
||
think, response = GeneralUtils.extractthink(response)
|
||
responsedict = nothing
|
||
try
|
||
_responsedict = JSON.parse(response)
|
||
responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
|
||
catch
|
||
println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
continue
|
||
end
|
||
|
||
# check whether all answer's key points are in responsedict
|
||
ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys)
|
||
if !ispass
|
||
errornote = errormsg
|
||
println("\nERROR YiemAgent extractWineAttributes_2() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
|
||
continue
|
||
end
|
||
|
||
# delete some key words from responsedict
|
||
for (k, v) in responsedict
|
||
if k ∈ ["sweetness_keyword", "acidity_keyword", "tannin_keyword", "intensity_keyword"]
|
||
delete!(responsedict, k)
|
||
end
|
||
end
|
||
|
||
# get result in String. Reject "N/A" value
|
||
result = ""
|
||
for (k, v) in responsedict
|
||
if typeof(v) <: Number
|
||
result *= "$k: $v, "
|
||
elseif typeof(v) == String && !occursin("N/A", v)
|
||
result *= "$k: $v, "
|
||
end
|
||
end
|
||
result = result[1:end-2] # remove the ending ", "
|
||
println("\n--- extractWineAttributes_2-2()")
|
||
println(result)
|
||
println("--- \n")
|
||
return result
|
||
end
|
||
error("extractWineAttributes_2() failed to get a response")
|
||
end
|
||
|
||
|
||
function paraphrase(text2textInstructLLM::Function, text::String)
|
||
systemmsg =
|
||
"""
|
||
Your name: N/A
|
||
Your vision:
|
||
- You are a helpful assistant who help the user to paraphrase their text.
|
||
Your mission:
|
||
- To help paraphrase the user's text
|
||
Mission's objective includes:
|
||
- To help paraphrase the user's text
|
||
Your responsibility includes:
|
||
1) To help paraphrase the user's text
|
||
Your responsibility does NOT includes:
|
||
1) N/A
|
||
Your profile:
|
||
- N/A
|
||
Additional information:
|
||
- N/A
|
||
|
||
At each round of conversation, you will be given the following information:
|
||
Text: The user's given text
|
||
|
||
You MUST follow the following guidelines:
|
||
- N/A
|
||
|
||
You should follow the following guidelines:
|
||
- N/A
|
||
|
||
You should then respond to the user with:
|
||
Paraphrase: Paraphrased text
|
||
|
||
You should only respond in format as described below:
|
||
Paraphrase: ...
|
||
|
||
Let's begin!
|
||
"""
|
||
#[PENDING] use JSON the same as extractWineAttributes_1 is better. change this function to use the same format use decisionMaker
|
||
header = ["Paraphrase:"]
|
||
dictkey = ["paraphrase"]
|
||
|
||
errornote = "N/A"
|
||
response = nothing # placeholder for show when error msg show up
|
||
|
||
|
||
for attempt in 1:10
|
||
usermsg = """
|
||
Text: $text
|
||
P.S. $errornote
|
||
"""
|
||
|
||
_prompt =
|
||
[
|
||
Dict("name" => "system", "text" => systemmsg),
|
||
Dict("name" => "user", "text" => usermsg)
|
||
]
|
||
|
||
# put in model format
|
||
prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)
|
||
|
||
try
|
||
response = text2textInstructLLM(prompt)
|
||
response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
|
||
think, response = GeneralUtils.extractthink(response)
|
||
# sometime the model response like this "here's how I would respond: ..."
|
||
if occursin("respond:", response)
|
||
errornote = "You don't need to intro your response"
|
||
error("\nparaphrase() response contain : ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
end
|
||
response = GeneralUtils.remove_french_accents(response)
|
||
response = replace(response, '*'=>"")
|
||
response = replace(response, '$' => "USD")
|
||
response = replace(response, '`' => "")
|
||
response = GeneralUtils.remove_french_accents(response)
|
||
|
||
# check whether response has all answer's key points
|
||
detected_kw = GeneralUtils.detect_keyword(header, response)
|
||
if 0 ∈ values(detected_kw)
|
||
errornote = "\nYiemAgent paraphrase() response does not have all answer's key points"
|
||
continue
|
||
elseif sum(values(detected_kw)) > length(header)
|
||
errornote = "\nnYiemAgent paraphrase() response has duplicated answer's key points"
|
||
continue
|
||
end
|
||
|
||
responsedict = GeneralUtils.textToDict(response, header;
|
||
dictKey=dictkey, symbolkey=true)
|
||
|
||
for i ∈ [:paraphrase]
|
||
if length(JSON.json(responsedict[i])) == 0
|
||
error("$i is empty ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
end
|
||
end
|
||
|
||
# check if there are more than 1 key per categories
|
||
for i ∈ [:paraphrase]
|
||
matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i)
|
||
if length(matchkeys) > 1
|
||
error("paraphrase() has more than one key per categories")
|
||
end
|
||
end
|
||
|
||
println("\nparaphrase() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
pprintln(Dict(responsedict))
|
||
|
||
result = responsedict["paraphrase"]
|
||
|
||
return result
|
||
catch e
|
||
io = IOBuffer()
|
||
showerror(io, e)
|
||
errorMsg = String(take!(io))
|
||
st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace()))
|
||
println("\nAttempt $attempt. Error occurred: $errorMsg\n$st ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||
end
|
||
end
|
||
error("paraphrase() failed to generate a response")
|
||
end
|
||
|
||
|
||
|
||
""" Attemp to correct LLM response's incorrect JSON response.
|
||
|
||
# Arguments
|
||
- `a::T1`
|
||
one of Yiem's agent
|
||
- `input::T2`
|
||
text to be send to virtual wine customer
|
||
|
||
# Return
|
||
- `correctjson::String`
|
||
corrected json string
|
||
|
||
# Example
|
||
```jldoctest
|
||
julia>
|
||
```
|
||
|
||
# Signature
|
||
"""
|
||
function jsoncorrection(config::T1, input::T2, correctJsonExample::T3;
|
||
maxattempt::Integer=3
|
||
) where {T1<:AbstractDict, T2<:AbstractString, T3<:AbstractString}
|
||
|
||
incorrectjson = deepcopy(input)
|
||
correctjson = nothing
|
||
|
||
for attempt in 1:maxattempt
|
||
try
|
||
d = copy(JSON.parsefile(incorrectjson))
|
||
correctjson = incorrectjson
|
||
return correctjson
|
||
catch e
|
||
@warn "Attempting to correct JSON string. Attempt $attempt"
|
||
e = """$e"""
|
||
if occursin("EOF", e)
|
||
e = split(e, "EOF")[1] * "EOF"
|
||
end
|
||
incorrectjson = deepcopy(input)
|
||
_prompt =
|
||
"""
|
||
Your goal are:
|
||
1) Use the expected JSON format as a guideline to check why the given JSON string failed to load and provide a corrected version that can be loaded by Python's json.load function.
|
||
2) Provide Corrected JSON string only. Do not provide any other info.
|
||
|
||
$correctJsonExample
|
||
|
||
Let's begin!
|
||
Given JSON string: $incorrectjson
|
||
The given JSON string failed to load previously because: $e
|
||
Corrected JSON string:
|
||
"""
|
||
|
||
# apply LLM specific instruct format
|
||
externalService = config["externalservice"]["text2textinstruct"]
|
||
llminfo = externalService["llminfo"]
|
||
prompt =
|
||
if llminfo["name"] == "llama3instruct"
|
||
formatLLMtext_llama3instruct("system", _prompt)
|
||
else
|
||
error("llm model name is not defied yet $(@__LINE__)")
|
||
end
|
||
|
||
# send formatted input to user using GeneralUtils.sendReceiveMqttMsg
|
||
msgMeta = GeneralUtils.generate_msgMeta(
|
||
externalService["mqtttopic"],
|
||
senderName= "jsoncorrection",
|
||
senderId= string(uuid4()),
|
||
receiverName= "text2textinstruct",
|
||
mqttBroker= config["mqttServerInfo"]["broker"],
|
||
mqttBrokerPort= config["mqttServerInfo"]["port"],
|
||
)
|
||
|
||
outgoingMsg = Dict(
|
||
"msgMeta"=> msgMeta,
|
||
"payload"=> Dict(
|
||
"text"=> prompt,
|
||
"kwargs"=> Dict(
|
||
"max_tokens"=> 512,
|
||
"stop"=> ["<|eot_id|>"],
|
||
)
|
||
)
|
||
)
|
||
result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120)
|
||
incorrectjson = result[:response][:text]
|
||
end
|
||
end
|
||
end
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
end # module llmfunction |