module interface

export addNewMessage, conversation, decisionMaker, reflector, generatechat,
  generalconversation, detectWineryName, generateSituationReport

using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
      DataFrames, CSV
using GeneralUtils
using ..type, ..util, ..llmfunction

# ------------------------------------------------------------------------------------------------ #
#                                        pythoncall setting                                        #
# ------------------------------------------------------------------------------------------------ #
# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252
# by setting the following variables, PythonCall.jl will use:
#     1. system's python and packages installed by system (via apt install)
# or  2. conda python and packages installed by conda
# if these setting are not set (comment out), PythonCall will use its own python and packages that
# installed by CondaPkg.jl (from env_preparation.jl)
# ENV["JULIA_CONDAPKG_BACKEND"] = "Null"      # set condapkg backend = none
# systemPython = split(read(`which python`, String), "\n")[1]     # system's python path
# ENV["JULIA_PYTHONCALL_EXE"] = systemPython  # find python location with $> which python  ex. raw"/root/conda/bin/python"

# using PythonCall
# const py_agents = PythonCall.pynew()
# const py_llms = PythonCall.pynew()
# function __init__()
#     # PythonCall.pycopy!(py_cv2, pyimport("cv2"))

#     # equivalent to     from urllib.request import urlopen    in python
#     PythonCall.pycopy!(py_agents, pyimport("langchain.agents"))
#     PythonCall.pycopy!(py_llms, pyimport("langchain.llms"))
# end

# ---------------------------------------------- 100 --------------------------------------------- #


macro executeStringFunction(functionStr, args...)
  # Parse the function string into an expression
  func_expr = Meta.parse(functionStr)

  # Create a new function with the parsed expression
  function_to_call = eval(Expr(:function,
    Expr(:call, func_expr, args...), func_expr.args[2:end]...))

  # Call the newly created function with the provided arguments
  function_to_call(args...)
end



