update
This commit is contained in:
+333
-10
@@ -282,7 +282,7 @@ 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 checkwine!(a::T, thoughtdict::AbstractDict
|
||||
function checkwine!(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())")
|
||||
@@ -293,19 +293,342 @@ function checkwine!(a::T, thoughtdict::AbstractDict
|
||||
_inventoryquery = "$wineattributes_1, $wineattributes_2"
|
||||
inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}"
|
||||
println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
||||
textresult, result_raw, _, _ = SQLexecution(a.context.executeSQL, sql)
|
||||
|
||||
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.
|
||||
- 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, _ = a.context.similarSQLVectorDB(searchterm)
|
||||
similarSQL_ = sql !== nothing ? sql : "None"
|
||||
context =
|
||||
"""
|
||||
<internal_context_for_assistant>
|
||||
<database_table_schema>
|
||||
$table_schema
|
||||
</database_table_schema>
|
||||
<possible SQL for user's search term>
|
||||
$similarSQL_
|
||||
</possible SQL for user's search term>
|
||||
<error_note>
|
||||
$errornote
|
||||
<error_note>
|
||||
</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.", result_raw=df, 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
|
||||
|
||||
Reference in New Issue
Block a user