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_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
      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

    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