""" Think and choose action

# Arguments
  - `config::T1`
    config
  - `state::T2`
    a game state

# Keyword Arguments
  
# Return        
  - `thoughtdict::Dict`

# Example
```jldoctest
julia> result = decisionMaker(agent)

OrderedDict{String, Any} with 4 entries:
  "plan"          => "The user provided an image of a sparkling white wine (Asolo Prosecco Bella Principessa from Italy) and requested a search for similar wines in the inventory. According to store guidelines, I must st…
  "action_name"   => "SEARCH_WINE_DATABASE"
  "action_input"  => "Sparkling white wine from Italy"
  "action_result" => "1) winery: Terrazze dell Etna, wine_name: Rose Brut.
```
"""
function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
  ) where {T<:agent}

  # lessonDict = copy(JSON.parsefile("lesson.json"))

  # lesson =
  # if isempty(lessonDict)
  #   ""
  # else
  #   lessons = Dict{String, Any}()
  #   for (k, v) in lessonDict
  #     lessons[k] = lessonDict[k][:lesson]
  #   end

  #   """
  #   You have attempted to help the user before and failed, either because your reasoning for the 
  #   recommendation was incorrect or your response did not exactly match the user expectation. 
  #   The following lesson(s) give a plan to avoid failing to help the user in the same way you 
  #   did previously. Use them to improve your strategy to help the user.

  #   Here are some lessons in JSON format:
  #   $(JSON.json(lessons))

  #   When providing the thought and action for the current trial, that into account these failed 
  #   trajectories and make sure not to repeat the same mistakes and incorrect answers. 
  #   """
  # end

  # recentevents_ind = GeneralUtils.recentElementsIndex(
  #   length(a.memory["events"]), recentevents; includelatest=true)

  requiredKeys = ["plan", "action_name", "action_input"]
  context = 
    """
    <internal_context_for_assistant>
    </internal_context_for_assistant>
    """

  # add context to text of the latest message (in the front). 
  # use for loop because in openai format, each msg may contain both text and image. 
  for d in a.chathistory[end]["content"]
    if d["type"] == "text"
      d["text"] = context * d["text"]
      break
    end
  end
  errornote = "N/A"
  response = nothing # placeholder for show when error msg show up

  for attempt in 1:maxattempt
    if attempt > 1
      println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    end
    
    msg = Dict(
      "model" => "gemma-4-E4B-it-UD-Q4_K_XL",
      "messages" => a.chathistory,
      "temperature" => 0.7
    )

    response = a.context.text2textInstructLLM(a.id, msg)
    response = GeneralUtils.clean_json_response(response)
    response = GeneralUtils.remove_french_accents(response)
    think, response = GeneralUtils.extractthink(response)
    response = String(split(response, ", observation")[1])  # in case LLM generate observation key which it isn't supposed to
    response = strip(response)
    
    # dollar sign in Julia means string interpolation
    while occursin('$', response)
      response = replace(response, '$' => "USD")
    end

    responsedict = nothing
    if occursin(requiredKeys[2], response) 
      try
        _responsedict = JSON.parse(response)
        responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
      catch
        println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
        continue
      end

    # fall back to normal text because LLM default to natural chat when it didn't use action_call
    else  
      try
        responsedict = OrderedDict(
          "plan"=> "I will talk to the user",
          "action_name"=> "CHAT_BOX",
          "action_input"=> response[2:end-1]  # remove { } at the front and back that added by clean_json_response
          )
      catch e
        println("\nERROR YiemAgent decisionMaker(). $e --(not qualify response)-> $response", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
        continue
      end
    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

    # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"]
    #   errornote = "Your previous attempt didn't use the given functions"
    #   println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    #   continue
    # end

    # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    # pprintln(responsedict)

    return responsedict
  end
  error("DecisionMaker failed to generate a thought ", response)
end



""" Assigns a scalar value to each new child node to be used for selec-
tion and backpropagation. This value effectively quantifies the agent's progress in task completion,
serving as a heuristic to steer the search algorithm towards the most promising regions of the tree.

# Arguments
  - `state<:AbstractDict`
    one of Yiem's agent
  - `text2textInstructLLM::Function`
    A function that handles communication to LLM service

# Return
  - `score::Integer`

# Example
```jldoctest
julia> 
```

# Signature
"""
function evaluator(a::T1, timeline, decisiondict, evaluateecontext
  ) where {T1<:agent}

  systemmsg = 
  """
  <Your role>
    - You are a master sommelier of an online wine store.
  </Your role>
  <Situation>
    - Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding.
  </Situation>
  <Your mission>
    - Improve a trainee sommelier decision based on the store policy and guidelines while ensuring seamless interactions between the trainee and customers.
  </Your mission>
  <At each round of conversation, you will be given the following information> 
    - trajectory: A conversation between your trainee and the customer that have occurred up until now
    - evaluatee_context: The context that evaluatee use to make a decision
    - evaluatee_decision: The decision made by the evaluatee, consists of the following elements:
      "plan" is the trainee's plan
      "action_name" is the name of the action taken, which can be one of the available tool name.
      "action_input" is the input to the action.
  </At each round of conversation, you will be given the following information>
  <You must follow the following policy>
    - Use only infomation provided by the store policy and guidelines as a bedrocks for your response.
  </You must follow the following policy>
  <You should follow the following guidelines>
    - The trainee's plan, action_name, and action_input must be logically consistent
    - The trainee's action_input should be in a proper format as specified by the tools.
    - The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the END_CONVER_GUIDELINE tool.
  </You should follow the following guidelines>
  <You should then respond to the user with>
    1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question.
      - Evaluate the correctness of each section and the overall trajectory based on the given question.
      - Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation.
      - Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached.
      - Do not generate additional thoughts or actions.
    2) decision_evaluation: 
      - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding.
    3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only.
    4) approval: Can be "yes" or "no". "no" if the suggestion contradict the trainee's decision; otherwise, it is "yes".

  </You should then respond to the user with>
  <You should only respond in JSON format as described below>
  {
    "trajectory_evaluation": "...",
    "decision_evaluation": "...",
    "suggestion": "...",
    "approval": "...",
  }
  </You should only respond in format as described below>

  Let's begin!
  """
  requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approval, :suggestion]
  errornote = "N/A"

  for attempt in 1:10
    evaluateecontext = replace(evaluateecontext, "<context>" => "")
    evaluateecontext = replace(evaluateecontext, "</context>" => "")

    context =
    """
    <context>
      <assistant_trajectories>
      $timeline
      </assistant_trajectories>
      <evaluatee_context>
      $evaluateecontext
      </evaluatee_context>
      <evaluatee_decision>
      {plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])}
      </evaluatee_decision>
      P.S. $errornote
    </context>
    """

    unformatPrompt =
      [
        Dict("name" => "system", "text" => systemmsg),
      ]

    # put in model format
    prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
    # add info
    prompt = prompt * context

    response = a.context.text2textInstructLLM(prompt; senderId=a.id)
    response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
    response = GeneralUtils.remove_french_accents(response)
    # response = replace(response, '$'=>"USD")
    think, response = GeneralUtils.extractthink(response)
    
    responsedict = nothing
    try
      responsedict = copy(JSON.parsefile(response))  
    catch
      println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    end

    # check whether all answer's key points are in responsedict
    ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys)
    if !ispass
      errornote = errormsg
      println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
      continue
    end

    # if accepted_as_answer ∉ ["yes", "no"] # [PENDING] add errornote into the prompt
    #   error("generated accepted_as_answer has wrong format")
    # end

    println("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    pprintln(Dict(responsedict))
    return responsedict
  end
  error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>")
end

""" Chat with llm.

# Example userinput

image_path = "test/large_image.png"
image_bytes = read(image_path)
base64_string = base64encode(image_bytes)

# 2. Match the MIME type according to your file extension (e.g., png, jpeg)
mime_type = "image/png" 
data1_uri = "data:<mime_type>;base64,<image1_base64_string>"

# 3. Construct payload with the Data URI
message => Dict(
    "role" => "user",
    "content" => [
      Dict("type" => "text", "text" => "Describe this image for me"),
      Dict(
        "type" => "image_url",
        "image_url" => Dict("url" => data_uri)
      )
    ]
  )

"""
function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}},
                      maximumMsg=50, max_think_loop::Integer=3)
  
  @info "YiemAgent conversation() 1 " @__LINE__
  userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"])

  # find text in usermsg
  usertext = nothing
  for (i, d) in enumerate(userinput["content"])
    if d["type"] == "text"
      d["text"] = GeneralUtils.remove_french_accents(d["text"])
      usertext = d["text"]
    end
  end

  if usertext == "newtopic"
    clearhistory(a)
    return "Okay. What shall we talk about?"
  else
    @info "YiemAgent conversation() 2 " @__LINE__
    # add usermsg to a.chathistory but how do I handle images?
    addNewMessage(a, "user", userinput; maximumMsg=maximumMsg)

    # thinking loop until AI wants to communicate with the user
    loopcount = 0
    while true
      loopcount += 1
      if loopcount > max_think_loop
        @info "YiemAgent conversation() 2-1 think count $loopcount " @__LINE__
        r = generatechat!(a)
        @info "YiemAgent conversation() 2-2 think count $loopcount " @__LINE__
        return r
      end

      @info "YiemAgent conversation() 2-3 think count $loopcount " @__LINE__
      thoughtdict, result_raw = think(a)
      if thoughtdict["action_name"] ∈ ["CHAT_BOX"]
        @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__
        assistant_response = Dict{String, Any}(
          "role" => "assistant",
          "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),]
        )
        addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)
        @info "YiemAgent conversation() 2-5 think count $loopcount " @__LINE__
        return thoughtdict["action_input"]
      # elseif thoughtdict["action_name"] ∈ ["CHAT_BOX"]
      #   @info "YiemAgent conversation() 2-4 think count $loopcount " @__LINE__
      #   assistant_response = Dict{String, Any}(
      #     "role" => "assistant",
      #     "content" => [
      #         Dict("type" => "text", "text" => thoughtdict["action_input"]),
      #         Dict( #WORKING put 1st image here
      #           "type" => "image_url",
      #           "image_url" => Dict("url" => image1_data_uri)
      #         ),
      #         Dict( #WORKING put 2nd image here
      #           "type" => "image_url",
      #           "image_url" => Dict("url" => image2_data_uri)
      #         ),
      #       ]
      #   )
      #   addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg)

      #   return thoughtdict["action_input"]  #XXX change output from string to dict
      else
        action_name = thoughtdict["action_name"]
        action_input = thoughtdict["action_input"]
        action_call = Dict{String, Any}(
            "role" => "action_call",
            "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),]
          )
        addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg)

        action_result = thoughtdict["action_result"]
        actionresult = Dict{String, Any}(
            "role" => "action_result",
            "content" => [Dict("type" => "text", "text" => "$action_result"),]
          )
        addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg)
      end
    end
  end
