update
This commit is contained in:
+2
-2
@@ -16,8 +16,8 @@ module YiemAgent
|
|||||||
include("llmfunction.jl")
|
include("llmfunction.jl")
|
||||||
using .llmfunction
|
using .llmfunction
|
||||||
|
|
||||||
include("core.jl")
|
include("agentCore.jl")
|
||||||
using .core
|
using .agentCore
|
||||||
|
|
||||||
include("api.jl")
|
include("api.jl")
|
||||||
using .api
|
using .api
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
module agentCore
|
||||||
|
|
||||||
|
# export prompt
|
||||||
|
|
||||||
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
|
DataFrames, Serde
|
||||||
|
using GeneralUtils
|
||||||
|
using ..type, ..util, ..llmfunction
|
||||||
|
|
||||||
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
Private agent loop. Runs in a background `@spawn` task.
|
||||||
|
|
||||||
|
Waits on `input_ch` and `followUpQueue`, processing whichever has a message first.
|
||||||
|
On each iteration, dispatches the message through `_process_message` and sends the result
|
||||||
|
to `output_ch`. Exits on `:shutdown` signal.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `agent::yiemAgent`: The agent whose loop to run
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `nothing` — the loop runs until `:shutdown` is received or an error occurs
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- This function is automatically spawned as a background task when a `yiemAgent` is created.
|
||||||
|
- On any error, logs the error with `@error` and exits the loop.
|
||||||
|
- Message priority: `input_ch` messages are checked before `followUpQueue` messages.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> # Called automatically by yiemAgent constructor
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function _agent_loop(agent::yiemAgent) #WORKING
|
||||||
|
try
|
||||||
|
while true
|
||||||
|
# Wait on either channel — the one with a message fires first
|
||||||
|
# Wait on either channel — the one with a message is taken first
|
||||||
|
msg = nothing
|
||||||
|
while msg === nothing
|
||||||
|
if isready(agent.input_ch)
|
||||||
|
msg = take!(agent.input_ch)
|
||||||
|
else
|
||||||
|
yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for shutdown signal
|
||||||
|
if msg === :shutdown
|
||||||
|
#TODO make sure every running tools ended properly
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
#TODO convert raw user msg to userMessage type
|
||||||
|
|
||||||
|
#TODO add userMessage to agent._state.messages
|
||||||
|
|
||||||
|
|
||||||
|
if isready(agent.followUpQueue)
|
||||||
|
msg = take!(agent.followUpQueue)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @spawn. Dispatch message through the processing pipeline.
|
||||||
|
result = _process_message(agent, msg)
|
||||||
|
|
||||||
|
# Send response to user
|
||||||
|
put!(agent.output_ch, result)
|
||||||
|
end
|
||||||
|
catch e
|
||||||
|
# On any error, send error response and exit the loop
|
||||||
|
@error "Agent loop failed" error=e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Process a single message through the agent pipeline.
|
||||||
|
|
||||||
|
This is the core processing function where LLM calls, tool execution, and response generation
|
||||||
|
should be implemented. Currently a placeholder that echoes back the received message.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `agent::yiemAgent`: The agent processing the message
|
||||||
|
- `msg`: The message to process (from `input_ch` or `followUpQueue`)
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- An `assistantMessage` instance with the processed response
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Implement the full processing pipeline:
|
||||||
|
1. Add `msg` to `agent._state.messages`
|
||||||
|
2. Call `agent.formatMsgForLLM(agent._state)` 1 to format for LLM
|
||||||
|
3. If `agent.preprocessMessages` is set, call it on the formatted messages
|
||||||
|
4. Call the LLM (blocking — the task waits here)
|
||||||
|
5. If agent has tools, handle tool calls in a loop
|
||||||
|
6. Build `assistantMessage` and return it
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> # Currently returns a placeholder echo response
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function _process_message(agent::yiemAgent, msg)
|
||||||
|
# WORKING Replace with actual processing logic
|
||||||
|
# check steering message
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 1. call agent.
|
||||||
|
# 2. Call agent.formatMsgForLLM(agent._state) to format for LLM
|
||||||
|
# 3. If preprocessMessages is set, call agent.preprocessMessages(...)
|
||||||
|
# 4. Call the LLM (blocking — the task waits here)
|
||||||
|
# 5. If agent has tools, handle tool calls in a loop
|
||||||
|
# 6. Build assistantMessage and return it
|
||||||
|
|
||||||
|
# Placeholder: echo back the message as a simple response
|
||||||
|
@warn "TODO: implement _process_message"
|
||||||
|
return assistantMessage(
|
||||||
|
role="assistant",
|
||||||
|
content=[textContent("Received: $(msg)")],
|
||||||
|
api="", model="", usage=nothing,
|
||||||
|
stopReason="end_turn",
|
||||||
|
errorMessage=nothing,
|
||||||
|
timestamp=now(),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end # end of module
|
||||||
+74
-2
@@ -13,7 +13,26 @@ using ..type, ..util, ..llmfunction
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Send a message to the agent's input channel.
|
Send a message to the agent's input channel.
|
||||||
|
|
||||||
Blocks if the input channel buffer is full (capacity 16 by default).
|
Blocks if the input channel buffer is full (capacity 16 by default).
|
||||||
|
The agent processes messages from `input_ch` in the background task.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `agent::yiemAgent`: The agent instance to send a message to
|
||||||
|
- `msg`: The message to send (any type accepted by the agent's processing pipeline)
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Use `take_response(agent)` to receive the agent's response after sending a message.
|
||||||
|
- Use `follow_up(agent, msg)` to send messages while the agent is still processing.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> run_agent(agent, "Hello!")
|
||||||
|
yiemAgent(...)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function run_agent(agent::yiemAgent, msg)
|
function run_agent(agent::yiemAgent, msg)
|
||||||
put!(agent.input_ch, msg)
|
put!(agent.input_ch, msg)
|
||||||
@@ -22,7 +41,23 @@ end
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Take a response from the agent's output channel.
|
Take a response from the agent's output channel.
|
||||||
|
|
||||||
Blocks until the agent sends a response.
|
Blocks until the agent sends a response.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `agent::yiemAgent`: The agent instance to receive a response from
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- An `assistantMessage` instance representing the agent's response
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Use `run_agent(agent, msg)` to send a message before calling this function.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> response = take_response(agent)
|
||||||
|
assistantMessage(...)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function take_response(agent::yiemAgent)
|
function take_response(agent::yiemAgent)
|
||||||
return take!(agent.output_ch)
|
return take!(agent.output_ch)
|
||||||
@@ -30,8 +65,27 @@ end
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Send a follow-up message while the agent is still processing.
|
Send a follow-up message while the agent is still processing.
|
||||||
Follow-up messages are processed after all input_ch messages
|
|
||||||
|
Follow-up messages are queued and processed after all `input_ch` messages
|
||||||
and before any tool call results are sent.
|
and before any tool call results are sent.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `agent::yiemAgent`: The agent instance to send a follow-up message to
|
||||||
|
- `msg`: The follow-up message to send
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Use `run_agent(agent, msg)` for the primary message and `follow_up(agent, msg)` for additional
|
||||||
|
messages while the agent is processing.
|
||||||
|
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> follow_up(agent, "Also consider red wines")
|
||||||
|
yiemAgent(...)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function follow_up(agent::yiemAgent, msg)
|
function follow_up(agent::yiemAgent, msg)
|
||||||
put!(agent.followUpQueue, msg)
|
put!(agent.followUpQueue, msg)
|
||||||
@@ -40,7 +94,25 @@ end
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Gracefully stop the agent.
|
Gracefully stop the agent.
|
||||||
Sends a :shutdown signal, waits for the task to finish, then closes all channels.
|
|
||||||
|
Sends a `:shutdown` signal to the input channel, waits for the background task to finish,
|
||||||
|
then closes all channels (`input_ch`, `output_ch`, `followUpQueue`).
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `agent::yiemAgent`: The agent instance to stop
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `nothing`
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- After calling `stop_agent`, the agent is no longer usable. A new agent must be created
|
||||||
|
for further interaction.
|
||||||
|
- If the background task throws a `TaskFailedException`, it is rethrown.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> stop_agent(agent)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function stop_agent(agent::yiemAgent)
|
function stop_agent(agent::yiemAgent)
|
||||||
put!(agent.input_ch, :shutdown)
|
put!(agent.input_ch, :shutdown)
|
||||||
|
|||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
module core
|
|
||||||
|
|
||||||
# export prompt
|
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
|
||||||
DataFrames, Serde
|
|
||||||
using GeneralUtils
|
|
||||||
using ..type, ..util, ..llmfunction
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
|
||||||
|
|
||||||
"""
|
|
||||||
Private agent loop. Runs in a background @task.
|
|
||||||
Waits on input_ch and followUpQueue, processing whichever has a message first.
|
|
||||||
"""
|
|
||||||
function _agent_loop(agent::yiemAgent)
|
|
||||||
try
|
|
||||||
while true
|
|
||||||
# Wait on either channel — the one with a message fires first
|
|
||||||
# Wait on either channel — the one with a message is taken first
|
|
||||||
msg = nothing
|
|
||||||
while msg === nothing
|
|
||||||
if isready(agent.input_ch)
|
|
||||||
msg = take!(agent.input_ch)
|
|
||||||
elseif isready(agent.followUpQueue)
|
|
||||||
msg = take!(agent.followUpQueue)
|
|
||||||
else
|
|
||||||
yield()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check for shutdown signal
|
|
||||||
if msg === :shutdown
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
# Dispatch message through the processing pipeline
|
|
||||||
result = _process_message(agent, msg)
|
|
||||||
|
|
||||||
# Send response to user
|
|
||||||
put!(agent.output_ch, result)
|
|
||||||
end
|
|
||||||
catch e
|
|
||||||
# On any error, send error response and exit the loop
|
|
||||||
@error "Agent loop failed" error=e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Process a single message through the agent pipeline.
|
|
||||||
This is where you add your LLM call, tool execution, etc.
|
|
||||||
"""
|
|
||||||
function _process_message(agent::yiemAgent, msg)
|
|
||||||
# WORKING Replace with actual processing logic
|
|
||||||
#
|
|
||||||
# 1. Add msg to agent._state.messages
|
|
||||||
# 2. Call agent.formatMsgForLLM(agent._state) to format for LLM
|
|
||||||
# 3. If preprocessMessages is set, call agent.preprocessMessages(...)
|
|
||||||
# 4. Call the LLM (blocking — the task waits here)
|
|
||||||
# 5. If agent has tools, handle tool calls in a loop
|
|
||||||
# 6. Build assistantMessage and return it
|
|
||||||
|
|
||||||
# Placeholder: echo back the message as a simple response
|
|
||||||
@warn "TODO: implement _process_message"
|
|
||||||
return assistantMessage(
|
|
||||||
role="assistant",
|
|
||||||
content=[textContent("Received: $(msg)")],
|
|
||||||
api="", model="", usage=nothing,
|
|
||||||
stopReason="end_turn",
|
|
||||||
errorMessage=nothing,
|
|
||||||
timestamp=now(),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # end of module
|
|
||||||
+331
-53
@@ -12,27 +12,29 @@ using ..type, ..util
|
|||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
""" Chatbox for chatting with virtual wine customer.
|
"""
|
||||||
|
Chat with a virtual wine customer for recommendations.
|
||||||
|
|
||||||
|
Formats the input for the configured LLM model and communicates via MQTT
|
||||||
|
to receive a response tuple containing text, selection, reward, and terminal status.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `a::T1`
|
- `a::T1`: An agent instance (subtype of `agent`) with `config` containing `externalservice`
|
||||||
one of Yiem's agent
|
and `mqttServerInfo` keys
|
||||||
- `input::T2`
|
- `input::T2`: Text to send to the virtual wine customer LLM
|
||||||
text to be send to virtual wine customer
|
|
||||||
|
|
||||||
# Return
|
# Returns
|
||||||
- `response::String`
|
- `Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}}`: A tuple of
|
||||||
response of virtual wine customer
|
`(response_text, select, reward, isterminal)` where `select` may be `Nothing`
|
||||||
# Example
|
|
||||||
```jldoctest
|
# Notes
|
||||||
julia>
|
- Requires `a.config["externalservice"]["virtualWineCustomer_1"]` with `llminfo` and `mqtttopic`.
|
||||||
```
|
- Requires `a.config["mqttServerInfo"]` with `broker` and `port`.
|
||||||
|
- Only supports `llama3instruct` model name (other models throw an error).
|
||||||
|
- Uses `GeneralUtils.sendReceiveMqttMsg` with a 120-second timeout.
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
- [] update docstring
|
- Add `recommend()` to compare wines
|
||||||
- [] add reccommend() to compare wine
|
|
||||||
|
|
||||||
# Signature
|
|
||||||
"""
|
"""
|
||||||
function virtualWineUserRecommendbox(a::T1, input
|
function virtualWineUserRecommendbox(a::T1, input
|
||||||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent}
|
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent}
|
||||||
@@ -73,27 +75,36 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
""" Chatbox for chatting with virtual wine customer.
|
"""
|
||||||
|
Chatbox for conversing with a virtual wine customer AI.
|
||||||
|
|
||||||
|
Formats the chat history with a system prompt, sends it via MQTT to a text2text instruct
|
||||||
|
LLM service, and parses the JSON response. Retries up to 5 times on failure.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `a::T1`
|
- `config::T1`: Configuration dictionary (subtype of `AbstractDict`) containing:
|
||||||
one of Yiem's agent
|
- `externalservice["text2text"]["mqtttopic"]`: MQTT topic for the LLM service
|
||||||
- `input::T2`
|
- `mqttServerInfo["broker"]`: MQTT broker address
|
||||||
text to be send to virtual wine customer
|
- `mqttServerInfo["port"]`: MQTT broker port
|
||||||
|
- `input::T2`: Current sommelier message text (subtype of `AbstractString`)
|
||||||
|
- `virtualCustomerChatHistory`: Chat history vector of dictionaries with `"name"` and `"text"` keys
|
||||||
|
|
||||||
# Return
|
# Returns
|
||||||
- `response::String`
|
- `Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}}`: A tuple of
|
||||||
response of virtual wine customer
|
`(text, select, reward, isterminal)` representing the virtual customer's response
|
||||||
# Example
|
|
||||||
|
# Notes
|
||||||
|
- The system prompt defines the virtual customer's persona and response format.
|
||||||
|
- Chat history role names are transformed: "user" → "you", "assistant" → "sommelier".
|
||||||
|
- Uses `jsoncorrection` to fix malformed LLM JSON responses.
|
||||||
|
- Retries up to 5 times on error before throwing.
|
||||||
|
- Uses `formatLLMtext` with `"llama3instruct"` format.
|
||||||
|
|
||||||
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia>
|
julia> result = YiemAgent.virtualWineUserChatbox(config, sommelier_msg, history)
|
||||||
|
("I'd like something under $50", nothing, 0, false)
|
||||||
```
|
```
|
||||||
|
|
||||||
# TODO
|
|
||||||
- [] update docs
|
|
||||||
- [x] write a prompt for virtual customer
|
|
||||||
|
|
||||||
# Signature
|
|
||||||
"""
|
"""
|
||||||
function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistory
|
function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistory
|
||||||
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:AbstractDict, T2<:AbstractString}
|
)::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:AbstractDict, T2<:AbstractString}
|
||||||
@@ -265,24 +276,36 @@ pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemms
|
|||||||
error("virtualWineUserChatbox failed to get a response")
|
error("virtualWineUserChatbox failed to get a response")
|
||||||
end
|
end
|
||||||
|
|
||||||
""" Search wine in stock.
|
"""
|
||||||
|
Search for wines in stock.
|
||||||
|
|
||||||
|
Executes a wine search via SQL (either through SQLLLM or direct query), optionally fetching
|
||||||
|
and base64-encoding bottle images for each result.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `a::T1`
|
- `a::T`: An agent instance (subtype of `agent`) with context containing `executeSQL`,
|
||||||
one of ChatAgent's agent.
|
`pg_conn_str`, and `agentconfig`
|
||||||
- `thoughtdict::AbstractDict`
|
- `thoughtdict::AbstractDict`: A dictionary containing `action_input` (the search query string)
|
||||||
# Return
|
|
||||||
A JSON string of available wine
|
|
||||||
|
|
||||||
# Example
|
# Keyword Arguments
|
||||||
|
- `useSQLLLM::Bool=false`: Whether to use SQLLLM for the query instead of direct SQL generation
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}}`: A named tuple with:
|
||||||
|
- `thoughtdict`: The input thoughtdict with `action_result` populated
|
||||||
|
- `result_raw`: Vector of wine dictionaries with image data, or `nothing`
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- When `useSQLLLM=false`, performs hard SQL filtering followed by vector search.
|
||||||
|
- Fetches bottle images from `http://192.168.88.106:8080/` and encodes as base64.
|
||||||
|
- Uses `wine_search_term_classification` to extract search conditions.
|
||||||
|
- Requires PostgreSQL connection info from `a.context.agentconfig["externalservice"]["sommpanion_db"]`.
|
||||||
|
|
||||||
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> using ChatAgent
|
julia> thoughtdict = OrderedDict("action_input" => "red wine under 50");
|
||||||
julia> agent = YiemAgent.sommelier(...)
|
julia> result = YiemAgent.search_wine_database!(agent, thoughtdict)
|
||||||
julia> thoughtdict =
|
(thoughtdict=OrderedDict{String, Any}(...), result_raw=[Dict(...), ...])
|
||||||
OrderedDict{String, Any}(
|
|
||||||
"plan" => "The user is asking a very specific question about a wine (Brunello di Montalcino from Tenuta CastelGiocondo). Although the policy suggests gathering budget, wine type, and occasion, the user has provided enough specific information (name, region, producer) to attempt a direct search in the database. I will use the SEARCH_WINE_DATABASE action to check if this specific wine is in our inventory.",
|
|
||||||
"action_name" => "SEARCH_WINE_DATABASE",
|
|
||||||
"action_input" => "Brunello di Montalcino from Tenuta CastelGiocondo")
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
||||||
@@ -371,6 +394,39 @@ function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Generate an SQL query from a natural language search term.
|
||||||
|
|
||||||
|
Uses an LLM to generate SQL based on the database schema relevant to the search term,
|
||||||
|
with validation and retry logic.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `a::T`: An agent instance (subtype of `agent`) with context containing:
|
||||||
|
- `find_related_tables_for_user_question`: Function to find relevant database tables
|
||||||
|
- `pg_conn_str`: PostgreSQL connection string
|
||||||
|
- `text2textInstructLLM`: LLM function for text generation
|
||||||
|
- `searchterm::String`: Natural language search term to convert to SQL
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `maxattempt::Int=10`: Maximum number of attempts to get a valid SQL response from the LLM
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `String`: A valid SQL query string
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Uses `gemma-4-E4B-it-UD-Q4_K_XL` model for SQL generation.
|
||||||
|
- Dynamically fetches only relevant table schemas for the search term.
|
||||||
|
- Validates LLM response contains required keys: "plan", "action_name", "action_input".
|
||||||
|
- `action_name` must be "RUNSQL"; `action_input` must not contain "RUNSQL".
|
||||||
|
- Strips triple backticks and extracts SQL from fenced code blocks.
|
||||||
|
- Throws on failure after `maxattempt` retries.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> sql = YiemAgent.generatesql(agent, "red wine under 50")
|
||||||
|
"SELECT ... WHERE w.wine_type = 'red' AND rw.price < 50;"
|
||||||
|
```
|
||||||
|
"""
|
||||||
function generatesql(a::T, searchterm::String,
|
function generatesql(a::T, searchterm::String,
|
||||||
; maxattempt=10
|
; maxattempt=10
|
||||||
)::String where {T<:agent}
|
)::String where {T<:agent}
|
||||||
@@ -655,6 +711,40 @@ julia> thoughtdict =
|
|||||||
```
|
```
|
||||||
julia> predefined_wine_search_sql(agent, thoughtdict["action_input"])
|
julia> predefined_wine_search_sql(agent, thoughtdict["action_input"])
|
||||||
"""
|
"""
|
||||||
|
"""
|
||||||
|
Classify a wine search term into hard SQL conditions and vector search words.
|
||||||
|
|
||||||
|
Uses an LLM to extract database column conditions from a natural language search term,
|
||||||
|
then classifies them into hard conditions (for SQL WHERE clauses) and vector search
|
||||||
|
entries (for approximate matching).
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `a::T`: An agent instance (subtype of `agent`) with context containing:
|
||||||
|
- `find_related_tables_for_user_question`: Function to find relevant tables
|
||||||
|
- `pg_conn_str`: PostgreSQL connection string
|
||||||
|
- `text2textInstructLLM`: LLM function for text generation
|
||||||
|
- `searchterm::String`: Natural language search term to classify
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `maxattempt::Int=10`: Maximum number of attempts to get a valid classification from the LLM
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `NamedTuple{(:hard_conditions, :vector_search), Tuple{Vector{JSON.Object{String, Any}}, Vector{JSON.Object{String, Any}}}}`:
|
||||||
|
- `hard_conditions`: Entries with standard operators (=, <>, !=, >, <, >=, <=) for SQL WHERE clauses
|
||||||
|
- `vector_search`: Entries with non-standard operators (LIKE, IN, IS NULL, etc.) for vector search
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Uses `gemma-4-E4B-it-UD-Q4_K_XL` model with JSON schema response format.
|
||||||
|
- Applies fuzzy correction for "fuzzy_correction" bucket columns via `resolve_entity`.
|
||||||
|
- Uses `classify_column` to determine the column type bucket.
|
||||||
|
- Uses `harvest_entity_catalog` and `resolve_entity` (threshold=0.9) for fuzzy matching.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> result = YiemAgent.wine_search_term_classification(agent, "dry red wine under 30")
|
||||||
|
(hard_conditions=[...], vector_search=[...])
|
||||||
|
```
|
||||||
|
"""
|
||||||
function wine_search_term_classification(a::T, searchterm::String,
|
function wine_search_term_classification(a::T, searchterm::String,
|
||||||
; maxattempt=10
|
; maxattempt=10
|
||||||
) where {T<:agent}
|
) where {T<:agent}
|
||||||
@@ -857,6 +947,35 @@ function wine_search_term_classification(a::T, searchterm::String,
|
|||||||
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
error("SQLLLM DecisionMaker() failed to generate a thought \n", response)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Build a SQL query from pre-classified wine search conditions.
|
||||||
|
|
||||||
|
Constructs a JOIN query across `wine`, `retailer_wine`, and `retailer` tables with
|
||||||
|
dynamic WHERE clauses based on the provided conditions.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `conditions::Vector{JSON.Object{String, Any}}`: Vector of condition objects, each containing:
|
||||||
|
- `table_name`: One of "wine", "retailer_wine" (other tables are skipped)
|
||||||
|
- `column_name`: The column to filter on
|
||||||
|
- `operator`: SQL comparison operator (=, <>, !=, >, <, >=, <=)
|
||||||
|
- `value`: The value to compare against (number or string)
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `String`: A complete SQL query with WHERE clause
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Supports table aliases: "wine" → "w", "retailer_wine" → "rw"
|
||||||
|
- Automatically handles numeric vs string value types in SQL formatting
|
||||||
|
- Strings are single-quote escaped (replaces "'" with "''")
|
||||||
|
- Returns a query with no WHERE clause if conditions vector is empty
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> cond = [JSON.Object{String, Any}("table_name"=>"wine", "column_name"=>"wine_type", "operator"=>"=", "value"=>"red")];
|
||||||
|
julia> YiemAgent.predefined_wine_search_sql(cond)
|
||||||
|
"SELECT ... FROM wine AS w JOIN ... WHERE w.wine_type = 'red';"
|
||||||
|
```
|
||||||
|
"""
|
||||||
function predefined_wine_search_sql(conditions::Vector{JSON.Object{String, Any}})::String
|
function predefined_wine_search_sql(conditions::Vector{JSON.Object{String, Any}})::String
|
||||||
# 1. Base SQL structure
|
# 1. Base SQL structure
|
||||||
base_query =
|
base_query =
|
||||||
@@ -934,6 +1053,36 @@ JOIN retailer AS r ON rw.retailer_id = r.retailer_id
|
|||||||
return string(base_query, where_sql, ";")
|
return string(base_query, where_sql, ";")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Execute a SQL query against the database and return formatted results.
|
||||||
|
|
||||||
|
Adds `ORDER BY RANDOM() LIMIT 2` for non-LIMITed queries, removes `DISTINCT`, and returns
|
||||||
|
either a formatted string result or the DataFrame.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `executeSQL::Function`: A function that executes SQL and returns results (e.g., PostgreSQL connection)
|
||||||
|
- `sql::T`: The SQL query string to execute (subtype of `AbstractString`)
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `NamedTuple{(:result_str, :result_raw, :success, :errormsg)}`: A named tuple with:
|
||||||
|
- `result_str::Union{String, Nothing}`: Formatted string representation of results
|
||||||
|
- `result_raw::Union{DataFrame, Nothing}`: The DataFrame result, or `nothing`
|
||||||
|
- `success::Bool`: Whether the query executed successfully
|
||||||
|
- `errormsg::Union{String, Nothing}`: Error message if failed, or `nothing`
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Removes `DISTINCT` keyword before execution (incompatible with `RANDOM()`)
|
||||||
|
- Appends `ORDER BY RANDOM() LIMIT 2` if query doesn't have `LIMIT` and ends with `;`
|
||||||
|
- Returns "No records found" message if zero rows
|
||||||
|
- Returns column count warning if more than 30 columns
|
||||||
|
- Randomly samples 2 rows if result has more than 2 rows
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> result = YiemAgent.SQLexecution(execute_fn, "SELECT * FROM wine;")
|
||||||
|
(result_str="...", result_raw=DataFrame(...), success=true, errormsg=nothing)
|
||||||
|
```
|
||||||
|
"""
|
||||||
function SQLexecution(executeSQL::Function, sql::T
|
function SQLexecution(executeSQL::Function, sql::T
|
||||||
)::NamedTuple where {T<:AbstractString}
|
)::NamedTuple where {T<:AbstractString}
|
||||||
|
|
||||||
@@ -984,6 +1133,26 @@ function SQLexecution(executeSQL::Function, sql::T
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
DEPRECATED: Search for wines in stock (legacy implementation).
|
||||||
|
|
||||||
|
Use `search_wine_database!` instead. This function uses the older approach with
|
||||||
|
`extractWineAttributes_1` and `extractWineAttributes_2` for attribute extraction.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `a::T`: An agent instance (subtype of `agent`)
|
||||||
|
- `thoughtdict::AbstractDict`: Dictionary containing `action_input` (search query)
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `useSQLLLM::Bool=false`: Whether to use SQLLLM for the query
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}}`
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- DEPRECATED: Use `search_wine_database!` for the current implementation.
|
||||||
|
- Calls `extractWineAttributes_1` and `extractWineAttributes_2` for attribute extraction.
|
||||||
|
"""
|
||||||
function DEPRECIATED_search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
function DEPRECIATED_search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false
|
||||||
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
|
)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent}
|
||||||
|
|
||||||
@@ -1044,16 +1213,38 @@ function DEPRECIATED_search_wine_database!(a::T, thoughtdict::AbstractDict; useS
|
|||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
Extract wine attributes from a user's search query.
|
||||||
|
|
||||||
|
Uses an LLM to parse natural language input into structured wine attributes including
|
||||||
|
name, winery, vintage, country, type, grape, price range, occasion, and food pairing.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `v::Integer`
|
- `a::T1`: An agent instance (subtype of `agent`) with context containing:
|
||||||
dummy variable
|
- `text2textInstructLLM`: LLM function for text generation
|
||||||
|
- `pg_conn_str`: PostgreSQL connection string
|
||||||
|
- `id`: Agent identifier
|
||||||
|
- `input::T2`: User's search query string (subtype of `AbstractString`)
|
||||||
|
|
||||||
# Return
|
# Keyword Arguments
|
||||||
|
- `maxattempt::Int=10`: Maximum number of attempts to get a valid response from the LLM
|
||||||
|
|
||||||
# Example
|
# Returns
|
||||||
|
- `String`: Comma-separated list of extracted attributes in the format `"key: value, key: value"`
|
||||||
|
Attributes with "N/A", empty, or "none" values are excluded.
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Uses `gemma-4-E4B-it-UD-Q4_K_XL` model with temperature 0.7.
|
||||||
|
- Extracts: wine_name, winery, vintage, country, wine_type, grape_varietal, tasting_notes,
|
||||||
|
wine_price_min, wine_price_max, occasion, food_to_be_paired_with_wine
|
||||||
|
- Validates response contains all required keys via `checkAgentResponse_JSON`.
|
||||||
|
- Applies fuzzy entity resolution via `harvest_entity_catalog` and `resolve_entity` (threshold=0.9).
|
||||||
|
- Strips "(some comment)" patterns from values.
|
||||||
|
- Removes keys: thought, tasting_notes, occasion, food_to_be_paired_with_wine, vintage from final output.
|
||||||
|
|
||||||
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia>
|
julia> YiemAgent.extractWineAttributes_1(agent, "red wine from Napa under 50")
|
||||||
|
"wine_name:N/A, winery:N/A, vintage:N/A, country:United States, wine_type:red, grape_varietal:N/A, wine_price_min:0, wine_price_max:50"
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
|
function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
|
||||||
@@ -1225,8 +1416,40 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
|
|||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
- TODO "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve.
|
Extract wine intensity, sweetness, tannin, and acidity attributes from a query.
|
||||||
- TODO French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why?
|
|
||||||
|
Uses an LLM with a conversion table to map descriptive words (e.g., "medium-bodied", "low acidity")
|
||||||
|
to integer ranges on a 1-5 scale.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `a::T1`: An agent instance (subtype of `agent`) with context containing:
|
||||||
|
- `text2textInstructLLM`: LLM function for text generation
|
||||||
|
- `id`: Agent identifier
|
||||||
|
- `input::T2`: User's query string containing descriptive wine preferences
|
||||||
|
(subtype of `AbstractString`)
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `String`: Comma-separated list of extracted attributes in the format
|
||||||
|
`"key: value, key: value"`. Only includes numeric values or non-N/A strings.
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Uses `gemma-4-E4B-it-UD-Q4_K_XL` model with temperature 0.7.
|
||||||
|
- Extracts: sweetness, acidity, tannin, intensity (each with min/max values).
|
||||||
|
- Applies `remove_french_accents` to the LLM response.
|
||||||
|
- Extracts thinking via `extractthink` before JSON parsing.
|
||||||
|
- Validates response contains all required keys via `checkAgentResponse_JSON`.
|
||||||
|
- Removes keyword fields (sweetness_keyword, acidity_keyword, etc.) from final output.
|
||||||
|
- Only includes values that are numbers or non-"N/A" strings.
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
- "French dry white wines with medium bod" — the LLM does not recognize sweetness. Use LLM self-questioning to solve.
|
||||||
|
- French Syrah, Viognier, under 100 — LLM extracts intensity of 3-5. Investigate why.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> YiemAgent.extractWineAttributes_2(agent, "medium-bodied, low acidity, medium tannin")
|
||||||
|
"acidity_min:1, acidity_max:2, tannin_min:3, tannin_max:4, intensity_min:3, intensity_max:4"
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
|
function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString}
|
||||||
|
|
||||||
@@ -1423,12 +1646,67 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Get a simplified DDL schema for a PostgreSQL table with sample values.
|
||||||
|
|
||||||
|
Establishes a new connection and delegates to the connection-based overload.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `pg_conn_str::String`: PostgreSQL connection string
|
||||||
|
- `table_name::String`: Name of the table to get schema for
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `schema_name::String="public"`: PostgreSQL schema name
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `String`: Formatted DDL schema string with sample values
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Delegates to `get_db_table_schema_simple_with_samples(conn, table_name, ...)` after creating
|
||||||
|
a new `LibPQ.Connection`.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> schema = YiemAgent.get_db_table_schema_simple_with_samples(conn_str, "wine")
|
||||||
|
"CREATE TABLE public.wine (\n wine_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n ...\n);"
|
||||||
|
```
|
||||||
|
"""
|
||||||
function get_db_table_schema_simple_with_samples(pg_conn_str::String, table_name::String;
|
function get_db_table_schema_simple_with_samples(pg_conn_str::String, table_name::String;
|
||||||
schema_name::String="public")::String
|
schema_name::String="public")::String
|
||||||
conn = LibPQ.Connection(pg_conn_str)
|
conn = LibPQ.Connection(pg_conn_str)
|
||||||
return get_db_table_schema_simple_with_samples(conn, table_name; schema_name=schema_name)
|
return get_db_table_schema_simple_with_samples(conn, table_name; schema_name=schema_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Get a simplified DDL schema for a PostgreSQL table with sample values.
|
||||||
|
|
||||||
|
Queries the PostgreSQL catalog for column metadata, fetches sample values, and builds
|
||||||
|
a DDL string with inline comments for each column.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `conn`: An active PostgreSQL connection (e.g., `LibPQ.Connection`)
|
||||||
|
- `table_name::String`: Name of the table to get schema for
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `schema_name::String="public"`: PostgreSQL schema name
|
||||||
|
- `sample_count::Int=3`: Number of non-null sample values to fetch per column
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `String`: Formatted DDL schema string in the style of `CREATE TABLE` statements
|
||||||
|
with sample values as inline comments
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Queries `pg_attribute`, `pg_class`, `pg_namespace`, `pg_attrdef`, and `pg_constraint`
|
||||||
|
for column metadata, defaults, and constraints.
|
||||||
|
- Fetches up to `sample_count` non-null values per column via `json_agg`.
|
||||||
|
- Throws an error if the table is not found.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> schema = YiemAgent.get_db_table_schema_simple_with_samples(conn, "wine")
|
||||||
|
"CREATE TABLE public.wine (\n wine_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n ...\n);"
|
||||||
|
```
|
||||||
|
"""
|
||||||
function get_db_table_schema_simple_with_samples(conn, table_name::String; schema_name::String="public", sample_count::Int=3)::String
|
function get_db_table_schema_simple_with_samples(conn, table_name::String; schema_name::String="public", sample_count::Int=3)::String
|
||||||
# 1. SQL query for catalog metadata
|
# 1. SQL query for catalog metadata
|
||||||
meta_sql = """
|
meta_sql = """
|
||||||
|
|||||||
+38
-10
@@ -194,6 +194,21 @@ end
|
|||||||
# Tool types
|
# Tool types
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
A tool available to the agent.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `name::String`: Tool identifier
|
||||||
|
- `label::String`: Human-readable tool name
|
||||||
|
- `description::String`: What the tool does
|
||||||
|
- `parameters::TParameters`: Tool parameters schema (JSON schema)
|
||||||
|
- `execute::Function`: Tool execution function
|
||||||
|
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
|
||||||
|
- `executionMode::Union{toolExecutionMode, Nothing}`: Override: run tool calls sequentially or in parallel
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A new `agentTool` instance
|
||||||
|
"""
|
||||||
struct agentTool{TParameters, TDetails} # A tool available to the agent
|
struct agentTool{TParameters, TDetails} # A tool available to the agent
|
||||||
name::String # Tool identifier
|
name::String # Tool identifier
|
||||||
label::String # Human-readable tool name
|
label::String # Human-readable tool name
|
||||||
@@ -209,6 +224,17 @@ end
|
|||||||
# Agent context
|
# Agent context
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
Snapshot of the agent's conversation context.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `systemPrompt::String`: System prompt for the agent
|
||||||
|
- `messages::Vector{agentMessage}`: Conversation messages
|
||||||
|
- `tools::Union{Vector{agentTool}, Nothing}`: Available tools
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A new `agentContext` instance
|
||||||
|
"""
|
||||||
struct agentContext # Snapshot of the agent's conversation context
|
struct agentContext # Snapshot of the agent's conversation context
|
||||||
systemPrompt::String # System prompt for the agent
|
systemPrompt::String # System prompt for the agent
|
||||||
messages::Vector{agentMessage} # Conversation messages
|
messages::Vector{agentMessage} # Conversation messages
|
||||||
@@ -266,9 +292,6 @@ function agentState(
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Tool call types
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
struct toolCall # A tool invocation from the LLM
|
struct toolCall # A tool invocation from the LLM
|
||||||
type::String # Always "function"
|
type::String # Always "function"
|
||||||
@@ -278,20 +301,25 @@ struct toolCall # A tool invocation from the LLM
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
"""
|
||||||
# Next turn context
|
Context for preparing the next conversation turn.
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
struct nextTurnContext # Context for preparing the next conversation turn
|
# Arguments
|
||||||
|
- `message::assistantMessage`: The assistant's message that just completed
|
||||||
|
- `toolResults::Vector{toolResultMessage}`: Tool results from this turn
|
||||||
|
- `context::agentContext`: Current conversation context
|
||||||
|
- `newMessages::Vector{agentMessage}`: Messages to append to the context
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A new `prepareNextTurnContext` instance
|
||||||
|
"""
|
||||||
|
struct prepareNextTurnContext # Context for preparing the next conversation turn
|
||||||
message::assistantMessage # The assistant's message that just completed
|
message::assistantMessage # The assistant's message that just completed
|
||||||
toolResults::Vector{toolResultMessage} # Tool results from this turn
|
toolResults::Vector{toolResultMessage} # Tool results from this turn
|
||||||
context::agentContext # Current conversation context
|
context::agentContext # Current conversation context
|
||||||
newMessages::Vector{agentMessage} # Messages to append to the context
|
newMessages::Vector{agentMessage} # Messages to append to the context
|
||||||
end
|
end
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# llmModel types
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
struct modelCost # Model pricing per 1M tokens
|
struct modelCost # Model pricing per 1M tokens
|
||||||
input::Float64 # Price per 1M input tokens
|
input::Float64 # Price per 1M input tokens
|
||||||
|
|||||||
+154
-106
@@ -10,45 +10,24 @@ using ..type
|
|||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
""" Clear agent chat history.
|
"""
|
||||||
|
Clear agent chat history.
|
||||||
|
|
||||||
|
Empties the conversation history, short-term memory, events log, and chatbox.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `a::agent`
|
- `a::T`: An agent instance (subtype of `agent`)
|
||||||
an agent
|
|
||||||
|
|
||||||
# Return
|
# Returns
|
||||||
- nothing
|
- `nothing`
|
||||||
|
|
||||||
# Example
|
# Notes
|
||||||
|
- Does not clear long-term memory; use `[PENDING] clear memory` when implemented.
|
||||||
|
|
||||||
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> using YiemAgent, MQTTClient, GeneralUtils
|
julia> YiemAgent.clearhistory(agent)
|
||||||
julia> client, connection = MakeConnection("test.mosquitto.org", 1883)
|
|
||||||
julia> connect(client, connection)
|
|
||||||
julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic")
|
|
||||||
julia> agentConfig = Dict(
|
|
||||||
"receiveprompt"=>Dict(
|
|
||||||
"mqtttopic"=> "testtopic/receive",
|
|
||||||
),
|
|
||||||
"receiveinternal"=>Dict(
|
|
||||||
"mqtttopic"=> "testtopic/internal",
|
|
||||||
),
|
|
||||||
"text2text"=>Dict(
|
|
||||||
"mqtttopic"=> "testtopic/text2text",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
julia> a = YiemAgent.sommelier(
|
|
||||||
client,
|
|
||||||
msgMeta,
|
|
||||||
agentConfig,
|
|
||||||
)
|
|
||||||
julia> YiemAgent.addNewMessage(a, "user", "hello")
|
|
||||||
julia> YiemAgent.clearhistory(a)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# TODO
|
|
||||||
- [PENDING] clear memory
|
|
||||||
|
|
||||||
# Signature
|
|
||||||
"""
|
"""
|
||||||
function clearhistory(a::T) where {T<:agent}
|
function clearhistory(a::T) where {T<:agent}
|
||||||
empty!(a.chathistory)
|
empty!(a.chathistory)
|
||||||
@@ -58,40 +37,29 @@ function clearhistory(a::T) where {T<:agent}
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
""" Add new message to agent.
|
"""
|
||||||
|
Add a new message to the agent's conversation history.
|
||||||
|
|
||||||
messages => Dict(
|
Automatically summarizes the oldest messages if the history exceeds `maximumMsg`.
|
||||||
"role" => "user",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "Describe this image for me"),
|
|
||||||
Dict(
|
|
||||||
"type" => "image_url",
|
|
||||||
"image_url" => Dict("url" => data_uri)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
Arguments\n
|
# Arguments
|
||||||
-----
|
- `a::T1`: An agent instance (subtype of `agent`)
|
||||||
a::agent
|
- `name::String`: Message sender role (e.g. "system", "user", "assistant")
|
||||||
an agent
|
- `userinput::T2`: Message dictionary to append (must contain "name" and "text" keys)
|
||||||
role::String
|
|
||||||
message sender role i.e. system, user or assistant
|
|
||||||
text::String
|
|
||||||
message text
|
|
||||||
|
|
||||||
Return\n
|
# Keyword Arguments
|
||||||
-----
|
- `maximumMsg::Integer=30`: Maximum number of messages before summarization kicks in
|
||||||
nothing
|
|
||||||
|
|
||||||
Example\n
|
# Returns
|
||||||
-----
|
- `nothing`
|
||||||
```jldoctest
|
|
||||||
|
|
||||||
```
|
# Notes
|
||||||
|
- When history length exceeds `maximumMsg`, the oldest messages are summarized automatically.
|
||||||
|
|
||||||
Signature\n
|
# Examples
|
||||||
-----
|
```jldoctest
|
||||||
|
julia> YiemAgent.addNewMessage(agent, "user", Dict("name" => "user", "text" => "hello"))
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function addNewMessage(a::T1, name::String, userinput::T2;
|
function addNewMessage(a::T1, name::String, userinput::T2;
|
||||||
maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict}
|
maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict}
|
||||||
@@ -168,6 +136,23 @@ function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert a vector of wine dictionaries to a formatted text string.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `vecd::Vector`: A vector of dictionaries, each representing a wine with key-value pairs
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A formatted string where each wine is numbered and each key-value pair is comma-separated
|
||||||
|
in the format: `"1) key1:value1,key2:value2 key3:value3 2) ..."`
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> vecd = [Dict("wine_name" => "Chateau A", "price" => "50")]
|
||||||
|
julia> YiemAgent.availableWineToText(vecd)
|
||||||
|
"1) wine_name:Chateau A,price:50 "
|
||||||
|
```
|
||||||
|
"""
|
||||||
function availableWineToText(vecd::Vector)::String
|
function availableWineToText(vecd::Vector)::String
|
||||||
# Initialize an empty string to hold the final text
|
# Initialize an empty string to hold the final text
|
||||||
rowtext = ""
|
rowtext = ""
|
||||||
@@ -189,34 +174,30 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
""" Create a dictionary representing an event with optional details.
|
"""
|
||||||
|
Create a dictionary representing an event with optional details.
|
||||||
|
|
||||||
# Arguments
|
# Keyword Arguments
|
||||||
- `event_description::Union{String, Nothing}`
|
- `event_description::Union{String, Nothing}`: A description of the event
|
||||||
A description of the event
|
- `timestamp::Union{DateTime, Nothing}`: The time when the event occurred
|
||||||
- `timestamp::Union{DateTime, Nothing}`
|
- `subject::Union{String, Nothing}`: The subject or entity associated with the event
|
||||||
The time when the event occurred
|
- `thought::Union{AbstractDict, Nothing}`: Any associated thoughts or metadata
|
||||||
- `subject::Union{String, Nothing}`
|
- `action_name::Union{String, Nothing}`: The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
|
||||||
The subject or entity associated with the event
|
- `action_input::Union{String, Nothing}`: Input or parameters for the action
|
||||||
- `thought::Union{AbstractDict, Nothing}`
|
- `location::Union{String, Nothing}`: Where the event took place
|
||||||
Any associated thoughts or metadata
|
- `equipment_used::Union{String, Nothing}`: Equipment involved in the event
|
||||||
- `action_name::Union{String, Nothing}`
|
- `material_used::Union{String, Nothing}`: Materials used during the event
|
||||||
The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
|
- `observation::Union{String, Nothing}`: Observation of the event
|
||||||
- `action_input::Union{String, Nothing}`
|
- `note::Union{String, Nothing}`: Additional notes or comments
|
||||||
Input or parameters for the action
|
|
||||||
- `location::Union{String, Nothing}`
|
|
||||||
Where the event took place
|
|
||||||
- `equipment_used::Union{String, Nothing}`
|
|
||||||
Equipment involved in the event
|
|
||||||
- `material_used::Union{String, Nothing}`
|
|
||||||
Materials used during the event
|
|
||||||
- `outcome::Union{String, Nothing}`
|
|
||||||
The result or consequence of the event after action execution
|
|
||||||
- `note::Union{String, Nothing}`
|
|
||||||
Additional notes or comments
|
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
A dictionary with event details as symbol-keyed key-value pairs
|
- A `Dict{String, Any}` with event details as string-keyed key-value pairs
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> YiemAgent.eventdict(action_name="CHAT", action_input="hello")
|
||||||
|
Dict{String, Any} with 11 entries: ...
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function eventdict(;
|
function eventdict(;
|
||||||
event_description::Union{String, Nothing}=nothing,
|
event_description::Union{String, Nothing}=nothing,
|
||||||
@@ -250,31 +231,31 @@ function eventdict(;
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
""" Create a formatted timeline string from a sequence of events.
|
"""
|
||||||
|
Create a formatted timeline string from a sequence of events.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `events::T1`
|
- `events::T1`: Vector of event dictionaries. Each must have `action_name` and `action_input` keys,
|
||||||
Vector of event dictionaries containing subject, action_input and optional outcome fields
|
and optionally `subject` and `observation` keys.
|
||||||
Each event dictionary should have the following keys:
|
|
||||||
- :subject - The subject or entity performing the action
|
# Keyword Arguments
|
||||||
- :action_input - The action or input performed by the subject
|
- `eventindex::Union{UnitRange, Nothing}=nothing`: Optional range of event indices to include.
|
||||||
- :observation - (Optional) The result or outcome of the action
|
If `nothing`, all events are included.
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `timeline::String`
|
- `timeline::String`: A formatted string where each event appears on its own line in the format:
|
||||||
A formatted string representing the events with their subjects, actions, and optional outcomes
|
`"Event_{index} {subject}> action_name: {action_name}, action_input: {action_input}"`
|
||||||
Format: "{index}) {subject}> {action_input} {outcome}\n" for each event
|
If `observation` is present, it is appended.
|
||||||
|
|
||||||
# Example
|
|
||||||
|
|
||||||
events = [
|
|
||||||
Dict("subject" => "User", "action_input" => "Hello", "observation" => nothing),
|
|
||||||
Dict("subject" => "Assistant", "action_input" => "Hi there!", "observation" => "with a smile")
|
|
||||||
]
|
|
||||||
timeline = createTimeline(events)
|
|
||||||
# 1) User> Hello
|
|
||||||
# 2) Assistant> Hi there! with a smile
|
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> events = [
|
||||||
|
Dict("subject" => "User", "action_input" => "Hello", "action_name" => "CHAT", "observation" => nothing),
|
||||||
|
Dict("subject" => "Assistant", "action_input" => "Hi there!", "action_name" => "CHAT", "observation" => "with a smile")
|
||||||
|
];
|
||||||
|
julia> YiemAgent.createTimeline(events)
|
||||||
|
"Event_1 User> action_name: CHAT, action_input: Hello\\nEvent_2 Assistant> action_name: CHAT, action_input: Hi there!\\n"
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
|
function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
|
||||||
) where {T1<:AbstractVector}
|
) where {T1<:AbstractVector}
|
||||||
@@ -308,6 +289,29 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin
|
|||||||
return timeline
|
return timeline
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create a formatted event log from a sequence of events.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `events::T1`: Vector of event dictionaries. Each must have `subject`, `action_name`, `action_input`,
|
||||||
|
and optionally `observation` keys.
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `index::Union{UnitRange, Nothing}=nothing`: Optional range of event indices to include.
|
||||||
|
If `nothing`, all events are included.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A `Vector{Dict{String, String}}` where each dictionary has `"name"` (from event subject) and
|
||||||
|
`"text"` (formatted action description) keys.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> events = [Dict("subject" => "User", "action_name" => "CHAT", "action_input" => "hello", "observation" => nothing)];
|
||||||
|
julia> log = YiemAgent.createEventsLog(events);
|
||||||
|
julia> log[1]["name"]
|
||||||
|
"User"
|
||||||
|
```
|
||||||
|
"""
|
||||||
function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
|
function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
|
||||||
) where {T1<:AbstractVector}
|
) where {T1<:AbstractVector}
|
||||||
# Initialize empty log array
|
# Initialize empty log array
|
||||||
@@ -347,6 +351,27 @@ function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create a formatted chat log from a sequence of chat entries.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `chatdict::T1`: Vector of chat entry dictionaries. Each must have `"name"` and `"text"` keys.
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `index::Union{UnitRange, Nothing}=nothing`: Optional range of entry indices to include.
|
||||||
|
If `nothing`, all entries are included.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A `Vector{Dict{String, String}}` where each dictionary has `"name"` and `"text"` keys
|
||||||
|
copied from the corresponding input entry.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> chats = [Dict("name" => "user", "text" => "hello"), Dict("name" => "assistant", "text" => "hi")];
|
||||||
|
julia> YiemAgent.createChatLog(chats)[1]["name"]
|
||||||
|
"user"
|
||||||
|
```
|
||||||
|
"""
|
||||||
function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
|
function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
|
||||||
) where {T1<:AbstractVector}
|
) where {T1<:AbstractVector}
|
||||||
# Initialize empty log array
|
# Initialize empty log array
|
||||||
@@ -373,6 +398,29 @@ function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if an agent's text response contains all required header keywords.
|
||||||
|
|
||||||
|
Validates that the response includes all required keywords without duplications.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `response::String`: The agent's text response to validate
|
||||||
|
- `requiredHeader::T`: Array of required keyword strings (subtype of `Array{String}`)
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `Tuple{Bool, Union{String, Nothing}}`: A two-element tuple where:
|
||||||
|
- First element: `true` if all required keywords are present and not duplicated, `false` otherwise
|
||||||
|
- Second element: An error description string if validation failed, or `nothing` if passed
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
- Uses `GeneralUtils.detectKeywordVariation` for flexible keyword matching.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```jldoctest
|
||||||
|
julia> ispass, err = YiemAgent.checkAgentResponse_text("hello world", ["hello"])
|
||||||
|
(true, nothing)
|
||||||
|
```
|
||||||
|
"""
|
||||||
function checkAgentResponse_text(response::String, requiredHeader::T
|
function checkAgentResponse_text(response::String, requiredHeader::T
|
||||||
)::Tuple where {T<:Array{String}}
|
)::Tuple where {T<:Array{String}}
|
||||||
detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response)
|
detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response)
|
||||||
|
|||||||
Reference in New Issue
Block a user