V0.8.0 async think loop #40

Merged
ton merged 50 commits from v0.8.0-async_think_loop into v0.8.0 2026-08-08 04:13:05 +00:00
7 changed files with 806 additions and 324 deletions
Showing only changes of commit 569f85333f - Show all commits
+2 -2
View File
@@ -16,8 +16,8 @@ module YiemAgent
include("llmfunction.jl")
using .llmfunction
include("core.jl")
using .core
include("agentCore.jl")
using .agentCore
include("api.jl")
using .api
+203
View File
@@ -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
View File
@@ -13,7 +13,26 @@ using ..type, ..util, ..llmfunction
"""
Send a message to the agent's input channel.
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)
put!(agent.input_ch, msg)
@@ -22,7 +41,23 @@ end
"""
Take a response from the agent's output channel.
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)
return take!(agent.output_ch)
@@ -30,8 +65,27 @@ end
"""
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.
# 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)
put!(agent.followUpQueue, msg)
@@ -40,7 +94,25 @@ end
"""
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)
put!(agent.input_ch, :shutdown)
-147
View File
@@ -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
+334 -56
View File
@@ -12,27 +12,29 @@ using ..type, ..util
# ---------------------------------------------- 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
- `a::T1`
one of Yiem's agent
- `input::T2`
text to be send to virtual wine customer
# Return
- `response::String`
response of virtual wine customer
# Example
```jldoctest
julia>
```
- `a::T1`: An agent instance (subtype of `agent`) with `config` containing `externalservice`
and `mqttServerInfo` keys
- `input::T2`: Text to send to the virtual wine customer LLM
# Returns
- `Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}}`: A tuple of
`(response_text, select, reward, isterminal)` where `select` may be `Nothing`
# Notes
- 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
- [] update docstring
- [] add reccommend() to compare wine
# Signature
- Add `recommend()` to compare wines
"""
function virtualWineUserRecommendbox(a::T1, input
)::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
- `a::T1`
one of Yiem's agent
- `input::T2`
text to be send to virtual wine customer
# Return
- `response::String`
response of virtual wine customer
# Example
- `config::T1`: Configuration dictionary (subtype of `AbstractDict`) containing:
- `externalservice["text2text"]["mqtttopic"]`: MQTT topic for the LLM service
- `mqttServerInfo["broker"]`: MQTT broker address
- `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
# Returns
- `Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}}`: A tuple of
`(text, select, reward, isterminal)` representing the virtual customer's response
# 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
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
)::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")
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
- `a::T1`
one of ChatAgent's agent.
- `thoughtdict::AbstractDict`
# Return
A JSON string of available wine
- `a::T`: An agent instance (subtype of `agent`) with context containing `executeSQL`,
`pg_conn_str`, and `agentconfig`
- `thoughtdict::AbstractDict`: A dictionary containing `action_input` (the search query string)
# 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
julia> using ChatAgent
julia> agent = YiemAgent.sommelier(...)
julia> thoughtdict =
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")
julia> thoughtdict = OrderedDict("action_input" => "red wine under 50");
julia> result = YiemAgent.search_wine_database!(agent, thoughtdict)
(thoughtdict=OrderedDict{String, Any}(...), result_raw=[Dict(...), ...])
```
"""
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
"""
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,
; maxattempt=10
)::String where {T<:agent}
@@ -655,6 +711,40 @@ julia> thoughtdict =
```
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,
; maxattempt=10
) 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)
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
# 1. Base SQL structure
base_query =
@@ -934,6 +1053,36 @@ JOIN retailer AS r ON rw.retailer_id = r.retailer_id
return string(base_query, where_sql, ";")
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
)::NamedTuple where {T<:AbstractString}
@@ -984,6 +1133,26 @@ function SQLexecution(executeSQL::Function, sql::T
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
)::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
"""
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
- `v::Integer`
dummy variable
# Return
- `a::T1`: An agent instance (subtype of `agent`) with context containing:
- `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`)
# Example
# Keyword Arguments
- `maxattempt::Int=10`: Maximum number of attempts to get a valid response from the LLM
# 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
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
@@ -1225,8 +1416,40 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
end
"""
- TODO "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve.
- TODO French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why?
Extract wine intensity, sweetness, tannin, and acidity attributes from a query.
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}
@@ -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;
schema_name::String="public")::String
conn = LibPQ.Connection(pg_conn_str)
return get_db_table_schema_simple_with_samples(conn, table_name; schema_name=schema_name)
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
# 1. SQL query for catalog metadata
meta_sql = """
+38 -10
View File
@@ -194,6 +194,21 @@ end
# 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
name::String # Tool identifier
label::String # Human-readable tool name
@@ -209,6 +224,17 @@ end
# 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
systemPrompt::String # System prompt for the agent
messages::Vector{agentMessage} # Conversation messages
@@ -266,9 +292,6 @@ function agentState(
)
end
# ============================================================================
# Tool call types
# ============================================================================
struct toolCall # A tool invocation from the LLM
type::String # Always "function"
@@ -278,20 +301,25 @@ struct toolCall # A tool invocation from the LLM
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
toolResults::Vector{toolResultMessage} # Tool results from this turn
context::agentContext # Current conversation context
newMessages::Vector{agentMessage} # Messages to append to the context
end
# ============================================================================
# llmModel types
# ============================================================================
struct modelCost # Model pricing per 1M tokens
input::Float64 # Price per 1M input tokens
+155 -107
View File
@@ -10,47 +10,26 @@ using ..type
# ---------------------------------------------- 100 --------------------------------------------- #
""" Clear agent chat history.
"""
Clear agent chat history.
Empties the conversation history, short-term memory, events log, and chatbox.
# Arguments
- `a::agent`
an agent
- `a::T`: An agent instance (subtype of `agent`)
# Return
- nothing
# Returns
- `nothing`
# Example
# Notes
- Does not clear long-term memory; use `[PENDING] clear memory` when implemented.
# Examples
```jldoctest
julia> using YiemAgent, MQTTClient, GeneralUtils
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)
julia> YiemAgent.clearhistory(agent)
```
# TODO
- [PENDING] clear memory
# Signature
"""
function clearhistory(a::T) where {T<:agent}
function clearhistory(a::T) where {T<:agent}
empty!(a.chathistory)
empty!(a.memory["shortmem"])
empty!(a.memory["events"])
@@ -58,40 +37,29 @@ function clearhistory(a::T) where {T<:agent}
end
""" Add new message to agent.
"""
Add a new message to the agent's conversation history.
messages => Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Describe this image for me"),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data_uri)
)
]
)
Automatically summarizes the oldest messages if the history exceeds `maximumMsg`.
Arguments\n
-----
a::agent
an agent
role::String
message sender role i.e. system, user or assistant
text::String
message text
# Arguments
- `a::T1`: An agent instance (subtype of `agent`)
- `name::String`: Message sender role (e.g. "system", "user", "assistant")
- `userinput::T2`: Message dictionary to append (must contain "name" and "text" keys)
Return\n
-----
nothing
# Keyword Arguments
- `maximumMsg::Integer=30`: Maximum number of messages before summarization kicks in
Example\n
-----
```jldoctest
# Returns
- `nothing`
```
# 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;
maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict}
@@ -168,6 +136,23 @@ function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String
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
# Initialize an empty string to hold the final text
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
- `event_description::Union{String, Nothing}`
A description of the event
- `timestamp::Union{DateTime, Nothing}`
The time when the event occurred
- `subject::Union{String, Nothing}`
The subject or entity associated with the event
- `thought::Union{AbstractDict, Nothing}`
Any associated thoughts or metadata
- `action_name::Union{String, Nothing}`
The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
- `action_input::Union{String, Nothing}`
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
# Keyword Arguments
- `event_description::Union{String, Nothing}`: A description of the event
- `timestamp::Union{DateTime, Nothing}`: The time when the event occurred
- `subject::Union{String, Nothing}`: The subject or entity associated with the event
- `thought::Union{AbstractDict, Nothing}`: Any associated thoughts or metadata
- `action_name::Union{String, Nothing}`: The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
- `action_input::Union{String, Nothing}`: 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
- `observation::Union{String, Nothing}`: Observation of the event
- `note::Union{String, Nothing}`: Additional notes or comments
# 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(;
event_description::Union{String, Nothing}=nothing,
@@ -250,31 +231,31 @@ function eventdict(;
end
""" Create a formatted timeline string from a sequence of events.
"""
Create a formatted timeline string from a sequence of events.
# Arguments
- `events::T1`
Vector of event dictionaries containing subject, action_input and optional outcome fields
Each event dictionary should have the following keys:
- :subject - The subject or entity performing the action
- :action_input - The action or input performed by the subject
- :observation - (Optional) The result or outcome of the action
- `events::T1`: Vector of event dictionaries. Each must have `action_name` and `action_input` keys,
and optionally `subject` and `observation` keys.
# Keyword Arguments
- `eventindex::Union{UnitRange, Nothing}=nothing`: Optional range of event indices to include.
If `nothing`, all events are included.
# Returns
- `timeline::String`
A formatted string representing the events with their subjects, actions, and optional outcomes
Format: "{index}) {subject}> {action_input} {outcome}\n" for each event
# 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
- `timeline::String`: A formatted string where each event appears on its own line in the format:
`"Event_{index} {subject}> action_name: {action_name}, action_input: {action_input}"`
If `observation` is present, it is appended.
# 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
) where {T1<:AbstractVector}
@@ -308,6 +289,29 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin
return timeline
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
) where {T1<:AbstractVector}
# Initialize empty log array
@@ -347,6 +351,27 @@ function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
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
) where {T1<:AbstractVector}
# Initialize empty log array
@@ -373,6 +398,29 @@ function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
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
)::Tuple where {T<:Array{String}}
detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response)