end


"""
# Arguments
  
# Return

# Example
```jldoctest
julia> 
```

"""
function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
  # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0)
  thoughtdict = decisionMaker(a)
  @info "YiemAgent think() 1 " @__LINE__
  # pprintln(thoughtdict)

  result_raw = nothing
  if thoughtdict["action_name"] ∈ ["CHAT_BOX"]
    @info "YiemAgent think() 2 " @__LINE__
    thoughtdict, result_raw = generatechat!(a)
    
  elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE"
    @info "YiemAgent think() 3 " @__LINE__
    thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict)
    
  elseif thoughtdict["action_name"] ∈ ["WINE_PRESENTATION_GUIDELINE"]
    @info "YiemAgent think() 4 " @__LINE__
    thoughtdict, result_raw =  wine_presentation_guideline!(a, thoughtdict)

  elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE" 
    @info "YiemAgent think() 5 " @__LINE__
    thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false)
    #WORKING result_raw will be a df. i need to get images so i can send to frontend
  else
    @info "YiemAgent think() 6 " @__LINE__
    error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
  end
  println("\n")
  @show thoughtdict
  @info "YiemAgent think() 7 " @__LINE__
  return (thoughtdict=thoughtdict, result_raw=result_raw)
end

function chatbox!(a::T, thoughtdict::AbstractDict
  )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
  thoughtdict["action_result"] = "Action result is the next user dialogue."
  return (thoughtdict=thoughtdict, result_raw=nothing)
end

function end_conversation_guideline!(a::T, thoughtdict::AbstractDict
  )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
  
  guideline =
    """
    <end_conversation_guideline>
      - Provide customer with store contact info and business hours
      - Invite customer to comeback
      <store_info>
        Business Hours: everyday 9.00-20.00
        Tel. 0863055790
      </store_info>
    </end_conversation_guideline>
    """
  thoughtdict["action_result"] = guideline

  return (thoughtdict=thoughtdict, result_raw=nothing)
end

function wine_presentation_guideline!(a::T, thoughtdict::AbstractDict
  )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
  
  guideline =
    """
    <wine_presentation_guideline>
    - Provide detailed introductions of the wines you've found to the user.
    - Explain how the wine could match the user's intention and what its effects might mean for the user's experience.
    - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience.
    - Provide your personal recommendation and provide a brief explanation of why you recommend it.
    - People don't describe wine quality level in numbers so use convertion_table if neccessary
      <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>
    </wine_presentation_guideline>
    """
  thoughtdict["action_result"] = guideline

  return (thoughtdict=thoughtdict, result_raw=nothing)
end


#PENDING
function generatechat!(a::T; maxattempt::Integer=10
  )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}

  # lessonDict = copy(JSON.parsefile("lesson.json"))

  # lesson =
  # if isempty(lessonDict)
  #   ""
  # else
  #   lessons = Dict{String, Any}()
  #   for (k, v) in lessonDict
  #     lessons[k] = lessonDict[k][:lesson]
  #   end

  #   """
  #   You have attempted to help the user before and failed, either because your reasoning for the 
  #   recommendation was incorrect or your response did not exactly match the user expectation. 
  #   The following lesson(s) give a plan to avoid failing to help the user in the same way you 
  #   did previously. Use them to improve your strategy to help the user.

  #   Here are some lessons in JSON format:
  #   $(JSON.json(lessons))

  #   When providing the thought and action for the current trial, that into account these failed 
  #   trajectories and make sure not to repeat the same mistakes and incorrect answers. 
  #   """
  # end

  # recentevents_ind = GeneralUtils.recentElementsIndex(
  #   length(a.memory["events"]), recentevents; includelatest=true)

  systemmsg = 
    """
    # store_policy
      - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
      - If you found wines in the store's database, they are in stock.
      - You can only recommend wines that are currently in our inventory
      - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
      - Ask the user one question at a time.
      - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database.
      - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
      - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
      - Spicy foods should be paired only with light red wines.
      - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user immediately if they are looking for these types of wines. Do not sell our wines as such.
      - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team.

    # store_guidelines
      - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting.
      - Customer may provide images for you to look up.
      - Encourage the customer to explore different options and try new things.
      - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
      - Your store carries only wine.
      - Vintage 0 means non-vintage.
      - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.

    # situation
      You are continuing the conversation with the user.

    # your role
      Your name is $(a.name). You are a helpful sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision.

    # objective
      - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences.
      - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences.

    # your responsibility includes
      - According to the store's policy and guidelines, continuing conversation with the customer using CHAT_BOX action.
      - Keep the conversation with the customer going smoothly

    # your responsibility does NOT includes
      - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
      - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
      - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.

    # 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 "CHAT_BOX
      3) "action_input", Dialogue you want to chat with the user 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": "..."
    """
  
  system_msg = Dict(
      "role" => "system",
      "content" => [
        Dict("type" => "text", "text" => systemmsg),
      ]
    )

  chathistory = deepcopy(a.chathistory[2:end])  # use deep copy because I want to replace system msg
  pushfirst!(chathistory, system_msg)

  requiredKeys = ["plan", "action_name", "action_input"]

  errornote = "N/A"
  response = nothing # placeholder for show when error msg show up

  for attempt in 1:maxattempt
    if attempt > 1
      println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    end
    
    msg = Dict(
      "model" => "gemma-4-E4B-it-UD-Q4_K_XL",
      "messages" => chathistory,
      "temperature" => 0.7
    )

    response = a.context.text2textInstructLLM(a.id, msg)
    response = GeneralUtils.clean_json_response(response)
    response = GeneralUtils.remove_french_accents(response)
    think, response = GeneralUtils.extractthink(response)
    response = String(split(response, ", observation")[1])  # in case LLM generate observation key which it isn't supposed to
    
    response = strip(response)

    responsedict = nothing
    if occursin(requiredKeys[2], response) 
      try
        _responsedict = JSON.parse(response)
        responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys)
      catch
        println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
        continue
      end

    # fall back to normal text because LLM default to natural chat when it didn't use action_call
    else  
      try
        responsedict = OrderedDict(
          "plan"=> "I will talk to the user",
          "action_name"=> "CHAT_BOX",
          "action_input"=> response[2:end-1]  # remove { } at the front and back that added by clean_json_response
          )
      catch e
        println("\nERROR YiemAgent decisionMaker(). $e --(not qualify response)-> $response", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
        continue
      end
    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 generatechat() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
      continue
    end

    # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"]
    #   errornote = "Your previous attempt didn't use the given functions"
    #   println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    #   continue
    # end

    # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    # pprintln(responsedict)

    return (thoughtdict=responsedict, result_raw=responsedict["action_input"])
  end
  error("YiemAgent generatechat() failed to generate a thought ", response)
end


function generatequestion(a, text2textInstructLLM::Function, timeline)::String
  systemmsg = 
  """
  Your role:
  Your name is $(a.name). You are a helpful English-speaking, website-based sommelier for $(a.retailername)'s online store currently talking with the user.
  Your goal includes: 
  1) Help the user select the best wines from your inventory that align with the user's preferences
  2) Thanks the user when they don't need any further assistance and invite them to comeback next time

  Your responsibility includes:
  1) From your point of view as a sommelier helping the user, ask yourself multiple questions based on the current situation

  Your responsibility does NOT includes:
    1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
    2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
    3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.

  At each round of conversation, you will be given the info:
  Additional info: ...
  Your recent events: latest 5 events of the situation

  You must follow the following guidelines:
  - Your question should be specific, self-contained and not require any additional context.
  - Once the user has chose their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time.

  You should follow the following guidelines:
  - Focus on the latest conversation
  - If the user interrupts, prioritize the user
  - If you don't already know, find out the user's budget
  - If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified
  - If you don't already know, find out the occasion for which the user is buying wine
  - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity
  - If you don't already know, find out what food will be served with wine
  - If you haven't already, introduce the wines you found in the database to the user first
  - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
  - All wines in your inventory are always in stock.
  - Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory.
  - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database.
  - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time.
  - Medium and full-bodied red wines are bad with spicy foods.
  - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store.

  You should then respond to the user with:
  1) Thought: State your thought about the current situation
  2) Q: "Ask yourself" at least three, but no more than five, questions about the situation from your perspective.
  3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions

  You must only respond in format as described below:
  Q1: ...
  A1: ...
  Q2: ...
  A2: ...
  ...

  Here are some examples:
  Q: What the user is looking for?
  A: The user is asking for a MPV car with 7-seat
  Q: What do I know?
  A: The user is looking for a car with 7-seat. Our dealer sell these kind of cars
  Q: What brands the user prefer?
  A: I don't know. The user didn't mentioned that. Let's find out.
  Q: What else do I need to know before proceeding?
  A: I don't know about the user budget, car's color, and other user's preferences yet. Let's find out more about the user's preferences.
  Q: I'm still lacking information regarding the user's preferences for the powertrain. I've asked the user twice already, but perhaps they're not familiar with this. What should I do.
  A: I'll proceed without asking the user about the powertrain.
  Q: The user is buying for her husband, should I dig in to get more information?
  A: Yes, I should. So that I have better idea about the user's preferences.
  Q: Why the user saying this?
  A: The user does not want an SUV because it does not have sliding doors
  Q: The user is asking for a cappuccino. Do I have it at my cafe?
  A: No I don't have.
  Q: Since I don't have a cappuccino but I have a Late, should I ask if they are okay with that?
  A: Yes, I should.
  Q: Are they allergic to milk?
  A: Since they mentioned a cappuccino before, it seems they are not allergic to milk.
  Q: Have I checked the inventory yet?
  A: No. I need more information from the user including ...
  Q: What else do I need to know?
  A: ...
  Q: Should I present my item to the user?
  A: Not yet, I will need to check my inventory first.
  Q: Should I check our inventory now?
  A: ...
  Q: What the user intend to do with the car?
  A: I don't know yet. Let's ask the user.
  Q: What do I have in our inventory?
  A: ...
  Q: Which items are within the user price range? And which items are out of the user price rance?
  A: ...
  Q: Do I have what the user is looking for in our stock? 
  A: ...
  Q: Am I certain about the information I'm going to share with the user, or should I verify the information first?
  A: ...
  Q: What should I do?
  A: ...
  Q: What shouldn't I do?
  A: ...
  Q: what kind of car suitable for off-road trip?
  A: A four-wheel drive SUV is a good choice for off-road trips.
  Q: What car specification would satisfy the user's needs?
  A: The user is seeking an eco-friendly vehicle that accommodates seven passengers, including seniors and children, with prioritized accessibility and efficient refueling. While electric vehicles (EVs) offer eco-friendly benefits, their long charging times make hybrid models more practical for fast refueling. Additionally, a lower ground level is essential for ease of entry/exit for seniors and children. A hybrid multi-purpose vehicle (MPV) emerges as the optimal solution, balancing sustainability, seating capacity, accessibility, and refueling efficiency.
  
  Let's begin!
  """

  header = ["Q1:"]
  dictkey = ["q1"]
  
  # context =
  #   if length(a.memory["shortmem"]["available_wine"]) != 0
  #     "Available wines you've found in your inventory so far: $(availableWineToText(a.memory["shortmem"]["available_wine"]))"
  #   else
  #     "N/A"
  #   end
  database_search_result = a.memory["shortmem"]["db_search_result"]

  # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent)
  # recentevents = a.memory[:events][recent_ind]
  # timeline = createTimeline(recentevents; eventindex=recent_ind)
  errornote = "N/A"
  response = nothing # store for show when error msg show up

  # recap =
  #   if length(a.memory[:recap]) <= recent
  #     "N/A"
  #   else 
  #     recapkeys = keys(a.memory[:recap])
  #     recapkeys_vec = [i for i in recapkeys]
  #     recapkeys_vec = recapkeys_vec[1:end-recent]
  #     tempmem = OrderedDict()
  #     for (k, v) in a.memory[:recap]
  #       if k ∈ recapkeys_vec
  #         tempmem[k] = v
  #       end
  #     end

  #     GeneralUtils.dictToString(tempmem)
  #   end

  llmkwargs=Dict(
    :num_ctx => 32768,
    :temperature => 0.5,
  )

  for attempt in 1:10
    if attempt > 1
      println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    end

    usermsg = 
      """
      Additional info: $database_search_result
      Your recent events: $timeline
      P.S. $errornote
      """

    _prompt =
      [
        Dict("name" => "system", "text" => systemmsg),
        Dict("name" => "user", "text" => usermsg)
      ]

    # put in model format
    prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)

    response = text2textInstructLLM(prompt; 
      modelsize="medium", llmkwargs=llmkwargs, senderId=a.id)
    response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
    think, response = GeneralUtils.extractthink(response)

    # make sure generatequestion() don't have wine name that is not from retailer inventory
    # check whether an agent recommend wines before checking inventory or recommend wines 
    # outside its inventory
    # ask LLM whether there are any winery mentioned in the response
    mentioned_winery = detectWineryName(a, response)
    if mentioned_winery != "None"
      mentioned_winery = String.(strip.(split(mentioned_winery, ",")))

      # check whether the wine is in event
      isWineInEvent = false
      for winename in mentioned_winery
        for event in a.memory["events"]
          if event["observation"] !== nothing && occursin(winename, event["observation"])
            isWineInEvent = true
            break
          end
        end
      end

      # if wine is mentioned but not in timeline or shortmem,
      # then the agent is not supposed to recommend the wine
      if isWineInEvent == false
        errornote = "Your previous attempt mentioned wines that are not in your inventory which is not allowed."
        println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
        continue
      end
    end

    q_number = count("Q", response)

    # check for valid response
    if q_number < 1
      errornote = "Your previous attempt has too few questions."
      println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
      # check whether "A1" is in the response, if not error.
    elseif !occursin("A1:", response)
      errornote = "Your previous attempt does not have A1:"
      println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    end

    # check whether response has all header
    detected_kw = GeneralUtils.detectKeywordVariation(header, response)
    if 0 ∈ values(detected_kw)
      errornote = "\nYour previous attempt did not have all points according to the required response format"
      println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    elseif sum(values(detected_kw)) > length(header)
      errornote = "\nYour previous attempt has duplicated points according to the required response format"
      println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    end

    responsedict = GeneralUtils.textToDict(response, header; 
      dictKey=dictkey, symbolkey=true)
    response = "Q1: " * responsedict["q1"]
    println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    try pprintln(response) catch e println(response) end

    return response
  end
  error("YiemAgent generatequestion() failed to generate a response ", response)
end


function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0
  )::OrderedDict
  
  systemmsg = 
  """
    You are an assistant being in the given events. 
    Your task is to writes a summary for each event seperately into an ongoing, interleaving series.
      
    At each round of conversation, you will be given the situation:
    Total events: number of events you need to summarize.
    Events timeline: ...
    Context: ...

    You should follow the following guidelines:
    - Use the word "user" and "assistant" instead of their name in the report

    You should then respond to the user with the following:
    Event: a detailed summary for each event without exaggerated details.
    
    You must only respond in format as described below:
    Event_1: ...
    Event_2: ...
    ...

    Here are some examples:
    Event_1: The user ask me about where to buy a toy.
    Event_2: I told the user to go to the store at 2nd floor.

    Event_1: The user greets the assistant by saying 'hello'.
    Event_2: The assistant respond warmly and inquire about how he can assist the user.

    Let's begin!
  """

  header = ["Event_$i:" for i in eachindex(a.memory["events"])]
  dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory["events"])])

  ind = GeneralUtils.nonRecentElementsIndex(length(a.memory["events"]), skiprecent)
  events = a.memory["events"][ind]
  timeline = createTimeline(events)

  errornote = "N/A"
  response = nothing # store for show when error msg show up
  for attempt in 1:10
    if attempt > 1 # use to prevent LLM generate the same respond over and over
      println("\nYiemAgent generateSituationReport() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    end

    usermsg = """
              Total events: $(length(events))
              Events timeline: $timeline
              P.S. $errornote
              """

    _prompt =
      [
        Dict("name" => "system", "text" => systemmsg),
        Dict("name" => "user", "text" => usermsg)
      ]

    # put in model format
    prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3")

    response = text2textInstructLLM(prompt; senderId=a.id)
    response = GeneralUtils.deFormatLLMtext(response, "qwen3")

    # check whether response has all header
    detected_kw = GeneralUtils.detectKeywordVariation(header, response)
    kwvalue = [i for i in values(detected_kw)]
    zeroind = findall(x -> x == 0, kwvalue)
    missingkeys = [header[i] for i in zeroind]
    if 0 ∈ values(detected_kw)
      errornote = "$missingkeys are missing in your previous attempt"
      println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    elseif sum(values(detected_kw)) > length(header)
      errornote = "Your previous response has duplicated events"
      println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    end

    responsedict = GeneralUtils.textToDict(response, header; 
                    dictKey=dictkey, symbolkey=true)
    
    println("\ngenerateSituationReport() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
    try pprintln(response) catch e println(response) end

    return responsedict
  end
  error("generateSituationReport failed to generate a response ", response)
end


function detectWineryName(a, text)
    systemmsg = 
    """
    You are a sommelier of a wine store. 
    Your task is to identify and list any winery names mentioned in the provided text.
      
    At each round of conversation, you will be given the situation:
    Text: a text describing the situation.

    Tips:
    - Winery usually contains Château, Chateau, Domaine, Côte, Cotes, St. de, or a combination of these words.

    You should then respond to the user with:
    Winery_names: A list of winery names mentioned in the text or "None" if no winery name is mentioned.
    
    You must only respond in format as described below:
    Winery_names: ...

    Here are some examples:
    Winery_names: Domaine Courbis, Chateau Lafite Rothschild, Matarromera Domaine Roulot, Château, Cotes

    Let's begin!
    """

    header = ["Winery_names:"]
    dictkey = ["winery_names"]
  
    response = nothing # placeholder for show when error msg show up

    for attempt in 1:10
      usermsg = """
                Text: $text
                """
      _prompt =
        [
          Dict("name" => "system", "text" => systemmsg),
          Dict("name" => "user", "text" => usermsg)
        ]

      # put in model format
      prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName)

      response = a.context.text2textInstructLLM(prompt; senderId=a.id)
      response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName)
      think, response = GeneralUtils.extractthink(response)
      println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      try pprintln(response) catch e println(response) end

     # check whether response has all header
    detected_kw = GeneralUtils.detectKeywordVariation(header, response)
    missingkeys = [k for (k, v) in detected_kw if v === nothing]

    if !isempty(missingkeys)
      errornote = "$missingkeys are missing from your previous response"
      println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    elseif sum([length(i) for i in values(detected_kw)]) > length(header)
      errornote = "\nYour previous attempt has duplicated points according to the required response format"
      println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
      continue
    end

      responsedict = GeneralUtils.textToDict(response, header; 
        dictKey=dictkey, symbolkey=true)

      result = responsedict["winery_names"]

      return result
    end
    error("detectWineryName failed to generate a response")
  end
  







































end # module interface






































































