From 368307742efef0946e94f130490a8d3ef83af5e4 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 1 Aug 2026 17:56:16 +0700 Subject: [PATCH 01/50] update --- src/type.jl | 564 ++++++++++++++++-------------------------------- src/type_OLD.jl | 375 ++++++++++++++++++++++++++++++++ 2 files changed, 564 insertions(+), 375 deletions(-) create mode 100644 src/type_OLD.jl diff --git a/src/type.jl b/src/type.jl index d554f2a..e04c124 100644 --- a/src/type.jl +++ b/src/type.jl @@ -1,375 +1,189 @@ -module type - -export agent, sommelier, companion, virtualcustomer, agentcontext - -using Dates, UUIDs, DataStructures, JSON, NATS -using GeneralUtils - -# ---------------------------------------------- 100 --------------------------------------------- # - - -mutable struct agentcontext - text2textInstructLLM::Function - getTextEmbedding::Function - executeSQL::Function - similarSQLVectorDB::Function - insertSQLVectorDB::Function - similarSommelierDecision::Function - insertSommelierDecision::Function - find_related_tables_for_user_question::Function - pg_conn_str::String - agentconfig::AbstractDict -end - -abstract type agent end - -mutable struct sommelier <: agent - name::String # agent name - id::String # agent id - retailername::String - retailerid::String - tools::Dict - maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{String, Any}} - memory::Dict{String, Any} - context::agentcontext - llmFormatName::String -end - -""" A sommelier agent. - -# Arguments - - `context::agentcontext` - Application context containing shared functions for LLM, SQL, and vector database operations. - -# Keyword Arguments - - `name::String` - Agent's name. Default: `"Assistant"` - - `id::String` - Agent's ID. Default: generated UUID string. - - `retailername::String` - Retailer name associated with the sommelier. Default: `"retailer_name"` - - `maxHistoryMsg::Integer` - Maximum history messages. Default: `20` - - `chathistory::Vector{Dict{String, String}}` - Chat history. Default: empty vector. - - `llmFormatName::String` - LLM format name. Default: `"granite3"` - -# Return - - `sommelier`: An instantiated sommelier agent. - -# Example -```julia -julia> using YiemAgent -julia> context = agentcontext( - text2textInstructLLM, - getTextEmbedding, - executeSQL, - similarSQLVectorDB, - insertSQLVectorDB, - similarSommelierDecision, - insertSommelierDecision - ) -julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop") -``` -""" -function sommelier( - context::agentcontext, # agent functions, db connect and other context - ; - name::String= "Assistant", - id::String= string(uuid4()), - retailername::String= "not specified", - retailerid::String= "not specified", - maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(), - llmFormatName::String= "granite3" - ) - - tools = Dict( # update input format - "chatbox"=> Dict( - "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - "output" => "" , - ), - "winestock"=> Dict( - "description" => "A handy tool for searching wine in your inventory that match the user preferences.", - "input" => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", - "output" => """Output are wines that match the search query in JSON format.""", - ), - ) - - """ Memory - - Chat history use openai format as follow: - - image1_path = "test/large_image.png" --- - image1_bytes = read(image1_path) | this part must be done - image1_base64_string = base64encode(image1_bytes) | in frontend - mime_type = "image/png" | not in agent code - data1_uri = "data:;base64," --- - - chathistory= [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => "You are a helpful assistant"), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => " - LLM context here... - - Do you know this wine? Just give me brief intro." - ), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ), - ] - ), - ] - - shortmem = Dict( - "1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), - "2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), - ... - ) - """ - memory = Dict{String, Any}( - "shortmem"=> OrderedDict{String, Any}(), - "scratchpad"=> "", - "recap"=> OrderedDict{String, Any}(), - ) - - newAgent = sommelier( - name, - id, - retailername, - retailerid, - tools, - maxHistoryMsg, - chathistory, - memory, - context, - llmFormatName - ) - systemmsg = - """ - # store_policy - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock. - - You can only recommend wines that are currently in our inventory - - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. - - Ask the user one question at a time. - - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. - - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. - - Spicy foods should be paired only with light red wines. - - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. - - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - - # store_guidelines - - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. - - Customer may provide images for you to look up. - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. - - User usually ask for something similar. This means you should use the search term based on the profile they like. - - # situation - You are having conversation with a customer. - - # your role - Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. - - # objective - - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. - - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. - - # your responsibility includes - - According to the store's policy and guidelines, and make an informed decision about what available_actions you need to use to achieve the objective. - - Keep the conversation with the customer going smoothly - - # your responsibility does NOT includes - - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - # you should then respond to the user with interleaving plan, action_name, action_input in JSON format - 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name - 3) "action_input", The input to the action you are about to perform according to your plan. - After the action is executed you gets "action_result". It is the output from the action you selected. - - # available actions - "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. - "SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD." - Example query 2: "Red or white wine, medium tannin, price under 700 USD" - Example query 3: "white wine from Tuscany, Italy or Bordeaux, France - "WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. - "END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. - """ - - system_msg = Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ) - - push!(newAgent.chathistory, system_msg) - - return newAgent -end - - -mutable struct virtualcustomer <: agent - name::String # agent name - id::String # agent id - systemmsg::String # system message - tools::Dict - maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{String, Any}} - memory::Dict{String, Any} - context # NamedTuple of functions - llmFormatName::String -end - -function virtualcustomer( - context, # NamedTuple of functions - ; - name::String= "Assistant", - id::String= string(uuid4()), - maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(), - llmFormatName::String= "granite3", - systemmsg::String= - """ - Your name: $name - Your sex: Female - Your role: You are a helpful assistant. - You should follow the following guidelines: - - Focus on the latest conversation. - - Your like to be short and concise. - - Let's begin! - """, - ) - - tools = Dict( # update input format - "chatbox"=> Dict( - "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - "output" => "" , - ), - ) - - """ Memory - Ref: Chat prompt format is openai - chathistory = [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => system_msg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ) - ] - ) - ] - """ - memory = Dict{String, Any}( - "shortmem"=> OrderedDict{String, Any}( - ), - "scratchpad"=> "", - "events"=> Vector{Dict{String, Any}}(), - "state"=> Dict{String, Any}( - ), - "recap"=> OrderedDict{String, Any}(), - ) - - newAgent = virtualcustomer( - name, - id, - systemmsg, - tools, - maxHistoryMsg, - chathistory, - memory, - context, - llmFormatName - ) - - return newAgent -end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module type \ No newline at end of file +module type + export agent, sommelier, companion, virtualcustomer, agentContext + + +using Dates, UUIDs, DataStructures, JSON, NATS +using GeneralUtils + +# ---------------------------------------------- 100 --------------------------------------------- # + + +# ============================================================================ +# Message types +# ============================================================================ +abstract type agentMessage end + +struct userMessage <: agentMessage + role::String + content::Vector{messageContent} + timestamp::Timestamp +end + +struct assistantMessage <: agentMessage + role::String + content::Vector{messageContent} + api::String + provider::String + model::String + usage::Usage + stop_reason::String + error_message::Union{String, Nothing} + timestamp::Timestamp +end + +struct toolResultMessage <: agentMessage + + role::String + tool_call_id::String + tool_name::String + content::Vector{messageContent} + details::Any + usage::Union{Usage, Nothing} + added_tool_names::Union{Vector{String}, Nothing} + is_error::Bool + timestamp::Timestamp +end + + +# ============================================================================ +# Message content types +# ============================================================================ + +abstract type messageContent end + +struct textContent <: messageContent + text::String +end + +struct imageContent <: messageContent + data::String + mime_type::String +end + + +# ============================================================================ +# Tool types +# ============================================================================ + +struct agentTool{TParameters, TDetails} + name::String + label::String + description::String + parameters::TParameters + execute::Function + prepare_arguments::Union{Function, Nothing} + execution_mode::Union{ToolExecutionMode, Nothing} +end + + +# ============================================================================ +# Agent context +# ============================================================================ + +struct agentContext + system_prompt::String + messages::Vector{agentMessage} + tools::Union{Vector{agentTool}, Nothing} +end + + + +# ============================================================================ +# Assistant message event types +# ============================================================================ + +abstract type assistantMessageEvent end + +struct startEvent <: assistantMessageEvent + partial::assistantMessage +end +struct textStartEvent <: assistantMessageEvent + content_index::Int64 + partial::assistantMessage +end +struct textDeltaEvent <: assistantMessageEvent + content_index::Int64 + delta::String + partial::assistantMessage +end +struct textEndEvent <: assistantMessageEvent + content_index::Int64 + content::String + partial::assistantMessage +end +struct doneEvent <: assistantMessageEvent + reason::String + usage::Usage + message::assistantMessage +end +struct errorEvent <: assistantMessageEvent + reason::String + error_message::Union{String, Nothing} + usage::Usage + error::assistantMessage +end + + + +# ============================================================================ +# Agent state +# ============================================================================ + +mutable struct agentState + system_prompt::String + model::Model + thinking_level::ThinkingLevel + tools::Vector{agentTool} + messages::Vector{agentMessage} + is_streaming::Bool + streaming_message::Union{agentMessage, Nothing} + pending_tool_calls::Set{String} + error_message::Union{String, Nothing} + + function agentState( + system_prompt::String="", + model::Model=Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0), + thinking_level::ThinkingLevel=THINKING_OFF, + tools::Vector{agentTool}=agentTool[], + messages::Vector{agentMessage}=agentMessage[], + ) + new( + system_prompt, + model, + thinking_level, + copy(tools), + copy(messages), + false, + nothing, + Set{String}(), + nothing, + ) + end +end + +# ============================================================================ +# Tool call types +# ============================================================================ + +struct toolCall + type::String + id::String + name::String + arguments::Dict{String, Any} + partial_json::Union{String, Nothing} +end + + +# ============================================================================ +# Next turn context +# ============================================================================ + +struct nextTurnContext + message::assistantMessage + tool_results::Vector{toolResultMessage} + context::agentContext + new_messages::Vector{agentMessage} +end + + +end # module type diff --git a/src/type_OLD.jl b/src/type_OLD.jl new file mode 100644 index 0000000..d554f2a --- /dev/null +++ b/src/type_OLD.jl @@ -0,0 +1,375 @@ +module type + +export agent, sommelier, companion, virtualcustomer, agentcontext + +using Dates, UUIDs, DataStructures, JSON, NATS +using GeneralUtils + +# ---------------------------------------------- 100 --------------------------------------------- # + + +mutable struct agentcontext + text2textInstructLLM::Function + getTextEmbedding::Function + executeSQL::Function + similarSQLVectorDB::Function + insertSQLVectorDB::Function + similarSommelierDecision::Function + insertSommelierDecision::Function + find_related_tables_for_user_question::Function + pg_conn_str::String + agentconfig::AbstractDict +end + +abstract type agent end + +mutable struct sommelier <: agent + name::String # agent name + id::String # agent id + retailername::String + retailerid::String + tools::Dict + maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized + chathistory::Vector{Dict{String, Any}} + memory::Dict{String, Any} + context::agentcontext + llmFormatName::String +end + +""" A sommelier agent. + +# Arguments + - `context::agentcontext` + Application context containing shared functions for LLM, SQL, and vector database operations. + +# Keyword Arguments + - `name::String` + Agent's name. Default: `"Assistant"` + - `id::String` + Agent's ID. Default: generated UUID string. + - `retailername::String` + Retailer name associated with the sommelier. Default: `"retailer_name"` + - `maxHistoryMsg::Integer` + Maximum history messages. Default: `20` + - `chathistory::Vector{Dict{String, String}}` + Chat history. Default: empty vector. + - `llmFormatName::String` + LLM format name. Default: `"granite3"` + +# Return + - `sommelier`: An instantiated sommelier agent. + +# Example +```julia +julia> using YiemAgent +julia> context = agentcontext( + text2textInstructLLM, + getTextEmbedding, + executeSQL, + similarSQLVectorDB, + insertSQLVectorDB, + similarSommelierDecision, + insertSommelierDecision + ) +julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop") +``` +""" +function sommelier( + context::agentcontext, # agent functions, db connect and other context + ; + name::String= "Assistant", + id::String= string(uuid4()), + retailername::String= "not specified", + retailerid::String= "not specified", + maxHistoryMsg::Integer= 20, + chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(), + llmFormatName::String= "granite3" + ) + + tools = Dict( # update input format + "chatbox"=> Dict( + "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", + "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", + "output" => "" , + ), + "winestock"=> Dict( + "description" => "A handy tool for searching wine in your inventory that match the user preferences.", + "input" => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", + "output" => """Output are wines that match the search query in JSON format.""", + ), + ) + + """ Memory + + Chat history use openai format as follow: + + image1_path = "test/large_image.png" --- + image1_bytes = read(image1_path) | this part must be done + image1_base64_string = base64encode(image1_bytes) | in frontend + mime_type = "image/png" | not in agent code + data1_uri = "data:;base64," --- + + chathistory= [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => "You are a helpful assistant"), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => " + LLM context here... + + Do you know this wine? Just give me brief intro." + ), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ), + ] + ), + ] + + shortmem = Dict( + "1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), + "2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), + ... + ) + """ + memory = Dict{String, Any}( + "shortmem"=> OrderedDict{String, Any}(), + "scratchpad"=> "", + "recap"=> OrderedDict{String, Any}(), + ) + + newAgent = sommelier( + name, + id, + retailername, + retailerid, + tools, + maxHistoryMsg, + chathistory, + memory, + context, + llmFormatName + ) + systemmsg = + """ + # store_policy + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock. + - You can only recommend wines that are currently in our inventory + - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. + - Ask the user one question at a time. + - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. + - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. + - Spicy foods should be paired only with light red wines. + - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. + - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. + + # store_guidelines + - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. + - Customer may provide images for you to look up. + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. + - User usually ask for something similar. This means you should use the search term based on the profile they like. + + # situation + You are having conversation with a customer. + + # your role + Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. + + # objective + - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. + - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. + + # your responsibility includes + - According to the store's policy and guidelines, and make an informed decision about what available_actions you need to use to achieve the objective. + - Keep the conversation with the customer going smoothly + + # your responsibility does NOT includes + - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. + - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. + - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + + # you should then respond to the user with interleaving plan, action_name, action_input in JSON format + 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. + 2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name + 3) "action_input", The input to the action you are about to perform according to your plan. + After the action is executed you gets "action_result". It is the output from the action you selected. + + # available actions + "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. + "SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD." + Example query 2: "Red or white wine, medium tannin, price under 700 USD" + Example query 3: "white wine from Tuscany, Italy or Bordeaux, France + "WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + "END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + """ + + system_msg = Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ) + + push!(newAgent.chathistory, system_msg) + + return newAgent +end + + +mutable struct virtualcustomer <: agent + name::String # agent name + id::String # agent id + systemmsg::String # system message + tools::Dict + maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized + chathistory::Vector{Dict{String, Any}} + memory::Dict{String, Any} + context # NamedTuple of functions + llmFormatName::String +end + +function virtualcustomer( + context, # NamedTuple of functions + ; + name::String= "Assistant", + id::String= string(uuid4()), + maxHistoryMsg::Integer= 20, + chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(), + llmFormatName::String= "granite3", + systemmsg::String= + """ + Your name: $name + Your sex: Female + Your role: You are a helpful assistant. + You should follow the following guidelines: + - Focus on the latest conversation. + - Your like to be short and concise. + + Let's begin! + """, + ) + + tools = Dict( # update input format + "chatbox"=> Dict( + "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", + "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", + "output" => "" , + ), + ) + + """ Memory + Ref: Chat prompt format is openai + chathistory = [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => system_msg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ) + ] + """ + memory = Dict{String, Any}( + "shortmem"=> OrderedDict{String, Any}( + ), + "scratchpad"=> "", + "events"=> Vector{Dict{String, Any}}(), + "state"=> Dict{String, Any}( + ), + "recap"=> OrderedDict{String, Any}(), + ) + + newAgent = virtualcustomer( + name, + id, + systemmsg, + tools, + maxHistoryMsg, + chathistory, + memory, + context, + llmFormatName + ) + + return newAgent +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +end # module type \ No newline at end of file -- 2.52.0 From 7c4e84ba93ae389eeb1dfe774d18f93e906b20f4 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 1 Aug 2026 21:38:31 +0700 Subject: [PATCH 02/50] update --- src/type.jl | 371 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 265 insertions(+), 106 deletions(-) diff --git a/src/type.jl b/src/type.jl index e04c124..aaa6c76 100644 --- a/src/type.jl +++ b/src/type.jl @@ -11,37 +11,36 @@ using GeneralUtils # ============================================================================ # Message types # ============================================================================ -abstract type agentMessage end +abstract type agentMessage end # Base type for all agent messages -struct userMessage <: agentMessage - role::String - content::Vector{messageContent} - timestamp::Timestamp +struct userMessage <: agentMessage # Message from the user + role::String # Always "user" + content::Vector{messageContent} # Text and/or image content + timestamp::Timestamp # When the message was sent end -struct assistantMessage <: agentMessage - role::String - content::Vector{messageContent} - api::String - provider::String - model::String - usage::Usage - stop_reason::String - error_message::Union{String, Nothing} - timestamp::Timestamp +struct assistantMessage <: agentMessage # Message from the AI assistant + role::String # Always "assistant" + content::Vector{messageContent} # Text and/or image content + api::String # API name used (e.g., "openai") + provider::String # Provider name (e.g., "anthropic") + model::String # Model identifier + usage::Usage # Token usage for this message + stopReason::String # Why generation stopped (e.g., "end_turn") + errorMessage::Union{String, Nothing} # Error if generation failed + timestamp::Timestamp # When the message was received end -struct toolResultMessage <: agentMessage - - role::String - tool_call_id::String - tool_name::String - content::Vector{messageContent} - details::Any - usage::Union{Usage, Nothing} - added_tool_names::Union{Vector{String}, Nothing} - is_error::Bool - timestamp::Timestamp +struct toolResultMessage <: agentMessage # Result returned from a tool execution + role::String # Always "tool" + toolCallId::String # ID matching the tool call + toolName::String # Name of the executed tool + content::Vector{messageContent} # Tool output content + details::Any # Additional tool-specific details + usage::Union{Usage, Nothing} # Token usage if applicable + addedToolNames::Union{Vector{String}, Nothing} # Tools added during execution + isError::Bool # Whether the tool call resulted in an error + timestamp::Timestamp # When the result was recorded end @@ -49,15 +48,15 @@ end # Message content types # ============================================================================ -abstract type messageContent end +abstract type messageContent end # Base type for message content -struct textContent <: messageContent - text::String +struct textContent <: messageContent # Plain text message content + text::String # The text content end -struct imageContent <: messageContent - data::String - mime_type::String +struct imageContent <: messageContent # Image message content + data::String # Base64-encoded image data + mimeType::String # MIME type (e.g., "image/png") end @@ -65,14 +64,14 @@ end # Tool types # ============================================================================ -struct agentTool{TParameters, TDetails} - name::String - label::String - description::String - parameters::TParameters - execute::Function - prepare_arguments::Union{Function, Nothing} - execution_mode::Union{ToolExecutionMode, Nothing} +struct agentTool{TParameters, TDetails} # A tool available to the agent + 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 end @@ -80,10 +79,10 @@ end # Agent context # ============================================================================ -struct agentContext - system_prompt::String - messages::Vector{agentMessage} - tools::Union{Vector{agentTool}, Nothing} +struct agentContext # Snapshot of the agent's conversation context + systemPrompt::String # System prompt for the agent + messages::Vector{agentMessage} # Conversation messages + tools::Union{Vector{agentTool}, Nothing} # Available tools end @@ -92,35 +91,35 @@ end # Assistant message event types # ============================================================================ -abstract type assistantMessageEvent end +abstract type assistantMessageEvent end # Base type for assistant message streaming events -struct startEvent <: assistantMessageEvent - partial::assistantMessage +struct startEvent <: assistantMessageEvent # Message generation started + partial::assistantMessage # The partial message at this point end -struct textStartEvent <: assistantMessageEvent - content_index::Int64 - partial::assistantMessage +struct textStartEvent <: assistantMessageEvent # Text content block started + contentIndex::Int64 # Index of the content block + partial::assistantMessage # The partial message at this point end -struct textDeltaEvent <: assistantMessageEvent - content_index::Int64 - delta::String - partial::assistantMessage +struct textDeltaEvent <: assistantMessageEvent # Text content block received a chunk + contentIndex::Int64 # Index of the content block + delta::String # New text chunk + partial::assistantMessage # The partial message at this point end -struct textEndEvent <: assistantMessageEvent - content_index::Int64 - content::String - partial::assistantMessage +struct textEndEvent <: assistantMessageEvent # Text content block completed + contentIndex::Int64 # Index of the content block + content::String # Complete text content + partial::assistantMessage # The partial message at this point end -struct doneEvent <: assistantMessageEvent - reason::String - usage::Usage - message::assistantMessage +struct doneEvent <: assistantMessageEvent # Message generation completed successfully + reason::String # Why generation stopped + usage::Usage # Token usage + message::assistantMessage # The completed message end -struct errorEvent <: assistantMessageEvent - reason::String - error_message::Union{String, Nothing} - usage::Usage - error::assistantMessage +struct errorEvent <: assistantMessageEvent # Message generation encountered an error + reason::String # Error reason + errorMessage::Union{String, Nothing} # Human-readable error + usage::Usage # Token usage (partial) + error::assistantMessage # The error message end @@ -129,48 +128,41 @@ end # Agent state # ============================================================================ -mutable struct agentState - system_prompt::String - model::Model - thinking_level::ThinkingLevel - tools::Vector{agentTool} - messages::Vector{agentMessage} - is_streaming::Bool - streaming_message::Union{agentMessage, Nothing} - pending_tool_calls::Set{String} - error_message::Union{String, Nothing} +mutable struct agentState # Mutable runtime state of an agent + systemPrompt::String # System prompt text + model::llmModel # LLM model to use + tools::Vector{agentTool} # Available tools + messages::Vector{agentMessage} # Conversation messages + pendingToolCalls::Vector{String} # Tool call IDs waiting for results + errorMessage::Union{String, Nothing} # Last error message +end - function agentState( - system_prompt::String="", - model::Model=Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0), - thinking_level::ThinkingLevel=THINKING_OFF, - tools::Vector{agentTool}=agentTool[], - messages::Vector{agentMessage}=agentMessage[], +function agentState( + systemPrompt::String="", + model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0), + tools::Vector{agentTool}=agentTool[], + messages::Vector{agentMessage}=agentMessage[], +) + agentState( + systemPrompt, + model, + deepcopy(tools), + deepcopy(messages), + Vector{String}(), + nothing, ) - new( - system_prompt, - model, - thinking_level, - copy(tools), - copy(messages), - false, - nothing, - Set{String}(), - nothing, - ) - end end # ============================================================================ # Tool call types # ============================================================================ -struct toolCall - type::String - id::String - name::String - arguments::Dict{String, Any} - partial_json::Union{String, Nothing} +struct toolCall # A tool invocation from the LLM + type::String # Always "function" + id::String # Unique tool call identifier + name::String # Tool name + arguments::Dict{String, Any} # Parsed tool arguments + partialJson::Union{String, Nothing} # Raw JSON string during streaming end @@ -178,12 +170,179 @@ end # Next turn context # ============================================================================ -struct nextTurnContext - message::assistantMessage - tool_results::Vector{toolResultMessage} - context::agentContext - new_messages::Vector{agentMessage} +struct nextTurnContext # 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 + output::Float64 # Price per 1M output tokens + cache_read::Float64 # Price per 1M cached read tokens + cache_write::Float64 # Price per 1M cache write tokens +end + +struct llmModel{Api} # LLM model configuration + id::String # Unique model identifier + name::String # Human-readable model name + api::Api # API type (parametric type) + provider::String # Provider name (e.g., "anthropic", "openai") + baseUrl::String # API endpoint base URL + reasoning::Bool # Whether the model supports chain-of-thought + input::Vector{String} # Supported input modalities (e.g., "text", "image") + cost::modelCost # Pricing information + contextWindow::Int64 # Maximum context length in tokens + maxTokens::Int64 # Maximum output tokens per completion +end + +# ============================================================================ +# Agent struct +# ============================================================================ + +mutable struct yiemAgent # High-level agent wrapper + _state::agentState # Current state (prompt, model, messages, tools, etc.) + conn::NATS.Connection # NATS connection for messaging + followUpQueue::pendingMessageQueue # Messages queued via followUp() when agent would stop + + formatMsgForLLM::Function # Convert agent messages to LLM message format + preprocessMessages ::Union{Function, Nothing} # Preprocess/transform messages before sending + streamFunction::streamFn # Stream function for streaming responses + getApiKey::Union{Function, Nothing} # Callback to retrieve API key + onPayload::Union{Function, Nothing} # Callback when a payload is sent to the API + onResponse::Union{Function, Nothing} # Callback when a full response is received + beforeToolCall::Union{Function, Nothing} # Callback invoked before executing a tool call + afterToolCall::Union{Function, Nothing} # Callback invoked after executing a tool call + prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn + prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context + activeRun::Union{activeRun, Nothing} # Active run state (promise, abort controller) + sessionId::Union{String, Nothing} # Optional session identifier + thinkingBudgets::Union{Dict{String, Int64}, Nothing} # Per-model thinking token budgets + transport::String # Transport mode ("auto" or explicit) + maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) + toolExecution::toolExecutionMode # Default: run tool calls sequentially or in parallel +end + +# Outer constructor — clean keyword API +function yiemAgent( + ; systemPrompt::String="", + model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0), + thinkingLevel::thinkingLevel=THINKING_OFF, + tools::Vector{agentTool}=agentTool[], + messages::Vector{agentMessage}=agentMessage[], + formatMsgForLLM::Function=defaultformatMsgForLLM, + preprocessMessages ::Union{Function, Nothing}=nothing, + streamFunction::streamFn=getDefaultStreamFn(), + getApiKey::Union{Function, Nothing}=nothing, + onPayload::Union{Function, Nothing}=nothing, + onResponse::Union{Function, Nothing}=nothing, + beforeToolCall::Union{Function, Nothing}=nothing, + afterToolCall::Union{Function, Nothing}=nothing, + prepareNextTurn::Union{Function, Nothing}=nothing, + prepareNextTurnWithContext::Union{Function, Nothing}=nothing, + sessionId::Union{String, Nothing}=nothing, + thinkingBudgets::Union{Dict{String, Int64}, Nothing}=nothing, + transport::String="auto", + maxRetryDelayMs::Union{Int64, Nothing}=nothing, + toolExecution::toolExecutionMode=EXECUTION_PARALLEL, + ) + new( + agentState(systemPrompt, model, tools, messages), + Set{Tuple{Function, Ref{Bool}}}(), + pendingMessageQueue(QUEUE_ONE_AT_A_TIME), + pendingMessageQueue(QUEUE_ONE_AT_A_TIME), + formatMsgForLLM, + preprocessMessages , + streamFunction, + getApiKey, + onPayload, + onResponse, + beforeToolCall, + afterToolCall, + prepareNextTurn, + prepareNextTurnWithContext, + nothing, + sessionId, + thinkingBudgets, + transport, + maxRetryDelayMs, + toolExecution, + ) end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + end # module type -- 2.52.0 From be42fc4738d65be02a17bcf4ffd5464104a58a30 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 1 Aug 2026 21:46:38 +0700 Subject: [PATCH 03/50] update --- README.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 371084c..4c2844d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,23 @@ -version 0.1.0 -TODO: - [WORKING] build MCTS() for planning - [] executeplan() to execute the plan - -Change from version: 0.0.9 - - \ No newline at end of file +# YiemAgent + +## TODO +- [ ] build prompt() +- [ ] build agent runLoop() +- [ ] build MCP server connector +- [ ] executeplan() to execute the plan +- [ ] add comprehensive tests + +## Changelog + +### Version 0.8.0 +- Converted snake_case fields to camelCase: + - `llmModel`: `base_url` → `baseUrl`, `context_window` → `contextWindow`, `max_tokens` → `maxTokens` +- Converted PascalCase type references to camelCase: + - `AgentState` → `agentState` + - `AgentTool` → `agentTool` + - `AgentMessage` → `agentMessage` + - `PendingMessageQueue` → `pendingMessageQueue` + - `ActiveRun` → `activeRun` + - `StreamFn` → `streamFn` + - `ThinkingLevel` → `thinkingLevel` + - `ToolExecutionMode` → `toolExecutionMode` -- 2.52.0 From 94dd44236dbb7c512601dc748e1cf43040847613 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 1 Aug 2026 21:47:30 +0700 Subject: [PATCH 04/50] update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c2844d..4702dd2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # YiemAgent ## TODO -- [ ] build prompt() +- [WORKING] build prompt() - [ ] build agent runLoop() - [ ] build MCP server connector - [ ] executeplan() to execute the plan -- 2.52.0 From 1cb9ca106b50f4b2b108bb37bfa6aba6a66c80cd Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 1 Aug 2026 23:01:52 +0700 Subject: [PATCH 05/50] update --- src/type.jl | 82 +++++++++++------------------------------------------ 1 file changed, 17 insertions(+), 65 deletions(-) diff --git a/src/type.jl b/src/type.jl index aaa6c76..315b6ce 100644 --- a/src/type.jl +++ b/src/type.jl @@ -86,44 +86,6 @@ struct agentContext # Snapshot of the agent's conversa end - -# ============================================================================ -# Assistant message event types -# ============================================================================ - -abstract type assistantMessageEvent end # Base type for assistant message streaming events - -struct startEvent <: assistantMessageEvent # Message generation started - partial::assistantMessage # The partial message at this point -end -struct textStartEvent <: assistantMessageEvent # Text content block started - contentIndex::Int64 # Index of the content block - partial::assistantMessage # The partial message at this point -end -struct textDeltaEvent <: assistantMessageEvent # Text content block received a chunk - contentIndex::Int64 # Index of the content block - delta::String # New text chunk - partial::assistantMessage # The partial message at this point -end -struct textEndEvent <: assistantMessageEvent # Text content block completed - contentIndex::Int64 # Index of the content block - content::String # Complete text content - partial::assistantMessage # The partial message at this point -end -struct doneEvent <: assistantMessageEvent # Message generation completed successfully - reason::String # Why generation stopped - usage::Usage # Token usage - message::assistantMessage # The completed message -end -struct errorEvent <: assistantMessageEvent # Message generation encountered an error - reason::String # Error reason - errorMessage::Union{String, Nothing} # Human-readable error - usage::Usage # Token usage (partial) - error::assistantMessage # The error message -end - - - # ============================================================================ # Agent state # ============================================================================ @@ -162,7 +124,6 @@ struct toolCall # A tool invocation from the LLM id::String # Unique tool call identifier name::String # Tool name arguments::Dict{String, Any} # Parsed tool arguments - partialJson::Union{String, Nothing} # Raw JSON string during streaming end @@ -207,23 +168,28 @@ end mutable struct yiemAgent # High-level agent wrapper _state::agentState # Current state (prompt, model, messages, tools, etc.) - conn::NATS.Connection # NATS connection for messaging - followUpQueue::pendingMessageQueue # Messages queued via followUp() when agent would stop + + input_ch::Channel # user sends prompt message to agent. + # if agent is idle, it process user message right away. + # if agent is running, it process user message after + # the current tool call finished. + + followUpQueue::Channel # Messages queued via followUp() during agent is + # running. After the agent loop process all input_ch + # and the agent isn't use tool call. it then process + # followUp message + + output_ch::Channel # agent respond message to user after it process all + # user message in input_ch and all followUp message. formatMsgForLLM::Function # Convert agent messages to LLM message format - preprocessMessages ::Union{Function, Nothing} # Preprocess/transform messages before sending - streamFunction::streamFn # Stream function for streaming responses - getApiKey::Union{Function, Nothing} # Callback to retrieve API key - onPayload::Union{Function, Nothing} # Callback when a payload is sent to the API - onResponse::Union{Function, Nothing} # Callback when a full response is received + preprocessMessages ::Union{Function, Nothing} # Preprocess/transform messages before sending to LLM beforeToolCall::Union{Function, Nothing} # Callback invoked before executing a tool call afterToolCall::Union{Function, Nothing} # Callback invoked after executing a tool call prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context - activeRun::Union{activeRun, Nothing} # Active run state (promise, abort controller) + activeRun::Union{Bool, Nothing} # tracks the currently executing agent run state sessionId::Union{String, Nothing} # Optional session identifier - thinkingBudgets::Union{Dict{String, Int64}, Nothing} # Per-model thinking token budgets - transport::String # Transport mode ("auto" or explicit) maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) toolExecution::toolExecutionMode # Default: run tool calls sequentially or in parallel end @@ -232,44 +198,30 @@ end function yiemAgent( ; systemPrompt::String="", model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0), - thinkingLevel::thinkingLevel=THINKING_OFF, tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], formatMsgForLLM::Function=defaultformatMsgForLLM, preprocessMessages ::Union{Function, Nothing}=nothing, - streamFunction::streamFn=getDefaultStreamFn(), - getApiKey::Union{Function, Nothing}=nothing, - onPayload::Union{Function, Nothing}=nothing, - onResponse::Union{Function, Nothing}=nothing, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, prepareNextTurn::Union{Function, Nothing}=nothing, prepareNextTurnWithContext::Union{Function, Nothing}=nothing, sessionId::Union{String, Nothing}=nothing, - thinkingBudgets::Union{Dict{String, Int64}, Nothing}=nothing, - transport::String="auto", maxRetryDelayMs::Union{Int64, Nothing}=nothing, toolExecution::toolExecutionMode=EXECUTION_PARALLEL, ) new( agentState(systemPrompt, model, tools, messages), - Set{Tuple{Function, Ref{Bool}}}(), - pendingMessageQueue(QUEUE_ONE_AT_A_TIME), - pendingMessageQueue(QUEUE_ONE_AT_A_TIME), + Channel(16), formatMsgForLLM, - preprocessMessages , - streamFunction, - getApiKey, + preprocessMessages, onPayload, onResponse, beforeToolCall, afterToolCall, prepareNextTurn, prepareNextTurnWithContext, - nothing, sessionId, - thinkingBudgets, - transport, maxRetryDelayMs, toolExecution, ) -- 2.52.0 From d2081333f652cce41ba030f2ddcb33194a91cbc7 Mon Sep 17 00:00:00 2001 From: narawat Date: Sun, 2 Aug 2026 17:58:57 +0700 Subject: [PATCH 06/50] update --- src/OLD_interface.jl | 1400 ++++++++++++++++++++++++++++++ src/{type_OLD.jl => OLD_type.jl} | 0 src/interface.jl | 39 +- src/type.jl | 4 +- 4 files changed, 1404 insertions(+), 39 deletions(-) create mode 100644 src/OLD_interface.jl rename src/{type_OLD.jl => OLD_type.jl} (100%) diff --git a/src/OLD_interface.jl b/src/OLD_interface.jl new file mode 100644 index 0000000..8c1ca5c --- /dev/null +++ b/src/OLD_interface.jl @@ -0,0 +1,1400 @@ +module interface + +export addNewMessage, conversation, decisionMaker, reflector, generatechat, + generalconversation, detectWineryName, generateSituationReport + +using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, + DataFrames, Serde +using GeneralUtils +using ..type, ..util, ..llmfunction + +# ------------------------------------------------------------------------------------------------ # +# pythoncall setting # +# ------------------------------------------------------------------------------------------------ # +# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252 +# by setting the following variables, PythonCall.jl will use: +# 1. system's python and packages installed by system (via apt install) +# or 2. conda python and packages installed by conda +# if these setting are not set (comment out), PythonCall will use its own python and packages that +# installed by CondaPkg.jl (from env_preparation.jl) +# ENV["JULIA_CONDAPKG_BACKEND"] = "Null" # set condapkg backend = none +# systemPython = split(read(`which python`, String), "\n")[1] # system's python path +# ENV["JULIA_PYTHONCALL_EXE"] = systemPython # find python location with $> which python ex. raw"/root/conda/bin/python" + +# using PythonCall +# const py_agents = PythonCall.pynew() +# const py_llms = PythonCall.pynew() +# function __init__() +# # PythonCall.pycopy!(py_cv2, pyimport("cv2")) + +# # equivalent to from urllib.request import urlopen in python +# PythonCall.pycopy!(py_agents, pyimport("langchain.agents")) +# PythonCall.pycopy!(py_llms, pyimport("langchain.llms")) +# end + +# ---------------------------------------------- 100 --------------------------------------------- # + + +macro executeStringFunction(functionStr, args...) + # Parse the function string into an expression + func_expr = Meta.parse(functionStr) + + # Create a new function with the parsed expression + function_to_call = eval(Expr(:function, + Expr(:call, func_expr, args...), func_expr.args[2:end]...)) + + # Call the newly created function with the provided arguments + function_to_call(args...) +end + + + +""" Think and choose action + +# Arguments + - `config::T1` + config + - `state::T2` + a game state + +# Keyword Arguments + +# Return + - `thoughtdict::Dict` + +# Example +```jldoctest +julia> result = decisionMaker(agent) + +OrderedDict{String, Any} with 4 entries: + "plan" => "The user provided an image of a sparkling white wine (Asolo Prosecco Bella Principessa from Italy) and requested a search for similar wines in the inventory. According to store guidelines, I must st… + "action_name" => "SEARCH_WINE_DATABASE" + "action_input" => "Sparkling white wine from Italy" + "action_result" => "1) winery: Terrazze dell Etna, wine_name: Rose Brut. +``` +""" +function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3 + ) where {T<:agent} + @info "YiemAgent decisionMaker() start " @__LINE__ + # lessonDict = copy(JSON.parsefile("lesson.json")) + + # lesson = + # if isempty(lessonDict) + # "" + # else + # lessons = Dict{String, Any}() + # for (k, v) in lessonDict + # lessons[k] = lessonDict[k][:lesson] + # end + + # """ + # You have attempted to help the user before and failed, either because your reasoning for the + # recommendation was incorrect or your response did not exactly match the user expectation. + # The following lesson(s) give a plan to avoid failing to help the user in the same way you + # did previously. Use them to improve your strategy to help the user. + + # Here are some lessons in JSON format: + # $(JSON.json(lessons)) + + # When providing the thought and action for the current trial, that into account these failed + # trajectories and make sure not to repeat the same mistakes and incorrect answers. + # """ + # end + + # recentevents_ind = GeneralUtils.recentElementsIndex( + # length(a.memory["events"]), recentevents; includelatest=true) + + requiredKeys = ["plan", "action_name", "action_input"] + context = + """ + + + """ + + # add context to text of the latest message (in the front). + # use for loop because in openai format, each msg may contain both text and image. + for d in a.chathistory[end]["content"] + if d["type"] == "text" + d["text"] = context * d["text"] + break + end + end + errornote = "N/A" + response = nothing # placeholder for show when error msg show up + + """ + { + "model": "your-model.gguf", + "messages": [ ... ], + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "agent_action", + "strict": true, + "schema": { + "type": "object", + "properties": { + "think": { + "type": "string", + "description": "Your step-by-step reasoning process. Explain why you are choosing this action." + }, + "action_name": { + "type": "string", + "enum": ["search_web", "get_weather", "calculate_math"], + "description": "The exact name of the tool to execute." + }, + "action_input": { + "type": "object", + "properties": { + "query": { "type": ["string", "null"], "description": "For search_web" }, + "location": { "type": ["string", "null"], "description": "For get_weather" }, + "equation": { "type": ["string", "null"], "description": "For calculate_math" } + }, + "required": ["query", "location", "equation"], + "additionalProperties": false + } + }, + "required": ["think", "action_name", "action_input"], + "additionalProperties": false + } + } + } + } + """ + + # strict output format + response_format = Dict( + "type"=> "json_schema", + "json_schema"=> Dict( + "name"=> "user_profile", + "strict"=> true, + "schema"=> Dict( + "type"=> "object", + "properties"=> Dict( + "plan"=> Dict("type"=> "string"), + "action_name"=> Dict("type"=> "string"), + "action_input"=> Dict("type"=> "string"), + ), + "required"=> ["plan", "action_name", "action_input"], + "additionalProperties"=> false + ) + ) + ) + + msg = Dict( + "model"=> "gemma-4-E4B-it-UD-Q4_K_XL", + "messages"=> a.chathistory, + "temperature"=> 0.7, + "response_format"=> response_format, + ) + + for attempt in 1:maxattempt + response = a.context.text2textInstructLLM(a.id, msg) + response = GeneralUtils.remove_french_accents(response) + # think, response = GeneralUtils.extractthink(response) + + # dollar sign in Julia means string interpolation + while occursin('$', response) + response = replace(response, '$' => "USD") + end + + # responsedict = nothing + # try + # responsedict = Serde.parse_yaml(response) + # catch e + # println("\nERROR YiemAgent decisionMaker() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + # continue + # end + + # # check whether all answer's key points are in responsedict + # println("\n---") + # println(responsedict) + # println("---\n") + # ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + + # if !ispass + # errornote = errormsg + # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + # continue + # end + + responsedict = JSON.parse(response) + + if responsedict["action_input"] == "CHAT_BOX" && + occursin("similar", responsedict["action_input"]) + + continue + end + + # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] + # errornote = "Your previous attempt didn't use the given functions" + # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end + + # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # pprintln(responsedict) + @info "YiemAgent decisionMaker() end " @__LINE__ + return responsedict + end + + # in case decisionMaker failed, force to use generatechat!() + responsedict = OrderedDict( + "plan"=> "N/A", + "action_name"=> "CHAT_BOX", + "action_input"=> "N/A" + ) + return responsedict +end + + + +""" Assigns a scalar value to each new child node to be used for selec- +tion and backpropagation. This value effectively quantifies the agent's progress in task completion, +serving as a heuristic to steer the search algorithm towards the most promising regions of the tree. + +# Arguments + - `state<:AbstractDict` + one of Yiem's agent + - `text2textInstructLLM::Function` + A function that handles communication to LLM service + +# Return + - `score::Integer` + +# Example +```jldoctest +julia> +``` + +# Signature +""" +function evaluator(a::T1, timeline, decisiondict, evaluateecontext + ) where {T1<:agent} + + systemmsg = + """ + + - You are a master sommelier of an online wine store. + + + - Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding. + + + - Improve a trainee sommelier decision based on the store policy and guidelines while ensuring seamless interactions between the trainee and customers. + + + - trajectory: A conversation between your trainee and the customer that have occurred up until now + - evaluatee_context: The context that evaluatee use to make a decision + - evaluatee_decision: The decision made by the evaluatee, consists of the following elements: + "plan" is the trainee's plan + "action_name" is the name of the action taken, which can be one of the available tool name. + "action_input" is the input to the action. + + + - Use only infomation provided by the store policy and guidelines as a bedrocks for your response. + + + - The trainee's plan, action_name, and action_input must be logically consistent + - The trainee's action_input should be in a proper format as specified by the tools. + - The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the END_CONVER_GUIDELINE tool. + + + 1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. + - Evaluate the correctness of each section and the overall trajectory based on the given question. + - Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation. + - Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached. + - Do not generate additional thoughts or actions. + 2) decision_evaluation: + - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. + 3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. + 4) approval: Can be "yes" or "no". "no" if the suggestion contradict the trainee's decision; otherwise, it is "yes". + + + + { + "trajectory_evaluation": "...", + "decision_evaluation": "...", + "suggestion": "...", + "approval": "...", + } + + + Let's begin! + """ + requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approval, :suggestion] + errornote = "N/A" + + for attempt in 1:10 + evaluateecontext = replace(evaluateecontext, "" => "") + evaluateecontext = replace(evaluateecontext, "" => "") + + context = + """ + + + $timeline + + + $evaluateecontext + + + {plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])} + + P.S. $errornote + + """ + + unformatPrompt = + [ + Dict("name" => "system", "text" => systemmsg), + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) + # add info + prompt = prompt * context + + response = a.context.text2textInstructLLM(prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + response = GeneralUtils.remove_french_accents(response) + # response = replace(response, '$'=>"USD") + think, response = GeneralUtils.extractthink(response) + + responsedict = nothing + try + responsedict = copy(JSON.parsefile(response)) + catch + println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # check whether all answer's key points are in responsedict + ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue + end + + # if accepted_as_answer ∉ ["yes", "no"] # [PENDING] add errornote into the prompt + # error("generated accepted_as_answer has wrong format") + # end + + println("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(Dict(responsedict)) + return responsedict + end + error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") +end + +""" Chat with llm. + +# Example userinput + +image_path = "test/large_image.png" +image_bytes = read(image_path) +base64_string = base64encode(image_bytes) + +# 2. Match the MIME type according to your file extension (e.g., png, jpeg) +mime_type = "image/png" +data1_uri = "data:;base64," + +# 3. Construct payload with the Data URI +message => Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Describe this image for me"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data_uri) + ) + ] + ) + +""" +function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}}, + maximumMsg=50, max_think_loop::Integer=3) + + @info "YiemAgent conversation() start " @__LINE__ + userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"]) + + # find text in usermsg + usertext = nothing + for (i, d) in enumerate(userinput["content"]) + if d["type"] == "text" + d["text"] = GeneralUtils.remove_french_accents(d["text"]) + usertext = d["text"] + end + end + + if usertext == "newtopic" + clearhistory(a) + return "Okay. What shall we talk about?" + else + + # add usermsg to a.chathistory but how do I handle images? + addNewMessage(a, "user", userinput; maximumMsg=maximumMsg) + + # thinking loop until AI wants to communicate with the user + loopcount = 0 + while true + loopcount += 1 + if loopcount > max_think_loop + + thoughtdict, result_raw = generatechat!(a) + assistant_response = Dict{String, Any}( + "role" => "assistant", + "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] + ) + addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) + + items_info = [] + send_item_ind = [] # index of the item being send to frontend + if haskey(a.memory["shortmem"], "items_info") + for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ + if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) + push!(items_info, deepcopy(item)) + push!(send_item_ind, i) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ + end + end + # remove sent items + deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ + end + + response_to_frontend = Dict{String, Any}( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => thoughtdict["action_input"]), + Dict( + "type" => "items_info", + "items_info" => items_info + ), + ] + ) + + return response_to_frontend + end + + + thoughtdict, result_raw = think(a) + + if thoughtdict["action_name"] ∈ ["CHAT_BOX"] + assistant_response = Dict{String, Any}( + "role" => "assistant", + "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] + ) + addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) + + items_info = [] + send_item_ind = [] # index of the item being send to frontend + if haskey(a.memory["shortmem"], "items_info") + for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ + if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) + push!(items_info, deepcopy(item)) + push!(send_item_ind, i) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ + end + end + # remove sent items + deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) + @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ + end + + response_to_frontend = Dict{String, Any}( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => thoughtdict["action_input"]), + Dict( + "type" => "items_info", + "items_info" => items_info + ), + ] + ) + + """ intended message to send to frontend should have the following format. + response_to_frontend = Dict{String, Any}( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => "assistant_text_response"), + Dict( + "type" => "items_info", + "items_info" => [ + Dict( + "wine_name"=> "wine name 1", + "wine_id"=> "...", + "image"=> base64 encoded image, + ... + ), + Dict( + "wine_name"=> "wine name 2", + "wine_id"=> "...", + "image"=> base64 encoded image, + ... + ), + ] + ), + ] + ) + """ + + + return response_to_frontend + else # still in action + + action_name = thoughtdict["action_name"] + action_input = thoughtdict["action_input"] + + action_call = Dict{String, Any}( + "role" => "action_call", + "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),] + ) + + addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg) + + action_result = thoughtdict["action_result"] + actionresult = Dict{String, Any}( + "role" => "action_result", + "content" => [Dict("type" => "text", "text" => "$action_result"),] + ) + + addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg) + @info "YiemAgent conversation() end think count $loopcount " @__LINE__ + end + end + end +end + + +""" +# Arguments + +# Return + +# Example +```jldoctest +julia> +``` + +""" +function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} + # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0) + @info "YiemAgent think() start " @__LINE__ + thoughtdict = decisionMaker(a) + @info "YiemAgent think() 1 " @__LINE__ + @show thoughtdict + println("---\n") + + result_raw = nothing + if thoughtdict["action_name"] ∈ ["CHAT_BOX"] + + # sometime CHAT_BOX input is too short. + # if thoughtdict["action_input] < 20 character, use generatechat!() + if length(thoughtdict["action_input"]) < 20 + thoughtdict, result_raw = generatechat!(a) + else + thoughtdict["action_result"] = "Action result is the next user dialogue." + result_raw = thoughtdict["action_input"] + end + + elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE" + + thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict) + + elseif thoughtdict["action_name"] ∈ ["WINE_PRESENTATION_GUIDELINE"] + + thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict) + + elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE" + + thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false) + if result_raw !== nothing && result_raw isa Vector + if haskey(a.memory["shortmem"], "items_info") + append!(a.memory["shortmem"]["items_info"], result_raw) + else + a.memory["shortmem"]["items_info"] = result_raw + end + end + + else + + error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + @info "YiemAgent think() end " @__LINE__ + # @show thoughtdict + println("---\n") + return (thoughtdict=thoughtdict, result_raw=result_raw) +end + +function chatbox!(a::T, thoughtdict::AbstractDict + )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} + thoughtdict["action_result"] = "Action result is the next user dialogue." + return (thoughtdict=thoughtdict, result_raw=nothing) +end + +function end_conversation_guideline!(a::T, thoughtdict::AbstractDict + )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} + + guideline = + """ + + - Provide customer with store contact info and business hours + - Invite customer to comeback + + Business Hours: everyday 9.00-20.00 + Tel. 0863055790 + + + """ + thoughtdict["action_result"] = guideline + + return (thoughtdict=thoughtdict, result_raw=nothing) +end + +function wine_presentation_guideline!(a::T, thoughtdict::AbstractDict + )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} + + guideline = + """ + + - Provide detailed introductions of the wines you've found to the user. + - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. + - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. + - Provide your personal recommendation and provide a brief explanation of why you recommend it. + - People don't describe wine quality level in numbers so use convertion_table if neccessary + + Intensity level: + 1 to 2: May correspond to "light-bodied" or a similar description. + 2 to 3: May correspond to "med light bodied", "medium light" or a similar description. + 3 to 4: May correspond to "medium bodied" or a similar description. + 4 to 5: May correspond to "med full bodied", "medium full" or a similar description. + 4 to 5: May correspond to "full bodied" or a similar description. + Sweetness level: + 1 to 2: May correspond to "dry", "no sweet" or a similar description. + 2 to 3: May correspond to "off dry", "less sweet" or a similar description. + 3 to 4: May correspond to "semi sweet" or a similar description. + 4 to 5: May correspond to "sweet" or a similar description. + 4 to 5: May correspond to "very sweet" or a similar description. + Tannin level: + 1 to 2: May correspond to "low tannin" or a similar description. + 2 to 3: May correspond to "semi low tannin" or a similar description. + 3 to 4: May correspond to "medium tannin" or a similar description. + 4 to 5: May correspond to "semi high tannin" or a similar description. + 4 to 5: May correspond to "high tannin" or a similar description. + Acidity level: + 1 to 2: May correspond to "low acidity" or a similar description. + 2 to 3: May correspond to "semi low acidity" or a similar description. + 3 to 4: May correspond to "medium acidity" or a similar description. + 4 to 5: May correspond to "semi high acidity" or a similar description. + 4 to 5: May correspond to "high acidity" or a similar description. + + + """ + thoughtdict["action_result"] = guideline + + return (thoughtdict=thoughtdict, result_raw=nothing) +end + + +#PENDING +function generatechat!(a::T; maxattempt::Integer=10 + )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} + @info "YiemAgent generatechat!() start " @__LINE__ + # lessonDict = copy(JSON.parsefile("lesson.json")) + + # lesson = + # if isempty(lessonDict) + # "" + # else + # lessons = Dict{String, Any}() + # for (k, v) in lessonDict + # lessons[k] = lessonDict[k][:lesson] + # end + + # """ + # You have attempted to help the user before and failed, either because your reasoning for the + # recommendation was incorrect or your response did not exactly match the user expectation. + # The following lesson(s) give a plan to avoid failing to help the user in the same way you + # did previously. Use them to improve your strategy to help the user. + + # Here are some lessons in JSON format: + # $(JSON.json(lessons)) + + # When providing the thought and action for the current trial, that into account these failed + # trajectories and make sure not to repeat the same mistakes and incorrect answers. + # """ + # end + + # recentevents_ind = GeneralUtils.recentElementsIndex( + # length(a.memory["events"]), recentevents; includelatest=true) + + systemmsg = + """ + # store_policy + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock. + - You can only recommend wines that are currently in our inventory + - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. + - Ask the user one question at a time. + - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. + - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. + - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. + - Spicy foods should be paired only with light red wines. + - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user immediately if they are looking for these types of wines. Do not sell our wines as such. + - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. + + # store_guidelines + - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. + - Customer may provide images for you to look up. + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. + + # situation + You are continuing the conversation with the user. + + # your role + Your name is $(a.name). You are a helpful sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision. + + # objective + - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. + - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. + + # your responsibility includes + - According to the store's policy and guidelines, continuing conversation with the customer using CHAT_BOX action. + - Keep the conversation with the customer going smoothly + + # your responsibility does NOT includes + - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. + - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. + - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + + # you should then respond to the user with interleaving plan, action_name, action_input in JSON format + 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. + 2) "action_name", action_name must be CHAT_BOX. + 3) "action_input", Dialogue you want to chat with the user according to your plan. + After the action is executed you gets "action_result". It is the output from the action you selected. + + # available actions + "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. + """ + + system_msg = Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ) + + chathistory = deepcopy(a.chathistory[2:end]) # use deep copy because I want to replace system msg + pushfirst!(chathistory, system_msg) + + requiredKeys = ["plan", "action_name", "action_input"] + + errornote = "N/A" + response = nothing # placeholder for show when error msg show up + + for attempt in 1:maxattempt + if attempt > 1 + println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + response_format = Dict( + "type"=> "json_schema", + "json_schema"=> Dict( + "name"=> "user_profile", + "strict"=> true, + "schema"=> Dict( + "type"=> "object", + "properties"=> Dict( + "plan"=> Dict( + "type"=> "string", + "description" => "Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.", + ), + "action_name"=> Dict( + "type"=> "string", + "description" => "action_name must be CHAT_BOX", + ), + "action_input"=> Dict( + "type"=> "string", + "description" => "Dialogue you want to chat with the user according to your plan.", + ), + ), + "required"=> ["plan", "action_name", "action_input"], + "additionalProperties"=> false + ) + ) + ) + + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => chathistory, + "temperature" => 0.7, + "response_format"=> response_format, + ) + + response = a.context.text2textInstructLLM(a.id, msg) + response = GeneralUtils.clean_json_response(response) + response = GeneralUtils.remove_french_accents(response) + think, response = GeneralUtils.extractthink(response) + + response = strip(response) + + responsedict = nothing + if occursin(requiredKeys[2], response) + try + _responsedict = JSON.parse(response) + responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) + catch + println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + else + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue + end + + + # check whether all answer's key points are in responsedict + ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue + end + + # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] + # errornote = "Your previous attempt didn't use the given functions" + # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # end + + # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # pprintln(responsedict) + responsedict["action_result"] = "Action result is the next user dialogue." + @info "YiemAgent generatechat!() end " @__LINE__ + return (thoughtdict=responsedict, result_raw=responsedict["action_input"]) + end + @info "YiemAgent generatechat() failed to generate a thought " @__LINE__ + error("YiemAgent generatechat() failed to generate a thought ", response) +end + + +function generatequestion(a, text2textInstructLLM::Function, timeline)::String + systemmsg = + """ + Your role: + Your name is $(a.name). You are a helpful English-speaking, website-based sommelier for $(a.retailername)'s online store currently talking with the user. + Your goal includes: + 1) Help the user select the best wines from your inventory that align with the user's preferences + 2) Thanks the user when they don't need any further assistance and invite them to comeback next time + + Your responsibility includes: + 1) From your point of view as a sommelier helping the user, ask yourself multiple questions based on the current situation + + Your responsibility does NOT includes: + 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. + 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. + 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + + At each round of conversation, you will be given the info: + Additional info: ... + Your recent events: latest 5 events of the situation + + You must follow the following guidelines: + - Your question should be specific, self-contained and not require any additional context. + - Once the user has chose their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + + You should follow the following guidelines: + - Focus on the latest conversation + - If the user interrupts, prioritize the user + - If you don't already know, find out the user's budget + - If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified + - If you don't already know, find out the occasion for which the user is buying wine + - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity + - If you don't already know, find out what food will be served with wine + - If you haven't already, introduce the wines you found in the database to the user first + - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - All wines in your inventory are always in stock. + - Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory. + - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. + - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. + - Medium and full-bodied red wines are bad with spicy foods. + - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. + + You should then respond to the user with: + 1) Thought: State your thought about the current situation + 2) Q: "Ask yourself" at least three, but no more than five, questions about the situation from your perspective. + 3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions + + You must only respond in format as described below: + Q1: ... + A1: ... + Q2: ... + A2: ... + ... + + Here are some examples: + Q: What the user is looking for? + A: The user is asking for a MPV car with 7-seat + Q: What do I know? + A: The user is looking for a car with 7-seat. Our dealer sell these kind of cars + Q: What brands the user prefer? + A: I don't know. The user didn't mentioned that. Let's find out. + Q: What else do I need to know before proceeding? + A: I don't know about the user budget, car's color, and other user's preferences yet. Let's find out more about the user's preferences. + Q: I'm still lacking information regarding the user's preferences for the powertrain. I've asked the user twice already, but perhaps they're not familiar with this. What should I do. + A: I'll proceed without asking the user about the powertrain. + Q: The user is buying for her husband, should I dig in to get more information? + A: Yes, I should. So that I have better idea about the user's preferences. + Q: Why the user saying this? + A: The user does not want an SUV because it does not have sliding doors + Q: The user is asking for a cappuccino. Do I have it at my cafe? + A: No I don't have. + Q: Since I don't have a cappuccino but I have a Late, should I ask if they are okay with that? + A: Yes, I should. + Q: Are they allergic to milk? + A: Since they mentioned a cappuccino before, it seems they are not allergic to milk. + Q: Have I checked the inventory yet? + A: No. I need more information from the user including ... + Q: What else do I need to know? + A: ... + Q: Should I present my item to the user? + A: Not yet, I will need to check my inventory first. + Q: Should I check our inventory now? + A: ... + Q: What the user intend to do with the car? + A: I don't know yet. Let's ask the user. + Q: What do I have in our inventory? + A: ... + Q: Which items are within the user price range? And which items are out of the user price rance? + A: ... + Q: Do I have what the user is looking for in our stock? + A: ... + Q: Am I certain about the information I'm going to share with the user, or should I verify the information first? + A: ... + Q: What should I do? + A: ... + Q: What shouldn't I do? + A: ... + Q: what kind of car suitable for off-road trip? + A: A four-wheel drive SUV is a good choice for off-road trips. + Q: What car specification would satisfy the user's needs? + A: The user is seeking an eco-friendly vehicle that accommodates seven passengers, including seniors and children, with prioritized accessibility and efficient refueling. While electric vehicles (EVs) offer eco-friendly benefits, their long charging times make hybrid models more practical for fast refueling. Additionally, a lower ground level is essential for ease of entry/exit for seniors and children. A hybrid multi-purpose vehicle (MPV) emerges as the optimal solution, balancing sustainability, seating capacity, accessibility, and refueling efficiency. + + Let's begin! + """ + + header = ["Q1:"] + dictkey = ["q1"] + + # context = + # if length(a.memory["shortmem"]["available_wine"]) != 0 + # "Available wines you've found in your inventory so far: $(availableWineToText(a.memory["shortmem"]["available_wine"]))" + # else + # "N/A" + # end + database_search_result = a.memory["shortmem"]["db_search_result"] + + # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) + # recentevents = a.memory[:events][recent_ind] + # timeline = createTimeline(recentevents; eventindex=recent_ind) + errornote = "N/A" + response = nothing # store for show when error msg show up + + # recap = + # if length(a.memory[:recap]) <= recent + # "N/A" + # else + # recapkeys = keys(a.memory[:recap]) + # recapkeys_vec = [i for i in recapkeys] + # recapkeys_vec = recapkeys_vec[1:end-recent] + # tempmem = OrderedDict() + # for (k, v) in a.memory[:recap] + # if k ∈ recapkeys_vec + # tempmem[k] = v + # end + # end + + # GeneralUtils.dictToString(tempmem) + # end + + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + ) + + for attempt in 1:10 + if attempt > 1 + println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + usermsg = + """ + Additional info: $database_search_result + Your recent events: $timeline + P.S. $errornote + """ + + _prompt = + [ + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + + response = text2textInstructLLM(prompt; + modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) + + # make sure generatequestion() don't have wine name that is not from retailer inventory + # check whether an agent recommend wines before checking inventory or recommend wines + # outside its inventory + # ask LLM whether there are any winery mentioned in the response + mentioned_winery = detectWineryName(a, response) + if mentioned_winery != "None" + mentioned_winery = String.(strip.(split(mentioned_winery, ","))) + + # check whether the wine is in event + isWineInEvent = false + for winename in mentioned_winery + for event in a.memory["events"] + if event["observation"] !== nothing && occursin(winename, event["observation"]) + isWineInEvent = true + break + end + end + end + + # if wine is mentioned but not in timeline or shortmem, + # then the agent is not supposed to recommend the wine + if isWineInEvent == false + errornote = "Your previous attempt mentioned wines that are not in your inventory which is not allowed." + println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + end + + q_number = count("Q", response) + + # check for valid response + if q_number < 1 + errornote = "Your previous attempt has too few questions." + println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + # check whether "A1" is in the response, if not error. + elseif !occursin("A1:", response) + errornote = "Your previous attempt does not have A1:" + println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # check whether response has all header + detected_kw = GeneralUtils.detectKeywordVariation(header, response) + if 0 ∈ values(detected_kw) + errornote = "\nYour previous attempt did not have all points according to the required response format" + println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "\nYour previous attempt has duplicated points according to the required response format" + println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + response = "Q1: " * responsedict["q1"] + println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + try pprintln(response) catch e println(response) end + + return response + end + error("YiemAgent generatequestion() failed to generate a response ", response) +end + + +function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 + )::OrderedDict + + systemmsg = + """ + You are an assistant being in the given events. + Your task is to writes a summary for each event seperately into an ongoing, interleaving series. + + At each round of conversation, you will be given the situation: + Total events: number of events you need to summarize. + Events timeline: ... + Context: ... + + You should follow the following guidelines: + - Use the word "user" and "assistant" instead of their name in the report + + You should then respond to the user with the following: + Event: a detailed summary for each event without exaggerated details. + + You must only respond in format as described below: + Event_1: ... + Event_2: ... + ... + + Here are some examples: + Event_1: The user ask me about where to buy a toy. + Event_2: I told the user to go to the store at 2nd floor. + + Event_1: The user greets the assistant by saying 'hello'. + Event_2: The assistant respond warmly and inquire about how he can assist the user. + + Let's begin! + """ + + header = ["Event_$i:" for i in eachindex(a.memory["events"])] + dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory["events"])]) + + ind = GeneralUtils.nonRecentElementsIndex(length(a.memory["events"]), skiprecent) + events = a.memory["events"][ind] + timeline = createTimeline(events) + + errornote = "N/A" + response = nothing # store for show when error msg show up + for attempt in 1:10 + if attempt > 1 # use to prevent LLM generate the same respond over and over + println("\nYiemAgent generateSituationReport() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + usermsg = """ + Total events: $(length(events)) + Events timeline: $timeline + P.S. $errornote + """ + + _prompt = + [ + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") + + response = text2textInstructLLM(prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, "qwen3") + + # check whether response has all header + detected_kw = GeneralUtils.detectKeywordVariation(header, response) + kwvalue = [i for i in values(detected_kw)] + zeroind = findall(x -> x == 0, kwvalue) + missingkeys = [header[i] for i in zeroind] + if 0 ∈ values(detected_kw) + errornote = "$missingkeys are missing in your previous attempt" + println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "Your previous response has duplicated events" + println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + + println("\ngenerateSituationReport() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + try pprintln(response) catch e println(response) end + + return responsedict + end + error("generateSituationReport failed to generate a response ", response) +end + + +function detectWineryName(a, text) + systemmsg = + """ + You are a sommelier of a wine store. + Your task is to identify and list any winery names mentioned in the provided text. + + At each round of conversation, you will be given the situation: + Text: a text describing the situation. + + Tips: + - Winery usually contains Château, Chateau, Domaine, Côte, Cotes, St. de, or a combination of these words. + + You should then respond to the user with: + Winery_names: A list of winery names mentioned in the text or "None" if no winery name is mentioned. + + You must only respond in format as described below: + Winery_names: ... + + Here are some examples: + Winery_names: Domaine Courbis, Chateau Lafite Rothschild, Matarromera Domaine Roulot, Château, Cotes + + Let's begin! + """ + + header = ["Winery_names:"] + dictkey = ["winery_names"] + + response = nothing # placeholder for show when error msg show up + + for attempt in 1:10 + usermsg = """ + Text: $text + """ + _prompt = + [ + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) + ] + + # put in model format + prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) + + response = a.context.text2textInstructLLM(prompt; senderId=a.id) + response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) + think, response = GeneralUtils.extractthink(response) + println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + try pprintln(response) catch e println(response) end + + # check whether response has all header + detected_kw = GeneralUtils.detectKeywordVariation(header, response) + missingkeys = [k for (k, v) in detected_kw if v === nothing] + + if !isempty(missingkeys) + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum([length(i) for i in values(detected_kw)]) > length(header) + errornote = "\nYour previous attempt has duplicated points according to the required response format" + println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + + result = responsedict["winery_names"] + + return result + end + error("detectWineryName failed to generate a response") + end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +end # module interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/type_OLD.jl b/src/OLD_type.jl similarity index 100% rename from src/type_OLD.jl rename to src/OLD_type.jl diff --git a/src/interface.jl b/src/interface.jl index 8c1ca5c..69a3b1b 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,52 +1,15 @@ module interface -export addNewMessage, conversation, decisionMaker, reflector, generatechat, - generalconversation, detectWineryName, generateSituationReport +export prompt using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, Serde using GeneralUtils using ..type, ..util, ..llmfunction -# ------------------------------------------------------------------------------------------------ # -# pythoncall setting # -# ------------------------------------------------------------------------------------------------ # -# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252 -# by setting the following variables, PythonCall.jl will use: -# 1. system's python and packages installed by system (via apt install) -# or 2. conda python and packages installed by conda -# if these setting are not set (comment out), PythonCall will use its own python and packages that -# installed by CondaPkg.jl (from env_preparation.jl) -# ENV["JULIA_CONDAPKG_BACKEND"] = "Null" # set condapkg backend = none -# systemPython = split(read(`which python`, String), "\n")[1] # system's python path -# ENV["JULIA_PYTHONCALL_EXE"] = systemPython # find python location with $> which python ex. raw"/root/conda/bin/python" - -# using PythonCall -# const py_agents = PythonCall.pynew() -# const py_llms = PythonCall.pynew() -# function __init__() -# # PythonCall.pycopy!(py_cv2, pyimport("cv2")) - -# # equivalent to from urllib.request import urlopen in python -# PythonCall.pycopy!(py_agents, pyimport("langchain.agents")) -# PythonCall.pycopy!(py_llms, pyimport("langchain.llms")) -# end - # ---------------------------------------------- 100 --------------------------------------------- # -macro executeStringFunction(functionStr, args...) - # Parse the function string into an expression - func_expr = Meta.parse(functionStr) - - # Create a new function with the parsed expression - function_to_call = eval(Expr(:function, - Expr(:call, func_expr, args...), func_expr.args[2:end]...)) - - # Call the newly created function with the provided arguments - function_to_call(args...) -end - """ Think and choose action diff --git a/src/type.jl b/src/type.jl index 315b6ce..ef15777 100644 --- a/src/type.jl +++ b/src/type.jl @@ -166,7 +166,9 @@ end # Agent struct # ============================================================================ -mutable struct yiemAgent # High-level agent wrapper +abstract type agent end + +mutable struct yiemAgent <: agent # High-level agent wrapper _state::agentState # Current state (prompt, model, messages, tools, etc.) input_ch::Channel # user sends prompt message to agent. -- 2.52.0 From 1ab97b3972537bac1f2482f74b85e0ababeb8692 Mon Sep 17 00:00:00 2001 From: narawat Date: Sun, 2 Aug 2026 20:57:53 +0700 Subject: [PATCH 07/50] add yiemAgent --- etc.jl | 38 ++++++++---- src/type.jl | 167 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 180 insertions(+), 25 deletions(-) diff --git a/etc.jl b/etc.jl index a35aae6..5d9b549 100644 --- a/etc.jl +++ b/etc.jl @@ -1,10 +1,28 @@ -# check if this column has vector embedding. if there is one, seach vector version instead - column_name_embedding = column_name * "_embedding" - if occursin(column_name_embedding, tables_schema[column_name_embedding]) - vector_column = Dict( - "table_name"=> table_name, - "column_name"=> column_name_embedding, - "operator"=> "vector_similarity", - "value"=> column_obj["value"] - ) - end \ No newline at end of file + + + +using Base.Threads + +println("Active Julia threads: ", nthreads()) + +# A CPU-heavy helper function +function compute_work(id, iterations) + println(" [Start] Task $id on Thread #", threadid()) + + total = 0.0 + for i in 1:iterations + total += sin(i) * cos(i) + end + + println(" [Done] Task $id on Thread #", threadid()) + return total +end + +# ==================================================================== +# 1. Basic @spawn and fetch +# ==================================================================== +println("\n--- 1. Single Task Spawning ---") + +# Threads.@spawn creates a Task and schedules it onto an available worker thread +task1 = Threads.@spawn compute_work("A", 10_000_000) +println(typeof(task1)) \ No newline at end of file diff --git a/src/type.jl b/src/type.jl index ef15777..c206fe4 100644 --- a/src/type.jl +++ b/src/type.jl @@ -1,5 +1,6 @@ module type - export agent, sommelier, companion, virtualcustomer, agentContext + export agent, sommelier, companion, virtualcustomer, agentContext, yiemAgent, + run_agent, take_response, follow_up, stop_agent using Dates, UUIDs, DataStructures, JSON, NATS @@ -168,6 +169,9 @@ end abstract type agent end +""" +docstring +""" mutable struct yiemAgent <: agent # High-level agent wrapper _state::agentState # Current state (prompt, model, messages, tools, etc.) @@ -176,13 +180,15 @@ mutable struct yiemAgent <: agent # High-level agent wrapper # if agent is running, it process user message after # the current tool call finished. - followUpQueue::Channel # Messages queued via followUp() during agent is + followUpQueue::Channel # Messages queued via follow_up() during agent is # running. After the agent loop process all input_ch - # and the agent isn't use tool call. it then process - # followUp message + # and the agent isn't using tool call, it processes + # followUp messages - output_ch::Channel # agent respond message to user after it process all - # user message in input_ch and all followUp message. + output_ch::Channel # agent sends response message to user after processing + # all user messages in input_ch and all followUp messages. + + _task::Union{Task, Nothing} # Background task running the agent loop formatMsgForLLM::Function # Convert agent messages to LLM message format preprocessMessages ::Union{Function, Nothing} # Preprocess/transform messages before sending to LLM @@ -190,35 +196,43 @@ mutable struct yiemAgent <: agent # High-level agent wrapper afterToolCall::Union{Function, Nothing} # Callback invoked after executing a tool call prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context - activeRun::Union{Bool, Nothing} # tracks the currently executing agent run state sessionId::Union{String, Nothing} # Optional session identifier maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) toolExecution::toolExecutionMode # Default: run tool calls sequentially or in parallel end -# Outer constructor — clean keyword API +""" +docstring +""" function yiemAgent( ; systemPrompt::String="", - model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0), + model=nothing, tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], formatMsgForLLM::Function=defaultformatMsgForLLM, - preprocessMessages ::Union{Function, Nothing}=nothing, + preprocessMessages::Union{Function, Nothing}=nothing, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, prepareNextTurn::Union{Function, Nothing}=nothing, prepareNextTurnWithContext::Union{Function, Nothing}=nothing, sessionId::Union{String, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing, - toolExecution::toolExecutionMode=EXECUTION_PARALLEL, + toolExecution=nothing, ) - new( + # Create channels: input (user -> agent), followUp (async queue), output (agent -> user) + input_ch = Channel(16) + followUp = Channel(32) + output_ch = Channel(16) + + # Create struct with a placeholder task, then spawn and replace it + agent = yiemAgent( agentState(systemPrompt, model, tools, messages), - Channel(16), + input_ch, + followUp, + output_ch, + nothing, # placeholder — replaced below formatMsgForLLM, preprocessMessages, - onPayload, - onResponse, beforeToolCall, afterToolCall, prepareNextTurn, @@ -227,6 +241,129 @@ function yiemAgent( maxRetryDelayMs, toolExecution, ) + + # Spawn the background loop and attach it + agent._task = @spawn _agent_loop(agent) + + return agent +end + +# ============================================================================ +# Agent loop — runs in background, processes messages from input_ch / followUp +# ============================================================================ + +""" +Private agent loop. Runs in a background @task. +Waits on input_ch and followUpQueue concurrently via select(). +""" +function _agent_loop(agent::yiemAgent) + try + while true + # Wait on either channel — the one with a message fires first + msg = select(agent.input_ch, agent.followUpQueue).val + + # 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 + try + put!(agent.output_ch, assistantMessage( + role="assistant", + content=[textContent("Agent error: $(sprint(showerror, e))")], + api="", model="", usage=nothing, + stopReason="error", + errorMessage=strip(sprint(showerror, e)), + timestamp=now(), + )) + catch e2 + @error "Failed to send error response" error=e2 + end + 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) + # TODO: 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 + +# ============================================================================ +# Public API — interaction helpers +# ============================================================================ + +""" +Send a message to the agent's input channel. +Blocks if the input channel buffer is full (capacity 16 by default). +""" +function run_agent(agent::yiemAgent, msg) + put!(agent.input_ch, msg) + return agent +end + +""" +Take a response from the agent's output channel. +Blocks until the agent sends a response. +""" +function take_response(agent::yiemAgent) + return take!(agent.output_ch) +end + +""" +Send a follow-up message while the agent is still processing. +Follow-up messages are processed after all input_ch messages +and before any tool call results are sent. +""" +function follow_up(agent::yiemAgent, msg) + put!(agent.followUpQueue, msg) + return agent +end + +""" +Gracefully stop the agent. +Sends a :shutdown signal, waits for the task to finish, then closes all channels. +""" +function stop_agent(agent::yiemAgent) + put!(agent.input_ch, :shutdown) + try + fetch(agent._task) + catch e + if e isa TaskFailedException + rethrow(e) + end + end + close(agent.input_ch) + close(agent.output_ch) + close(agent.followUpQueue) + return nothing end -- 2.52.0 From c626d2cec5d39da8324ab28f82096bbd1e9f920c Mon Sep 17 00:00:00 2001 From: narawat Date: Mon, 3 Aug 2026 07:27:06 +0700 Subject: [PATCH 08/50] update --- src/type.jl | 52 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/type.jl b/src/type.jl index c206fe4..0f70ad1 100644 --- a/src/type.jl +++ b/src/type.jl @@ -6,6 +6,17 @@ module type using Dates, UUIDs, DataStructures, JSON, NATS using GeneralUtils +# ============================================================================ +# Simple type aliases / definitions +# ============================================================================ + +const Timestamp = DateTime + +struct Usage + inputTokens::Int64 + outputTokens::Int64 +end + # ---------------------------------------------- 100 --------------------------------------------- # @@ -20,6 +31,10 @@ struct userMessage <: agentMessage # Message from the user timestamp::Timestamp # When the message was sent end +function userMessage(; role="user", content=Vector{messageContent}(), timestamp=now()) + return userMessage(role, content, timestamp) +end + struct assistantMessage <: agentMessage # Message from the AI assistant role::String # Always "assistant" content::Vector{messageContent} # Text and/or image content @@ -32,6 +47,12 @@ struct assistantMessage <: agentMessage # Message from the AI assistant timestamp::Timestamp # When the message was received end +function assistantMessage(; role="assistant", content=Vector{messageContent}(), + api="", provider="", model="", usage=Usage(0, 0), stopReason="end_turn", + errorMessage=nothing, timestamp=now()) + return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp) +end + struct toolResultMessage <: agentMessage # Result returned from a tool execution role::String # Always "tool" toolCallId::String # ID matching the tool call @@ -44,6 +65,12 @@ struct toolResultMessage <: agentMessage # Result returned from a tool execut timestamp::Timestamp # When the result was recorded end +function toolResultMessage(; role="tool", toolCallId="", toolName="", + content=Vector{messageContent}(), details=nothing, usage=nothing, + addedToolNames=nothing, isError=false, timestamp=now()) + return toolResultMessage(role, toolCallId, toolName, content, details, usage, addedToolNames, isError, timestamp) +end + # ============================================================================ # Message content types @@ -55,11 +82,19 @@ struct textContent <: messageContent # Plain text message content text::String # The text content end +function textContent(; text="") + return textContent(text) +end + struct imageContent <: messageContent # Image message content data::String # Base64-encoded image data mimeType::String # MIME type (e.g., "image/png") end +function imageContent(; data="", mimeType="") + return imageContent(data, mimeType) +end + # ============================================================================ # Tool types @@ -256,7 +291,7 @@ end Private agent loop. Runs in a background @task. Waits on input_ch and followUpQueue concurrently via select(). """ -function _agent_loop(agent::yiemAgent) +function _agent_loop(agent::yiemAgent) #WORKING try while true # Wait on either channel — the one with a message fires first @@ -275,18 +310,7 @@ function _agent_loop(agent::yiemAgent) end catch e # On any error, send error response and exit the loop - try - put!(agent.output_ch, assistantMessage( - role="assistant", - content=[textContent("Agent error: $(sprint(showerror, e))")], - api="", model="", usage=nothing, - stopReason="error", - errorMessage=strip(sprint(showerror, e)), - timestamp=now(), - )) - catch e2 - @error "Failed to send error response" error=e2 - end + @error "Agent loop failed" error=e end end @@ -295,7 +319,7 @@ 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) - # TODO: Replace with actual processing logic + # PENDING Replace with actual processing logic # # 1. Add msg to agent._state.messages # 2. Call agent.formatMsgForLLM(agent._state) to format for LLM -- 2.52.0 From 92eee0716833e618869931a917dd5084042ec256 Mon Sep 17 00:00:00 2001 From: narawat Date: Mon, 3 Aug 2026 11:23:45 +0700 Subject: [PATCH 09/50] update --- src/YiemAgent.jl | 7 +- src/api.jl | 329 +++++++ src/core.jl | 147 ++++ src/interface.jl | 1363 ----------------------------- src/type.jl | 258 +++--- {src => src_OLD}/OLD_interface.jl | 0 {src => src_OLD}/OLD_type.jl | 0 7 files changed, 624 insertions(+), 1480 deletions(-) create mode 100644 src/api.jl create mode 100644 src/core.jl delete mode 100644 src/interface.jl rename {src => src_OLD}/OLD_interface.jl (100%) rename {src => src_OLD}/OLD_type.jl (100%) diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl index 63951b9..d75b129 100644 --- a/src/YiemAgent.jl +++ b/src/YiemAgent.jl @@ -16,8 +16,11 @@ module YiemAgent include("llmfunction.jl") using .llmfunction - include("interface.jl") - using .interface + include("core.jl") + using .core + + include("api.jl") + using .api # ---------------------------------------------- 100 --------------------------------------------- # diff --git a/src/api.jl b/src/api.jl new file mode 100644 index 0000000..7894951 --- /dev/null +++ b/src/api.jl @@ -0,0 +1,329 @@ +module api + +export prompt + +using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, + DataFrames, Serde +using GeneralUtils +using ..type, ..util, ..llmfunction + +# ---------------------------------------------- 100 --------------------------------------------- # + + + +""" +Send a message to the agent's input channel. +Blocks if the input channel buffer is full (capacity 16 by default). +""" +function run_agent(agent::yiemAgent, msg) + put!(agent.input_ch, msg) + return agent +end + +""" +Take a response from the agent's output channel. +Blocks until the agent sends a response. +""" +function take_response(agent::yiemAgent) + return take!(agent.output_ch) +end + +""" +Send a follow-up message while the agent is still processing. +Follow-up messages are processed after all input_ch messages +and before any tool call results are sent. +""" +function follow_up(agent::yiemAgent, msg) + put!(agent.followUpQueue, msg) + return agent +end + +""" +Gracefully stop the agent. +Sends a :shutdown signal, waits for the task to finish, then closes all channels. +""" +function stop_agent(agent::yiemAgent) + put!(agent.input_ch, :shutdown) + try + fetch(agent._task) + catch e + if e isa TaskFailedException + rethrow(e) + end + end + close(agent.input_ch) + close(agent.output_ch) + close(agent.followUpQueue) + return nothing +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" Recursively convert dictionary-like variable (e.g. JSON.Object) into an OrderedDict. + +The function walks any nested structure composed of `AbstractDict` (e.g., `JSON.Object`, +`Dict`, `OrderedDict`) and `AbstractArray` and produces a new tree where +every dictionary-like node is an `OrderedDict` and every array-like node is a `Vector{Any}`. +Scalar values (numbers, strings, booleans, `nothing`, etc.) are returned unchanged. +Does **not** mutate the input; it always allocates new containers. + +# Arguments +- `x` + Any Julia value. If `x` is an `AbstractDict` it will be converted to an `OrderedDict`; + if it is an `AbstractArray` its elements will be processed recursively. + +# Keyword Arguments +- `keytype::Type=Any` + The key type for the output OrderedDict. Use `String` for `OrderedDict{String,Any}`, + `Symbol` for `OrderedDict{Symbol,Any}`, or `Any` to preserve original key types. +- `sort_order::Union{Nothing, Vector}=nothing` + Vector of keys specifying the desired order. Keys are arranged in the specified order + first, followed by any remaining keys. + +# Return +- A newly allocated nested structure composed of `OrderedDict{keytype,Any}` and `Vector{Any}` + that mirrors the input shape but uses ordered Julia containers. + +# Notes +- The function treats any `AbstractDict` as a mapping source, so it works with + `JSON.Object`, `Dict`, `OrderedDict`, etc. +- Arrays are returned as `Vector{Any}` with their elements processed recursively. + +# Examples +```jldoctest +julia> using JSON, DataStructures +julia> d = Dict( + "a" => 4, + "b" => 6, + "c" => Dict( + "d"=>7, + :e=>Dict( + "f"=>"hey", + "g"=>Dict( + "world"=>[1, "2", 3, Dict(:dd=>4.7)] + ) + ) + ) + ) + +julia> jsonstring = JSON.json(d) +julia> A1 = JSON.parse(jsonstring) # A1 type is JSON.Object +julia> A2 = dictify(A1; keytype=String) +OrderedDict{String,Any} with 3 entries: + "a" => 4 + "b" => 6 + "c" => OrderedDict("d"=>7, "e"=>Dict("f"=>"hey", "g"=>Dict("world"=>[1, "2", 3, 4.7]))) + +julia> A3 = dictify(A1; keytype=Symbol) +OrderedDict{Symbol,Any} with 3 entries: + :a => 4 + :b => 6 + :c => OrderedDict(:d=>7, :e=>Dict("f"=>"hey", "g"=>Dict("world"=>[1, "2", 3, 4.7]))) + +julia> B1 = dictify(d; keytype=String) +OrderedDict{String, Any} with 3 entries: +``` + +**With sort_order:** +```jldoctest +julia> d = Dict("a"=>1, "b"=>2, "c"=>3) +julia> dictify(d; sort_order=["c", "a"]) +OrderedDict{String,Int} with 3 entries: + "c" => 3 + "a" => 1 + "b" => 2 +``` +""" +function dictify(x::T; keytype::Type=Any, sort_order::Union{Nothing, Vector}=nothing + )::OrderedDict where {T<:AbstractDict} + + # this function is example +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +end # module interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core.jl b/src/core.jl new file mode 100644 index 0000000..382a76b --- /dev/null +++ b/src/core.jl @@ -0,0 +1,147 @@ +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 \ No newline at end of file diff --git a/src/interface.jl b/src/interface.jl deleted file mode 100644 index 69a3b1b..0000000 --- a/src/interface.jl +++ /dev/null @@ -1,1363 +0,0 @@ -module interface - -export prompt - -using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, - DataFrames, Serde -using GeneralUtils -using ..type, ..util, ..llmfunction - -# ---------------------------------------------- 100 --------------------------------------------- # - - - - -""" Think and choose action - -# Arguments - - `config::T1` - config - - `state::T2` - a game state - -# Keyword Arguments - -# Return - - `thoughtdict::Dict` - -# Example -```jldoctest -julia> result = decisionMaker(agent) - -OrderedDict{String, Any} with 4 entries: - "plan" => "The user provided an image of a sparkling white wine (Asolo Prosecco Bella Principessa from Italy) and requested a search for similar wines in the inventory. According to store guidelines, I must st… - "action_name" => "SEARCH_WINE_DATABASE" - "action_input" => "Sparkling white wine from Italy" - "action_result" => "1) winery: Terrazze dell Etna, wine_name: Rose Brut. -``` -""" -function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3 - ) where {T<:agent} - @info "YiemAgent decisionMaker() start " @__LINE__ - # lessonDict = copy(JSON.parsefile("lesson.json")) - - # lesson = - # if isempty(lessonDict) - # "" - # else - # lessons = Dict{String, Any}() - # for (k, v) in lessonDict - # lessons[k] = lessonDict[k][:lesson] - # end - - # """ - # You have attempted to help the user before and failed, either because your reasoning for the - # recommendation was incorrect or your response did not exactly match the user expectation. - # The following lesson(s) give a plan to avoid failing to help the user in the same way you - # did previously. Use them to improve your strategy to help the user. - - # Here are some lessons in JSON format: - # $(JSON.json(lessons)) - - # When providing the thought and action for the current trial, that into account these failed - # trajectories and make sure not to repeat the same mistakes and incorrect answers. - # """ - # end - - # recentevents_ind = GeneralUtils.recentElementsIndex( - # length(a.memory["events"]), recentevents; includelatest=true) - - requiredKeys = ["plan", "action_name", "action_input"] - context = - """ - - - """ - - # add context to text of the latest message (in the front). - # use for loop because in openai format, each msg may contain both text and image. - for d in a.chathistory[end]["content"] - if d["type"] == "text" - d["text"] = context * d["text"] - break - end - end - errornote = "N/A" - response = nothing # placeholder for show when error msg show up - - """ - { - "model": "your-model.gguf", - "messages": [ ... ], - "response_format": { - "type": "json_schema", - "json_schema": { - "name": "agent_action", - "strict": true, - "schema": { - "type": "object", - "properties": { - "think": { - "type": "string", - "description": "Your step-by-step reasoning process. Explain why you are choosing this action." - }, - "action_name": { - "type": "string", - "enum": ["search_web", "get_weather", "calculate_math"], - "description": "The exact name of the tool to execute." - }, - "action_input": { - "type": "object", - "properties": { - "query": { "type": ["string", "null"], "description": "For search_web" }, - "location": { "type": ["string", "null"], "description": "For get_weather" }, - "equation": { "type": ["string", "null"], "description": "For calculate_math" } - }, - "required": ["query", "location", "equation"], - "additionalProperties": false - } - }, - "required": ["think", "action_name", "action_input"], - "additionalProperties": false - } - } - } - } - """ - - # strict output format - response_format = Dict( - "type"=> "json_schema", - "json_schema"=> Dict( - "name"=> "user_profile", - "strict"=> true, - "schema"=> Dict( - "type"=> "object", - "properties"=> Dict( - "plan"=> Dict("type"=> "string"), - "action_name"=> Dict("type"=> "string"), - "action_input"=> Dict("type"=> "string"), - ), - "required"=> ["plan", "action_name", "action_input"], - "additionalProperties"=> false - ) - ) - ) - - msg = Dict( - "model"=> "gemma-4-E4B-it-UD-Q4_K_XL", - "messages"=> a.chathistory, - "temperature"=> 0.7, - "response_format"=> response_format, - ) - - for attempt in 1:maxattempt - response = a.context.text2textInstructLLM(a.id, msg) - response = GeneralUtils.remove_french_accents(response) - # think, response = GeneralUtils.extractthink(response) - - # dollar sign in Julia means string interpolation - while occursin('$', response) - response = replace(response, '$' => "USD") - end - - # responsedict = nothing - # try - # responsedict = Serde.parse_yaml(response) - # catch e - # println("\nERROR YiemAgent decisionMaker() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - # continue - # end - - # # check whether all answer's key points are in responsedict - # println("\n---") - # println(responsedict) - # println("---\n") - # ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - - # if !ispass - # errornote = errormsg - # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - # continue - # end - - responsedict = JSON.parse(response) - - if responsedict["action_input"] == "CHAT_BOX" && - occursin("similar", responsedict["action_input"]) - - continue - end - - # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] - # errornote = "Your previous attempt didn't use the given functions" - # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(responsedict) - @info "YiemAgent decisionMaker() end " @__LINE__ - return responsedict - end - - # in case decisionMaker failed, force to use generatechat!() - responsedict = OrderedDict( - "plan"=> "N/A", - "action_name"=> "CHAT_BOX", - "action_input"=> "N/A" - ) - return responsedict -end - - - -""" Assigns a scalar value to each new child node to be used for selec- -tion and backpropagation. This value effectively quantifies the agent's progress in task completion, -serving as a heuristic to steer the search algorithm towards the most promising regions of the tree. - -# Arguments - - `state<:AbstractDict` - one of Yiem's agent - - `text2textInstructLLM::Function` - A function that handles communication to LLM service - -# Return - - `score::Integer` - -# Example -```jldoctest -julia> -``` - -# Signature -""" -function evaluator(a::T1, timeline, decisiondict, evaluateecontext - ) where {T1<:agent} - - systemmsg = - """ - - - You are a master sommelier of an online wine store. - - - - Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding. - - - - Improve a trainee sommelier decision based on the store policy and guidelines while ensuring seamless interactions between the trainee and customers. - - - - trajectory: A conversation between your trainee and the customer that have occurred up until now - - evaluatee_context: The context that evaluatee use to make a decision - - evaluatee_decision: The decision made by the evaluatee, consists of the following elements: - "plan" is the trainee's plan - "action_name" is the name of the action taken, which can be one of the available tool name. - "action_input" is the input to the action. - - - - Use only infomation provided by the store policy and guidelines as a bedrocks for your response. - - - - The trainee's plan, action_name, and action_input must be logically consistent - - The trainee's action_input should be in a proper format as specified by the tools. - - The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the END_CONVER_GUIDELINE tool. - - - 1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. - - Evaluate the correctness of each section and the overall trajectory based on the given question. - - Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation. - - Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached. - - Do not generate additional thoughts or actions. - 2) decision_evaluation: - - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. - 3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. - 4) approval: Can be "yes" or "no". "no" if the suggestion contradict the trainee's decision; otherwise, it is "yes". - - - - { - "trajectory_evaluation": "...", - "decision_evaluation": "...", - "suggestion": "...", - "approval": "...", - } - - - Let's begin! - """ - requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approval, :suggestion] - errornote = "N/A" - - for attempt in 1:10 - evaluateecontext = replace(evaluateecontext, "" => "") - evaluateecontext = replace(evaluateecontext, "" => "") - - context = - """ - - - $timeline - - - $evaluateecontext - - - {plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])} - - P.S. $errornote - - """ - - unformatPrompt = - [ - Dict("name" => "system", "text" => systemmsg), - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.context.text2textInstructLLM(prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - response = GeneralUtils.remove_french_accents(response) - # response = replace(response, '$'=>"USD") - think, response = GeneralUtils.extractthink(response) - - responsedict = nothing - try - responsedict = copy(JSON.parsefile(response)) - catch - println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether all answer's key points are in responsedict - ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - # if accepted_as_answer ∉ ["yes", "no"] # [PENDING] add errornote into the prompt - # error("generated accepted_as_answer has wrong format") - # end - - println("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(Dict(responsedict)) - return responsedict - end - error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") -end - -""" Chat with llm. - -# Example userinput - -image_path = "test/large_image.png" -image_bytes = read(image_path) -base64_string = base64encode(image_bytes) - -# 2. Match the MIME type according to your file extension (e.g., png, jpeg) -mime_type = "image/png" -data1_uri = "data:;base64," - -# 3. Construct payload with the Data URI -message => Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Describe this image for me"), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data_uri) - ) - ] - ) - -""" -function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}}, - maximumMsg=50, max_think_loop::Integer=3) - - @info "YiemAgent conversation() start " @__LINE__ - userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"]) - - # find text in usermsg - usertext = nothing - for (i, d) in enumerate(userinput["content"]) - if d["type"] == "text" - d["text"] = GeneralUtils.remove_french_accents(d["text"]) - usertext = d["text"] - end - end - - if usertext == "newtopic" - clearhistory(a) - return "Okay. What shall we talk about?" - else - - # add usermsg to a.chathistory but how do I handle images? - addNewMessage(a, "user", userinput; maximumMsg=maximumMsg) - - # thinking loop until AI wants to communicate with the user - loopcount = 0 - while true - loopcount += 1 - if loopcount > max_think_loop - - thoughtdict, result_raw = generatechat!(a) - assistant_response = Dict{String, Any}( - "role" => "assistant", - "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] - ) - addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) - - items_info = [] - send_item_ind = [] # index of the item being send to frontend - if haskey(a.memory["shortmem"], "items_info") - for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ - if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) - push!(items_info, deepcopy(item)) - push!(send_item_ind, i) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - end - # remove sent items - deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - - response_to_frontend = Dict{String, Any}( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => thoughtdict["action_input"]), - Dict( - "type" => "items_info", - "items_info" => items_info - ), - ] - ) - - return response_to_frontend - end - - - thoughtdict, result_raw = think(a) - - if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - assistant_response = Dict{String, Any}( - "role" => "assistant", - "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] - ) - addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) - - items_info = [] - send_item_ind = [] # index of the item being send to frontend - if haskey(a.memory["shortmem"], "items_info") - for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ - if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) - push!(items_info, deepcopy(item)) - push!(send_item_ind, i) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - end - # remove sent items - deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - - response_to_frontend = Dict{String, Any}( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => thoughtdict["action_input"]), - Dict( - "type" => "items_info", - "items_info" => items_info - ), - ] - ) - - """ intended message to send to frontend should have the following format. - response_to_frontend = Dict{String, Any}( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => "assistant_text_response"), - Dict( - "type" => "items_info", - "items_info" => [ - Dict( - "wine_name"=> "wine name 1", - "wine_id"=> "...", - "image"=> base64 encoded image, - ... - ), - Dict( - "wine_name"=> "wine name 2", - "wine_id"=> "...", - "image"=> base64 encoded image, - ... - ), - ] - ), - ] - ) - """ - - - return response_to_frontend - else # still in action - - action_name = thoughtdict["action_name"] - action_input = thoughtdict["action_input"] - - action_call = Dict{String, Any}( - "role" => "action_call", - "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),] - ) - - addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg) - - action_result = thoughtdict["action_result"] - actionresult = Dict{String, Any}( - "role" => "action_result", - "content" => [Dict("type" => "text", "text" => "$action_result"),] - ) - - addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg) - @info "YiemAgent conversation() end think count $loopcount " @__LINE__ - end - end - end -end - - -""" -# Arguments - -# Return - -# Example -```jldoctest -julia> -``` - -""" -function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0) - @info "YiemAgent think() start " @__LINE__ - thoughtdict = decisionMaker(a) - @info "YiemAgent think() 1 " @__LINE__ - @show thoughtdict - println("---\n") - - result_raw = nothing - if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - - # sometime CHAT_BOX input is too short. - # if thoughtdict["action_input] < 20 character, use generatechat!() - if length(thoughtdict["action_input"]) < 20 - thoughtdict, result_raw = generatechat!(a) - else - thoughtdict["action_result"] = "Action result is the next user dialogue." - result_raw = thoughtdict["action_input"] - end - - elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE" - - thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict) - - elseif thoughtdict["action_name"] ∈ ["WINE_PRESENTATION_GUIDELINE"] - - thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict) - - elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE" - - thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false) - if result_raw !== nothing && result_raw isa Vector - if haskey(a.memory["shortmem"], "items_info") - append!(a.memory["shortmem"]["items_info"], result_raw) - else - a.memory["shortmem"]["items_info"] = result_raw - end - end - - else - - error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - @info "YiemAgent think() end " @__LINE__ - # @show thoughtdict - println("---\n") - return (thoughtdict=thoughtdict, result_raw=result_raw) -end - -function chatbox!(a::T, thoughtdict::AbstractDict - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - thoughtdict["action_result"] = "Action result is the next user dialogue." - return (thoughtdict=thoughtdict, result_raw=nothing) -end - -function end_conversation_guideline!(a::T, thoughtdict::AbstractDict - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - - guideline = - """ - - - Provide customer with store contact info and business hours - - Invite customer to comeback - - Business Hours: everyday 9.00-20.00 - Tel. 0863055790 - - - """ - thoughtdict["action_result"] = guideline - - return (thoughtdict=thoughtdict, result_raw=nothing) -end - -function wine_presentation_guideline!(a::T, thoughtdict::AbstractDict - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - - guideline = - """ - - - Provide detailed introductions of the wines you've found to the user. - - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. - - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. - - Provide your personal recommendation and provide a brief explanation of why you recommend it. - - People don't describe wine quality level in numbers so use convertion_table if neccessary - - Intensity level: - 1 to 2: May correspond to "light-bodied" or a similar description. - 2 to 3: May correspond to "med light bodied", "medium light" or a similar description. - 3 to 4: May correspond to "medium bodied" or a similar description. - 4 to 5: May correspond to "med full bodied", "medium full" or a similar description. - 4 to 5: May correspond to "full bodied" or a similar description. - Sweetness level: - 1 to 2: May correspond to "dry", "no sweet" or a similar description. - 2 to 3: May correspond to "off dry", "less sweet" or a similar description. - 3 to 4: May correspond to "semi sweet" or a similar description. - 4 to 5: May correspond to "sweet" or a similar description. - 4 to 5: May correspond to "very sweet" or a similar description. - Tannin level: - 1 to 2: May correspond to "low tannin" or a similar description. - 2 to 3: May correspond to "semi low tannin" or a similar description. - 3 to 4: May correspond to "medium tannin" or a similar description. - 4 to 5: May correspond to "semi high tannin" or a similar description. - 4 to 5: May correspond to "high tannin" or a similar description. - Acidity level: - 1 to 2: May correspond to "low acidity" or a similar description. - 2 to 3: May correspond to "semi low acidity" or a similar description. - 3 to 4: May correspond to "medium acidity" or a similar description. - 4 to 5: May correspond to "semi high acidity" or a similar description. - 4 to 5: May correspond to "high acidity" or a similar description. - - - """ - thoughtdict["action_result"] = guideline - - return (thoughtdict=thoughtdict, result_raw=nothing) -end - - -#PENDING -function generatechat!(a::T; maxattempt::Integer=10 - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - @info "YiemAgent generatechat!() start " @__LINE__ - # lessonDict = copy(JSON.parsefile("lesson.json")) - - # lesson = - # if isempty(lessonDict) - # "" - # else - # lessons = Dict{String, Any}() - # for (k, v) in lessonDict - # lessons[k] = lessonDict[k][:lesson] - # end - - # """ - # You have attempted to help the user before and failed, either because your reasoning for the - # recommendation was incorrect or your response did not exactly match the user expectation. - # The following lesson(s) give a plan to avoid failing to help the user in the same way you - # did previously. Use them to improve your strategy to help the user. - - # Here are some lessons in JSON format: - # $(JSON.json(lessons)) - - # When providing the thought and action for the current trial, that into account these failed - # trajectories and make sure not to repeat the same mistakes and incorrect answers. - # """ - # end - - # recentevents_ind = GeneralUtils.recentElementsIndex( - # length(a.memory["events"]), recentevents; includelatest=true) - - systemmsg = - """ - # store_policy - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock. - - You can only recommend wines that are currently in our inventory - - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. - - Ask the user one question at a time. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. - - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. - - Spicy foods should be paired only with light red wines. - - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user immediately if they are looking for these types of wines. Do not sell our wines as such. - - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - - # store_guidelines - - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. - - Customer may provide images for you to look up. - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. - - # situation - You are continuing the conversation with the user. - - # your role - Your name is $(a.name). You are a helpful sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision. - - # objective - - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. - - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. - - # your responsibility includes - - According to the store's policy and guidelines, continuing conversation with the customer using CHAT_BOX action. - - Keep the conversation with the customer going smoothly - - # your responsibility does NOT includes - - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - # you should then respond to the user with interleaving plan, action_name, action_input in JSON format - 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) "action_name", action_name must be CHAT_BOX. - 3) "action_input", Dialogue you want to chat with the user according to your plan. - After the action is executed you gets "action_result". It is the output from the action you selected. - - # available actions - "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. - """ - - system_msg = Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ) - - chathistory = deepcopy(a.chathistory[2:end]) # use deep copy because I want to replace system msg - pushfirst!(chathistory, system_msg) - - requiredKeys = ["plan", "action_name", "action_input"] - - errornote = "N/A" - response = nothing # placeholder for show when error msg show up - - for attempt in 1:maxattempt - if attempt > 1 - println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - response_format = Dict( - "type"=> "json_schema", - "json_schema"=> Dict( - "name"=> "user_profile", - "strict"=> true, - "schema"=> Dict( - "type"=> "object", - "properties"=> Dict( - "plan"=> Dict( - "type"=> "string", - "description" => "Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.", - ), - "action_name"=> Dict( - "type"=> "string", - "description" => "action_name must be CHAT_BOX", - ), - "action_input"=> Dict( - "type"=> "string", - "description" => "Dialogue you want to chat with the user according to your plan.", - ), - ), - "required"=> ["plan", "action_name", "action_input"], - "additionalProperties"=> false - ) - ) - ) - - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => chathistory, - "temperature" => 0.7, - "response_format"=> response_format, - ) - - response = a.context.text2textInstructLLM(a.id, msg) - response = GeneralUtils.clean_json_response(response) - response = GeneralUtils.remove_french_accents(response) - think, response = GeneralUtils.extractthink(response) - - response = strip(response) - - responsedict = nothing - if occursin(requiredKeys[2], response) - try - _responsedict = JSON.parse(response) - responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) - catch - println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - else - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - - # check whether all answer's key points are in responsedict - ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] - # errornote = "Your previous attempt didn't use the given functions" - # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(responsedict) - responsedict["action_result"] = "Action result is the next user dialogue." - @info "YiemAgent generatechat!() end " @__LINE__ - return (thoughtdict=responsedict, result_raw=responsedict["action_input"]) - end - @info "YiemAgent generatechat() failed to generate a thought " @__LINE__ - error("YiemAgent generatechat() failed to generate a thought ", response) -end - - -function generatequestion(a, text2textInstructLLM::Function, timeline)::String - systemmsg = - """ - Your role: - Your name is $(a.name). You are a helpful English-speaking, website-based sommelier for $(a.retailername)'s online store currently talking with the user. - Your goal includes: - 1) Help the user select the best wines from your inventory that align with the user's preferences - 2) Thanks the user when they don't need any further assistance and invite them to comeback next time - - Your responsibility includes: - 1) From your point of view as a sommelier helping the user, ask yourself multiple questions based on the current situation - - Your responsibility does NOT includes: - 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - At each round of conversation, you will be given the info: - Additional info: ... - Your recent events: latest 5 events of the situation - - You must follow the following guidelines: - - Your question should be specific, self-contained and not require any additional context. - - Once the user has chose their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - You should follow the following guidelines: - - Focus on the latest conversation - - If the user interrupts, prioritize the user - - If you don't already know, find out the user's budget - - If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified - - If you don't already know, find out the occasion for which the user is buying wine - - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity - - If you don't already know, find out what food will be served with wine - - If you haven't already, introduce the wines you found in the database to the user first - - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - All wines in your inventory are always in stock. - - Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - Medium and full-bodied red wines are bad with spicy foods. - - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. - - You should then respond to the user with: - 1) Thought: State your thought about the current situation - 2) Q: "Ask yourself" at least three, but no more than five, questions about the situation from your perspective. - 3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions - - You must only respond in format as described below: - Q1: ... - A1: ... - Q2: ... - A2: ... - ... - - Here are some examples: - Q: What the user is looking for? - A: The user is asking for a MPV car with 7-seat - Q: What do I know? - A: The user is looking for a car with 7-seat. Our dealer sell these kind of cars - Q: What brands the user prefer? - A: I don't know. The user didn't mentioned that. Let's find out. - Q: What else do I need to know before proceeding? - A: I don't know about the user budget, car's color, and other user's preferences yet. Let's find out more about the user's preferences. - Q: I'm still lacking information regarding the user's preferences for the powertrain. I've asked the user twice already, but perhaps they're not familiar with this. What should I do. - A: I'll proceed without asking the user about the powertrain. - Q: The user is buying for her husband, should I dig in to get more information? - A: Yes, I should. So that I have better idea about the user's preferences. - Q: Why the user saying this? - A: The user does not want an SUV because it does not have sliding doors - Q: The user is asking for a cappuccino. Do I have it at my cafe? - A: No I don't have. - Q: Since I don't have a cappuccino but I have a Late, should I ask if they are okay with that? - A: Yes, I should. - Q: Are they allergic to milk? - A: Since they mentioned a cappuccino before, it seems they are not allergic to milk. - Q: Have I checked the inventory yet? - A: No. I need more information from the user including ... - Q: What else do I need to know? - A: ... - Q: Should I present my item to the user? - A: Not yet, I will need to check my inventory first. - Q: Should I check our inventory now? - A: ... - Q: What the user intend to do with the car? - A: I don't know yet. Let's ask the user. - Q: What do I have in our inventory? - A: ... - Q: Which items are within the user price range? And which items are out of the user price rance? - A: ... - Q: Do I have what the user is looking for in our stock? - A: ... - Q: Am I certain about the information I'm going to share with the user, or should I verify the information first? - A: ... - Q: What should I do? - A: ... - Q: What shouldn't I do? - A: ... - Q: what kind of car suitable for off-road trip? - A: A four-wheel drive SUV is a good choice for off-road trips. - Q: What car specification would satisfy the user's needs? - A: The user is seeking an eco-friendly vehicle that accommodates seven passengers, including seniors and children, with prioritized accessibility and efficient refueling. While electric vehicles (EVs) offer eco-friendly benefits, their long charging times make hybrid models more practical for fast refueling. Additionally, a lower ground level is essential for ease of entry/exit for seniors and children. A hybrid multi-purpose vehicle (MPV) emerges as the optimal solution, balancing sustainability, seating capacity, accessibility, and refueling efficiency. - - Let's begin! - """ - - header = ["Q1:"] - dictkey = ["q1"] - - # context = - # if length(a.memory["shortmem"]["available_wine"]) != 0 - # "Available wines you've found in your inventory so far: $(availableWineToText(a.memory["shortmem"]["available_wine"]))" - # else - # "N/A" - # end - database_search_result = a.memory["shortmem"]["db_search_result"] - - # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) - # recentevents = a.memory[:events][recent_ind] - # timeline = createTimeline(recentevents; eventindex=recent_ind) - errornote = "N/A" - response = nothing # store for show when error msg show up - - # recap = - # if length(a.memory[:recap]) <= recent - # "N/A" - # else - # recapkeys = keys(a.memory[:recap]) - # recapkeys_vec = [i for i in recapkeys] - # recapkeys_vec = recapkeys_vec[1:end-recent] - # tempmem = OrderedDict() - # for (k, v) in a.memory[:recap] - # if k ∈ recapkeys_vec - # tempmem[k] = v - # end - # end - - # GeneralUtils.dictToString(tempmem) - # end - - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) - - for attempt in 1:10 - if attempt > 1 - println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - usermsg = - """ - Additional info: $database_search_result - Your recent events: $timeline - P.S. $errornote - """ - - _prompt = - [ - Dict("name" => "system", "text" => systemmsg), - Dict("name" => "user", "text" => usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - - response = text2textInstructLLM(prompt; - modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - - # make sure generatequestion() don't have wine name that is not from retailer inventory - # check whether an agent recommend wines before checking inventory or recommend wines - # outside its inventory - # ask LLM whether there are any winery mentioned in the response - mentioned_winery = detectWineryName(a, response) - if mentioned_winery != "None" - mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - - # check whether the wine is in event - isWineInEvent = false - for winename in mentioned_winery - for event in a.memory["events"] - if event["observation"] !== nothing && occursin(winename, event["observation"]) - isWineInEvent = true - break - end - end - end - - # if wine is mentioned but not in timeline or shortmem, - # then the agent is not supposed to recommend the wine - if isWineInEvent == false - errornote = "Your previous attempt mentioned wines that are not in your inventory which is not allowed." - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - - q_number = count("Q", response) - - # check for valid response - if q_number < 1 - errornote = "Your previous attempt has too few questions." - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - # check whether "A1" is in the response, if not error. - elseif !occursin("A1:", response) - errornote = "Your previous attempt does not have A1:" - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - if 0 ∈ values(detected_kw) - errornote = "\nYour previous attempt did not have all points according to the required response format" - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - response = "Q1: " * responsedict["q1"] - println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - try pprintln(response) catch e println(response) end - - return response - end - error("YiemAgent generatequestion() failed to generate a response ", response) -end - - -function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 - )::OrderedDict - - systemmsg = - """ - You are an assistant being in the given events. - Your task is to writes a summary for each event seperately into an ongoing, interleaving series. - - At each round of conversation, you will be given the situation: - Total events: number of events you need to summarize. - Events timeline: ... - Context: ... - - You should follow the following guidelines: - - Use the word "user" and "assistant" instead of their name in the report - - You should then respond to the user with the following: - Event: a detailed summary for each event without exaggerated details. - - You must only respond in format as described below: - Event_1: ... - Event_2: ... - ... - - Here are some examples: - Event_1: The user ask me about where to buy a toy. - Event_2: I told the user to go to the store at 2nd floor. - - Event_1: The user greets the assistant by saying 'hello'. - Event_2: The assistant respond warmly and inquire about how he can assist the user. - - Let's begin! - """ - - header = ["Event_$i:" for i in eachindex(a.memory["events"])] - dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory["events"])]) - - ind = GeneralUtils.nonRecentElementsIndex(length(a.memory["events"]), skiprecent) - events = a.memory["events"][ind] - timeline = createTimeline(events) - - errornote = "N/A" - response = nothing # store for show when error msg show up - for attempt in 1:10 - if attempt > 1 # use to prevent LLM generate the same respond over and over - println("\nYiemAgent generateSituationReport() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - usermsg = """ - Total events: $(length(events)) - Events timeline: $timeline - P.S. $errornote - """ - - _prompt = - [ - Dict("name" => "system", "text" => systemmsg), - Dict("name" => "user", "text" => usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") - - response = text2textInstructLLM(prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, "qwen3") - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) - errornote = "$missingkeys are missing in your previous attempt" - println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "Your previous response has duplicated events" - println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - - println("\ngenerateSituationReport() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - try pprintln(response) catch e println(response) end - - return responsedict - end - error("generateSituationReport failed to generate a response ", response) -end - - -function detectWineryName(a, text) - systemmsg = - """ - You are a sommelier of a wine store. - Your task is to identify and list any winery names mentioned in the provided text. - - At each round of conversation, you will be given the situation: - Text: a text describing the situation. - - Tips: - - Winery usually contains Château, Chateau, Domaine, Côte, Cotes, St. de, or a combination of these words. - - You should then respond to the user with: - Winery_names: A list of winery names mentioned in the text or "None" if no winery name is mentioned. - - You must only respond in format as described below: - Winery_names: ... - - Here are some examples: - Winery_names: Domaine Courbis, Chateau Lafite Rothschild, Matarromera Domaine Roulot, Château, Cotes - - Let's begin! - """ - - header = ["Winery_names:"] - dictkey = ["winery_names"] - - response = nothing # placeholder for show when error msg show up - - for attempt in 1:10 - usermsg = """ - Text: $text - """ - _prompt = - [ - Dict("name" => "system", "text" => systemmsg), - Dict("name" => "user", "text" => usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - - response = a.context.text2textInstructLLM(prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - try pprintln(response) catch e println(response) end - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - missingkeys = [k for (k, v) in detected_kw if v === nothing] - - if !isempty(missingkeys) - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum([length(i) for i in values(detected_kw)]) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - - result = responsedict["winery_names"] - - return result - end - error("detectWineryName failed to generate a response") - end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module interface - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/type.jl b/src/type.jl index 0f70ad1..9b5cb62 100644 --- a/src/type.jl +++ b/src/type.jl @@ -31,6 +31,23 @@ struct userMessage <: agentMessage # Message from the user timestamp::Timestamp # When the message was sent end +""" +Create a new user message. + +# Arguments +- `role::String`: Always "user" +- `content::Vector{messageContent}`: Text and/or image content +- `timestamp::Timestamp`: When the message was sent + +# Returns +- A new `userMessage` instance + +# Examples +```julia +julia> msg = userMessage(content=[textContent("Hello")]) +userMessage("user", [textContent("Hello")], DateTime(...)) +``` +""" function userMessage(; role="user", content=Vector{messageContent}(), timestamp=now()) return userMessage(role, content, timestamp) end @@ -47,6 +64,29 @@ struct assistantMessage <: agentMessage # Message from the AI assistant timestamp::Timestamp # When the message was received end +""" +Create a new assistant message. + +# Arguments +- `role::String`: Always "assistant" +- `content::Vector{messageContent}`: Text and/or image content +- `api::String`: API name used (e.g., "openai") +- `provider::String`: Provider name (e.g., "anthropic") +- `model::String`: Model identifier +- `usage::Usage`: Token usage for this message +- `stopReason::String`: Why generation stopped (e.g., "end_turn") +- `errorMessage::Union{String, Nothing}`: Error if generation failed +- `timestamp::Timestamp`: When the message was received + +# Returns +- A new `assistantMessage` instance + +# Examples +```julia +julia> msg = assistantMessage(content=[textContent("Hello!")], model="gpt-4") +assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "end_turn", nothing, DateTime(...)) +``` +""" function assistantMessage(; role="assistant", content=Vector{messageContent}(), api="", provider="", model="", usage=Usage(0, 0), stopReason="end_turn", errorMessage=nothing, timestamp=now()) @@ -65,6 +105,29 @@ struct toolResultMessage <: agentMessage # Result returned from a tool execut timestamp::Timestamp # When the result was recorded end +""" +Create a new tool result message. + +# Arguments +- `role::String`: Always "tool" +- `toolCallId::String`: ID matching the tool call +- `toolName::String`: Name of the executed tool +- `content::Vector{messageContent}`: Tool output content +- `details::Any`: Additional tool-specific details +- `usage::Union{Usage, Nothing}`: Token usage if applicable +- `addedToolNames::Union{Vector{String}, Nothing}`: Tools added during execution +- `isError::Bool`: Whether the tool call resulted in an error +- `timestamp::Timestamp`: When the result was recorded + +# Returns +- A new `toolResultMessage` instance + +# Examples +```julia +julia> msg = toolResultMessage(toolCallId="call_123", toolName="search", content=[textContent("results")]) +toolResultMessage("tool", "call_123", "search", [textContent("results")], nothing, nothing, nothing, false, DateTime(...)) +``` +""" function toolResultMessage(; role="tool", toolCallId="", toolName="", content=Vector{messageContent}(), details=nothing, usage=nothing, addedToolNames=nothing, isError=false, timestamp=now()) @@ -82,6 +145,21 @@ struct textContent <: messageContent # Plain text message content text::String # The text content end +""" +Create plain text message content. + +# Arguments +- `text::String`: The text content + +# Returns +- A new `textContent` instance + +# Examples +```julia +julia> content = textContent("Hello, world!") +textContent("Hello, world!") +``` +""" function textContent(; text="") return textContent(text) end @@ -91,6 +169,22 @@ struct imageContent <: messageContent # Image message content mimeType::String # MIME type (e.g., "image/png") end +""" +Create image message content. + +# Arguments +- `data::String`: Base64-encoded image data +- `mimeType::String`: MIME type (e.g., "image/png") + +# Returns +- A new `imageContent` instance + +# Examples +```julia +julia> img = imageContent(data="base64data...", mimeType="image/png") +imageContent("base64data...", "image/png") +``` +""" function imageContent(; data="", mimeType="") return imageContent(data, mimeType) end @@ -135,6 +229,27 @@ mutable struct agentState # Mutable runtime state of an agen errorMessage::Union{String, Nothing} # Last error message end +""" +Create a new mutable agent state. + +Creates a deep copy of the provided tools and messages to isolate the +new state from external references. + +# Arguments +- `systemPrompt::String`: System prompt text +- `model::llmModel`: LLM model to use (defaults to an unknown model) +- `tools::Vector{agentTool}`: Available tools (deep copied) +- `messages::Vector{agentMessage}`: Conversation messages (deep copied) + +# Returns +- A new `agentState` instance with an empty pending tool calls list and no error + +# Examples +```julia +julia> state = agentState(systemPrompt="You are a helpful assistant") +agentState("You are a helpful assistant", ..., agentTool[], agentMessage[], String[], nothing) +``` +""" function agentState( systemPrompt::String="", model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0), @@ -237,7 +352,34 @@ mutable struct yiemAgent <: agent # High-level agent wrapper end """ -docstring +Create a new yiemAgent instance with a background loop task. + +Spawns a background `@spawn` task that runs the agent loop, listening +on `input_ch` and `followUpQueue` channels concurrently. + +# Keyword Arguments +- `systemPrompt::String`: System prompt for the agent +- `model`: LLM model to use +- `tools::Vector{agentTool}`: Available tools (default: empty) +- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty) +- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`) +- `preprocessMessages::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) +- `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`) +- `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`) +- `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`) +- `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`) +- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`) +- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`) +- `toolExecution`: Default tool execution mode (sequential or parallel) (default: `nothing`) + +# Returns +- A new `yiemAgent` instance with an active background task + +# Examples +```julia +julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model) +yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...) +``` """ function yiemAgent( ; systemPrompt::String="", @@ -283,120 +425,6 @@ function yiemAgent( return agent end -# ============================================================================ -# Agent loop — runs in background, processes messages from input_ch / followUp -# ============================================================================ - -""" -Private agent loop. Runs in a background @task. -Waits on input_ch and followUpQueue concurrently via select(). -""" -function _agent_loop(agent::yiemAgent) #WORKING - try - while true - # Wait on either channel — the one with a message fires first - msg = select(agent.input_ch, agent.followUpQueue).val - - # 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) - # PENDING 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 - -# ============================================================================ -# Public API — interaction helpers -# ============================================================================ - -""" -Send a message to the agent's input channel. -Blocks if the input channel buffer is full (capacity 16 by default). -""" -function run_agent(agent::yiemAgent, msg) - put!(agent.input_ch, msg) - return agent -end - -""" -Take a response from the agent's output channel. -Blocks until the agent sends a response. -""" -function take_response(agent::yiemAgent) - return take!(agent.output_ch) -end - -""" -Send a follow-up message while the agent is still processing. -Follow-up messages are processed after all input_ch messages -and before any tool call results are sent. -""" -function follow_up(agent::yiemAgent, msg) - put!(agent.followUpQueue, msg) - return agent -end - -""" -Gracefully stop the agent. -Sends a :shutdown signal, waits for the task to finish, then closes all channels. -""" -function stop_agent(agent::yiemAgent) - put!(agent.input_ch, :shutdown) - try - fetch(agent._task) - catch e - if e isa TaskFailedException - rethrow(e) - end - end - close(agent.input_ch) - close(agent.output_ch) - close(agent.followUpQueue) - return nothing -end - - - - - - - - diff --git a/src/OLD_interface.jl b/src_OLD/OLD_interface.jl similarity index 100% rename from src/OLD_interface.jl rename to src_OLD/OLD_interface.jl diff --git a/src/OLD_type.jl b/src_OLD/OLD_type.jl similarity index 100% rename from src/OLD_type.jl rename to src_OLD/OLD_type.jl -- 2.52.0 From 569f85333fba63c5b50d298dd7ec01679a38bf51 Mon Sep 17 00:00:00 2001 From: narawat Date: Mon, 3 Aug 2026 18:03:51 +0700 Subject: [PATCH 10/50] update --- src/YiemAgent.jl | 4 +- src/agentCore.jl | 203 +++++++++++++++++++++++ src/api.jl | 76 ++++++++- src/core.jl | 147 ----------------- src/llmfunction.jl | 390 ++++++++++++++++++++++++++++++++++++++------- src/type.jl | 48 ++++-- src/util.jl | 262 +++++++++++++++++------------- 7 files changed, 806 insertions(+), 324 deletions(-) create mode 100644 src/agentCore.jl delete mode 100644 src/core.jl diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl index d75b129..822e9fb 100644 --- a/src/YiemAgent.jl +++ b/src/YiemAgent.jl @@ -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 diff --git a/src/agentCore.jl b/src/agentCore.jl new file mode 100644 index 0000000..c5cde25 --- /dev/null +++ b/src/agentCore.jl @@ -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 \ No newline at end of file diff --git a/src/api.jl b/src/api.jl index 7894951..f8769bb 100644 --- a/src/api.jl +++ b/src/api.jl @@ -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) diff --git a/src/core.jl b/src/core.jl deleted file mode 100644 index 382a76b..0000000 --- a/src/core.jl +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 55ef96f..2cc1b3d 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -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 = """ diff --git a/src/type.jl b/src/type.jl index 9b5cb62..6e57efc 100644 --- a/src/type.jl +++ b/src/type.jl @@ -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 diff --git a/src/util.jl b/src/util.jl index e6e9f69..9460d97 100644 --- a/src/util.jl +++ b/src/util.jl @@ -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) -- 2.52.0 From 937d52053fde4954c613c3571744f2065f7fef7c Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 12:13:22 +0700 Subject: [PATCH 11/50] update --- src/agentCore.jl | 48 +++++++++++++++++++++++++----------------------- src/api.jl | 20 ++++++++++---------- src/type.jl | 23 +++++++++++------------ 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index c5cde25..2a55205 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -12,9 +12,9 @@ using ..type, ..util, ..llmfunction """ Private agent loop. Runs in a background `@spawn` task. -Waits on `input_ch` and `followUpQueue`, processing whichever has a message first. +Waits on `inputChannel` and `followUpChannel`, 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. +to `outputChannel`. Exits on `:shutdown` signal. # Arguments - `agent::yiemAgent`: The agent whose loop to run @@ -25,22 +25,26 @@ to `output_ch`. Exits on `:shutdown` signal. # 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. +- Message priority: `inputChannel` messages are checked before `followUpChannel` messages. # Examples ```jldoctest julia> # Called automatically by yiemAgent constructor ``` """ -function _agent_loop(agent::yiemAgent) #WORKING +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) + if isready(agent.inputChannel) + msg = take!(agent.inputChannel) + + #TODO convert raw user msg to userMessage type + + #TODO add userMessage to agent._state.messages else yield() end @@ -52,20 +56,19 @@ function _agent_loop(agent::yiemAgent) #WORKING 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. + # Dispatch message through the processing pipeline result = _process_message(agent, msg) - # Send response to user - put!(agent.output_ch, result) + + + # check followUp message. if there are, add them all to agent.inputChannel + + + + + # Send response to user if no user message in both agent.inputChannel and agent.followUpChannel, + # output the result + put!(agent.outputChannel, result) end catch e # On any error, send error response and exit the loop @@ -73,6 +76,7 @@ function _agent_loop(agent::yiemAgent) #WORKING end end + """ Process a single message through the agent pipeline. @@ -81,7 +85,7 @@ should be implemented. Currently a placeholder that echoes back the received mes # Arguments - `agent::yiemAgent`: The agent processing the message -- `msg`: The message to process (from `input_ch` or `followUpQueue`) +- `msg`: The message to process (from `inputChannel` or `followUpChannel`) # Returns - An `assistantMessage` instance with the processed response @@ -102,14 +106,12 @@ julia> # Currently returns a placeholder echo response """ function _process_message(agent::yiemAgent, msg) # WORKING Replace with actual processing logic - # check steering message + + while (# ) - - - # 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) diff --git a/src/api.jl b/src/api.jl index f8769bb..9412301 100644 --- a/src/api.jl +++ b/src/api.jl @@ -15,7 +15,7 @@ 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. +The agent processes messages from `inputChannel` in the background task. # Arguments - `agent::yiemAgent`: The agent instance to send a message to @@ -35,7 +35,7 @@ yiemAgent(...) ``` """ function run_agent(agent::yiemAgent, msg) - put!(agent.input_ch, msg) + put!(agent.inputChannel, msg) return agent end @@ -60,13 +60,13 @@ assistantMessage(...) ``` """ function take_response(agent::yiemAgent) - return take!(agent.output_ch) + return take!(agent.outputChannel) end """ Send a follow-up message while the agent is still processing. -Follow-up messages are queued and processed after all `input_ch` messages +Follow-up messages are queued and processed after all `inputChannel` messages and before any tool call results are sent. # Arguments @@ -88,7 +88,7 @@ yiemAgent(...) ``` """ function follow_up(agent::yiemAgent, msg) - put!(agent.followUpQueue, msg) + put!(agent.followUpChannel, msg) return agent end @@ -96,7 +96,7 @@ end Gracefully stop the agent. Sends a `:shutdown` signal to the input channel, waits for the background task to finish, -then closes all channels (`input_ch`, `output_ch`, `followUpQueue`). +then closes all channels (`inputChannel`, `outputChannel`, `followUpChannel`). # Arguments - `agent::yiemAgent`: The agent instance to stop @@ -115,7 +115,7 @@ julia> stop_agent(agent) ``` """ function stop_agent(agent::yiemAgent) - put!(agent.input_ch, :shutdown) + put!(agent.inputChannel, :shutdown) try fetch(agent._task) catch e @@ -123,9 +123,9 @@ function stop_agent(agent::yiemAgent) rethrow(e) end end - close(agent.input_ch) - close(agent.output_ch) - close(agent.followUpQueue) + close(agent.inputChannel) + close(agent.outputChannel) + close(agent.followUpChannel) return nothing end diff --git a/src/type.jl b/src/type.jl index 6e57efc..1856a56 100644 --- a/src/type.jl +++ b/src/type.jl @@ -353,18 +353,17 @@ docstring mutable struct yiemAgent <: agent # High-level agent wrapper _state::agentState # Current state (prompt, model, messages, tools, etc.) - input_ch::Channel # user sends prompt message to agent. + inputChannel::Channel # user sends prompt message to agent. # if agent is idle, it process user message right away. # if agent is running, it process user message after # the current tool call finished. - followUpQueue::Channel # Messages queued via follow_up() during agent is - # running. After the agent loop process all input_ch - # and the agent isn't using tool call, it processes - # followUp messages + followUpChannel::Channel # Buffers messages the user sends while the agent is busy. + # Processed after all inputChannel messages are handled + # and the agent is idle (not using a tool call). - output_ch::Channel # agent sends response message to user after processing - # all user messages in input_ch and all followUp messages. + outputChannel::Channel # agent sends response message to user after processing + # all user messages in inputChannel and all followUp messages. _task::Union{Task, Nothing} # Background task running the agent loop @@ -383,7 +382,7 @@ end Create a new yiemAgent instance with a background loop task. Spawns a background `@spawn` task that runs the agent loop, listening -on `input_ch` and `followUpQueue` channels concurrently. +on `inputChannel` and `followUpChannel` channels concurrently. # Keyword Arguments - `systemPrompt::String`: System prompt for the agent @@ -425,16 +424,16 @@ function yiemAgent( toolExecution=nothing, ) # Create channels: input (user -> agent), followUp (async queue), output (agent -> user) - input_ch = Channel(16) + inputChannel = Channel(16) followUp = Channel(32) - output_ch = Channel(16) + outputChannel = Channel(16) # Create struct with a placeholder task, then spawn and replace it agent = yiemAgent( agentState(systemPrompt, model, tools, messages), - input_ch, + inputChannel, followUp, - output_ch, + outputChannel, nothing, # placeholder — replaced below formatMsgForLLM, preprocessMessages, -- 2.52.0 From 52eaeab5fc02ecb24ba191aef4e7515e2788563e Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 14:51:53 +0700 Subject: [PATCH 12/50] update --- src/agentCore.jl | 41 +++++++++++++++++++++++------------------ src/type.jl | 1 + 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 2a55205..2355c73 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -35,18 +35,19 @@ julia> # Called automatically by yiemAgent constructor 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.inputChannel) - msg = take!(agent.inputChannel) - - #TODO convert raw user msg to userMessage type - - #TODO add userMessage to agent._state.messages - else + if isready(agent.inputChannel) && agent._state.activeRun == false + # allow _process_message() to run yield() + elseif isready(agent.inputChannel) && agent._state.activeRun == true + msg = fetch!(agent.inputChannel) + # Check for shutdown signal + if msg === :shutdown + msg = take!(agent.inputChannel) + end + else + error("undefined condition: isready(inputChannel)=$(isready(agent.inputChannel)), activeRun=$(agent._state.activeRun), msg=$msg") end end @@ -56,19 +57,23 @@ function _agent_loop(agent::yiemAgent) break end + # make active + agent._state.activeRun = true # Dispatch message through the processing pipeline result = _process_message(agent, msg) - - # check followUp message. if there are, add them all to agent.inputChannel + hasMore = isready(agent.followUpChannel) + while isready(agent.followUpChannel) + followMsg = take!(agent.followUpChannel) + put!(agent.inputChannel, followMsg) + hasMore = true + end - - - - # Send response to user if no user message in both agent.inputChannel and agent.followUpChannel, - # output the result - put!(agent.outputChannel, result) + if !isready(agent.inputChannel) && !hasMore + # no more messages queued — safe to send response + put!(agent.outputChannel, result) + end end catch e # On any error, send error response and exit the loop @@ -107,7 +112,7 @@ julia> # Currently returns a placeholder echo response function _process_message(agent::yiemAgent, msg) # WORKING Replace with actual processing logic - while (# ) + diff --git a/src/type.jl b/src/type.jl index 1856a56..a2d8bc0 100644 --- a/src/type.jl +++ b/src/type.jl @@ -252,6 +252,7 @@ mutable struct agentState # Mutable runtime state of an agen tools::Vector{agentTool} # Available tools messages::Vector{agentMessage} # Conversation messages pendingToolCalls::Vector{String} # Tool call IDs waiting for results + activeRun::Bool # is agent processing user message? errorMessage::Union{String, Nothing} # Last error message end -- 2.52.0 From 2072ebe5541f0c5c53d9c1df2cb022c59fa1bfe4 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 19:45:08 +0700 Subject: [PATCH 13/50] update --- src/agentCore.jl | 108 +++++++++++++++++++++++++++++++++++++---------- src/type.jl | 1 + 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 2355c73..4cf2f7b 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -34,45 +34,109 @@ julia> # Called automatically by yiemAgent constructor """ function _agent_loop(agent::yiemAgent) try + processing_task = nothing + + """ cases: + 1) agent -> idle, user msg -> nothing + typeof(processing_task) == Nothing + agent._state.activeRun -> false + agent.inputChannel -> nothing + agent.followUpChannel -> nothing + + 2) agent -> idle, user msg -> new msg + typeof(processing_task) == Nothing + agent._state.activeRun -> false + agent.inputChannel -> new msg + agent.followUpChannel -> nothing + + 3) agent -> running, user msg -> nothing + typeof(processing_task) == Task, istaskdone(processing_task) -> false + agent._state.activeRun -> true + agent.inputChannel -> nothing + agent.followUpChannel -> nothing + + 4) agent -> running, user msg -> new msg + typeof(processing_task) == Task, istaskdone(processing_task) -> false + agent._state.activeRun -> true + agent.inputChannel -> new msg + agent.followUpChannel -> nothing + + 5) agent -> running, user msg -> nothing, user msg follow up -> new msg + typeof(processing_task) == Task, istaskdone(processing_task) -> false + agent._state.activeRun -> true + agent.inputChannel -> nothing + agent.followUpChannel -> new msg + + 6) agent -> idle, user msg -> nothing + typeof(processing_task) == Task, istaskdone(processing_task) -> true + agent._state.activeRun -> false + agent.inputChannel -> nothing + agent.followUpChannel -> nothing + """ + + while true + result = nothing msg = nothing while msg === nothing - if isready(agent.inputChannel) && agent._state.activeRun == false - # allow _process_message() to run - yield() - elseif isready(agent.inputChannel) && agent._state.activeRun == true - msg = fetch!(agent.inputChannel) - # Check for shutdown signal - if msg === :shutdown - msg = take!(agent.inputChannel) - end + if isready(agent.inputChannel) + + # message will be taken then process in _process_message() + msg = fetch!(agent.inputChannel) else - error("undefined condition: isready(inputChannel)=$(isready(agent.inputChannel)), activeRun=$(agent._state.activeRun), msg=$msg") + yield() end end # Check for shutdown signal if msg === :shutdown + # Drain all remaining messages in the input channel + if isready(agent.inputChannel) + while isready(agent.inputChannel) + _ = take!(agent.inputChannel) + end + end + if isready(agent.followUpChannel) + while isready(agent.followUpChannel) + _ = take!(agent.followUpChannel) + end + end + #TODO make sure every running tools ended properly break end # make active - agent._state.activeRun = true - # Dispatch message through the processing pipeline - result = _process_message(agent, msg) - - # check followUp message. if there are, add them all to agent.inputChannel - hasMore = isready(agent.followUpChannel) - while isready(agent.followUpChannel) - followMsg = take!(agent.followUpChannel) - put!(agent.inputChannel, followMsg) - hasMore = true + if agent._state.activeRun == false + # Dispatch message through the processing pipeline + processing_task = @spawn _process_message(agent, msg) + agent._state.activeRun = true end - if !isready(agent.inputChannel) && !hasMore - # no more messages queued — safe to send response + # during agent runs, check followUp message after _process_message() is done + if typeof(processing_task) == Task && istaskdone(processing_task) == false + # if followUp message available, add them all to agent.inputChannel + if isready(agent.followUpChannel) + while isready(agent.followUpChannel) + followMsg = take!(agent.followUpChannel) + put!(agent.inputChannel, followMsg) + end + end + + continue # continue to process user message in the next loop + elseif typeof(processing_task) == Task && istaskdone(processing_task) == true + # if agent runs is done but followUpChannel has messages, discard all message in it. + # when agent work is done it should not accept follow up msg. + # user should put new message in inputChannel instead + if isready(agent.followUpChannel) + while isready(agent.followUpChannel) + _ = take!(agent.followUpChannel) + end + end + result = fetch(processing_task) put!(agent.outputChannel, result) + agent._state.activeRun = false + processing_task = nothing end end catch e diff --git a/src/type.jl b/src/type.jl index a2d8bc0..dfd5b30 100644 --- a/src/type.jl +++ b/src/type.jl @@ -289,6 +289,7 @@ function agentState( deepcopy(tools), deepcopy(messages), Vector{String}(), + false, nothing, ) end -- 2.52.0 From 96ff9f09241172d3d4e40a8d07eb7494fb38fdf8 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 21:42:42 +0700 Subject: [PATCH 14/50] update --- src/agentCore.jl | 30 ++++++++++++++++++------------ src/type.jl | 42 +++++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 4cf2f7b..6cc94d2 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -163,7 +163,7 @@ should be implemented. Currently a placeholder that echoes back the received mes - 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 + 3. If `agent.preprocessContext` 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 @@ -173,19 +173,23 @@ should be implemented. Currently a placeholder that echoes back the received mes julia> # Currently returns a placeholder echo response ``` """ -function _process_message(agent::yiemAgent, msg) - # WORKING Replace with actual processing logic +function _process_message(agent::yiemAgent, msg)::assistantMessage + # WORKING - + # take every messages from agent.inputChannel, convert them into userMessage + # and add them to agent._state.messages + # call agent.preprocessContext() + # Call agent.formatMsgForLLM(agent._state) to format for LLM + # Call the LLM (blocking — the task waits here) - # 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 + # call toolArgumentValidation() to make sure soon-to-call tools has valid arguments + + # If agent has tools, handle tool calls in a loop + + # Build assistantMessage and return it # Placeholder: echo back the message as a simple response @warn "TODO: implement _process_message" @@ -200,6 +204,11 @@ function _process_message(agent::yiemAgent, msg) end +function createToolResultMessage()::toolResultMessage + +end + + @@ -264,9 +273,6 @@ end - - - diff --git a/src/type.jl b/src/type.jl index dfd5b30..37ab0fc 100644 --- a/src/type.jl +++ b/src/type.jl @@ -355,24 +355,32 @@ docstring mutable struct yiemAgent <: agent # High-level agent wrapper _state::agentState # Current state (prompt, model, messages, tools, etc.) - inputChannel::Channel # user sends prompt message to agent. - # if agent is idle, it process user message right away. - # if agent is running, it process user message after - # the current tool call finished. - - followUpChannel::Channel # Buffers messages the user sends while the agent is busy. - # Processed after all inputChannel messages are handled - # and the agent is idle (not using a tool call). - - outputChannel::Channel # agent sends response message to user after processing - # all user messages in inputChannel and all followUp messages. + # user sends prompt message to agent. if agent is idle, it process user message right away. + # if agent is running, it process user message after the current tool call finished. + inputChannel::Channel + + # Buffers messages the user sends while the agent is busy. Processed after all inputChannel + # messages are handled and the agent is idle (not using a tool call). + followUpChannel::Channel + # agent sends response message to user after processing all user messages in inputChannel + # and all followUp messages. + outputChannel::Channel + _task::Union{Task, Nothing} # Background task running the agent loop formatMsgForLLM::Function # Convert agent messages to LLM message format - preprocessMessages ::Union{Function, Nothing} # Preprocess/transform messages before sending to LLM - beforeToolCall::Union{Function, Nothing} # Callback invoked before executing a tool call - afterToolCall::Union{Function, Nothing} # Callback invoked after executing a tool call + + # Preprocess/transform messages and context (modify, filter, prune, inject, reorder, add context) + # for a single LLM call in _process_message()'s loop returns new Vector{agentMessage} + preprocessContext ::Union{Function, Nothing} + + # Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..) + beforeToolCall::Union{Function, Nothing} + + # Callback invoked after executing a tool call to sanitize tools output so the output is ready + # to be converted into toolResults message + afterToolCall::Union{Function, Nothing} prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context sessionId::Union{String, Nothing} # Optional session identifier @@ -392,7 +400,7 @@ on `inputChannel` and `followUpChannel` channels concurrently. - `tools::Vector{agentTool}`: Available tools (default: empty) - `messages::Vector{agentMessage}`: Initial conversation messages (default: empty) - `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`) -- `preprocessMessages::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) +- `preprocessContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) - `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`) - `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`) - `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`) @@ -416,7 +424,7 @@ function yiemAgent( tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], formatMsgForLLM::Function=defaultformatMsgForLLM, - preprocessMessages::Union{Function, Nothing}=nothing, + preprocessContext::Union{Function, Nothing}=nothing, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, prepareNextTurn::Union{Function, Nothing}=nothing, @@ -438,7 +446,7 @@ function yiemAgent( outputChannel, nothing, # placeholder — replaced below formatMsgForLLM, - preprocessMessages, + preprocessContext, beforeToolCall, afterToolCall, prepareNextTurn, -- 2.52.0 From e622a522dc7008813dd47069ee063cf3381d4262 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 21:45:00 +0700 Subject: [PATCH 15/50] update --- src/type.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/type.jl b/src/type.jl index 37ab0fc..f283de3 100644 --- a/src/type.jl +++ b/src/type.jl @@ -369,11 +369,12 @@ mutable struct yiemAgent <: agent # High-level agent wrapper _task::Union{Task, Nothing} # Background task running the agent loop - formatMsgForLLM::Function # Convert agent messages to LLM message format - # Preprocess/transform messages and context (modify, filter, prune, inject, reorder, add context) # for a single LLM call in _process_message()'s loop returns new Vector{agentMessage} preprocessContext ::Union{Function, Nothing} + + # Convert preprocessContext()'s new Vector{agentMessage} to LLM message format + formatMsgForLLM::Function # Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..) beforeToolCall::Union{Function, Nothing} -- 2.52.0 From 6f971abe5d9038b7e9dc615fa0420bb124be76d1 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 21:46:49 +0700 Subject: [PATCH 16/50] update --- src/type.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/type.jl b/src/type.jl index f283de3..99dbdfa 100644 --- a/src/type.jl +++ b/src/type.jl @@ -369,8 +369,9 @@ mutable struct yiemAgent <: agent # High-level agent wrapper _task::Union{Task, Nothing} # Background task running the agent loop - # Preprocess/transform messages and context (modify, filter, prune, inject, reorder, add context) - # for a single LLM call in _process_message()'s loop returns new Vector{agentMessage} + # Preprocess/transform messages and context (modify, filter, prune, inject, reorder, + # add context from memory, ...) for a single LLM call in _process_message()'s loop. + # returns new Vector{agentMessage} preprocessContext ::Union{Function, Nothing} # Convert preprocessContext()'s new Vector{agentMessage} to LLM message format -- 2.52.0 From af0fa3010714a830d5ea6c26ee84d04e67bd0600 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 21:50:58 +0700 Subject: [PATCH 17/50] update --- src/type.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/type.jl b/src/type.jl index 99dbdfa..6cda17d 100644 --- a/src/type.jl +++ b/src/type.jl @@ -425,8 +425,8 @@ function yiemAgent( model=nothing, tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], - formatMsgForLLM::Function=defaultformatMsgForLLM, preprocessContext::Union{Function, Nothing}=nothing, + formatMsgForLLM::Function=defaultformatMsgForLLM, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, prepareNextTurn::Union{Function, Nothing}=nothing, @@ -447,8 +447,8 @@ function yiemAgent( followUp, outputChannel, nothing, # placeholder — replaced below - formatMsgForLLM, preprocessContext, + formatMsgForLLM, beforeToolCall, afterToolCall, prepareNextTurn, -- 2.52.0 From c6f97be63a8b90399bb75dc8ea31d4eb7919adab Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 22:03:56 +0700 Subject: [PATCH 18/50] update --- src/type.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/type.jl b/src/type.jl index 6cda17d..63193e6 100644 --- a/src/type.jl +++ b/src/type.jl @@ -369,8 +369,8 @@ mutable struct yiemAgent <: agent # High-level agent wrapper _task::Union{Task, Nothing} # Background task running the agent loop - # Preprocess/transform messages and context (modify, filter, prune, inject, reorder, - # add context from memory, ...) for a single LLM call in _process_message()'s loop. + # Preprocess/transform messages and context (modify, filter, prune, inject context from memory, + # reorder, ...) for a single LLM call in _process_message()'s loop. # returns new Vector{agentMessage} preprocessContext ::Union{Function, Nothing} -- 2.52.0 From 2c0f25189bc19c4fde3d5f428d9a73598da3ed7c Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 22:17:58 +0700 Subject: [PATCH 19/50] update --- src/type.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/type.jl b/src/type.jl index 63193e6..72cb728 100644 --- a/src/type.jl +++ b/src/type.jl @@ -375,7 +375,9 @@ mutable struct yiemAgent <: agent # High-level agent wrapper preprocessContext ::Union{Function, Nothing} # Convert preprocessContext()'s new Vector{agentMessage} to LLM message format - formatMsgForLLM::Function + formatMsgForLLM::Function + + llmCall::Function # Actually invoke the LLM to get a completion response # Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..) beforeToolCall::Union{Function, Nothing} @@ -402,6 +404,7 @@ on `inputChannel` and `followUpChannel` channels concurrently. - `tools::Vector{agentTool}`: Available tools (default: empty) - `messages::Vector{agentMessage}`: Initial conversation messages (default: empty) - `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`) +- `llmCall::Function`: Function to invoke the LLM (required) - `preprocessContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) - `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`) - `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`) @@ -427,6 +430,7 @@ function yiemAgent( messages::Vector{agentMessage}=agentMessage[], preprocessContext::Union{Function, Nothing}=nothing, formatMsgForLLM::Function=defaultformatMsgForLLM, + llmCall::Function, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, prepareNextTurn::Union{Function, Nothing}=nothing, @@ -449,6 +453,7 @@ function yiemAgent( nothing, # placeholder — replaced below preprocessContext, formatMsgForLLM, + llmCall, beforeToolCall, afterToolCall, prepareNextTurn, -- 2.52.0 From a90203dc0a741eb2f161c4c69542270981331cf0 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 23:04:44 +0700 Subject: [PATCH 20/50] update --- src/type.jl | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/type.jl b/src/type.jl index 72cb728..3362e05 100644 --- a/src/type.jl +++ b/src/type.jl @@ -376,11 +376,17 @@ mutable struct yiemAgent <: agent # High-level agent wrapper # Convert preprocessContext()'s new Vector{agentMessage} to LLM message format formatMsgForLLM::Function - - llmCall::Function # Actually invoke the LLM to get a completion response + + # Actually invoke the LLM to get a completion response. The LLM response comes back as an + # assistantMessage whose content is an array of content blocks. + # Each block has a type — "text", "thinking", or "toolCall". + # The code filters for type === "toolCall" blocks, then passes them to executeToolCalls(). + llmCall::Function # Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..) beforeToolCall::Union{Function, Nothing} + + executeToolCalls::Function # execute tool calls # Callback invoked after executing a tool call to sanitize tools output so the output is ready # to be converted into toolResults message @@ -389,7 +395,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context sessionId::Union{String, Nothing} # Optional session identifier maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) - toolExecution::toolExecutionMode # Default: run tool calls sequentially or in parallel + parallelToolExecute::Bool # Default: false end """ @@ -412,7 +418,7 @@ on `inputChannel` and `followUpChannel` channels concurrently. - `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`) - `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`) - `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`) -- `toolExecution`: Default tool execution mode (sequential or parallel) (default: `nothing`) +- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`) # Returns - A new `yiemAgent` instance with an active background task @@ -437,7 +443,7 @@ function yiemAgent( prepareNextTurnWithContext::Union{Function, Nothing}=nothing, sessionId::Union{String, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing, - toolExecution=nothing, + parallelToolExecute::Bool=false, ) # Create channels: input (user -> agent), followUp (async queue), output (agent -> user) inputChannel = Channel(16) @@ -460,7 +466,7 @@ function yiemAgent( prepareNextTurnWithContext, sessionId, maxRetryDelayMs, - toolExecution, + parallelToolExecute, ) # Spawn the background loop and attach it -- 2.52.0 From ba13ccc4b9e43518e4ce4e4f75b63982d650cfaa Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 4 Aug 2026 23:17:13 +0700 Subject: [PATCH 21/50] update --- src/agentCore.jl | 17 ++++++++++------- src/type.jl | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 6cc94d2..c2656c2 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -176,18 +176,21 @@ julia> # Currently returns a placeholder echo response function _process_message(agent::yiemAgent, msg)::assistantMessage # WORKING - # take every messages from agent.inputChannel, convert them into userMessage - # and add them to agent._state.messages + # loop until LLM didn't use tool calls + while + # take every messages from agent.inputChannel, convert them into userMessage + # and add them to agent._state.messages - # call agent.preprocessContext() + # call agent.preprocessContext() - # Call agent.formatMsgForLLM(agent._state) to format for LLM + # Call agent.formatMsgForLLM(agent._state) to format for LLM - # Call the LLM (blocking — the task waits here) + # Call the LLM (blocking — the task waits here) - # call toolArgumentValidation() to make sure soon-to-call tools has valid arguments + # call toolArgumentValidation() to make sure soon-to-call tools has valid arguments - # If agent has tools, handle tool calls in a loop + # If agent has tools, handle tool calls in a loop, save tool + end # Build assistantMessage and return it diff --git a/src/type.jl b/src/type.jl index 3362e05..bf6e0ce 100644 --- a/src/type.jl +++ b/src/type.jl @@ -204,7 +204,7 @@ A tool available to the agent. - `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 +- `parallelExecute::Union{toolparallelExecute, Nothing}`: Override: run tool calls sequentially or in parallel # Returns - A new `agentTool` instance @@ -216,7 +216,7 @@ struct agentTool{TParameters, TDetails} # A tool available to the agent 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 + parallelExecute::Union{toolparallelExecute, Nothing} # Override: run tool calls sequentially or in parallel end -- 2.52.0 From a3f9e39249dc26b121f1e286ad0a3e6448524324 Mon Sep 17 00:00:00 2001 From: narawat Date: Wed, 5 Aug 2026 04:21:44 +0700 Subject: [PATCH 22/50] update --- etc.jl | 294 +++++++++++++++++++++++++++++++++++++++++++---- src/agentCore.jl | 16 +-- src/type.jl | 2 +- 3 files changed, 279 insertions(+), 33 deletions(-) diff --git a/etc.jl b/etc.jl index 5d9b549..46735d2 100644 --- a/etc.jl +++ b/etc.jl @@ -1,28 +1,272 @@ - - - -using Base.Threads - -println("Active Julia threads: ", nthreads()) - -# A CPU-heavy helper function -function compute_work(id, iterations) - println(" [Start] Task $id on Thread #", threadid()) - - total = 0.0 - for i in 1:iterations - total += sin(i) * cos(i) - end - - println(" [Done] Task $id on Thread #", threadid()) - return total +struct preparedToolCall + tool::AgentTool + toolCall::AgentToolCall + args::Any end -# ==================================================================== -# 1. Basic @spawn and fetch -# ==================================================================== -println("\n--- 1. Single Task Spawning ---") +struct immediateOutcome + result::AgentToolResult + isError::Bool +end -# Threads.@spawn creates a Task and schedules it onto an available worker thread -task1 = Threads.@spawn compute_work("A", 10_000_000) -println(typeof(task1)) \ No newline at end of file +struct executedOutcome + result::AgentToolResult + isError::Bool +end + +struct finalizedOutcome + toolCall::AgentToolCall + result::AgentToolResult + isError::Bool +end + +struct toolCallBatch + messages::Vector{ToolResultMessage} + terminate::Bool +end + +# ── helpers ───────────────────────────────────────────────────── + +function createErrorToolResult(msg::String)::AgentToolResult + return AgentToolResult([TextContent("text", msg)], Dict{Any,Any}()) +end + +function createToolResultMessage(f::finalizedOutcome)::ToolResultMessage + return ToolResultMessage( + "toolResult", f.toolCall.id, f.toolCall.name, + f.result.content, f.result.details, f.result.usage, + get(f.result, :addedToolNames, String[]), f.isError, now_millis() + ) +end + +function shouldTerminate(batches::Vector{finalizedOutcome})::Bool + return !isempty(batches) && all(b -> b.result.terminate, batches) +end + +# ── per-call preparation ──────────────────────────────────────── + +function prepareToolCall( + context::AgentContext, + assistantMsg::AssistantMessage, + toolCall::AgentToolCall, + config::AgentLoopConfig, + signal::Union{Nothing,AbortSignal}, +)::Union{preparedToolCall,immediateOutcome} + + tool = find(t -> t.name == toolCall.name, context.tools) + if tool === nothing + return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true) + end + + try + # 1. prepare arguments (tool-specific transform) + preparedArgs = prepareToolCallArguments(tool, toolCall) + validatedArgs = validateToolArguments(tool, preparedArgs) + + # 2. beforeToolCall hook + if config.before_tool_call !== nothing + before = config.before_tool_call( + AssistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal + ) + if signal !== nothing && signal.aborted + return immediateOutcome(createErrorToolResult("Operation aborted"), true) + end + if before !== nothing && before.block + return immediateOutcome( + createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true) + end + end + + return preparedToolCall(tool, toolCall, validatedArgs) + catch err + return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true) + end +end + +# ── per-call execution ────────────────────────────────────────── + +function executePreparedToolCall( + prep::preparedToolCall, + signal::Union{Nothing,AbortSignal}, + emit::AgentEventSink, +)::executedOutcome + + updateEvents = Promise[] + accepting = true + + try + result = prep.tool.execute( + prep.toolCall.id, prep.args, signal, + partialResult -> begin + if accepting + push!(updateEvents, + emit(ToolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name, + prep.toolCall.arguments, partialResult))) + end + end + ) + accepting = false + wait.(updateEvents) + return executedOutcome(result, false) + catch err + accepting = false + wait.(updateEvents) + return executedOutcome(createErrorToolResult(sprint(showerror, err)), true) + end +end + +# ── per-call finalization ─────────────────────────────────────── + +function finalizeExecutedToolCall( + context::AgentContext, + assistantMsg::AssistantMessage, + prep::preparedToolCall, + executed::executedOutcome, + config::AgentLoopConfig, + signal::Union{Nothing,AbortSignal}, +)::finalizedOutcome + + result = executed.result + isError = executed.isError + + if config.afterToolCalls !== nothing + try + after = config.afterToolCalls( + AfterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal + ) + if after !== nothing + result = merge(result, Dict(:content=>get(after,:content,result.content), + :details=>get(after,:details,result.details), + :usage=>get(after,:usage,result.usage), + :terminate=>get(after,:terminate,result.terminate))) + isError = get(after, :is_error, isError) + end + catch err + result = createErrorToolResult(sprint(showerror, err)) + isError = true + end + end + + return finalizedOutcome(prep.toolCall, result, isError) +end + +function emitToolExecutionEnd(finalized::finalizedOutcome, emit::AgentEventSink) + emit(ToolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, + finalized.result, finalized.isError)) +end + +# ── sequential execution ──────────────────────────────────────── + +function executeToolCallsSequential( + context::AgentContext, + assistantMsg::AssistantMessage, + toolCalls::Vector{AgentToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing,AbortSignal}, + emit::AgentEventSink, +)::toolCallBatch + + finalizedCalls = finalizedOutcome[] + messages = ToolResultMessage[] + + for tc in toolCalls + emit(ToolExecStartEvent(tc.id, tc.name, tc.arguments)) + + prep = prepareToolCall(context, assistantMsg, tc, config, signal) + + if prep isa immediateOutcome + finalized = finalizedOutcome(tc, prep.result, prep.isError) + else + executed = executePreparedToolCall(prep, signal, emit) + finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) + end + + emitToolExecutionEnd(finalized, emit) + push!(messages, createToolResultMessage(finalized)) + push!(finalizedCalls, finalized) + + if signal !== nothing && signal.aborted + break + end + end + + return toolCallBatch(messages, shouldTerminate(finalizedCalls)) +end + +# ── parallel execution ────────────────────────────────────────── + +function executeToolCallsParallel( + context::AgentContext, + assistantMsg::AssistantMessage, + toolCalls::Vector{AgentToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing,AbortSignal}, + emit::AgentEventSink, +)::toolCallBatch + + # Each entry: finalizedOutcome (already done) or Task → finalizedOutcome (pending) + entries = Union{finalizedOutcome,Task{finalizedOutcome}}[] + + for tc in toolCalls + emit(ToolExecStartEvent(tc.id, tc.name, tc.arguments)) + + prep = prepareToolCall(context, assistantMsg, tc, config, signal) + + if prep isa immediateOutcome + finalized = finalizedOutcome(tc, prep.result, prep.isError) + emitToolExecutionEnd(finalized, emit) + push!(entries, finalized) + else + # spawn lazy computation task + task = Task() do + executed = executePreparedToolCall(prep, signal, emit) + finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) + emitToolExecutionEnd(finalized, emit) + return finalized + end + schedule(task) + push!(entries, task) + end + + if signal !== nothing && signal.aborted + break + end + end + + # Wait for all tasks, collect in order + finalizedCalls = finalizedOutcome[] + for entry in entries + outcome = entry isa Task ? fetch(entry) : entry + push!(finalizedCalls, outcome) + end + + messages = ToolResultMessage[] + for f in finalizedCalls + push!(messages, createToolResultMessage(f)) + end + + return toolCallBatch(messages, shouldTerminate(finalizedCalls)) +end + +# ── dispatcher ────────────────────────────────────────────────── + +function executeToolCalls( + context::AgentContext, + assistantMsg::AssistantMessage, + toolCalls::Vector{AgentToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing,AbortSignal}, + emit::AgentEventSink, +)::toolCallBatch + + # Check if any tool is marked sequential, or config forces sequential + hasSequential = any(tc -> + any(t -> t.name == tc.name && get(t.executionMode, "parallel") == "sequential", + context.tools), toolCalls) + + if config.tool_execution == "sequential" || hasSequential + return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) + else + return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) + end +end diff --git a/src/agentCore.jl b/src/agentCore.jl index c2656c2..71f3bdb 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -176,7 +176,7 @@ julia> # Currently returns a placeholder echo response function _process_message(agent::yiemAgent, msg)::assistantMessage # WORKING - # loop until LLM didn't use tool calls + # loop until llmCall() response didn't use tool calls while # take every messages from agent.inputChannel, convert them into userMessage # and add them to agent._state.messages @@ -185,11 +185,15 @@ function _process_message(agent::yiemAgent, msg)::assistantMessage # Call agent.formatMsgForLLM(agent._state) to format for LLM - # Call the LLM (blocking — the task waits here) + # Call llmCall() (blocking — the task waits here) - # call toolArgumentValidation() to make sure soon-to-call tools has valid arguments + # if LLM use tool calls - # If agent has tools, handle tool calls in a loop, save tool + # call executeToolCalls() + + # else + # break out of while loop + end # Build assistantMessage and return it @@ -207,9 +211,7 @@ function _process_message(agent::yiemAgent, msg)::assistantMessage end -function createToolResultMessage()::toolResultMessage - -end + diff --git a/src/type.jl b/src/type.jl index bf6e0ce..71276fe 100644 --- a/src/type.jl +++ b/src/type.jl @@ -386,7 +386,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper # Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..) beforeToolCall::Union{Function, Nothing} - executeToolCalls::Function # execute tool calls + executeToolCalls::Function # execute tool calls () # Callback invoked after executing a tool call to sanitize tools output so the output is ready # to be converted into toolResults message -- 2.52.0 From afc866a3a5ffeac87b0db2e4f553d610192266bf Mon Sep 17 00:00:00 2001 From: narawat Date: Wed, 5 Aug 2026 10:27:17 +0700 Subject: [PATCH 23/50] update --- etc.jl | 488 +++++++++++++++++++++++++++++------ src/MCTSexamplePrompt.py | 537 --------------------------------------- src/agentCore.jl | 2 + src/type.jl | 11 +- 4 files changed, 418 insertions(+), 620 deletions(-) delete mode 100644 src/MCTSexamplePrompt.py diff --git a/etc.jl b/etc.jl index 46735d2..beb9929 100644 --- a/etc.jl +++ b/etc.jl @@ -1,57 +1,211 @@ +# ── executeToolCalls() Julia pseudo code ────────────────────────── +# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit + +""" + preparedToolCall(tool, toolCall, args) + +Intermediate state between tool call validation and execution. +This struct is created after `prepareToolCall` succeeds and serves +as the bridge to the execution phase. Keeping the resolved tool, +original call metadata, and validated args together avoids repeated +lookups and allows the execution phase to access all necessary data +without carrying the full context through the call chain. +""" struct preparedToolCall - tool::AgentTool - toolCall::AgentToolCall - args::Any + tool::agentTool # The resolved tool definition from the context + toolCall::agentToolCall # The original tool call from the assistant + args::any # Validated (and coerced) argument values end +""" + immediateOutcome(result, isError) + +A tool call that was resolved without actual execution — either +because the tool was not found, validation failed, or a +`beforeToolCall` hook blocked the call. The result is produced +immediately and emitted as a tool result message. + +Returning an outcome instead of throwing an exception is intentional: +it lets the agent feed the error back to the LLM as a tool result so +the model can recover — for example, by re-issuing a tool call with +corrected arguments after a validation failure. +""" struct immediateOutcome - result::AgentToolResult - isError::Bool + result::agentToolResult # The pre-computed tool result + isError::bool # Whether this outcome represents an error end +""" + executedOutcome(result, isError) + +A tool call that has been executed by `tool.execute()` but has not +yet been through the `afterToolCall` hook. This intermediate state +is necessary because the hook may mutate the result (content, usage, +termination, error status). Keeping execution and finalization separate +allows the hook to inspect the raw result and decide whether to +transform it or replace it entirely. +""" struct executedOutcome - result::AgentToolResult - isError::Bool + result::agentToolResult # The tool's execution result + isError::bool # Whether execution raised an error end +""" + finalizedOutcome(toolCall, result, isError) + +The complete outcome of a tool call after both execution and the +`afterToolCall` hook. This is the final form that is used to +construct the `toolResultMessage` emitted to the agent loop. + +The three-phase design (prepare → execute → finalize) exists so that +each phase has a single responsibility: preparation handles validation +and gating, execution performs the actual work, and finalization +applies post-processing hooks. This separation allows the agent loop +to emit `tool_execution_end` events with the finalized data while +keeping each phase independently testable and swappable. +""" struct finalizedOutcome - toolCall::AgentToolCall - result::AgentToolResult - isError::Bool + toolCall::agentToolCall # The original tool call reference + result::agentToolResult # The final tool result (post-afterToolCall) + isError::bool # Whether the call failed or was blocked end +""" + ToolCallBatch(messages, terminate) + +A batch of tool result messages from executing one or more tool calls. +The `terminate` flag indicates whether all tools in the batch requested +termination, which causes the agent loop to stop processing further turns. + +This flag is set by the tool implementation (not the end user) to signal +that the agent should not call the LLM again. Typical use cases: + +- Task completion: a tool like `deploy` or `submit` finishes its work and + returns `terminate: true` so the agent stops instead of asking the LLM + what to do next. +- Unrecoverable error: a tool hits a fatal condition (e.g. database + connection lost, auth token expired) and returns `terminate: true` so + the agent stops with an error message rather than retrying. +- Async handoff: a tool triggers a long-running external operation and + wants the agent to stop now; the external system will later resume the + agent via `continue()`. + +If `terminate` is `false` (default), the agent loop feeds the tool results +back to the LLM for another turn. +""" struct toolCallBatch - messages::Vector{ToolResultMessage} - terminate::Bool + messages::vector{toolResultMessage} # Tool result messages for this batch + terminate::bool # Whether the batch should terminate the loop end -# ── helpers ───────────────────────────────────────────────────── +""" + createErrorToolResult(msg) -function createErrorToolResult(msg::String)::AgentToolResult - return AgentToolResult([TextContent("text", msg)], Dict{Any,Any}()) +Builds an `agentToolResult` containing a single text content item +with the provided error message and an empty details dictionary. +Used when a tool call cannot be executed due to errors. + +Returning a result instead of throwing ensures that errors at any +point in the tool call pipeline are fed back to the LLM as a tool +result message. This allows the model to see the error and decide +whether to retry, re-issue the call with different arguments, or +report failure to the user. +""" +function createErrorToolResult(msg::String)::agentToolResult + return agentToolResult([textContent("text", msg)], dict{any,any}()) end -function createToolResultMessage(f::finalizedOutcome)::ToolResultMessage - return ToolResultMessage( +""" + createToolResultMessage(f) + +Constructs a `toolResultMessage` from a `finalizedOutcome`. +Normalizes missing content to an empty array and includes +the `addedToolNames` field only when the tool dynamically +registered new tools during execution. + +This conversion is necessary because the tool result is an +`agentToolResult` used by tool implementations, while the agent +loop consumes `toolResultMessage` objects that become part of the +conversation history. The message format includes metadata like +timestamp and tool call ID that the raw result does not carry, +and it is the object emitted via `message_start`/`message_end` +events so the LLM receives the result as a proper assistant/user +message in the context window. +""" +function createToolResultMessage(f::finalizedOutcome)::toolResultMessage + return toolResultMessage( "toolResult", f.toolCall.id, f.toolCall.name, f.result.content, f.result.details, f.result.usage, - get(f.result, :addedToolNames, String[]), f.isError, now_millis() + get(f.result, :addedToolNames, string[]), f.isError, nowMillis() ) end -function shouldTerminate(batches::Vector{finalizedOutcome})::Bool +""" + shouldTerminate(finalizedCalls) + +Returns `true` only when every finalized call in the batch has +`result.terminate == true`. All tools must agree — if any tool +did not request termination, the agent continues. This prevents +a single tool that happens to set `terminate: true` (e.g. for +metadata purposes) from accidentally stopping the agent when +other tools in the batch did not intend to terminate. +""" +function shouldTerminate(batches::vector{finalizedOutcome})::bool return !isempty(batches) && all(b -> b.result.terminate, batches) end -# ── per-call preparation ──────────────────────────────────────── +""" + prepareToolCallArguments(tool, toolCall) +Calls the tool's optional `prepareArguments` hook to transform +the raw argument values from the LLM before schema validation. +If the tool has no hook or the hook returns the same object +reference, the original call is returned unchanged. + +This hook allows tools to normalize arguments that the LLM may +have produced in a non-standard format — for example, converting +a date string to a timestamp, expanding a short file path to an +absolute path, or normalizing casing. It runs before schema +validation so the validator sees the normalized form rather than +raw LLM output. +""" +function prepareToolCallArguments(tool::agentTool, toolCall::agentToolCall)::agentToolCall + if tool.prepareArguments === nothing + return toolCall + end + prepared = tool.prepareArguments(toolCall.arguments) + if prepared == toolCall.arguments + return toolCall + end + return merge(toolCall, dict(:arguments => prepared)) +end + +""" + prepareToolCall(context, assistantMsg, toolCall, config, signal) + +Resolves the tool by name, prepares and validates its arguments, +and runs the `beforeToolCall` hook. Returns a `preparedToolCall` +if successful or an `immediateOutcome` if the tool is not found, +validation fails, the hook blocks execution, or the signal is +aborted. Errors during preparation are caught and returned as +immediate error outcomes so the agent loop can feed them back +to the model. + +The key design decision here is that preparation never throws. +Every failure path returns an `immediateOutcome` with an error +result. This ensures the agent loop always receives a valid tool +result message for every tool call the assistant requested, +regardless of whether preparation succeeded. The LLM can then +use the error message to decide whether to retry with different +arguments or acknowledge the failure. +""" function prepareToolCall( - context::AgentContext, - assistantMsg::AssistantMessage, - toolCall::AgentToolCall, - config::AgentLoopConfig, - signal::Union{Nothing,AbortSignal}, -)::Union{preparedToolCall,immediateOutcome} + context::agentContext, + assistantMsg::assistantMessage, + toolCall::agentToolCall, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, +)::union{preparedToolCall,immediateOutcome} tool = find(t -> t.name == toolCall.name, context.tools) if tool === nothing @@ -60,13 +214,13 @@ function prepareToolCall( try # 1. prepare arguments (tool-specific transform) - preparedArgs = prepareToolCallArguments(tool, toolCall) - validatedArgs = validateToolArguments(tool, preparedArgs) + prepared = prepareToolCallArguments(tool, toolCall) + validatedArgs = validateToolArguments(tool, prepared) - # 2. beforeToolCall hook - if config.before_tool_call !== nothing - before = config.before_tool_call( - AssistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal + # 2. beforeToolCall hook — can block + if config.beforeToolCall !== nothing + before = config.beforeToolCall( + assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal ) if signal !== nothing && signal.aborted return immediateOutcome(createErrorToolResult("Operation aborted"), true) @@ -85,13 +239,32 @@ end # ── per-call execution ────────────────────────────────────────── +""" + executePreparedToolCall(prep, signal, emit) + +Executes the tool by calling `tool.execute()` with the validated +arguments, the abort signal, and a callback for streaming partial +results. Emits `toolExecutionUpdate` events for each partial +result batch. Waits for all pending update events to settle before +returning. Catches execution errors and returns them as an error +outcome. The `accepting` guard prevents emitting updates after +the call has finished. + +Long-running tools (e.g. file uploads, model training, web scraping) +may take seconds or minutes. The streaming update mechanism allows +UI listeners and other consumers to show progress in real time rather +than waiting for the entire call to complete. The `accepting` guard +ensures that if the tool's execute function yields after emitting +updates but before returning, no duplicate or stale updates are +emitted after the result has already been captured. +""" function executePreparedToolCall( prep::preparedToolCall, - signal::Union{Nothing,AbortSignal}, - emit::AgentEventSink, + signal::union{nothing,abortSignal}, + emit::agentEventSink, )::executedOutcome - updateEvents = Promise[] + updateEvents = promise[] accepting = true try @@ -100,7 +273,7 @@ function executePreparedToolCall( partialResult -> begin if accepting push!(updateEvents, - emit(ToolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name, + emit(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name, prep.toolCall.arguments, partialResult))) end end @@ -117,29 +290,56 @@ end # ── per-call finalization ─────────────────────────────────────── +""" + finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) + +Runs the `afterToolCall` hook on the executed result, allowing +the consumer to mutate the result content, details, usage, +termination flag, or error status. Catches errors from the +hook and converts them to error outcomes. Returns a +`finalizedOutcome` that is used to construct the tool result +message. + +The `afterToolCall` hook exists as a post-processing step that +runs after every tool call regardless of success or failure. +Common use cases include: + +- Masking sensitive data from result content before the LLM + sees it (e.g. removing API keys from error messages). +- Normalizing usage tracking data into a consistent format. +- Inspecting the result and deciding to flip `terminate: true` + based on business logic (e.g. "if deployment failed, stop + the agent rather than retrying"). +- Wrapping an error result in a friendlier message for the LLM + to understand. + +If the hook itself throws, the error is caught and the result +becomes an error outcome. This ensures the tool pipeline never +breaks due to a buggy hook. +""" function finalizeExecutedToolCall( - context::AgentContext, - assistantMsg::AssistantMessage, + context::agentContext, + assistantMsg::assistantMessage, prep::preparedToolCall, executed::executedOutcome, - config::AgentLoopConfig, - signal::Union{Nothing,AbortSignal}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, )::finalizedOutcome result = executed.result isError = executed.isError - if config.afterToolCalls !== nothing + if config.afterToolCall !== nothing try - after = config.afterToolCalls( - AfterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal + after = config.afterToolCall( + afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal ) if after !== nothing - result = merge(result, Dict(:content=>get(after,:content,result.content), + result = merge(result, dict(:content=>get(after,:content,result.content), :details=>get(after,:details,result.details), :usage=>get(after,:usage,result.usage), :terminate=>get(after,:terminate,result.terminate))) - isError = get(after, :is_error, isError) + isError = get(after, :isError, isError) end catch err result = createErrorToolResult(sprint(showerror, err)) @@ -150,27 +350,59 @@ function finalizeExecutedToolCall( return finalizedOutcome(prep.toolCall, result, isError) end -function emitToolExecutionEnd(finalized::finalizedOutcome, emit::AgentEventSink) - emit(ToolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, +""" + emitToolExecutionEnd(finalized, emit) + +Emits the `toolExecutionEnd` event with the finalized outcome, +signalling to listeners that the tool call has completed. + +This event is part of the tool execution lifecycle: +`tool_execution_start` → (zero or more `tool_execution_update` events) → +`tool_execution_end`. Listeners (such as the TUI or logging systems) +use this lifecycle to track individual tool calls. The event carries +the final result so listeners have all the data they need without +requiring external state lookups. +""" +function emitToolExecutionEnd(finalized::finalizedOutcome, emit::agentEventSink) + emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, finalized.result, finalized.isError)) end # ── sequential execution ──────────────────────────────────────── +""" + executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) + +Executes tool calls one at a time in the order they appear. For each +call: emits `toolExecutionStart`, runs `prepareToolCall`, +then either resolves the immediate outcome or executes/finalizes +the prepared call. Emits `toolExecutionEnd` and creates the tool +result message before proceeding to the next call. Respects the +abort signal — if aborted, remaining calls are skipped. Returns +a batch with `terminate` determined by whether all results set +the termination flag. + +Sequential execution is required when tool calls have implicit +dependencies — for example, a `create_database` tool must complete +before `create_table` can reference it. It is also the safer +default because it prevents race conditions when multiple tools +share state (e.g. writing to the same file or API rate limits). +Use parallel only when you are confident the tools are independent. +""" function executeToolCallsSequential( - context::AgentContext, - assistantMsg::AssistantMessage, - toolCalls::Vector{AgentToolCall}, - config::AgentLoopConfig, - signal::Union{Nothing,AbortSignal}, - emit::AgentEventSink, -)::toolCallBatch + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::agentEventSink, +)::agentToolCallBatch finalizedCalls = finalizedOutcome[] - messages = ToolResultMessage[] + messages = toolResultMessage[] for tc in toolCalls - emit(ToolExecStartEvent(tc.id, tc.name, tc.arguments)) + emit(toolExecStartEvent(tc.id, tc.name, tc.arguments)) prep = prepareToolCall(context, assistantMsg, tc, config, signal) @@ -195,20 +427,40 @@ end # ── parallel execution ────────────────────────────────────────── -function executeToolCallsParallel( - context::AgentContext, - assistantMsg::AssistantMessage, - toolCalls::Vector{AgentToolCall}, - config::AgentLoopConfig, - signal::Union{Nothing,AbortSignal}, - emit::AgentEventSink, -)::toolCallBatch +""" + executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) - # Each entry: finalizedOutcome (already done) or Task → finalizedOutcome (pending) - entries = Union{finalizedOutcome,Task{finalizedOutcome}}[] +Prepares all tool calls concurrently and spawns a task for each +prepared call. Immediate outcomes are resolved instantly. Task +entries are collected in order, then `fetch`ed to await all +concurrent executions. Tool result messages are created from +finalized outcomes in order and returned as a batch. Respects +the abort signal — if aborted during preparation, remaining +calls are skipped. Finalization order preserves the original +call order. + +Parallel execution is appropriate when the assistant requests +independent tools — for example, reading multiple files, querying +separate databases, or making independent API calls. It reduces +wall-clock time compared to sequential execution. The tradeoff is +that parallel calls can overwhelm external resources (rate limits, +connection pools, disk I/O). Finalization preserves the original +call order so tool result messages appear in the same order the +assistant requested them, regardless of which call finishes first. +""" +function executeToolCallsParallel( + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::agentEventSink, +)::agentToolCallBatch + + entries = union{finalizedOutcome,task{finalizedOutcome}}[] for tc in toolCalls - emit(ToolExecStartEvent(tc.id, tc.name, tc.arguments)) + emit(toolExecStartEvent(tc.id, tc.name, tc.arguments)) prep = prepareToolCall(context, assistantMsg, tc, config, signal) @@ -217,8 +469,7 @@ function executeToolCallsParallel( emitToolExecutionEnd(finalized, emit) push!(entries, finalized) else - # spawn lazy computation task - task = Task() do + task = task() do executed = executePreparedToolCall(prep, signal, emit) finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) emitToolExecutionEnd(finalized, emit) @@ -233,14 +484,13 @@ function executeToolCallsParallel( end end - # Wait for all tasks, collect in order finalizedCalls = finalizedOutcome[] for entry in entries - outcome = entry isa Task ? fetch(entry) : entry + outcome = entry isa task ? fetch(entry) : entry push!(finalizedCalls, outcome) end - messages = ToolResultMessage[] + messages = toolResultMessage[] for f in finalizedCalls push!(messages, createToolResultMessage(f)) end @@ -248,25 +498,99 @@ function executeToolCallsParallel( return toolCallBatch(messages, shouldTerminate(finalizedCalls)) end -# ── dispatcher ────────────────────────────────────────────────── +""" + executeToolCalls(context, assistantMsg, toolCalls, config, signal, emit) + +Dispatches to sequential or parallel execution. Uses sequential mode +when `config.toolExecution == "sequential"` or when any of the +tool calls reference a tool with `executionMode: "sequential"`. +Otherwise uses parallel execution. This is the entry point called +from `streamAssistantResponse` in the agent loop. + +The sequential mode takes priority over parallel because it is the +safe default. If even one tool in a batch is marked sequential, all +tools execute sequentially — this prevents a single dependent tool +from racing with an otherwise independent one. The per-tool +`executionMode` allows fine-grained control (e.g. most tools are +parallel but a specific write tool is sequential), while the config-level +`toolExecution` provides a global override. +""" function executeToolCalls( - context::AgentContext, - assistantMsg::AssistantMessage, - toolCalls::Vector{AgentToolCall}, - config::AgentLoopConfig, - signal::Union{Nothing,AbortSignal}, - emit::AgentEventSink, -)::toolCallBatch + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::agentEventSink, +)::agentToolCallBatch - # Check if any tool is marked sequential, or config forces sequential hasSequential = any(tc -> any(t -> t.name == tc.name && get(t.executionMode, "parallel") == "sequential", context.tools), toolCalls) - if config.tool_execution == "sequential" || hasSequential + if config.toolExecution == "sequential" || hasSequential return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) else return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) end end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MCTSexamplePrompt.py b/src/MCTSexamplePrompt.py deleted file mode 100644 index 9b52f9e..0000000 --- a/src/MCTSexamplePrompt.py +++ /dev/null @@ -1,537 +0,0 @@ -https://github.com/andyz245/LanguageAgentTreeSearch/blob/main/hotpot/hotpot.py - - -standard_prompt = ''' -Write a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: {input} -''' - -reflection_prompt = '''You are an advanced reasoning agent that can improve based on self refection. You will be given a previous reasoning trial in which you were given access to an Docstore API environment and a question to answer. You were unsuccessful in answering the question either because you guessed the wrong answer with Finish[], or you used up your set number of reasoning steps. In a few sentences, Diagnose a possible reason for failure and devise a new, concise, high level plan that aims to mitigate the same failure. Use complete sentences. -Here are some examples: -Previous Trial: -Question: The Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of what? -Thought 1: I need to search Rome Protocols, find the three Prime Ministers, then find what they were assassinated as part of. -Action 1: Search[Rome Protocols] -Observation 1: The Rome Protocols were a series of three international agreements concluded in Rome on 17 March 1934 between the governments of Austria, Hungary and Italy. They were signed by Italian Prime Minister Benito Mussolini, Austrian Chancellor Engelbert Dollfuss and Hungarian Prime Minister Gyula Gömbös. All of the protocols went into effect on 12 July 1934 and were registered in League of Nations Treaty Series on 12 December 1934. -Thought 2: The three Prime Ministers were Benito Mussolini, Engelbert Dollfuss, and Gyula Gömbös. I need to search Benito Mussolini and find what he was assassinated as part of. -Action 2: Search[Benito Mussolini] -Observation 2: Benito Amilcare Andrea Mussolini (UK: MU(U)SS-ə-LEE-nee, US: MOOSS-, Italian: [beˈniːto aˈmilkare anˈdrɛːa mussoˈliːni]; 29 July 1883 – 28 April 1945) was an Italian politician and journalist who founded and led the National Fascist Party (PNF). He was Prime Minister of Italy from the March on Rome in 1922 until his deposition in 1943, as well as "Duce" of Italian fascism from the establishment of the Italian Fasces of Combat in 1919 until his summary execution in 1945 by Italian partisans. As dictator of Italy and principal founder of fascism, Mussolini inspired and supported the international spread of fascist movements during the inter-war period.Mussolini was originally a socialist politician and a journalist at the Avanti! newspaper. In 1912, he became a member of the National Directorate of the Italian Socialist Party (PSI), but he was expelled from the PSI for advocating military intervention in World War I, in opposition to the party's stance on neutrality. In 1914, Mussolini founded a new journal, Il Popolo d'Italia, and served in the Royal Italian Army during the war until he was wounded and discharged in 1917. Mussolini denounced the PSI, his views now centering on Italian nationalism instead of socialism, and later founded the fascist movement which came to oppose egalitarianism and class conflict, instead advocating "revolutionary nationalism" transcending class lines. On 31 October 1922, following the March on Rome (28–30 October), Mussolini was appointed prime minister by King Victor Emmanuel III, becoming the youngest individual to hold the office up to that time. After removing all political opposition through his secret police and outlawing labor strikes, Mussolini and his followers consolidated power through a series of laws that transformed the nation into a one-party dictatorship. Within five years, Mussolini had established dictatorial authority by both legal and illegal means and aspired to create a totalitarian state. In 1929, Mussolini signed the Lateran Treaty with the Holy See to establish Vatican City. -Mussolini's foreign policy aimed to restore the ancient grandeur of the Roman Empire by expanding Italian colonial possessions and the fascist sphere of influence. In the 1920s, he ordered the Pacification of Libya, instructed the bombing of Corfu over an incident with Greece, established a protectorate over Albania, and incorporated the city of Fiume into the Italian state via agreements with Yugoslavia. In 1936, Ethiopia was conquered following the Second Italo-Ethiopian War and merged into Italian East Africa (AOI) with Eritrea and Somalia. In 1939, Italian forces annexed Albania. Between 1936 and 1939, Mussolini ordered the successful Italian military intervention in Spain in favor of Francisco Franco during the Spanish Civil War. Mussolini's Italy initially tried to avoid the outbreak of a second global war, sending troops at the Brenner Pass to delay Anschluss and taking part in the Stresa Front, the Lytton Report, the Treaty of Lausanne, the Four-Power Pact and the Munich Agreement. However, Italy then alienated itself from Britain and France by aligning with Germany and Japan. Germany invaded Poland on 1 September 1939, resulting in declarations of war by France and the UK and the start of World War II. -On 10 June 1940, Mussolini decided to enter the war on the Axis side. Despite initial success, the subsequent Axis collapse on multiple fronts and eventual Allied invasion of Sicily made Mussolini lose the support of the population and members of the Fascist Party. As a consequence, early on 25 July 1943, the Grand Council of Fascism passed a motion of no confidence in Mussolini; later that day King Victor Emmanuel III dismissed him as head of government and had him placed in custody, appointing Pietro Badoglio to succeed him as Prime Minister. After the king agreed to an armistice with the Allies, on 12 September 1943 Mussolini was rescued from captivity in the Gran Sasso raid by German paratroopers and Waffen-SS commandos led by Major Otto-Harald Mors. Adolf Hitler, after meeting with the rescued former dictator, then put Mussolini in charge of a puppet regime in northern Italy, the Italian Social Republic (Italian: Repubblica Sociale Italiana, RSI), informally known as the Salò Republic, causing a civil war. In late April 1945, in the wake of near total defeat, Mussolini and his mistress Clara Petacci attempted to flee to Switzerland, but both were captured by Italian communist partisans and summarily executed by firing squad on 28 April 1945 near Lake Como. The bodies of Mussolini and his mistress were then taken to Milan, where they were hung upside down at a service station to publicly confirm their demise. -Thought 3: Benito Mussolini was assassinated as part of the collapse of the Axis powers during World War II. So the Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of World War II. -Action 3: Finish[World War II] - -Reflection: I searched one of the prime ministers involved in the signing, then attemted to answer right away. I should have searched each of the prime ministers, then looked up 'death' on each of their pages in order to get more information before answering. - -Previous Trial: -Question: Kam Heskin plays Paige Morgan in a 2004 film directed by who? -Thought 1: I need to search Kam Heskin and Paige Morgan, find the 2004 film they are in, then find the director of the film. -Action 1: Search[Kam Heskin] -Observation 1: Kam Erika Heskin (born May 8, 1973) is an American actress. She began her career playing Caitlin Deschanel on the NBC daytime soap opera Sunset Beach (1998–1999), before appearing in films Planet of the Apes (2001) and Catch Me If You Can (2002). Heskin went on in 2003 to play Elizabeth Bennet in an independent film Pride & Prejudice: A Latter-Day Comedy, Robin in Charmed (2004), and Paige Morgan in The Prince and Me film franchise (2006–2010). -Thought 2: Kam Heskin plays Paige Morgan in The Prince and Me film franchise. I need to search The Prince and Me film franchise and find the director of the 2004 film. -Action 2: Search[The Prince and Me film franchise] -Observation 2: Could not find [The Prince and Me film franchise]. Similar: ['Kam Heskin', 'Prince of Persia', 'Harry Potter and the Half-Blood Prince (film)', 'Prince of Persia: The Sands of Time (film)', 'The Chronicles of Narnia (film series)', 'Majestic Prince (franchise)', 'Kingsman (franchise)', 'The Little Prince (2015 film)', 'Shrek (franchise)', 'Harry Potter (film series)'] -Thought 3: I need to search The Prince and Me (2004 film) instead. -Action 3: Search[The Prince and Me (2004 film)] -Observation 3: Could not find [The Prince and Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Kam Heskin', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince of Persia', 'Brooklynn Prince', 'Prince (musician)'] -Thought 4: I need to search The Prince & Me (2004 film). -Action 4: Search[The Prince & Me (2004 film)] -Observation 4: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince'] -Thought 5: I need to search The Prince & Me (2004 film) instead. -Action 5: Search[The Prince & Me (2004 film)] -Observation 5: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince'] -Thought 6: I need to search The Prince & Me (2004 film). -Action 6: Search[The Prince & Me (2004 film)] -Observation 6: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince'] - -Reflection: I got stuck in a loop where I kept trying to search 'The Prince & Me (2004 film)' but the page could not be found. Instead I should have tried to search the similar results that had a similar name to see and they were made in 2004. - -Previous Trial: -Question: The Oberoi family is part of a hotel company that has a head office in what city? -Thought 1: I need to search the Oberoi family and find the city where the hotel company's head office is located. -Action 1: Search[Oberoi family] -Observation 1: The Oberoi Group is a luxury hotel group with its head office in New Delhi, India.[2] Founded in 1934, the company owns and operates 32 luxury hotels and two river cruise ships in 7 countries, primarily under its Oberoi Hotels & Resorts and Trident brands.[3] The group also operates The Oberoi Centre for Learning and Development, which is regarded as one of Asia's top institutions for hospitality education.[4]. The foundations of the Oberoi Group date back to 1934 when The Rai Bahadur Mohan Singh Oberoi, the founder of the group, bought two properties: the Maidens in Delhi and the Clarke's in Shimla.[5] In the following years Oberoi, assisted by his two sons, Tilak Raj Singh Oberoi and Prithvi Raj Singh Oberoi (P.R.S. Oberoi -Thought 2: The Oberoi Group has its head office in New Delhi, India. I just need to finish with New Delhi. -Action 2: Finish[New Delhi] -Observation 2: Episode finished, reward = 0 - -Reflection: I appeared to have retrieved the correct information about The Oberoi Family and the location of it's head office, and provided a corresponding answer. However this answer does not exactly match the ground truth answer so I should try a different wording, such as Delhi. - -Previous trial: -{trajectory}Reflection:''' - -cot_prompt = ''' -Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county? -Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded. -Action 1: Search[Guitars for Wounded Warriors] -Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales. -Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in -Action 2: Search[New Paltz (village), New York] -Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3] -Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County. -Action 3: Finish[Ulster County] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -{input} -''' - -cot_prompt_short = ''' -Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -{input} -''' - -cot_prompt_feedback_short = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -You have attempted to answer the following question before and failed. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question. - -{trajectories} - -{input} -''' - -cot_prompt_feedback = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county? -Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded. -Action 1: Search[Guitars for Wounded Warriors] -Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales. -Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in -Action 2: Search[New Paltz (village), New York] -Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3] -Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County. -Action 3: Finish[Ulster County] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -You have attempted to answer the following question before and failed, either because your reasoning for the answer was incorrect or the phrasing of your response did not exactly match the answer. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question. - -{trajectories} -When providing the thought and action for the current trial, that into account these failed trajectories and make sure not to repeat the same mistakes and incorrect answers. - -{input} -''' - -vote_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a list of trajectories, decide which trajectory is most promising. Analyze each trajectory in detail and consider possible errors, then conclude in the last line "The best trajectory is {s}", where s the integer id of the trajectory. -''' - -compare_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Briefly analyze the correctness of the following two trajectories. Conclude in the last line "The more correct trajectory is 1", "The more correct trajectory is 2", or "The two trajectories are similarly correct". -''' - -score_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, analyze the following trajectory, then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. -''' - -value_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -Thus the correctness score is 3 - -Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe? -Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United. -Action 1: Search[David Beckham's football manager] -Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park']. -Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager. -Action 2: Search[Manchester United] -Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields. -Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe. -Action 3: Search[Manchester United managers] -Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers. -Action 4: Search[list of Manchester United managers] -Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thus the correctness score is 4 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -Thus the correctness score is 10 - -{input} -''' - -value_prompt_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -Thus the correctness score is 3 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -Thus the correctness score is 10 - -{trajectories} -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -Thus the correctness score is 10 - -{input} -''' - -value_prompt_reasoning = '''You are an advanced reasoning agent that can improve based on self refection. Analyze the trajectories of your previous solutions to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested. -Thus the correctness score is 3 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action. -Thus the correctness score is 10 - -Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe? -Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United. -Action 1: Search[David Beckham's football manager] -Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park']. -Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager. -Action 2: Search[Manchester United] -Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields. -Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe. -Action 3: Search[Manchester United managers] -Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers. -Action 4: Search[list of Manchester United managers] -Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager. -Thus the correctness score is 4 - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search. -Thus the correctness score is 10 - -{input} -''' - -value_prompt_reasoning_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners it is reasonable to checkof the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested. -Thus the correctness score is 3 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action. -Thus the correctness score is 10 - -Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe? -Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United. -Action 1: Search[David Beckham's football manager] -Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park']. -Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager. -Action 2: Search[Manchester United] -Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields. -Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe. -Action 3: Search[Manchester United managers] -Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers. -Action 4: Search[list of Manchester United managers] -Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager. -Thus the correctness score is 4 - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search. -Thus the correctness score is 10 - -{trajectories} - -{input} -''' - -value_prompt_reasoning_feedback_short = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested. -Thus the correctness score is 3 - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search. -Thus the correctness score is 10 - -{trajectories} - -{input} -''' - -rap_prompt = ''' -Solve a question answering task with interleaving Thought and Action steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -Provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Thought 2: Then I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains] -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county? -Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded. -Action 1: Search[Guitars for Wounded Warriors] -Thought 2: I need to search New Paltz (village), New York and find which county it is in -Action 2: Search[New Paltz (village), New York] -Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County. -Action 3: Finish[Ulster County] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Thought 2: I can look up "named after" for finding the specific individual Milhouse is named after. -Action 2: Lookup[named after] -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -{input} -''' \ No newline at end of file diff --git a/src/agentCore.jl b/src/agentCore.jl index 71f3bdb..2860165 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -191,6 +191,8 @@ function _process_message(agent::yiemAgent, msg)::assistantMessage # call executeToolCalls() + # save toolResults to agent._state.messages + # else # break out of while loop diff --git a/src/type.jl b/src/type.jl index 71276fe..897e72d 100644 --- a/src/type.jl +++ b/src/type.jl @@ -295,7 +295,7 @@ function agentState( end -struct toolCall # A tool invocation from the LLM +struct agentToolCall # A tool invocation from the LLM type::String # Always "function" id::String # Unique tool call identifier name::String # Tool name @@ -528,6 +528,15 @@ end + + + + + + + + + -- 2.52.0 From ec28e0ff54695d00ba9e11f15698a810f2866e8a Mon Sep 17 00:00:00 2001 From: narawat Date: Wed, 5 Aug 2026 19:09:56 +0700 Subject: [PATCH 24/50] update --- etc.jl | 594 ------------------------------------ src/agentCore.jl | 765 +++++++++++++++++++++++++++++++++++++++++++++++ src/type.jl | 188 ++++++++++++ 3 files changed, 953 insertions(+), 594 deletions(-) diff --git a/etc.jl b/etc.jl index beb9929..4ce0950 100644 --- a/etc.jl +++ b/etc.jl @@ -1,596 +1,2 @@ # ── executeToolCalls() Julia pseudo code ────────────────────────── # Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit - -""" - preparedToolCall(tool, toolCall, args) - -Intermediate state between tool call validation and execution. -This struct is created after `prepareToolCall` succeeds and serves -as the bridge to the execution phase. Keeping the resolved tool, -original call metadata, and validated args together avoids repeated -lookups and allows the execution phase to access all necessary data -without carrying the full context through the call chain. -""" -struct preparedToolCall - tool::agentTool # The resolved tool definition from the context - toolCall::agentToolCall # The original tool call from the assistant - args::any # Validated (and coerced) argument values -end - -""" - immediateOutcome(result, isError) - -A tool call that was resolved without actual execution — either -because the tool was not found, validation failed, or a -`beforeToolCall` hook blocked the call. The result is produced -immediately and emitted as a tool result message. - -Returning an outcome instead of throwing an exception is intentional: -it lets the agent feed the error back to the LLM as a tool result so -the model can recover — for example, by re-issuing a tool call with -corrected arguments after a validation failure. -""" -struct immediateOutcome - result::agentToolResult # The pre-computed tool result - isError::bool # Whether this outcome represents an error -end - -""" - executedOutcome(result, isError) - -A tool call that has been executed by `tool.execute()` but has not -yet been through the `afterToolCall` hook. This intermediate state -is necessary because the hook may mutate the result (content, usage, -termination, error status). Keeping execution and finalization separate -allows the hook to inspect the raw result and decide whether to -transform it or replace it entirely. -""" -struct executedOutcome - result::agentToolResult # The tool's execution result - isError::bool # Whether execution raised an error -end - -""" - finalizedOutcome(toolCall, result, isError) - -The complete outcome of a tool call after both execution and the -`afterToolCall` hook. This is the final form that is used to -construct the `toolResultMessage` emitted to the agent loop. - -The three-phase design (prepare → execute → finalize) exists so that -each phase has a single responsibility: preparation handles validation -and gating, execution performs the actual work, and finalization -applies post-processing hooks. This separation allows the agent loop -to emit `tool_execution_end` events with the finalized data while -keeping each phase independently testable and swappable. -""" -struct finalizedOutcome - toolCall::agentToolCall # The original tool call reference - result::agentToolResult # The final tool result (post-afterToolCall) - isError::bool # Whether the call failed or was blocked -end - -""" - ToolCallBatch(messages, terminate) - -A batch of tool result messages from executing one or more tool calls. -The `terminate` flag indicates whether all tools in the batch requested -termination, which causes the agent loop to stop processing further turns. - -This flag is set by the tool implementation (not the end user) to signal -that the agent should not call the LLM again. Typical use cases: - -- Task completion: a tool like `deploy` or `submit` finishes its work and - returns `terminate: true` so the agent stops instead of asking the LLM - what to do next. -- Unrecoverable error: a tool hits a fatal condition (e.g. database - connection lost, auth token expired) and returns `terminate: true` so - the agent stops with an error message rather than retrying. -- Async handoff: a tool triggers a long-running external operation and - wants the agent to stop now; the external system will later resume the - agent via `continue()`. - -If `terminate` is `false` (default), the agent loop feeds the tool results -back to the LLM for another turn. -""" -struct toolCallBatch - messages::vector{toolResultMessage} # Tool result messages for this batch - terminate::bool # Whether the batch should terminate the loop -end - -""" - createErrorToolResult(msg) - -Builds an `agentToolResult` containing a single text content item -with the provided error message and an empty details dictionary. -Used when a tool call cannot be executed due to errors. - -Returning a result instead of throwing ensures that errors at any -point in the tool call pipeline are fed back to the LLM as a tool -result message. This allows the model to see the error and decide -whether to retry, re-issue the call with different arguments, or -report failure to the user. -""" -function createErrorToolResult(msg::String)::agentToolResult - return agentToolResult([textContent("text", msg)], dict{any,any}()) -end - -""" - createToolResultMessage(f) - -Constructs a `toolResultMessage` from a `finalizedOutcome`. -Normalizes missing content to an empty array and includes -the `addedToolNames` field only when the tool dynamically -registered new tools during execution. - -This conversion is necessary because the tool result is an -`agentToolResult` used by tool implementations, while the agent -loop consumes `toolResultMessage` objects that become part of the -conversation history. The message format includes metadata like -timestamp and tool call ID that the raw result does not carry, -and it is the object emitted via `message_start`/`message_end` -events so the LLM receives the result as a proper assistant/user -message in the context window. -""" -function createToolResultMessage(f::finalizedOutcome)::toolResultMessage - return toolResultMessage( - "toolResult", f.toolCall.id, f.toolCall.name, - f.result.content, f.result.details, f.result.usage, - get(f.result, :addedToolNames, string[]), f.isError, nowMillis() - ) -end - -""" - shouldTerminate(finalizedCalls) - -Returns `true` only when every finalized call in the batch has -`result.terminate == true`. All tools must agree — if any tool -did not request termination, the agent continues. This prevents -a single tool that happens to set `terminate: true` (e.g. for -metadata purposes) from accidentally stopping the agent when -other tools in the batch did not intend to terminate. -""" -function shouldTerminate(batches::vector{finalizedOutcome})::bool - return !isempty(batches) && all(b -> b.result.terminate, batches) -end - -""" - prepareToolCallArguments(tool, toolCall) - -Calls the tool's optional `prepareArguments` hook to transform -the raw argument values from the LLM before schema validation. -If the tool has no hook or the hook returns the same object -reference, the original call is returned unchanged. - -This hook allows tools to normalize arguments that the LLM may -have produced in a non-standard format — for example, converting -a date string to a timestamp, expanding a short file path to an -absolute path, or normalizing casing. It runs before schema -validation so the validator sees the normalized form rather than -raw LLM output. -""" -function prepareToolCallArguments(tool::agentTool, toolCall::agentToolCall)::agentToolCall - if tool.prepareArguments === nothing - return toolCall - end - prepared = tool.prepareArguments(toolCall.arguments) - if prepared == toolCall.arguments - return toolCall - end - return merge(toolCall, dict(:arguments => prepared)) -end - -""" - prepareToolCall(context, assistantMsg, toolCall, config, signal) - -Resolves the tool by name, prepares and validates its arguments, -and runs the `beforeToolCall` hook. Returns a `preparedToolCall` -if successful or an `immediateOutcome` if the tool is not found, -validation fails, the hook blocks execution, or the signal is -aborted. Errors during preparation are caught and returned as -immediate error outcomes so the agent loop can feed them back -to the model. - -The key design decision here is that preparation never throws. -Every failure path returns an `immediateOutcome` with an error -result. This ensures the agent loop always receives a valid tool -result message for every tool call the assistant requested, -regardless of whether preparation succeeded. The LLM can then -use the error message to decide whether to retry with different -arguments or acknowledge the failure. -""" -function prepareToolCall( - context::agentContext, - assistantMsg::assistantMessage, - toolCall::agentToolCall, - config::agentLoopConfig, - signal::union{nothing,abortSignal}, -)::union{preparedToolCall,immediateOutcome} - - tool = find(t -> t.name == toolCall.name, context.tools) - if tool === nothing - return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true) - end - - try - # 1. prepare arguments (tool-specific transform) - prepared = prepareToolCallArguments(tool, toolCall) - validatedArgs = validateToolArguments(tool, prepared) - - # 2. beforeToolCall hook — can block - if config.beforeToolCall !== nothing - before = config.beforeToolCall( - assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal - ) - if signal !== nothing && signal.aborted - return immediateOutcome(createErrorToolResult("Operation aborted"), true) - end - if before !== nothing && before.block - return immediateOutcome( - createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true) - end - end - - return preparedToolCall(tool, toolCall, validatedArgs) - catch err - return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true) - end -end - -# ── per-call execution ────────────────────────────────────────── - -""" - executePreparedToolCall(prep, signal, emit) - -Executes the tool by calling `tool.execute()` with the validated -arguments, the abort signal, and a callback for streaming partial -results. Emits `toolExecutionUpdate` events for each partial -result batch. Waits for all pending update events to settle before -returning. Catches execution errors and returns them as an error -outcome. The `accepting` guard prevents emitting updates after -the call has finished. - -Long-running tools (e.g. file uploads, model training, web scraping) -may take seconds or minutes. The streaming update mechanism allows -UI listeners and other consumers to show progress in real time rather -than waiting for the entire call to complete. The `accepting` guard -ensures that if the tool's execute function yields after emitting -updates but before returning, no duplicate or stale updates are -emitted after the result has already been captured. -""" -function executePreparedToolCall( - prep::preparedToolCall, - signal::union{nothing,abortSignal}, - emit::agentEventSink, -)::executedOutcome - - updateEvents = promise[] - accepting = true - - try - result = prep.tool.execute( - prep.toolCall.id, prep.args, signal, - partialResult -> begin - if accepting - push!(updateEvents, - emit(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name, - prep.toolCall.arguments, partialResult))) - end - end - ) - accepting = false - wait.(updateEvents) - return executedOutcome(result, false) - catch err - accepting = false - wait.(updateEvents) - return executedOutcome(createErrorToolResult(sprint(showerror, err)), true) - end -end - -# ── per-call finalization ─────────────────────────────────────── - -""" - finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) - -Runs the `afterToolCall` hook on the executed result, allowing -the consumer to mutate the result content, details, usage, -termination flag, or error status. Catches errors from the -hook and converts them to error outcomes. Returns a -`finalizedOutcome` that is used to construct the tool result -message. - -The `afterToolCall` hook exists as a post-processing step that -runs after every tool call regardless of success or failure. -Common use cases include: - -- Masking sensitive data from result content before the LLM - sees it (e.g. removing API keys from error messages). -- Normalizing usage tracking data into a consistent format. -- Inspecting the result and deciding to flip `terminate: true` - based on business logic (e.g. "if deployment failed, stop - the agent rather than retrying"). -- Wrapping an error result in a friendlier message for the LLM - to understand. - -If the hook itself throws, the error is caught and the result -becomes an error outcome. This ensures the tool pipeline never -breaks due to a buggy hook. -""" -function finalizeExecutedToolCall( - context::agentContext, - assistantMsg::assistantMessage, - prep::preparedToolCall, - executed::executedOutcome, - config::agentLoopConfig, - signal::union{nothing,abortSignal}, -)::finalizedOutcome - - result = executed.result - isError = executed.isError - - if config.afterToolCall !== nothing - try - after = config.afterToolCall( - afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal - ) - if after !== nothing - result = merge(result, dict(:content=>get(after,:content,result.content), - :details=>get(after,:details,result.details), - :usage=>get(after,:usage,result.usage), - :terminate=>get(after,:terminate,result.terminate))) - isError = get(after, :isError, isError) - end - catch err - result = createErrorToolResult(sprint(showerror, err)) - isError = true - end - end - - return finalizedOutcome(prep.toolCall, result, isError) -end - -""" - emitToolExecutionEnd(finalized, emit) - -Emits the `toolExecutionEnd` event with the finalized outcome, -signalling to listeners that the tool call has completed. - -This event is part of the tool execution lifecycle: -`tool_execution_start` → (zero or more `tool_execution_update` events) → -`tool_execution_end`. Listeners (such as the TUI or logging systems) -use this lifecycle to track individual tool calls. The event carries -the final result so listeners have all the data they need without -requiring external state lookups. -""" -function emitToolExecutionEnd(finalized::finalizedOutcome, emit::agentEventSink) - emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, - finalized.result, finalized.isError)) -end - -# ── sequential execution ──────────────────────────────────────── - -""" - executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) - -Executes tool calls one at a time in the order they appear. For each -call: emits `toolExecutionStart`, runs `prepareToolCall`, -then either resolves the immediate outcome or executes/finalizes -the prepared call. Emits `toolExecutionEnd` and creates the tool -result message before proceeding to the next call. Respects the -abort signal — if aborted, remaining calls are skipped. Returns -a batch with `terminate` determined by whether all results set -the termination flag. - -Sequential execution is required when tool calls have implicit -dependencies — for example, a `create_database` tool must complete -before `create_table` can reference it. It is also the safer -default because it prevents race conditions when multiple tools -share state (e.g. writing to the same file or API rate limits). -Use parallel only when you are confident the tools are independent. -""" -function executeToolCallsSequential( - context::agentContext, - assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, - config::agentLoopConfig, - signal::union{nothing,abortSignal}, - emit::agentEventSink, -)::agentToolCallBatch - - finalizedCalls = finalizedOutcome[] - messages = toolResultMessage[] - - for tc in toolCalls - emit(toolExecStartEvent(tc.id, tc.name, tc.arguments)) - - prep = prepareToolCall(context, assistantMsg, tc, config, signal) - - if prep isa immediateOutcome - finalized = finalizedOutcome(tc, prep.result, prep.isError) - else - executed = executePreparedToolCall(prep, signal, emit) - finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) - end - - emitToolExecutionEnd(finalized, emit) - push!(messages, createToolResultMessage(finalized)) - push!(finalizedCalls, finalized) - - if signal !== nothing && signal.aborted - break - end - end - - return toolCallBatch(messages, shouldTerminate(finalizedCalls)) -end - -# ── parallel execution ────────────────────────────────────────── - -""" - executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) - -Prepares all tool calls concurrently and spawns a task for each -prepared call. Immediate outcomes are resolved instantly. Task -entries are collected in order, then `fetch`ed to await all -concurrent executions. Tool result messages are created from -finalized outcomes in order and returned as a batch. Respects -the abort signal — if aborted during preparation, remaining -calls are skipped. Finalization order preserves the original -call order. - -Parallel execution is appropriate when the assistant requests -independent tools — for example, reading multiple files, querying -separate databases, or making independent API calls. It reduces -wall-clock time compared to sequential execution. The tradeoff is -that parallel calls can overwhelm external resources (rate limits, -connection pools, disk I/O). Finalization preserves the original -call order so tool result messages appear in the same order the -assistant requested them, regardless of which call finishes first. -""" -function executeToolCallsParallel( - context::agentContext, - assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, - config::agentLoopConfig, - signal::union{nothing,abortSignal}, - emit::agentEventSink, -)::agentToolCallBatch - - entries = union{finalizedOutcome,task{finalizedOutcome}}[] - - for tc in toolCalls - emit(toolExecStartEvent(tc.id, tc.name, tc.arguments)) - - prep = prepareToolCall(context, assistantMsg, tc, config, signal) - - if prep isa immediateOutcome - finalized = finalizedOutcome(tc, prep.result, prep.isError) - emitToolExecutionEnd(finalized, emit) - push!(entries, finalized) - else - task = task() do - executed = executePreparedToolCall(prep, signal, emit) - finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) - emitToolExecutionEnd(finalized, emit) - return finalized - end - schedule(task) - push!(entries, task) - end - - if signal !== nothing && signal.aborted - break - end - end - - finalizedCalls = finalizedOutcome[] - for entry in entries - outcome = entry isa task ? fetch(entry) : entry - push!(finalizedCalls, outcome) - end - - messages = toolResultMessage[] - for f in finalizedCalls - push!(messages, createToolResultMessage(f)) - end - - return toolCallBatch(messages, shouldTerminate(finalizedCalls)) -end - - -""" - executeToolCalls(context, assistantMsg, toolCalls, config, signal, emit) - -Dispatches to sequential or parallel execution. Uses sequential mode -when `config.toolExecution == "sequential"` or when any of the -tool calls reference a tool with `executionMode: "sequential"`. -Otherwise uses parallel execution. This is the entry point called -from `streamAssistantResponse` in the agent loop. - -The sequential mode takes priority over parallel because it is the -safe default. If even one tool in a batch is marked sequential, all -tools execute sequentially — this prevents a single dependent tool -from racing with an otherwise independent one. The per-tool -`executionMode` allows fine-grained control (e.g. most tools are -parallel but a specific write tool is sequential), while the config-level -`toolExecution` provides a global override. -""" -function executeToolCalls( - context::agentContext, - assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, - config::agentLoopConfig, - signal::union{nothing,abortSignal}, - emit::agentEventSink, -)::agentToolCallBatch - - hasSequential = any(tc -> - any(t -> t.name == tc.name && get(t.executionMode, "parallel") == "sequential", - context.tools), toolCalls) - - if config.toolExecution == "sequential" || hasSequential - return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) - else - return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) - end -end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/agentCore.jl b/src/agentCore.jl index 2860165..2c2cca7 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -213,6 +213,771 @@ function _process_message(agent::yiemAgent, msg)::assistantMessage end +""" + executeToolCalls(context, assistantMsg, toolCalls, config, signal, emit) + +Dispatches to sequential or parallel execution. Uses sequential mode +when `config.toolExecution == "sequential"` or when any of the +tool calls reference a tool with `executionMode: "sequential"`. +Otherwise uses parallel execution. This is the entry point called +from `streamAssistantResponse` in the agent loop. + +The sequential mode takes priority over parallel because it is the +safe default. If even one tool in a batch is marked sequential, all +tools execute sequentially — this prevents a single dependent tool +from racing with an otherwise independent one. The per-tool +`executionMode` allows fine-grained control (e.g. most tools are +parallel but a specific write tool is sequential), while the config-level +`toolExecution` provides a global override. +""" +function executeToolCalls( + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::Function, +)::agentToolCallBatch + + hasSequential = false + for tc in toolCalls + for t in context.tools + if t.name == tc.name && get(t.executionMode, "parallel") == "sequential" + hasSequential = true + break + end + end + if hasSequential + break + end + end + + if config.toolExecution == "sequential" || hasSequential + return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) + else + return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) + end +end + + + +""" + createErrorToolResult(msg::String) -> agentToolResult + +Builds an `agentToolResult` containing a single text content item +with the provided error message and an empty details dictionary. +Used when a tool call cannot be executed due to errors. + +Returning a result instead of throwing ensures that errors at any +point in the tool call pipeline are fed back to the LLM as a tool +result message. This allows the model to see the error and decide +whether to retry, re-issue the call with different arguments, or +report failure to the user. + +# Arguments +- `msg::String`: The error message to embed in the result + +# Returns +- `agentToolResult`: A result with `content = [textContent("text", msg)]` + +# Examples +```julia +julia> createErrorToolResult("Tool not found") +agentToolResult([textContent("text", "Tool not found")], Dict{Any,Any}()) +``` +""" +function createErrorToolResult(msg::String)::agentToolResult + return agentToolResult([textContent("text", msg)], dict{any,any}()) +end + +""" + createToolResultMessage(f::finalizedOutcome) -> toolResultMessage + +Constructs a `toolResultMessage` from a `finalizedOutcome`. +Normalizes missing content to an empty array and includes the +`addedToolNames` field only when the tool dynamically registered +new tools during execution. + +This conversion is necessary because the tool result is an +`agentToolResult` used by tool implementations, while the agent +loop consumes `toolResultMessage` objects that become part of the +conversation history. The message format includes metadata like +timestamp and tool call ID that the raw result does not carry, and +it is the object emitted via `messageStart`/`messageEnd` events +so the LLM receives the result as a proper assistant/user message +in the context window. + +# Arguments +- `f::finalizedOutcome`: The finalized tool call outcome + +# Returns +- `toolResultMessage`: A message ready for the agent loop context + +# Examples +```julia +julia> outcome = finalizedOutcome(tc, agentToolResult(content, details, usage, false), false); +julia> createToolResultMessage(outcome) +toolResultMessage("toolResult", "call_1", "search_wine", content, details, usage, [], false, 1234567890) +``` +""" +function createToolResultMessage(f::finalizedOutcome)::toolResultMessage + return toolResultMessage( + "toolResult", f.toolCall.id, f.toolCall.name, + f.result.content, f.result.details, f.result.usage, + get(f.result, :addedToolNames, string[]), f.isError, nowMillis() + ) +end + +""" + shouldTerminate(finalizedCalls::Vector{finalizedOutcome}) -> Bool + +Returns `true` only when every finalized call in the batch has +`result.terminate == true`. All tools must agree — if any tool +did not request termination, the agent continues. This prevents +a single tool that happens to set `terminate: true` (e.g. for +metadata purposes) from accidentally stopping the agent when +other tools in the batch did not intend to terminate. + +# Arguments +- `finalizedCalls`: Vector of finalized tool call outcomes + +# Returns +- `Bool`: `true` if all calls requested termination + +# Examples +```julia +julia> shouldTerminate(finalizedOutcome[]) +false + +julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], dict{any,any}(), dict{any,any}(), false), false) for _ in 1:2]) +false + +julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], dict{any,any}(), dict{any,any}(), true), false) for _ in 1:2]) +true +``` +""" +function shouldTerminate(batches::vector{finalizedOutcome})::bool + return !isempty(batches) && all(b -> b.result.terminate, batches) +end + +""" + prepareToolCallArguments(tool::agentTool, toolCall::agentToolCall) -> agentToolCall + +Calls the tool's optional `prepareArguments` hook to transform the +raw argument values from the LLM before schema validation. If the +tool has no hook or the hook returns the same object reference, the +original call is returned unchanged. + +This hook allows tools to normalize arguments that the LLM may have +produced in a non-standard format — for example, converting a date +string to a timestamp, expanding a short file path to an absolute +path, or normalizing casing. It runs before schema validation so +the validator sees the normalized form rather than raw LLM output. + +# Arguments +- `tool::agentTool`: The tool definition (may have a `prepareArguments` hook) +- `toolCall::agentToolCall`: The raw tool call from the assistant + +# Returns +- `agentToolCall`: The tool call with potentially transformed arguments + +# Examples +```julia +# No prepareArguments hook — returns input unchanged +prepareToolCallArguments(noHookTool, tc) +# => tc # same reference + +# With hook that normalizes arguments +prepareToolCallArguments(normalizeTool, tc) +# => agentToolCall{..., arguments=Dict("date" => 1700000000)} # "2024-01-15" → timestamp +``` +""" +function prepareToolCallArguments(tool::agentTool, toolCall::agentToolCall)::agentToolCall + if tool.prepareArguments === nothing + return toolCall + end + prepared = tool.prepareArguments(toolCall.arguments) + if prepared == toolCall.arguments + return toolCall + end + return merge(toolCall, dict(:arguments => prepared)) +end + +""" + prepareToolCall(context, assistantMsg, toolCall, config, signal) -> + Union{preparedToolCall,immediateOutcome} + +Resolves the tool by name, prepares and validates its arguments, +and runs the `beforeToolCall` hook. Returns a `preparedToolCall` +if successful or an `immediateOutcome` if the tool is not found, +validation fails, the hook blocks execution, or the signal is +aborted. Errors during preparation are caught and returned as +immediate error outcomes so the agent loop can feed them back +to the model. + +The key design decision here is that preparation never throws. +Every failure path returns an `immediateOutcome` with an error +result. This ensures the agent loop always receives a valid tool +result message for every tool call the assistant requested, +regardless of whether preparation succeeded. The LLM can then +use the error message to decide whether to retry with different +arguments or acknowledge the failure. + +# Arguments +- `context::agentContext`: Current agent context with tools and messages +- `assistantMsg::assistantMessage`: The assistant message containing the tool call +- `toolCall::agentToolCall`: The tool call to prepare +- `config::agentLoopConfig`: Loop configuration (may include `beforeToolCall`) +- `signal::Union{Nothing,AbortSignal}`: Optional abort signal + +# Returns +- `preparedToolCall`: If preparation succeeded (tool found, arguments valid, not blocked) +- `immediateOutcome`: If preparation failed (tool missing, invalid args, blocked, aborted) + +# Notes +- Tool lookup is by name via `context.tools` +- Validation uses `validateToolArguments` which coerces types per the tool schema +- The `beforeToolCall` hook can block execution by returning `{ block: true }` + +# Examples +```julia +# Success path +prepareToolCall(context, msg, tc, config, signal) +# => preparedToolCall(tool, tc, validatedArgs) + +# Tool not found +prepareToolCall(context, msg, tcNoMatch, config, signal) +# => immediateOutcome(createErrorToolResult("Tool fake_tool not found"), true) + +# Validation failure +prepareToolCall(context, msg, tcBadArgs, config, signal) +# => immediateOutcome(createErrorToolResult("Validation failed..."), true) + +# Aborted during preparation +prepareToolCall(context, msg, tc, config, abortedSignal) +# => immediateOutcome(createErrorToolResult("Operation aborted"), true) +``` +""" +function prepareToolCall( + context::agentContext, + assistantMsg::assistantMessage, + toolCall::agentToolCall, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, +)::union{preparedToolCall,immediateOutcome} + + tool = find(t -> t.name == toolCall.name, context.tools) + if tool === nothing + return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true) + end + + try + # 1. prepare arguments (tool-specific transform) + prepared = prepareToolCallArguments(tool, toolCall) + validatedArgs = validateToolArguments(tool, prepared) + + # 2. beforeToolCall hook — can block + if config.beforeToolCall !== nothing + before = config.beforeToolCall( + assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal + ) + if signal !== nothing && signal.aborted + return immediateOutcome(createErrorToolResult("Operation aborted"), true) + end + if before !== nothing && before.block + return immediateOutcome( + createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true) + end + end + + return preparedToolCall(tool, toolCall, validatedArgs) + catch err + return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true) + end +end + +# ── per-call execution ────────────────────────────────────────── + +""" + executePreparedToolCall(prep, signal, emit) -> executedOutcome + +Executes the tool by calling `tool.execute()` with the validated +arguments, the abort signal, and a callback for streaming partial +results. Emits `toolExecutionUpdate` events for each partial +result batch. Waits for all pending update events to settle before +returning. Catches execution errors and returns them as an error +outcome. The `accepting` guard prevents emitting updates after +the call has finished. + +Long-running tools (e.g. file uploads, model training, web scraping) +may take seconds or minutes. The streaming update mechanism allows +UI listeners and other consumers to show progress in real time rather +than waiting for the entire call to complete. The `accepting` guard +ensures that if the tool's execute function yields after emitting +updates but before returning, no duplicate or stale updates are +emitted after the result has already been captured. + +# Arguments +- `prep::preparedToolCall`: The prepared tool call (resolved tool + validated args) +- `signal::Union{Nothing,AbortSignal}`: Optional abort signal +- `emit::Function`: Event emitter for lifecycle events + +# Returns +- `executedOutcome`: The execution result and whether it was an error + +# Examples +```julia +# Successful execution +executePreparedToolCall(prep, nothing, emit) +# => executedOutcome(agentToolResult([textContent("text", "Found 3 wines")], dict{any,any}(), dict{any,any}()), false) + +# Execution error +executePreparedToolCall(prep, nothing, emit) +# => executedOutcome(createErrorToolResult("Connection timeout"), true) +``` +""" +function executePreparedToolCall( + prep::preparedToolCall, + signal::union{nothing,abortSignal}, + emit::Function, +)::executedOutcome + + updateEvents = promise[] + accepting = true + + try + result = prep.tool.execute( + prep.toolCall.id, prep.args, signal, + partialResult -> begin + if accepting + push!(updateEvents, + emit(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name, + prep.toolCall.arguments, partialResult))) + end + end + ) + accepting = false + wait.(updateEvents) + return executedOutcome(result, false) + catch err + accepting = false + wait.(updateEvents) + return executedOutcome(createErrorToolResult(sprint(showerror, err)), true) + end +end + +# ── per-call finalization ─────────────────────────────────────── + +""" + finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) -> + finalizedOutcome + +Runs the `afterToolCall` hook on the executed result, allowing +the consumer to mutate the result content, details, usage, +termination flag, or error status. Catches errors from the +hook and converts them to error outcomes. Returns a +`finalizedOutcome` that is used to construct the tool result +message. + +The `afterToolCall` hook exists as a post-processing step that +runs after every tool call regardless of success or failure. +Common use cases include: + + - Masking sensitive data from result content before the LLM + sees it (e.g. removing API keys from error messages). + - Normalizing usage tracking data into a consistent format. + - Inspecting the result and deciding to flip `terminate: true` + based on business logic (e.g. "if deployment failed, stop + the agent rather than retrying"). + - Wrapping an error result in a friendlier message for the LLM + to understand. + +If the hook itself throws, the error is caught and the result +becomes an error outcome. This ensures the tool pipeline never +breaks due to a buggy hook. + +# Arguments +- `context::agentContext`: Current agent context +- `assistantMsg::assistantMessage`: The assistant message that made the tool call +- `prep::preparedToolCall`: The originally prepared tool call +- `executed::executedOutcome`: The raw execution result +- `config::agentLoopConfig`: Loop configuration (may include `afterToolCall`) +- `signal::Union{Nothing,AbortSignal}`: Optional abort signal + +# Returns +- `finalizedOutcome`: The finalized outcome ready for message construction + +# Examples +```julia +# No afterToolCall hook — returns executed result unchanged +finalizeExecutedToolCall(context, msg, prep, execOk, config, nothing) +# => finalizedOutcome(tc, execOk.result, false) + +# afterToolCall masks sensitive data +finalizeExecutedToolCall(context, msg, prep, execOk, configWithHook, nothing) +# => finalizedOutcome(tc, maskedResult, false) + +# afterToolCall flips terminate based on business logic +finalizeExecutedToolCall(context, msg, prep, execFail, config, nothing) +# => finalizedOutcome(tc, {terminate: true}, true) +``` +""" +function finalizeExecutedToolCall( + context::agentContext, + assistantMsg::assistantMessage, + prep::preparedToolCall, + executed::executedOutcome, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, +)::finalizedOutcome + + result = executed.result + isError = executed.isError + + if config.afterToolCall !== nothing + try + after = config.afterToolCall( + afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal + ) + if after !== nothing + result = merge(result, dict(:content=>get(after,:content,result.content), + :details=>get(after,:details,result.details), + :usage=>get(after,:usage,result.usage), + :terminate=>get(after,:terminate,result.terminate))) + isError = get(after, :isError, isError) + end + catch err + result = createErrorToolResult(sprint(showerror, err)) + isError = true + end + end + + return finalizedOutcome(prep.toolCall, result, isError) +end + +""" + emitToolExecutionEnd(finalized, emit) + +Emits the `toolExecutionEnd` event with the finalized outcome, +signalling to listeners that the tool call has completed. + +This event is part of the tool execution lifecycle: +`toolExecutionStart` → (zero or more `toolExecutionUpdate` events) → +`toolExecutionEnd`. Listeners (such as the TUI or logging systems) +use this lifecycle to track individual tool calls. The event carries +the final result so listeners have all the data they need without +requiring external state lookups. + +# Arguments +- `finalized::finalizedOutcome`: The finalized outcome to report +- `emit::Function`: Event emitter + +# Notes +- Part of a three-event lifecycle per tool call +- Carries the complete result so listeners need no external lookups + +# Examples +```julia +# Emits a single event; returns nothing +emitToolExecutionEnd(finalized, emit) +# (emit receives toolExecEndEvent("call_1", "search_wine", result, false)) +``` +""" +function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function) + emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, + finalized.result, finalized.isError)) +end + +# ── sequential execution ──────────────────────────────────────── + +""" + executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) -> + agentToolCallBatch + +Executes tool calls one at a time in the order they appear. For each +call: emits `toolExecutionStart`, runs `prepareToolCall`, +then either resolves the immediate outcome or executes/finalizes +the prepared call. Emits `toolExecutionEnd` and creates the tool +result message before proceeding to the next call. Respects the +abort signal — if aborted, remaining calls are skipped. Returns +a batch with `terminate` determined by whether all results set +the termination flag. + +Sequential execution is required when tool calls have implicit +dependencies — for example, a `create_database` tool must complete +before `create_table` can reference it. It is also the safer +default because it prevents race conditions when multiple tools +share state (e.g. writing to the same file or API rate limits). +Use parallel only when you are confident the tools are independent. + +# Arguments +- `context::agentContext`: Current agent context +- `assistantMsg::assistantMessage`: The assistant message containing tool calls +- `toolCalls::Vector{agentToolCall}`: Tool calls to execute (ordered) +- `config::agentLoopConfig`: Loop configuration +- `signal::Union{Nothing,AbortSignal}`: Optional abort signal +- `emit::Function`: Event emitter + +# Returns +- `agentToolCallBatch`: Result messages and termination flag + +# Notes +- Calls execute strictly in order; each completes fully before the next begins +- Aborting during one call skips all remaining calls +- If any call returns `terminate: true`, it is included in the batch but does + not force termination unless all calls do + +# Examples +```julia +# Two independent reads — both succeed +executeToolCallsSequential(ctx, msg, [readTc, readTc2], config, nothing, emit) +# => agentToolCallBatch([result1, result2], false) + +# One tool fails, next is skipped due to abort +executeToolCallsSequential(ctx, msg, [tc1, tc2, tc3], config, abortedSignal, emit) +# => agentToolCallBatch([result1], false) # tc2 failed, tc3 skipped + +# All tools request termination +executeToolCallsSequential(ctx, msg, [deployTc], config, nothing, emit) +# => agentToolCallBatch([deployResult], true) +``` +""" +function executeToolCallsSequential( + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::Function, +)::agentToolCallBatch + + finalizedCalls = finalizedOutcome[] + messages = toolResultMessage[] + + for tc in toolCalls + emit(toolExecStartEvent(tc.id, tc.name, tc.arguments)) + + prep = prepareToolCall(context, assistantMsg, tc, config, signal) + + if prep isa immediateOutcome + finalized = finalizedOutcome(tc, prep.result, prep.isError) + else + executed = executePreparedToolCall(prep, signal, emit) + finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) + end + + emitToolExecutionEnd(finalized, emit) + push!(messages, createToolResultMessage(finalized)) + push!(finalizedCalls, finalized) + + if signal !== nothing && signal.aborted + break + end + end + + return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) +end + +# ── parallel execution ────────────────────────────────────────── + +""" + executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) -> + agentToolCallBatch + +Prepares all tool calls concurrently and spawns a task for each +prepared call. Immediate outcomes are resolved instantly. Task +entries are collected in order, then `fetch`ed to await all +concurrent executions. Tool result messages are created from +finalized outcomes in order and returned as a batch. Respects +the abort signal — if aborted during preparation, remaining +calls are skipped. Finalization order preserves the original +call order. + +Parallel execution is appropriate when the assistant requests +independent tools — for example, reading multiple files, querying +separate databases, or making independent API calls. It reduces +wall-clock time compared to sequential execution. The tradeoff is +that parallel calls can overwhelm external resources (rate limits, +connection pools, disk I/O). Finalization preserves the original +call order so tool result messages appear in the same order the +assistant requested them, regardless of which call finishes first. + +# Arguments +- `context::agentContext`: Current agent context +- `assistantMsg::assistantMessage`: The assistant message containing tool calls +- `toolCalls::Vector{agentToolCall}`: Tool calls to execute (order preserved in output) +- `config::agentLoopConfig`: Loop configuration +- `signal::Union{Nothing,AbortSignal}`: Optional abort signal +- `emit::Function`: Event emitter + +# Returns +- `agentToolCallBatch`: Result messages (in original call order) and termination flag + +# Notes +- All tool calls are prepared before any execution begins +- Execution tasks run concurrently; `fetch` waits for completion in order +- Immediate outcomes (errors/blocks) resolve instantly without spawning tasks +- Aborting during preparation skips remaining preparations but does not + cancel tasks already running + +# Examples +```julia +# Three independent reads — all succeed, results ordered by original call order +executeToolCallsParallel(ctx, msg, [readA, readB, readC], config, nothing, emit) +# => agentToolCallBatch([resultA, resultB, resultC], false) + +# Mix of immediate error and concurrent success +executeToolCallsParallel(ctx, msg, [badTc, goodTc], config, nothing, emit) +# => agentToolCallBatch([errorResult, goodResult], false) + +# Abort during preparation +executeToolCallsParallel(ctx, msg, [tc1, tc2, tc3], config, abortedSignal, emit) +# => agentToolCallBatch([...], false) # only prepared calls complete +``` +""" +function executeToolCallsParallel( + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::Function, +)::agentToolCallBatch + + entries = union{finalizedOutcome,task{finalizedOutcome}}[] + + for tc in toolCalls + emit(toolExecStartEvent(tc.id, tc.name, tc.arguments)) + + prep = prepareToolCall(context, assistantMsg, tc, config, signal) + + if prep isa immediateOutcome + finalized = finalizedOutcome(tc, prep.result, prep.isError) + emitToolExecutionEnd(finalized, emit) + push!(entries, finalized) + else + task = task() do + executed = executePreparedToolCall(prep, signal, emit) + finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) + emitToolExecutionEnd(finalized, emit) + return finalized + end + schedule(task) + push!(entries, task) + end + + if signal !== nothing && signal.aborted + break + end + end + + finalizedCalls = finalizedOutcome[] + for entry in entries + outcome = entry isa task ? fetch(entry) : entry + push!(finalizedCalls, outcome) + end + + messages = toolResultMessage[] + for f in finalizedCalls + push!(messages, createToolResultMessage(f)) + end + + return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) +end + + +""" + executeToolCalls(context, assistantMsg, toolCalls, config, signal, emit) -> + agentToolCallBatch + +Dispatches to sequential or parallel execution. Uses sequential mode +when `config.toolExecution == "sequential"` or when any of the +tool calls reference a tool with `executionMode: "sequential"`. +Otherwise uses parallel execution. This is the entry point called +from `streamAssistantResponse` in the agent loop. + +The sequential mode takes priority over parallel because it is the +safe default. If even one tool in a batch is marked sequential, all +tools execute sequentially — this prevents a single dependent tool +from racing with an otherwise independent one. The per-tool +`executionMode` allows fine-grained control (e.g. most tools are +parallel but a specific write tool is sequential), while the config-level +`toolExecution` provides a global override. + +# Arguments +- `context::agentContext`: Current agent context (used for per-tool `executionMode` lookup) +- `assistantMsg::assistantMessage`: The assistant message containing tool calls +- `toolCalls::Vector{agentToolCall}`: Tool calls to execute +- `config::agentLoopConfig`: Loop configuration (`toolExecution` mode) +- `signal::Union{Nothing,AbortSignal}`: Optional abort signal +- `emit::Function`: Event emitter + +# Returns +- `agentToolCallBatch`: The result batch from the selected execution strategy + +# Notes +- Per-tool `executionMode` is checked against `context.tools` for each tool call +- If any tool is sequential, the entire batch runs sequentially +- `config.toolExecution` can override all per-tool settings globally + +# Examples +```julia +# Parallel dispatch — no sequential tools in batch +executeToolCalls(ctx, msg, [searchTc, fetchTc], configParallel, nothing, emit) +# => agentToolCallBatch(results, false) # parallel execution + +# Sequential fallback — one tool is marked sequential +executeToolCalls(ctx, msg, [searchTc, writeTc], configParallel, nothing, emit) +# => agentToolCallBatch(results, false) # sequential because writeTc is sequential + +# Global override — config forces sequential regardless of per-tool settings +executeToolCalls(ctx, msg, [searchTc, fetchTc], configSequential, nothing, emit) +# => agentToolCallBatch(results, false) # sequential because config says so +``` +""" +function executeToolCalls( + context::agentContext, + assistantMsg::assistantMessage, + toolCalls::vector{agentToolCall}, + config::agentLoopConfig, + signal::union{nothing,abortSignal}, + emit::Function, +)::agentToolCallBatch + + hasSequential = false + for tc in toolCalls + for t in context.tools + if t.name == tc.name && get(t.executionMode, "parallel") == "sequential" + hasSequential = true + break + end + end + if hasSequential + break + end + end + + if config.toolExecution == "sequential" || hasSequential + return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) + else + return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) + end +end + + + + + + + + + + + + + + + diff --git a/src/type.jl b/src/type.jl index 897e72d..99cff35 100644 --- a/src/type.jl +++ b/src/type.jl @@ -396,6 +396,7 @@ mutable struct yiemAgent <: agent # High-level agent wrapper sessionId::Union{String, Nothing} # Optional session identifier maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) parallelToolExecute::Bool # Default: false + agentEventSink::Function # agent emits its status via this function end """ @@ -419,6 +420,7 @@ on `inputChannel` and `followUpChannel` channels concurrently. - `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`) - `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`) - `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`) +- `agentEventSink::Function`: Callback to receive agent events # Returns - A new `yiemAgent` instance with an active background task @@ -444,6 +446,7 @@ function yiemAgent( sessionId::Union{String, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing, parallelToolExecute::Bool=false, + agentEventSink::Function, ) # Create channels: input (user -> agent), followUp (async queue), output (agent -> user) inputChannel = Channel(16) @@ -467,6 +470,7 @@ function yiemAgent( sessionId, maxRetryDelayMs, parallelToolExecute, + agentEventSink, ) # Spawn the background loop and attach it @@ -477,6 +481,190 @@ end +""" + preparedToolCall(tool, toolCall, args) + +Intermediate state between tool call validation and execution. +Created after `prepareToolCall` succeeds; serves as the bridge to +the execution phase. Keeping the resolved tool, original call +metadata, and validated args together avoids repeated lookups and +allows the execution phase to access all necessary data without +carrying the full context through the call chain. + +# Fields +- `tool::agentTool`: The resolved tool definition from the context +- `toolCall::agentToolCall`: The original tool call from the assistant +- `args::any`: Validated (and coerced) argument values + +# Examples +```julia +# After prepareToolCall succeeds, the agent holds a preparedToolCall +prep = preparedToolCall( + tool, # agentTool found in context.tools + toolCall, # {id: "call_1", name: "search_wine", arguments: "{\"query\": \"red wine\"}"} + validatedArgs # Dict("query" => "red wine") +) +``` +""" +struct preparedToolCall + tool::agentTool # The resolved tool definition from the context + toolCall::agentToolCall # The original tool call from the assistant + args::any # Validated (and coerced) argument values +end + +""" + immediateOutcome(result, isError) + +A tool call that was resolved without actual execution — either +because the tool was not found, validation failed, or a +`beforeToolCall` hook blocked the call. The result is produced +immediately and emitted as a tool result message. + +Returning an outcome instead of throwing an exception is intentional: +it lets the agent feed the error back to the LLM as a tool result so +the model can recover — for example, by re-issuing a tool call with +corrected arguments after a validation failure. + +# Fields +- `result::agentToolResult`: The pre-computed tool result +- `isError::bool`: Whether this outcome represents an error + +# Examples +```julia +# Tool not found — immediate error +immediateOutcome( + createErrorToolResult("Tool search_wine not found"), + true +) + +# beforeToolCall hook blocked execution +immediateOutcome( + createErrorToolResult("Tool execution was blocked"), + true +) +``` +""" +struct immediateOutcome + result::agentToolResult # The pre-computed tool result + isError::bool # Whether this outcome represents an error +end + +""" + executedOutcome(result, isError) + +A tool call that has been executed by `tool.execute()` but has not +yet been through the `afterToolCall` hook. This intermediate state +is necessary because the hook may mutate the result (content, usage, +termination, error status). Keeping execution and finalization +separate allows the hook to inspect the raw result and decide +whether to transform it or replace it entirely. + +# Fields +- `result::agentToolResult`: The tool's execution result +- `isError::bool`: Whether execution raised an error + +# Examples +```julia +# Successful execution +executedOutcome( + agentToolResult([textContent("text", "Found 3 wines")], dict{any,any}(), dict{any,any}()), + false +) + +# Execution error +executedOutcome( + createErrorToolResult("Connection timeout"), + true +) +``` +""" +struct executedOutcome + result::agentToolResult # The tool's execution result + isError::bool # Whether execution raised an error +end + +""" + finalizedOutcome(toolCall, result, isError) + +The complete outcome of a tool call after both execution and the +`afterToolCall` hook. This is the final form used to construct +the `toolResultMessage` emitted to the agent loop. + +The three-phase design (prepare → execute → finalize) exists so +that each phase has a single responsibility: preparation handles +validation and gating, execution performs the actual work, and +finalization applies post-processing hooks. This separation allows +the agent loop to emit `toolExecutionEnd` events with the +finalized data while keeping each phase independently testable +and swappable. + +# Fields +- `toolCall::agentToolCall`: The original tool call reference +- `result::agentToolResult`: The final tool result (post-afterToolCall) +- `isError::bool`: Whether the call failed or was blocked + +# Examples +```julia +# Normal successful finalization +finalizedOutcome(tc, agentToolResult(content, details, usage, false), false) + +# afterToolCall mutated result and set terminate +finalizedOutcome(tc, agentToolResult(content, details, usage, true), false) +``` +""" +struct finalizedOutcome + toolCall::agentToolCall # The original tool call reference + result::agentToolResult # The final tool result (post-afterToolCall) + isError::bool # Whether the call failed or was blocked +end + +""" + agentToolCallBatch(messages, terminate) + +A batch of tool result messages from executing one or more tool calls. +The `terminate` flag indicates whether all tools in the batch +requested termination, which causes the agent loop to stop +processing further turns. + +This flag is set by the tool implementation (not the end user) to +signal that the agent should not call the LLM again. Typical use +cases: + + - Task completion: a tool like `deploy` or `submit` finishes its + work and returns `terminate: true` so the agent stops instead + of asking the LLM what to do next. + - Unrecoverable error: a tool hits a fatal condition (e.g. + database connection lost, auth token expired) and returns + `terminate: true` so the agent stops with an error message + rather than retrying. + - Async handoff: a tool triggers a long-running external + operation and wants the agent to stop now; the external system + will later resume the agent via `continue()`. + +If `terminate` is `false` (default), the agent loop feeds the tool +results back to the LLM for another turn. + +# Fields +- `messages::Vector{toolResultMessage}`: Tool result messages for this batch +- `terminate::Bool`: Whether the batch should terminate the loop + +# Examples +```julia +# Batch of 3 tool results, no termination +agentToolCallBatch(resultMessages, false) + +# All tools requested termination +agentToolCallBatch(resultMessages, true) + +# Empty batch — terminate is false regardless +agentToolCallBatch(toolResultMessage[], false) +``` +""" +struct agentToolCallBatch + messages::vector{toolResultMessage} # Tool result messages for this batch + terminate::bool # Whether the batch should terminate the loop +end + -- 2.52.0 From 4cb01c71bb9cfecaa9ff0c995cd7eb7639f8279b Mon Sep 17 00:00:00 2001 From: narawat Date: Wed, 5 Aug 2026 19:33:40 +0700 Subject: [PATCH 25/50] update --- src/agentCore.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 2c2cca7..c830f7e 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -81,7 +81,7 @@ function _agent_loop(agent::yiemAgent) while msg === nothing if isready(agent.inputChannel) - # message will be taken then process in _process_message() + # message will be taken in _process_message() msg = fetch!(agent.inputChannel) else yield() @@ -109,7 +109,7 @@ function _agent_loop(agent::yiemAgent) # make active if agent._state.activeRun == false # Dispatch message through the processing pipeline - processing_task = @spawn _process_message(agent, msg) + processing_task = @spawn _process_message(agent) agent._state.activeRun = true end @@ -173,7 +173,7 @@ should be implemented. Currently a placeholder that echoes back the received mes julia> # Currently returns a placeholder echo response ``` """ -function _process_message(agent::yiemAgent, msg)::assistantMessage +function _process_message(agent::yiemAgent)::assistantMessage # WORKING # loop until llmCall() response didn't use tool calls @@ -187,13 +187,13 @@ function _process_message(agent::yiemAgent, msg)::assistantMessage # Call llmCall() (blocking — the task waits here) - # if LLM use tool calls + # if (LLM use tool calls) # call executeToolCalls() # save toolResults to agent._state.messages - # else + # else (LLM not use tool calls) # break out of while loop end -- 2.52.0 From 8a2da0f5c390d02f1381430ea5a09ef95e0a01d4 Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 15:32:56 +0700 Subject: [PATCH 26/50] update --- src/YiemAgent.jl | 4 +- src/agentCore.jl | 186 ++++++------ src/type.jl | 214 +++++++++++--- src/util.jl | 505 -------------------------------- src/utils.jl | 179 +++++++++++ {src => src_OLD}/llmfunction.jl | 0 6 files changed, 458 insertions(+), 630 deletions(-) delete mode 100644 src/util.jl create mode 100644 src/utils.jl rename {src => src_OLD}/llmfunction.jl (100%) diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl index 822e9fb..e021f3c 100644 --- a/src/YiemAgent.jl +++ b/src/YiemAgent.jl @@ -10,8 +10,8 @@ module YiemAgent include("type.jl") using .type - include("util.jl") - using .util + include("utils.jl") + using .utils include("llmfunction.jl") using .llmfunction diff --git a/src/agentCore.jl b/src/agentCore.jl index c830f7e..dd27893 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -1,11 +1,11 @@ module agentCore -# export prompt +export _agent_loop using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, Serde using GeneralUtils -using ..type, ..util, ..llmfunction +using ..type, ..utils, ..llmfunction # ---------------------------------------------- 100 --------------------------------------------- # @@ -74,7 +74,6 @@ function _agent_loop(agent::yiemAgent) agent.followUpChannel -> nothing """ - while true result = nothing msg = nothing @@ -106,7 +105,7 @@ function _agent_loop(agent::yiemAgent) break end - # make active + # start _process_message loop if agent._state.activeRun == false # Dispatch message through the processing pipeline processing_task = @spawn _process_message(agent) @@ -122,8 +121,8 @@ function _agent_loop(agent::yiemAgent) put!(agent.inputChannel, followMsg) end end - continue # continue to process user message in the next loop + elseif typeof(processing_task) == Task && istaskdone(processing_task) == true # if agent runs is done but followUpChannel has messages, discard all message in it. # when agent work is done it should not accept follow up msg. @@ -135,8 +134,8 @@ function _agent_loop(agent::yiemAgent) end result = fetch(processing_task) put!(agent.outputChannel, result) - agent._state.activeRun = false - processing_task = nothing + agent._state.activeRun = false # reset + processing_task = nothing # reset end end catch e @@ -163,7 +162,7 @@ should be implemented. Currently a placeholder that echoes back the received mes - 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.preprocessContext` is set, call it on the formatted messages + 3. If `agent.prepareContext` 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 @@ -174,93 +173,116 @@ julia> # Currently returns a placeholder echo response ``` """ function _process_message(agent::yiemAgent)::assistantMessage - # WORKING - # loop until llmCall() response didn't use tool calls - while - # take every messages from agent.inputChannel, convert them into userMessage - # and add them to agent._state.messages + final_response = nothing + while true + # call agent.prepareContext() + preparedContext = agent.prepareContext(agent._state) - # call agent.preprocessContext() - - # Call agent.formatMsgForLLM(agent._state) to format for LLM + #WORKING Call agent.formatMsgForLLM(agent._state) to format for LLM + formatted_messages = agent.formatMsgForLLM(preparedContext) # Call llmCall() (blocking — the task waits here) + response = agent.llmCall(formatted_messages) - # if (LLM use tool calls) + # Check if LLM used tool calls (inspect content for tool_call blocks) + has_tool_calls = false + tool_call_list = agentToolCall[] + + for content_block in response.content + if content_block isa Dict + if get(content_block, :type, "") == "tool_calls" + has_tool_calls = true + for tc_data in get(content_block, :tool_calls, []) + tc = agentToolCall( + type="function", + id=get(tc_data, :id, string(uuid4())), + name=get(tc_data, :function, Dict{String,Any}())[:name], + arguments=get(tc_data, :function, Dict{String,Any}())[:arguments], + ) + push!(tool_call_list, tc) + end + elseif get(content_block, :type, "") == "tool_call" + has_tool_calls = true + tc_data = content_block + tc = agentToolCall( + type="function", + id=get(tc_data, :id, string(uuid4())), + name=get(tc_data, :name, ""), + arguments=get(tc_data, :arguments, Dict{String,Any}()), + ) + push!(tool_call_list, tc) + end + end + end + + if has_tool_calls && length(tool_call_list) > 0 + # Build context and config for executeToolCalls + context = agentContext( + agent._state.systemPrompt, + agent._state.messages, + agent._state.tools, + ) + + config = agentLoopConfig( + agent._state.tools, + agent.beforeToolCall, + agent.afterToolCall, + agent.parallelToolExecute ? "parallel" : "sequential", + ) + + signal = nothing + emit = agent.agentEventSink # call executeToolCalls() + batch = executeToolCalls(context, response, tool_call_list, config, signal, emit) # save toolResults to agent._state.messages + for tool_result in batch.messages + push!(agent._state.messages, tool_result) + end - # else (LLM not use tool calls) - # break out of while loop - + if batch.terminate + # If batch requested termination, build a final response + final_content = [textContent("Tool execution completed.")] + for tool_result in batch.messages + for content_block in tool_result.content + if content_block isa textContent + append!(final_content, [content_block]) + elseif content_block isa Dict + if haskey(content_block, :text) + push!(final_content, textContent(content_block[:text])) + end + end + end + end + final_response = assistantMessage( + role="assistant", + content=final_content, + api=response.api, + model=response.model, + usage=response.usage, + stopReason="tool_use_terminated", + errorMessage=if any(x -> x.isError, batch.messages) + "One or more tool calls failed" + else + nothing + end, + timestamp=now(), + ) + break + end + else + # LLM did not use tool calls — this is the final response + final_response = response + break + end end - # 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(), - ) + return final_response end -""" - executeToolCalls(context, assistantMsg, toolCalls, config, signal, emit) - -Dispatches to sequential or parallel execution. Uses sequential mode -when `config.toolExecution == "sequential"` or when any of the -tool calls reference a tool with `executionMode: "sequential"`. -Otherwise uses parallel execution. This is the entry point called -from `streamAssistantResponse` in the agent loop. - -The sequential mode takes priority over parallel because it is the -safe default. If even one tool in a batch is marked sequential, all -tools execute sequentially — this prevents a single dependent tool -from racing with an otherwise independent one. The per-tool -`executionMode` allows fine-grained control (e.g. most tools are -parallel but a specific write tool is sequential), while the config-level -`toolExecution` provides a global override. -""" -function executeToolCalls( - context::agentContext, - assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, - config::agentLoopConfig, - signal::union{nothing,abortSignal}, - emit::Function, -)::agentToolCallBatch - - hasSequential = false - for tc in toolCalls - for t in context.tools - if t.name == tc.name && get(t.executionMode, "parallel") == "sequential" - hasSequential = true - break - end - end - if hasSequential - break - end - end - - if config.toolExecution == "sequential" || hasSequential - return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal, emit) - else - return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal, emit) - end -end - - - """ createErrorToolResult(msg::String) -> agentToolResult @@ -1040,10 +1062,6 @@ end - - - - diff --git a/src/type.jl b/src/type.jl index 99cff35..c7cb570 100644 --- a/src/type.jl +++ b/src/type.jl @@ -1,15 +1,11 @@ module type - export agent, sommelier, companion, virtualcustomer, agentContext, yiemAgent, + export agentContext, yiemAgent, run_agent, take_response, follow_up, stop_agent using Dates, UUIDs, DataStructures, JSON, NATS using GeneralUtils -# ============================================================================ -# Simple type aliases / definitions -# ============================================================================ - const Timestamp = DateTime struct Usage @@ -17,12 +13,10 @@ struct Usage outputTokens::Int64 end -# ---------------------------------------------- 100 --------------------------------------------- # - -# ============================================================================ -# Message types -# ============================================================================ +# ------------------------------------------------------------------------------------------------ # +# Message types # +# ------------------------------------------------------------------------------------------------ # abstract type agentMessage end # Base type for all agent messages struct userMessage <: agentMessage # Message from the user @@ -135,9 +129,9 @@ function toolResultMessage(; role="tool", toolCallId="", toolName="", end -# ============================================================================ -# Message content types -# ============================================================================ +# ------------------------------------------------------------------------------------------------ # +# Message content types # +# ------------------------------------------------------------------------------------------------ # abstract type messageContent end # Base type for message content @@ -190,9 +184,9 @@ function imageContent(; data="", mimeType="") end -# ============================================================================ -# Tool types -# ============================================================================ +# ------------------------------------------------------------------------------------------------ # +# Tool types # +# ------------------------------------------------------------------------------------------------ # """ A tool available to the agent. @@ -220,9 +214,9 @@ struct agentTool{TParameters, TDetails} # A tool available to the agent end -# ============================================================================ -# Agent context -# ============================================================================ +# ------------------------------------------------------------------------------------------------ # +# Agent context # +# ------------------------------------------------------------------------------------------------ # """ Snapshot of the agent's conversation context. @@ -242,15 +236,18 @@ struct agentContext # Snapshot of the agent's conversa end -# ============================================================================ -# Agent state -# ============================================================================ +# ------------------------------------------------------------------------------------------------ # +# Agent state # +# ------------------------------------------------------------------------------------------------ # mutable struct agentState # Mutable runtime state of an agent systemPrompt::String # System prompt text model::llmModel # LLM model to use tools::Vector{agentTool} # Available tools - messages::Vector{agentMessage} # Conversation messages + + # messages history includes userMessage, assistantMessage, toolResultMessage + messages::Vector{agentMessage} + pendingToolCalls::Vector{String} # Tool call IDs waiting for results activeRun::Bool # is agent processing user message? errorMessage::Union{String, Nothing} # Last error message @@ -343,9 +340,148 @@ struct llmModel{Api} # LLM model configuration maxTokens::Int64 # Maximum output tokens per completion end -# ============================================================================ -# Agent struct -# ============================================================================ +# ------------------------------------------------------------------------------------------------ # +# Agent loop configuration & tool execution types # +# ------------------------------------------------------------------------------------------------ # + +""" +Configuration for the agent tool execution loop. + +# Arguments +- `tools::Vector{agentTool}`: Available tools +- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution +- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution +- `toolExecution::String`: Execution mode — "sequential" or "parallel" +""" +struct agentLoopConfig + tools::Vector{agentTool} + beforeToolCall::Union{Function, Nothing} + afterToolCall::Union{Function, Nothing} + toolExecution::String +end + +""" +Signal for aborting ongoing operations. + +# Arguments +- `aborted::Bool`: Whether the operation has been aborted +""" +struct abortSignal + aborted::Bool +end + +""" +Result returned by tool execution before the `afterToolCall` hook. + +# Arguments +- `content::Vector{messageContent}`: Tool output content +- `details::Dict{Any,Any}`: Tool-specific details +- `usage::Union{Usage, Nothing}`: Token usage if applicable +- `terminate::Bool`: Whether tool requests termination of the agent loop +""" +struct agentToolResult + content::Vector{messageContent} + details::Dict{Any,Any} + usage::Union{Usage, Nothing} + terminate::Bool +end + +""" +Function type for parallel execution override on a tool. + +# Arguments +- Context for parallel execution + +# Returns +- `agentToolCallBatch`: The result batch from parallel execution +""" +const toolparallelExecute = Function + +""" +Context passed to the `beforeToolCall` hook. + +# Arguments +- `message::assistantMessage`: The assistant message containing the tool call +- `toolCall::agentToolCall`: The tool call being prepared +- `args::Dict{String,Any}`: Validated tool arguments +- `context::agentContext`: Current conversation context +""" +struct assistantMsgCtx + message::assistantMessage + toolCall::agentToolCall + args::Dict{String,Any} + context::agentContext +end + +""" +Context passed to the `afterToolCall` hook. + +# Arguments +- `message::assistantMessage`: The assistant message containing the tool call +- `toolCall::agentToolCall`: The tool call that was executed +- `args::Dict{String,Any}`: Tool arguments +- `result::agentToolResult`: The raw tool result +- `isError::Bool`: Whether execution resulted in an error +- `context::agentContext`: Current conversation context +""" +struct afterCtx + message::assistantMessage + toolCall::agentToolCall + args::Dict{String,Any} + result::agentToolResult + isError::Bool + context::agentContext +end + +""" +Event emitted when a tool call execution starts. + +# Arguments +- `toolCallId::String`: ID of the tool call +- `toolName::String`: Name of the tool +- `arguments::Dict{String,Any}`: Tool arguments +""" +struct toolExecStartEvent + toolCallId::String + toolName::String + arguments::Dict{String,Any} +end + +""" +Event emitted with partial results during tool execution. + +# Arguments +- `toolCallId::String`: ID of the tool call +- `toolName::String`: Name of the tool +- `arguments::Dict{String,Any}`: Tool arguments +- `partialResult::Any`: The partial result data +""" +struct toolExecUpdateEvent + toolCallId::String + toolName::String + arguments::Dict{String,Any} + partialResult::Any +end + +""" +Event emitted when a tool call execution ends. + +# Arguments +- `toolCallId::String`: ID of the tool call +- `toolName::String`: Name of the tool +- `result::agentToolResult`: The final tool result +- `isError::Bool`: Whether execution resulted in an error +""" +struct toolExecEndEvent + toolCallId::String + toolName::String + result::agentToolResult + isError::Bool +end + +# ------------------------------------------------------------------------------------------------ # +# Agent struct # +# ------------------------------------------------------------------------------------------------ # abstract type agent end @@ -367,14 +503,14 @@ mutable struct yiemAgent <: agent # High-level agent wrapper # and all followUp messages. outputChannel::Channel - _task::Union{Task, Nothing} # Background task running the agent loop + _agent_loop::Union{Task, Nothing} # agent loop running in the background # Preprocess/transform messages and context (modify, filter, prune, inject context from memory, # reorder, ...) for a single LLM call in _process_message()'s loop. # returns new Vector{agentMessage} - preprocessContext ::Union{Function, Nothing} + prepareContext ::Union{Function, Nothing} - # Convert preprocessContext()'s new Vector{agentMessage} to LLM message format + # Convert prepareContext()'s new Vector{agentMessage} to LLM message format formatMsgForLLM::Function # Actually invoke the LLM to get a completion response. The LLM response comes back as an @@ -391,8 +527,8 @@ mutable struct yiemAgent <: agent # High-level agent wrapper # Callback invoked after executing a tool call to sanitize tools output so the output is ready # to be converted into toolResults message afterToolCall::Union{Function, Nothing} - prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn - prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context + # prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn + # prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context sessionId::Union{String, Nothing} # Optional session identifier maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) parallelToolExecute::Bool # Default: false @@ -412,7 +548,7 @@ on `inputChannel` and `followUpChannel` channels concurrently. - `messages::Vector{agentMessage}`: Initial conversation messages (default: empty) - `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`) - `llmCall::Function`: Function to invoke the LLM (required) -- `preprocessContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) +- `prepareContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) - `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`) - `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`) - `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`) @@ -436,13 +572,13 @@ function yiemAgent( model=nothing, tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], - preprocessContext::Union{Function, Nothing}=nothing, + prepareContext::Union{Function, Nothing}=nothing, formatMsgForLLM::Function=defaultformatMsgForLLM, llmCall::Function, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, - prepareNextTurn::Union{Function, Nothing}=nothing, - prepareNextTurnWithContext::Union{Function, Nothing}=nothing, + # prepareNextTurn::Union{Function, Nothing}=nothing, + # prepareNextTurnWithContext::Union{Function, Nothing}=nothing, sessionId::Union{String, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing, parallelToolExecute::Bool=false, @@ -460,13 +596,13 @@ function yiemAgent( followUp, outputChannel, nothing, # placeholder — replaced below - preprocessContext, + prepareContext, formatMsgForLLM, llmCall, beforeToolCall, afterToolCall, - prepareNextTurn, - prepareNextTurnWithContext, + # prepareNextTurn, + # prepareNextTurnWithContext, sessionId, maxRetryDelayMs, parallelToolExecute, @@ -474,7 +610,7 @@ function yiemAgent( ) # Spawn the background loop and attach it - agent._task = @spawn _agent_loop(agent) + agent._agent_loop = @spawn _agent_loop(agent) return agent end diff --git a/src/util.jl b/src/util.jl deleted file mode 100644 index 9460d97..0000000 --- a/src/util.jl +++ /dev/null @@ -1,505 +0,0 @@ -module util - -export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, - availableWineToText, createEventsLog, createChatLog, checkAgentResponse_JSON, - checkAgentResponse_text - -using UUIDs, Dates, DataStructures, HTTP, JSON -using GeneralUtils -using ..type - -# ---------------------------------------------- 100 --------------------------------------------- # - -""" -Clear agent chat history. - -Empties the conversation history, short-term memory, events log, and chatbox. - -# Arguments -- `a::T`: An agent instance (subtype of `agent`) - -# Returns -- `nothing` - -# Notes -- Does not clear long-term memory; use `[PENDING] clear memory` when implemented. - -# Examples -```jldoctest -julia> YiemAgent.clearhistory(agent) -``` -""" -function clearhistory(a::T) where {T<:agent} - empty!(a.chathistory) - empty!(a.memory["shortmem"]) - empty!(a.memory["events"]) - a.memory["chatbox"] = "" -end - - -""" -Add a new message to the agent's conversation history. - -Automatically summarizes the oldest messages if the history exceeds `maximumMsg`. - -# 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) - -# Keyword Arguments -- `maximumMsg::Integer=30`: Maximum number of messages before summarization kicks in - -# Returns -- `nothing` - -# Notes -- When history length exceeds `maximumMsg`, the oldest messages are summarized automatically. - -# 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} - # if name ∉ ["system", "user", "assistant"] # guard against typo - # error("name is not in agent.availableRole $(@__LINE__)") - # end - - #TODO summarize the oldest 10 message - if length(a.chathistory) > maximumMsg - summarize(a.chathistory) - else - # userinput["timestamp"] = Dates.now() - push!(a.chathistory, userinput) - end -end - - -""" Converts a vector of dictionaries to a formatted string. -This function takes in a vector of dictionaries and outputs a single string where each dictionary's keys are prefixed by their values. - -# Arguments - - `vecd::Vector` - A vector of dictionaries containing chat messages - - `withkey::Bool` - Whether to include the name as a prefix in the output text. Default is true - - `range::Union{Nothing,UnitRange,Int}` - Optional range of messages to include. If nothing, includes all messages - -# Returns - A formatted string where each line contains either: - - If withkey=true: "name> message\n" - - If withkey=false: "message\n" - -# Example - -julia> using Revise -julia> using GeneralUtils -julia> vecd = [Dict("name" => "John", "text" => "Hello"), Dict("name" => "Jane", "text" => "Goodbye")] -julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true) -"John> Hello\nJane> Goodbye\n" -``` -""" -function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String - # Initialize an empty string to hold the final text - text = "" - - # Get the elements within the specified range, or all elements if no range provided - elements = isnothing(range) ? vecd : vecd[range] - - # Determine whether to include the key in the output text or not - if withkey - # Loop through each dictionary in the input vector - for d in elements - # Extract the 'name' and 'text' keys from the dictionary - name = titlecase(d[:name]) - _text = d[:text] - - # Append the formatted string to the text variable - text *= "$name> $_text \n" - end - else - # Loop through each dictionary in the input vector - for d in elements - # Iterate over all key-value pairs in the dictionary - for (k, v) in d - # Append the formatted string to the text variable - text *= "$v \n" - end - end - end - - # Return the final text - return text -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 = "" - # Loop through each dictionary in the input vector - for (i, d) in enumerate(vecd) - # Iterate over all key-value pairs in the dictionary - temp = [] - for (k, v) in d - # Append the formatted string to the text variable - t = "$k:$v" - push!(temp, t) - end - _rowtext = join(temp, ',') - rowtext *= "$i) $_rowtext " - end - - return rowtext -end - - - -""" -Create a dictionary representing an event with optional details. - -# 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 `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, - timestamp::Union{DateTime, Nothing}=nothing, - subject::Union{String, Nothing}=nothing, - thought::Union{AbstractDict, Nothing}=nothing, - action_name::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENT_WINE_GUIDELINE", etc - action_input::Union{String, Nothing}=nothing, - location::Union{String, Nothing}=nothing, - equipment_used::Union{String, Nothing}=nothing, - material_used::Union{String, Nothing}=nothing, - observation::Union{String, Nothing}=nothing, - note::Union{String, Nothing}=nothing, - ) - - d = Dict{String, Any}( - "event_description"=> event_description, - "timestamp"=> timestamp, - "subject"=> subject, - "thought"=> thought, - "action_name"=> action_name, - "action_input"=> action_input, - "location"=> location, - "equipment_used"=> equipment_used, - "material_used"=> material_used, - "observation"=> observation, - "note"=> note, - ) - - return d -end - - -""" -Create a formatted timeline string from a sequence of events. - -# Arguments -- `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 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} - # Initialize empty timeline string - timeline = "" - - # Determine which indices to use - either provided range or full length - ind = - if eventindex !== nothing - [eventindex...] - else - 1:length(events) - end - - # Iterate through events and format each one - for i in ind - event = events[i] - # If no outcome exists, format without outcome -# if event["action_name"] == "CHAT_BOX" - # timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\n" - # elseif event["action_name"] == "CHECKINVENTORY" && event["observation"] === nothing - # timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: Not done yet.\n" - if event["action_name"] == "SEARCH_WINE_DATABASE" - timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: $(event["observation"])\\n" - else - timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\\n" - end - end - - # Return formatted timeline string - 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 - log = Dict{String, String}[] - - # Determine which indices to use - either provided range or full length - ind = - if index !== nothing - [index...] - else - 1:length(events) - end - - # Iterate through events and format each one - for i in ind - event = events[i] - # If no outcome exists, format without outcome - if event["observation"] === nothing - subject = event["subject"] - action_name = event["action_name"] - action_input = event["action_input"] - str = "action_name: $action_name, action_input: $action_input" - d = Dict{String, String}("name"=>subject, "text"=>str) - push!(log, d) - else - subject = event["subject"] - action_name = event["action_name"] - action_input = event["action_input"] - observation = event["observation"] - str = "action_name: $action_name, action_input: $action_input, observation: $observation" - d = Dict{String, String}("name"=>subject, "text"=>str) - push!(log, d) - end - end - - return log -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 - log = Dict{String, String}[] - - # Determine which indices to use - either provided range or full length - ind = - if index !== nothing - [index...] - else - 1:length(chatdict) - end - - # Iterate through events and format each one - for i in ind - event = chatdict[i] - subject = event["name"] - text = event["text"] - d = Dict{String, String}("name"=>subject, "text"=>text) - push!(log, d) - end - - return log -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) - missingkeys = [k for (k, v) in detected_kw if v === nothing] - ispass = false - errormsg = nothing - if !isempty(missingkeys) - errormsg = "$missingkeys are missing from your previous response" - ispass = false - elseif sum([length(i) for i in values(detected_kw)]) > length(requiredHeader) - errormsg = "Your previous attempt has duplicated points according to the required response format" - ispass = false - else - ispass = true - end - return (ispass, errormsg) -end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module util \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..ab0b7e0 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,179 @@ +module utils + +export clearhistory, availableWineToText, prepareContext + +using UUIDs, Dates, DataStructures, HTTP, JSON +using GeneralUtils +using ..type + +# ---------------------------------------------- 100 --------------------------------------------- # + +""" +Clear agent chat history. + +Empties the conversation history, short-term memory, events log, and chatbox. + +# Arguments +- `a::T`: An agent instance (subtype of `agent`) + +# Returns +- `nothing` + +# Notes +- Does not clear long-term memory; use `[PENDING] clear memory` when implemented. + +# Examples +```jldoctest +julia> YiemAgent.clearhistory(agent) +``` +""" +function clearhistory(a::T) where {T<:agent} + empty!(a.chathistory) + empty!(a.memory["shortmem"]) + empty!(a.memory["events"]) + a.memory["chatbox"] = "" +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 = "" + # Loop through each dictionary in the input vector + for (i, d) in enumerate(vecd) + # Iterate over all key-value pairs in the dictionary + temp = [] + for (k, v) in d + # Append the formatted string to the text variable + t = "$k:$v" + push!(temp, t) + end + _rowtext = join(temp, ',') + rowtext *= "$i) $_rowtext " + end + + return rowtext +end + + + +""" + prepareContext(state::agentState) -> Vector{agentMessage} + +Returns a deep copy of the messages from the given `agentState`, ready +to be sent to the LLM. Override this function to inject additional +context — such as retrieved documents, current time, user preferences, +or any other relevant information — into the message list before +formatting and calling the LLM. + +By default, returns an exact copy of `state.messages` without +modification. + +# Arguments +- `state::agentState`: The current agent state containing conversation history + +# Returns +- `Vector{agentMessage}`: A deep copy of the messages to be sent to the LLM + +# Examples +```julia +# Default: returns a deep copy of messages +prepareContext(state) == deepcopy(state.messages) + +# Override to inject system context: +# function Base.prepareContext(state::agentState) +# msgs = deepcopy(state.messages) +# pushfirst!(msgs, textMessage("system", "You are a helpful assistant.")) +# return msgs +# end +``` +""" +function prepareContext(state::agentState)::Vector{agentMessage} + messages = deepcopy(state.messages) # messages that will be send to LLM + + #TODO adjust/modify and inject additional context into messages + + return messages +end + + +function formatMsgForLLM() + +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +end # module util \ No newline at end of file diff --git a/src/llmfunction.jl b/src_OLD/llmfunction.jl similarity index 100% rename from src/llmfunction.jl rename to src_OLD/llmfunction.jl -- 2.52.0 From 8232946e91171b97485be0b6be46eccc78ff4b7a Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 18:29:31 +0700 Subject: [PATCH 27/50] update --- src/utils.jl | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index ab0b7e0..b82735a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -113,8 +113,28 @@ function prepareContext(state::agentState)::Vector{agentMessage} end -function formatMsgForLLM() - +""" convert preparedcontext into openai message format ready to be used by LLM +""" +function formatMsgForLLM(messages::Vector{agentMessage})::Vector{Dict{String, Any}} + """ openai message format + message = Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => + " + Do you have Brunello di Montalcino from Tenuta CastelGiocon? + "), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ) + """ + + # convert various messages to openai message format + + return openai_message end -- 2.52.0 From e592441387c6225c721066c605049d93f0fb6740 Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 18:40:09 +0700 Subject: [PATCH 28/50] update --- src/utils.jl | 59 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index b82735a..f274980 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -116,19 +116,40 @@ end """ convert preparedcontext into openai message format ready to be used by LLM """ function formatMsgForLLM(messages::Vector{agentMessage})::Vector{Dict{String, Any}} - """ openai message format - message = Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => - " - Do you have Brunello di Montalcino from Tenuta CastelGiocon? - "), + """ openai message format example + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ) - ] + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ), + Dict( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => "let me check."), + ] + ), + Dict( + "role" => "toolResult", + "content" => [ + Dict("type" => "text", "text" => "name: Chateau Montelena ..."), + ] + ), + ], + "temperature" => 0.7 ) """ @@ -141,7 +162,19 @@ end - +message = Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => + " + Do you have something similar to the one in the image? + "), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ) -- 2.52.0 From dde9b019ddc2a436834d450655a74418cabcb9fb Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 18:44:28 +0700 Subject: [PATCH 29/50] update --- src/utils.jl | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index f274980..de364ed 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -162,22 +162,6 @@ end -message = Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => - " - Do you have something similar to the one in the image? - "), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ) - ] - ) - - - -- 2.52.0 From ebd79aa6866ad06d47f0748c838da38b8fbe307e Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 18:59:39 +0700 Subject: [PATCH 30/50] update --- src/utils.jl | 87 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index de364ed..911eee2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -116,46 +116,59 @@ end """ convert preparedcontext into openai message format ready to be used by LLM """ function formatMsgForLLM(messages::Vector{agentMessage})::Vector{Dict{String, Any}} - """ openai message format example - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ) - ] - ), - Dict( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => "let me check."), - ] - ), - Dict( - "role" => "toolResult", - "content" => [ - Dict("type" => "text", "text" => "name: Chateau Montelena ..."), - ] - ), - ], - "temperature" => 0.7 + + openai_messages = Vector{Dict{String, Any}}() + + for msg in messages + if msg isa userMessage + push!(openai_messages, _toOpenAIUserMsg(msg)) + elseif msg isa assistantMessage + push!(openai_messages, _toOpenAIAssistantMsg(msg)) + elseif msg isa toolResultMessage + push!(openai_messages, _toOpenAIToolResultMsg(msg)) + end + end + + return openai_messages +end + +function _toOpenAIUserMsg(msg::userMessage)::Dict{String, Any} + return Dict{String, Any}( + "role" => "user", + "content" => _toOpenAIBlocks(msg.content), ) - """ +end - # convert various messages to openai message format +function _toOpenAIAssistantMsg(msg::assistantMessage)::Dict{String, Any} + return Dict{String, Any}( + "role" => "assistant", + "content" => _toOpenAIBlocks(msg.content), + ) +end - return openai_message +function _toOpenAIToolResultMsg(msg::toolResultMessage)::Dict{String, Any} + return Dict{String, Any}( + "role" => "tool", + "tool_call_id" => msg.toolCallId, + "name" => msg.toolName, + "content" => _toOpenAIBlocks(msg.content), + ) +end + +function _toOpenAIBlocks(contents::Vector{messageContent})::Vector{Dict{String, Any}} + blocks = Vector{Dict{String, Any}}() + for c in contents + if c isa textContent + push!(blocks, Dict("type" => "text", "text" => c.text)) + elseif c isa imageContent + data_uri = "data:$(c.mimeType);base64,$(c.data)" + push!(blocks, Dict( + "type" => "image_url", + "image_url" => Dict("url" => data_uri), + )) + end + end + return blocks end -- 2.52.0 From 51fca0aa8c930c7ed0f31a21e97ba2fbfde44cb7 Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 19:30:59 +0700 Subject: [PATCH 31/50] update --- src/agentCore.jl | 2 +- src/utils.jl | 89 +++++++++++++++++++++--------------------------- 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index dd27893..18defc9 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -179,7 +179,7 @@ function _process_message(agent::yiemAgent)::assistantMessage # call agent.prepareContext() preparedContext = agent.prepareContext(agent._state) - #WORKING Call agent.formatMsgForLLM(agent._state) to format for LLM + # Call agent.formatMsgForLLM(agent._state) to format for LLM formatted_messages = agent.formatMsgForLLM(preparedContext) # Call llmCall() (blocking — the task waits here) diff --git a/src/utils.jl b/src/utils.jl index 911eee2..38729f4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -115,60 +115,47 @@ end """ convert preparedcontext into openai message format ready to be used by LLM """ -function formatMsgForLLM(messages::Vector{agentMessage})::Vector{Dict{String, Any}} - - openai_messages = Vector{Dict{String, Any}}() - - for msg in messages - if msg isa userMessage - push!(openai_messages, _toOpenAIUserMsg(msg)) - elseif msg isa assistantMessage - push!(openai_messages, _toOpenAIAssistantMsg(msg)) - elseif msg isa toolResultMessage - push!(openai_messages, _toOpenAIToolResultMsg(msg)) - end - end - - return openai_messages -end - -function _toOpenAIUserMsg(msg::userMessage)::Dict{String, Any} - return Dict{String, Any}( - "role" => "user", - "content" => _toOpenAIBlocks(msg.content), +function formatMsgForLLM(preparedContext::Vector{agentMessage})::Dict{String, Any} + """ openai message format example + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ), + Dict( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => "let me check."), + ] + ), + Dict( + "role" => "toolResult", + "content" => [ + Dict("type" => "text", "text" => "name: Chateau Montelena ..."), + ] + ), + ], + "temperature" => 0.7 ) -end + """ -function _toOpenAIAssistantMsg(msg::assistantMessage)::Dict{String, Any} - return Dict{String, Any}( - "role" => "assistant", - "content" => _toOpenAIBlocks(msg.content), - ) -end + # convert various messages to openai message format -function _toOpenAIToolResultMsg(msg::toolResultMessage)::Dict{String, Any} - return Dict{String, Any}( - "role" => "tool", - "tool_call_id" => msg.toolCallId, - "name" => msg.toolName, - "content" => _toOpenAIBlocks(msg.content), - ) -end - -function _toOpenAIBlocks(contents::Vector{messageContent})::Vector{Dict{String, Any}} - blocks = Vector{Dict{String, Any}}() - for c in contents - if c isa textContent - push!(blocks, Dict("type" => "text", "text" => c.text)) - elseif c isa imageContent - data_uri = "data:$(c.mimeType);base64,$(c.data)" - push!(blocks, Dict( - "type" => "image_url", - "image_url" => Dict("url" => data_uri), - )) - end - end - return blocks + return openai_message end -- 2.52.0 From d0bacbb53899e25f118bab796a9598af21c28a37 Mon Sep 17 00:00:00 2001 From: narawat Date: Thu, 6 Aug 2026 19:39:42 +0700 Subject: [PATCH 32/50] update --- src/utils.jl | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 38729f4..2efd78b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -153,9 +153,79 @@ function formatMsgForLLM(preparedContext::Vector{agentMessage})::Dict{String, An ) """ - # convert various messages to openai message format + messages = Dict{String, Any}[] - return openai_message + for msg in preparedContext + if msg isa userMessage + push!(messages, _userMessageToOpenAI(msg)) + elseif msg isa assistantMessage + push!(messages, _assistantMessageToOpenAI(msg)) + elseif msg isa toolResultMessage + push!(messages, _toolResultMessageToOpenAI(msg)) + end + end + + return Dict("messages" => messages) +end + + +""" +Convert a userMessage to OpenAI message format. +""" +function _userMessageToOpenAI(msg::userMessage)::Dict{String, Any} + return Dict( + "role" => "user", + "content" => _messageContentToBlocks(msg.content) + ) +end + + +""" +Convert an assistantMessage to OpenAI message format. +""" +function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any} + return Dict( + "role" => "assistant", + "content" => _messageContentToBlocks(msg.content) + ) +end + + +""" +Convert a toolResultMessage to OpenAI message format. +""" +function _toolResultMessageToOpenAI(msg::toolResultMessage)::Dict{String, Any} + return Dict( + "role" => "tool", + "tool_call_id" => msg.toolCallId, + "content" => _messageContentToBlocks(msg.content) + ) +end + + +""" +Convert a vector of messageContent to OpenAI content blocks. + +Each textContent becomes a text block, each imageContent becomes +an image_url block. +""" +function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{String, Any}} + blocks = Vector{Dict{String, Any}}() + + for c in contents + if c isa textContent + push!(blocks, Dict("type" => "text", "text" => c.text)) + elseif c isa imageContent + push!(blocks, Dict( + "type" => "image_url", + "image_url" => Dict( + "url" => "data:$(c.mimeType);base64,$(c.data)" + ) + )) + end + end + + return blocks end -- 2.52.0 From 0a6af36b345b2ac0ac730e55a549ce23fefa652f Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 08:46:48 +0700 Subject: [PATCH 33/50] update --- src/agentCore.jl | 4 +- src/type.jl | 4 +- src/utils.jl | 18 ++- test/chatting_with_agent.jl | 296 ---------------------------------- test/prompttest.jl | 159 ------------------ test/prompttest_1.jl | 87 ---------- test/prompttest_2.jl | 119 -------------- test/{test1.jl => runtest.jl} | 0 test/runtests.jl | 223 ------------------------- 9 files changed, 18 insertions(+), 892 deletions(-) delete mode 100644 test/chatting_with_agent.jl delete mode 100644 test/prompttest.jl delete mode 100644 test/prompttest_1.jl delete mode 100644 test/prompttest_2.jl rename test/{test1.jl => runtest.jl} (100%) delete mode 100644 test/runtests.jl diff --git a/src/agentCore.jl b/src/agentCore.jl index 18defc9..311d00e 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -184,8 +184,10 @@ function _process_message(agent::yiemAgent)::assistantMessage # Call llmCall() (blocking — the task waits here) response = agent.llmCall(formatted_messages) + + error(5555555) - # Check if LLM used tool calls (inspect content for tool_call blocks) + #WORKING Check if LLM used tool calls (inspect content for tool_call blocks) has_tool_calls = false tool_call_list = agentToolCall[] diff --git a/src/type.jl b/src/type.jl index c7cb570..264cdd7 100644 --- a/src/type.jl +++ b/src/type.jl @@ -245,7 +245,7 @@ mutable struct agentState # Mutable runtime state of an agen model::llmModel # LLM model to use tools::Vector{agentTool} # Available tools - # messages history includes userMessage, assistantMessage, toolResultMessage + # messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt messages::Vector{agentMessage} pendingToolCalls::Vector{String} # Tool call IDs waiting for results @@ -568,7 +568,7 @@ yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...) ``` """ function yiemAgent( - ; systemPrompt::String="", + ; systemPrompt::String="You are helpful assistant.", model=nothing, tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], diff --git a/src/utils.jl b/src/utils.jl index 2efd78b..0612a78 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -103,13 +103,21 @@ prepareContext(state) == deepcopy(state.messages) # return msgs # end ``` -""" -function prepareContext(state::agentState)::Vector{agentMessage} - messages = deepcopy(state.messages) # messages that will be send to LLM +""" #WORKING +function prepareContext(state::agentState)::agentContext - #TODO adjust/modify and inject additional context into messages + #TODO filter tools from state.tools based on user intend in user message and tool description + filteredTools = state.tools - return messages + #TODO add tools to current system prompt + preparedSystemPrompt = state.systemPrompt + + #TODO add system prompt, adjust/modify and inject additional context into messages + preparedMessages = deepcopy(state.messages) # messages that will be send to LLM + + agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools) + + return agentCtx end diff --git a/test/chatting_with_agent.jl b/test/chatting_with_agent.jl deleted file mode 100644 index 5e66a0f..0000000 --- a/test/chatting_with_agent.jl +++ /dev/null @@ -1,296 +0,0 @@ -using Revise -using JSON, JSON3, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames -using YiemAgent, GeneralUtils -using Base.Threads - -# ---------------------------------------------- 100 --------------------------------------------- # - - - -# load config -config = JSON.parsefile("/appfolder/app/dev/YiemAgent/test/config.json") -# config = copy(JSON.parsefile("../mountvolume/config.json")) - - -function executeSQL(sql::T) where {T<:AbstractString} - host = config[:externalservice][:wineDB][:host] - port = config[:externalservice][:wineDB][:port] - dbname = config[:externalservice][:wineDB][:dbname] - user = config[:externalservice][:wineDB][:user] - password = config[:externalservice][:wineDB][:password] - DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") - result = LibPQ.execute(DBconnection, sql) - close(DBconnection) - return result -end - -function executeSQLVectorDB(sql) - host = config[:externalservice][:SQLVectorDB][:host] - port = config[:externalservice][:SQLVectorDB][:port] - dbname = config[:externalservice][:SQLVectorDB][:dbname] - user = config[:externalservice][:SQLVectorDB][:user] - password = config[:externalservice][:SQLVectorDB][:password] - DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") - result = LibPQ.execute(DBconnection, sql) - close(DBconnection) - return result -end - -function text2textInstructLLM(prompt::String; maxattempt::Integer=3, modelsize::String="medium", - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.1, - ) - ) - msgMeta = GeneralUtils.generate_msgMeta( - config[:externalservice][:loadbalancer][:mqtttopic]; - msgPurpose="inference", - senderName="yiemagent", - senderId=sessionId, - receiverName="text2textinstruct_$modelsize", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :text => prompt, - :kwargs => llmkwargs - ) - ) - - response = nothing - for attempts in 1:maxattempt - _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=180, maxattempt=maxattempt) - payload = _response[:response] - if _response[:success] && payload[:text] !== nothing - response = _response[:response][:text] - break - else - println("\n attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(outgoingMsg) - println(" attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - sleep(3) - end - end - - return response -end - -# get text embedding from a LLM service -function getEmbedding(text::T) where {T<:AbstractString} - msgMeta = GeneralUtils.generate_msgMeta( - config[:externalservice][:loadbalancer][:mqtttopic]; - msgPurpose="embedding", - senderName="yiemagent", - senderId=sessionId, - receiverName="textembedding", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :text => [text] # must be a vector of string - ) - ) - - response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120, maxattempt=3) - embedding = response[:response][:embeddings] - return embedding -end - -function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3, - vectorDB::Function; limit::Integer=1 - )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} - # get embedding from LLM service - embedding = getEmbedding(text)[1] - # check whether there is close enough vector already store in vectorDB. if no, add, else skip - sql = """ - SELECT *, $embeddingColumnName <-> '$embedding' as distance - FROM $tablename - ORDER BY distance LIMIT $limit; - """ - response = vectorDB(sql) - df = DataFrame(response) - return df -end - -function similarSQLVectorDB(query; maxdistance::Integer=100) - tablename = "sqlllm_decision_repository" - # get embedding of the query - df = findSimilarTextFromVectorDB(query, tablename, - "function_input_embedding", executeSQLVectorDB) - # println(df[1, [:id, :function_output]]) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - # distance = 100 # CHANGE this is for testing only - if row != 0 && distance < maxdistance - # if there is usable SQL, return it. - output_b64 = df[1, :function_output_base64] # pick the closest match - output_str = String(base64decode(output_b64)) - rowid = df[1, :id] - println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - return (dict=output_str, distance=distance) - else - println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - return (dict=nothing, distance=nothing) - end -end - -function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} - tablename = "sqlllm_decision_repository" - # get embedding of the query - # query = state[:thoughtHistory][:question] - df = findSimilarTextFromVectorDB(query, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row == 0 || distance > maxdistance # no close enough SQL stored in the database - query_embedding = getEmbedding(query)[1] - query = replace(query, "'" => "") - sql_base64 = base64encode(SQL) - sql_ = replace(SQL, "'" => "") - - sql = """ - INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); - """ - # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # println(sql) - _ = executeSQLVectorDB(sql) - end -end - - -function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3 - )::Union{AbstractDict, Nothing} where {T1<:AbstractString} - tablename = "sommelier_decision_repository" - # find similar - println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__) - df = findSimilarTextFromVectorDB(recentevents, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row != 0 && distance < maxdistance - # if there is usable decision, return it. - rowid = df[1, :id] - println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) - output_b64 = df[1, :function_output_base64] # pick the closest match - _output_str = String(base64decode(output_b64)) - output = copy(JSON.parsefile(_output_str)) - return output - else - println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) - return nothing - end -end - - -function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5 - ) where {T1<:AbstractString, T2<:AbstractDict} - tablename = "sommelier_decision_repository" - # find similar - df = findSimilarTextFromVectorDB(recentevents, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row == 0 || distance > maxdistance # no close enough SQL stored in the database - recentevents_embedding = getEmbedding(recentevents)[1] - recentevents = replace(recentevents, "'" => "") - decision_json = JSON.json(decision) - decision_base64 = base64encode(decision_json) - decision = replace(decision_json, "'" => "") - - sql = """ - INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding'); - """ - println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) - println(sql) - _ = executeSQLVectorDB(sql) - else - println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__) - end -end - - -sessionId = "12345" - -externalFunction = ( - getEmbedding=getEmbedding, - text2textInstructLLM=text2textInstructLLM, - executeSQL=executeSQL, - similarSQLVectorDB=similarSQLVectorDB, - insertSQLVectorDB=insertSQLVectorDB, - similarSommelierDecision=similarSommelierDecision, - insertSommelierDecision=insertSommelierDecision, - ) - - - -a = YiemAgent.sommelier( - externalFunction; - name="Ton", - id=sessionId, # agent instance id - retailername="Yiem", -) - -while true - print("\nyour respond: ") - user_answer = readline() - response = YiemAgent.conversation(agent; - userinput=Dict(:text=> user_answer), - maximumMsg=50) - println("\n$response") -end - - -# response = YiemAgent.conversation(a, Dict(:text=> "I want to get a French red wine under 100.")) - - -""" -hello I want to get a bottle of red wine for my boss. I have a budget around 50 dollars. Show me some options. - -I have no idea about his wine taste but he likes spicy food. - - -""" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/prompttest.jl b/test/prompttest.jl deleted file mode 100644 index b75dd4c..0000000 --- a/test/prompttest.jl +++ /dev/null @@ -1,159 +0,0 @@ -using Revise -using YiemAgent, GeneralUtils, JSON3, DataStructures - -thoughtDict = OrderedDict( - :Question=> "Hello, I would like a get a bottle of wine", - :Thought_1=> "The customer wants to buy a bottle of wine, but we need more information about their preferences.", - :Action_1=> Dict( - :name=> "chatbox", - :input=> "What occasion are you buying the wine for?", - ), - :Observation_1=> "We are having a wedding pary this weekend.", - - :Thought_2=> "A wedding party is a great occasion to have a good bottle of wine.", - :Action_2=> Dict( - :name=> "chatbox", - :input=> "What type of food will you be serving with the wine?", - ), - :Observation_2=> "I think it is Thai dishes", - - :Thought_3=> "Now that I know the occasion and food, I need to ask about the budget.", - :Action_3=> Dict( - :name=> "chatbox", - :input=> "What is your budget for this wine?", - ), - :Observation_3=> "50 bucks", - - :Thought_4=> "With a budget of \$50, we have a wide range of options. Now that I know it's a wedding party and Thai dishes, I need to ask about the type of wine they prefer.", - :Action_4=> Dict( - :name=> "chatbox", - :input=> "What type of wine are you looking for? (Red, White, Sparkling, Rose, Dessert, Fortified)", - ), - :Observation_4=> "Sparkling please.", - - :Thought_5=> "Now that I know the occasion, food, budget and preferred type of wine, it's time to check our inventory for the best matching wine.", - :Action_5=> Dict( - :name=> "winestock", - :input=> "wine with budget \$50, Thai dishes, sparkling, wedding party", - ), - :Observation_5=> "I found the following wine in stock {1 : Zena Crown Vista, 2 : Schrader Cabernet Sauvignon}", - - :Thought_6=> "Now that I have all the information, it's time to recommend a wine that fits their preferences.", - :Action_6=> Dict( - :name=> "recommendation", - :input=> "I recommend Zena Crown Vista for its sparkling and affordable price.", - ), - :Observation_6=> "I don't like it. Do you have another option?", - ) - -_thoughtJsonStr = JSON.json(thoughtDict) -thoughtJsonStr = _thoughtJsonStr[1:end-1] # remove } at the end -# @show thoughtJsonStr - -_, latestThoughtIndice = GeneralUtils.findHighestIndexKey(thoughtDict, "Thought") -nextThoughtIndice = latestThoughtIndice + 1 - -_prompt = -""" -You are a helpful sommelier working for a wine store. -Your goal is to reccommend the best wine from your inventory that match the user preferences. - -You must follow the following criteria: -1) Get to know what occasion the user is buying wine for -2) Get to know what food the user will have with wine -3) Get to know how much the user willing to spend -4) Get to know type of wine the user is looking for e.g. Red, White, Sparkling, Rose, Dessert, Fortified -5) Get to know what characteristics of wine the user is looking for - e.g. tannin, sweetness, intensity, acidity -6) Check your inventory for the best wine that match the user preference -7) Recommend wine to the user - -You should only respond with interleaving Thought, Action, Observation steps. -Thought can reason about the current situation, and Action can be three types: -1) winestock[query], which you can use to find wine in your inventory. The more input data the better. -2) chatbox[text], which you can use to interact with the user. -3) recommendation[answer], which returns your wine reccommendation to the user. - -You should only respond in JSON format as describe below: -{ - "Thought": "your reasoning", - "Action": {"name": "action to take", "input": "Action input"}, - "Observation": "result of the action" -} - -Here are some examples: -{ -"Question": "I would like to buy a sedan with 8 seats.", -"Thought_1": "Our showroom carries various vehicle model. But I'm not sure whether we have a models that fits the user demand, I need to check our inventory.", -"Action_1": {"name": "inventory", "input": "sedan with 8 seats."}, -"Observation_1": "Several model has 8 seats. Available color are black, red green" -} -{ - "Thought_2": "I have to ask the user what color he likes.", - "Action_2": {"name": "chatbox", "input": "Which color do you like?"} - "Observation_2": "I'll take black." -} -{ - "Thought_3": "There is only one model that fits the user preference. It's Yiem model A", - "Action_3": {"name": "recommendation", "input": "I recommend a Yiem model A"} -} - -Let's begin! - -$(JSON.json(thoughtDict)) -{Thought_$nextThoughtIndice -""" - -prompt = YiemAgent.formatLLMtext_llama3instruct("system", _prompt) -@show prompt -msgMeta = Dict(:requestResponse => nothing, - :msgPurpose => nothing, - :receiverId => nothing, - :getPost => nothing, - :msgId => "4c7111e0-c30e-44c3-8f85-1c8b3f03a8be", - :acknowledgestatus => nothing, - :replyToMsgId => nothing, - :msgFormatVersion => nothing, - :mqttServerInfo => Dict(:port => 1883, :broker => "mqtt.yiem.cc"), - :sendTopic => "/loadbalancer/requestingservice", - :receiverName => "text2textinstruct", - :replyTopic => nothing, - :senderName => "decisionMaker", - :senderSelfnote => nothing, - :senderId => "testingSessionID", - :timeStamp => "2024-05-04T08:06:23.561" - ) - -outgoingMsg = Dict( - :msgMeta=> msgMeta, - :payload=> Dict( - :text=> prompt, - ) -) - - - -_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) -thoughtJsonStr = _response[:response][:text] - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/prompttest_1.jl b/test/prompttest_1.jl deleted file mode 100644 index 229fbc6..0000000 --- a/test/prompttest_1.jl +++ /dev/null @@ -1,87 +0,0 @@ -using Revise # remove when this package is completed -using YiemAgent, GeneralUtils, JSON3, MQTTClient, Dates, UUIDs, DataStructures -using Base.Threads - -# ---------------------------------------------- 100 --------------------------------------------- # - -config = copy(JSON.parsefile("config.json")) - -instanceInternalTopic = config[:serviceInternalTopic][:mqtttopic] * "/1" - -client, connection = MakeConnection(config[:mqttServerInfo][:broker], - config[:mqttServerInfo][:port]) - -receiveUserMsgChannel = Channel{Dict}(4) -receiveInternalMsgChannel = Channel{Dict}(4) - -msgMeta = GeneralUtils.generate_msgMeta( - "N/A", - replyTopic = config[:servicetopic][:mqtttopic] # ask frontend reply to this instance_chat_topic - ) - -agentConfig = Dict( - :mqttServerInfo=> config[:mqttServerInfo], - :receivemsg=> Dict( - :prompt=> config[:servicetopic][:mqtttopic], # topic to receive prompt i.e. frontend send msg to this topic - :internal=> instanceInternalTopic, - ), - :externalservice=> config[:externalservice], -) - -# Instantiate an agent -tools=Dict( # update input format - "askbox"=> Dict( - :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - :input => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - :output => "" , - :func => nothing, - ), - # "winestock"=> Dict( - # :description => "A handy tool for searching wine in your inventory that match the user preferences.", - # :input => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", - # :output => """Output are wines that match the search query in JSON format.""", - # :func => ChatAgent.winestock, - # ), - "finalanswer"=> Dict( - :description => "Useful for when you are ready to recommend wines to the user.", - :input => """{\"finalanswer\": \"some text\"}.{\"finalanswer\": \"I recommend Zena Crown Vista\"}""", - :output => "" , - :func => nothing, - ), - ) - - a = YiemAgent.sommelier( - receiveUserMsgChannel, - receiveInternalMsgChannel, - agentConfig, - name= "assistant", - id= "testingSessionID", # agent instance id - tools=tools, - ) - - - - - - - -input = -OrderedDict{String, Any}(:question => "Hello, I would like a get a bottle of wine", :thought_1 => "It's great that the user is looking for a bottle of wine. To give them a personalized recommendation, I need to know more about their preferences.", :action_1 => Dict{String, Any}(:name => "chatbox", :input => "What occasion are you planning to use this wine for?"), :observation_1 => "We are holding a wedding party", :thought_2 => "A wedding party is a great occasion for a special bottle of wine. I need to know what type of food will be served, and how much the user is willing to spend.", :action_2 => Dict{String, Any}(:name => "chatbox", :input => "What type of food will you be serving at the wedding?"), :observation_2 => "It will be Thai dishes.", :thought_3 => "The type of wine that pairs well with Thai dishes is usually a crisp and refreshing white wine, but I also need to consider the budget and personal preferences.", :action_3 => Dict{String, Any}(:name => "chatbox", :input => "How much are you willing to spend on this bottle of wine?"), :observation_3 => "I would spend up to 50 bucks.", :thought_4 => "I have a good idea of the occasion, food, and budget. Now I need to know what type of wine the user is looking for.", :action_4 => Dict{String, Any}(:name => "chatbox", :input => "What type of wine are you usually looking for? Red, White, Sparkling, Rose, Dessert or Fortified?"), :observation_4 => "I like full-bodied Red wine with low tannin.", :thought_5 => "Now that I have all the necessary information, I can start searching for a suitable wine in our inventory.", :action_5 => Dict{String, Any}(:name => "winestock", :input => "red wine with low tannins"), :observation_5 => "I found the following wines in our stock: \n{\n 1: El Enemigo Cabernet Franc 2019\n2: Tantara Chardonnay 2017\n\n}\n", :thought_6 => "Now that I have the information about the wine, it's time to make a recommendation.", :action_6 => Dict{String, Any}(:name => "recommendbox", :input => "El Enemigo Cabernet Franc 2019"), :observation_6 => "I don't like the one you recommend. I want dry wine.") - - -result = YiemAgent.jsoncorrection(a, input) - - - - - - - - - - - - - - - diff --git a/test/prompttest_2.jl b/test/prompttest_2.jl deleted file mode 100644 index 58f3e2d..0000000 --- a/test/prompttest_2.jl +++ /dev/null @@ -1,119 +0,0 @@ -using Revise -using YiemAgent, GeneralUtils, JSON3, DataStructures, LibPQ -using SQLLLM - - -# _prompt = -# """ -# You are a helpful assistant. -# answer the following question: -# From the following CSV text: -# "{\"tabledescription\":[\"The customer table stores information about customers. It includes details such as first name, last name, display name, username, password, gender, country, telephone number, email, birthdate, additional_search_term, other attributes (in JSON format) and a description.\",\"The wine table stores information about different wines. It includes details namely id, name, brand, manufacturer, region, country, wine_type, grape_variety, serving_temperature, intensity, sweetness, tannin, acidity, fizziness, additional_search_term, other attributes (in JSON format) and a description.\",\"The wine_food table represents the association between wines and food items. It estab" ⋯ 477 bytes ⋯ "ed to retailer names, usernames, passwords, addresses, contact persons, telephone numbers, email addresses, additional_search_term, other attributes (in JSON format) and a description.\",\"The retailer_wine table represents the relationship between retailers and wines. It stores information about the wines available from which retailers, including vintage, their price, and the currency.\",\"The retailer_food table represents the relationship between retailers and food items. It stores information about the food items available from which retailers, including their price and the currency.\"],\"tablename\":[\"customer\",\"wine\",\"wine_food\",\"food\",\"retailer\",\"retailer_wine\",\"retailer_food\"]}" -# What is the description of table wine? -# """ - -# prompt = YiemAgent.formatLLMtext_llama3instruct("system", _prompt) -# @show prompt -# msgMeta = Dict(:requestResponse => nothing, -# :msgPurpose => nothing, -# :receiverId => nothing, -# :getPost => nothing, -# :msgId => "4c7111e0-c30e-44c3-8f85-1c8b3f03a8be", -# :acknowledgestatus => nothing, -# :replyToMsgId => nothing, -# :msgFormatVersion => nothing, -# :mqttServerInfo => Dict(:port => 1883, :broker => "mqtt.yiem.cc"), -# :sendTopic => "/loadbalancer/requestingservice", -# :receiverName => "text2textinstruct", -# :replyTopic => nothing, -# :senderName => "decisionMaker", -# :senderSelfnote => nothing, -# :senderId => "testingSessionID", -# :timeStamp => "2024-05-04T08:06:23.561" -# ) - -# outgoingMsg = Dict( -# :msgMeta=> msgMeta, -# :payload=> Dict( -# :text=> prompt, -# ) -# ) - - - -# _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) -# result = _response[:response][:text] - - - - - - - -DBconnection = LibPQ.Connection("host=192.168.88.12 port=5432 dbname=yiem_wine_assistant user=yiem password=yiem@Postgres_0.0") - -tableinfo, df1, df2, df3 = SQLLLM.tableinfo(DBconnection, "wine") - - -_prompt = -""" -You are a helpful assistant helping to answer user question from a database table. - -$tableinfo - -Are there any chardonnay? -""" - -prompt = YiemAgent.formatLLMtext_llama3instruct("system", _prompt) -@show prompt -msgMeta = Dict(:requestResponse => nothing, - :msgPurpose => nothing, - :receiverId => nothing, - :getPost => nothing, - :msgId => "4c7111e0-c30e-44c3-8f85-1c8b3f03a8be", - :acknowledgestatus => nothing, - :replyToMsgId => nothing, - :msgFormatVersion => nothing, - :mqttServerInfo => Dict(:port => 1883, :broker => "mqtt.yiem.cc"), - :sendTopic => "/loadbalancer/requestingservice", - :receiverName => "text2textinstruct", - :replyTopic => nothing, - :senderName => "decisionMaker", - :senderSelfnote => nothing, - :senderId => "testingSessionID", - :timeStamp => "2024-05-04T08:06:23.561" - ) - -outgoingMsg = Dict( - :msgMeta=> msgMeta, - :payload=> Dict( - :text=> prompt, - ) -) - - - -_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg) -result2 = _response[:response][:text] - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/test1.jl b/test/runtest.jl similarity index 100% rename from test/test1.jl rename to test/runtest.jl diff --git a/test/runtests.jl b/test/runtests.jl deleted file mode 100644 index e3da741..0000000 --- a/test/runtests.jl +++ /dev/null @@ -1,223 +0,0 @@ - -using JSON, Dates, UUIDs, PrettyPrinting, Base64, NATS, HTTP -using GeneralUtils, msghandler - -config = JSON.parsefile("./appconfig.json") - -agent_conn = NATS.connect(config["nats_server_info"]["url"]) - -function text2text_instruct_llm(sender_id::String, openai_msg::Dict{String, Any}) - payloads = [("msg", openai_msg, "dictionary")] # List of tuples - _, msg_envelope_json_str = msghandler.smartpack( - config["externalservice"]["servicesloadbalancer"]["nats"], - payloads; - sender_id=sender_id, - msg_purpose="text2text", - broker_url=config["nats_server_info"]["url"], - fileserver_url=config["externalservice"]["fileserver"]["url"]) - - reply = NATS.request(agent_conn, - config["externalservice"]["servicesloadbalancer"]["nats"], - msg_envelope_json_str, timeout=120) - - incoming_env_json_str = String(reply.payload) - incoming_env = msghandler.smartunpack(incoming_env_json_str) - _llm_response = incoming_env["payloads"][1][2] - llm_response = _llm_response["choices"][1]["message"]["content"] - return llm_response -end - - - - -# 1. Read local file and encode to base64 string -image1_path = "test/large_image.png" -image1_bytes = read(image1_path) -image1_base64_string = base64encode(image1_bytes) - -# 2. Match the MIME type according to your file extension (e.g., png, jpeg) -mime_type = "image/png" -data1_uri = "data:$(mime_type);base64,$(image1_base64_string)" - -# 3. Construct payload with the Data URI -openai_msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ) - ] - ) - ], - "temperature" => 0.7 -) - -llm_response = text2text_instruct_llm(openai_msg) - - - -# 1. Read local file and encode to base64 string -image2_path = "test/large_image.png" -image2_bytes = read(image2_path) -image2_base64_string = base64encode(image2_bytes) - -# 2. Match the MIME type according to your file extension (e.g., png, jpeg) -mime_type = "image/png" -data2_uri = "data:$(mime_type);base64,$(image2_base64_string)" - -systemmsg = - """ - # Store Policy - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock. - - You can only recommend wines that are currently in our inventory - - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. - - Ask the user one question at a time. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. - - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. - - Spicy foods should be paired only with light red wines. - - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. - - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - - # Store Guidelines - - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. - - Customer may provide images for you to look up. - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. - - # Situation - Your customer is coming into the store - - # Role - Your name is Janie. You are a helpful sommelier for website-based Yiem Wine's wine store. You are working under your mentor supervision. - - # Objective - 1. Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. - 2. Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. - - # Responsibility Includes - 1. According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the objective - 2. Keep the conversation with the customer going smoothly - 3. Obey your mentor's suggestions. - - # Responsibility Does NOT Include - - 1. Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - 2. Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - 3. Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - # You should then respond to the user with interleaving plan, action_name, action_input - 1) plan: Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name - 3) action_input: The input to the action you are about to perform according to your plan. - After the action is executed you gets "action_result". It is the output from the action you selected. - - # You should only respond in JSON format as described below - "plan": "...", - "action_name": "...", - "action_input": "..." - - # Available Actions - - **CHAT_BOX** which you can use to talk with the user. - - **SEARCH_WINE_DATABASE** allows you to check information about wines you want in your inventory's database. The input is text that specify supported search criteria includeing: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - - Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." - - Example query 2: "Red or white wine, medium tannin, price under 700 USD" - - Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France - - **PRESENT_WINE_GUIDELINE** which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. - - **END_CONVER_GUIDELINE** which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. - """ - - -openai_msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), - ] - ), - Dict( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => - """ - " I will greet the customer warmly as Janie, acknowledge their request to find a similar wine for their wedding party based on the image, identify the wine type and country (Italian Sparkling Wine), and then use the SEARCH_WINE_DATABASE action to search the inventory for suitable options.\n CHAT_BOX\n Hello! I'm Janie, and I'd be delighted to help you find the perfect wine for your wedding party. That beautiful wine in the image appears to be an Italian sparkling wine, which is wonderful for a celebration like a wedding! Since you have an unlimited budget, I can certainly look for some truly exceptional options. To start, I will check our inventory for similar Italian sparkling wines that are perfect for a wedding celebration. User response in the next message " - """ - ), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "ok"), - ] - ), - ], - "temperature" => 0.7 -) - -llm_response = text2text_instruct_llm(openai_msg) - - - - - -# ---------------------------------------------- 100 --------------------------------------------- # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 2.52.0 From d63ddd3d27b65832bb5e2d46ed1a4968090cc711 Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 09:24:41 +0700 Subject: [PATCH 34/50] update --- src/utils.jl | 85 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 0612a78..a958535 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -74,36 +74,39 @@ end """ - prepareContext(state::agentState) -> Vector{agentMessage} + prepareContext(state::agentState) -> agentContext -Returns a deep copy of the messages from the given `agentState`, ready -to be sent to the LLM. Override this function to inject additional -context — such as retrieved documents, current time, user preferences, -or any other relevant information — into the message list before -formatting and calling the LLM. +Prepares an `agentContext` from the given `agentState` for sending to +the LLM. By default, it deep copies the system prompt, messages, and +tools from `state` into a new `agentContext`. -By default, returns an exact copy of `state.messages` without -modification. +Override this function to customize the context — such as filtering +tools based on the user's intent, modifying the system prompt, injecting +additional context (retrieved documents, current time, user preferences), +or pruning and reordering messages before formatting and calling the LLM. # Arguments -- `state::agentState`: The current agent state containing conversation history +- `state::agentState`: The current agent state containing conversation history, + system prompt, tools, and other configuration # Returns -- `Vector{agentMessage}`: A deep copy of the messages to be sent to the LLM +- `agentContext`: An `agentContext` containing the prepared system prompt, + messages, and tools to be sent to the LLM # Examples ```julia -# Default: returns a deep copy of messages -prepareContext(state) == deepcopy(state.messages) +# Default: returns an agentContext with deep copies of system prompt, messages, and tools +prepareContext(state).messages == deepcopy(state.messages) -# Override to inject system context: -# function Base.prepareContext(state::agentState) +# Override to filter tools and inject system context: +# function prepareContext(state::agentState) # msgs = deepcopy(state.messages) -# pushfirst!(msgs, textMessage("system", "You are a helpful assistant.")) -# return msgs +# sysPrompt = state.systemPrompt * "\\nCurrent time: $(now())" +# tools = filter(t -> contains(t.description, "wine"), state.tools) +# return agentContext(sysPrompt, msgs, tools) # end ``` -""" #WORKING +""" function prepareContext(state::agentState)::agentContext #TODO filter tools from state.tools based on user intend in user message and tool description @@ -121,9 +124,38 @@ function prepareContext(state::agentState)::agentContext end -""" convert preparedcontext into openai message format ready to be used by LLM """ -function formatMsgForLLM(preparedContext::Vector{agentMessage})::Dict{String, Any} + formatMsgForLLM(ctx::agentContext) -> Dict{String, Any} + +Converts an `agentContext` into OpenAI-compatible message format +ready to be sent to the LLM. The system prompt is converted into +a system role message, followed by user, assistant, and tool result +messages. + +This function can be overridden in `yiemAgent` to produce custom +LLM message formats for different APIs/providers. + +# Arguments +- `ctx::agentContext`: The prepared context containing system prompt, + messages, and tools + +# Returns +- `Dict{String, Any}`: A dictionary with `"messages"` key containing + an array of OpenAI-format message dicts + +# Examples +```julia +# Default output: +formatMsgForLLm(ctx) == Dict("messages" => [ + Dict("role" => "system", "content" => [...]), + Dict("role" => "user", "content" => [...]), + Dict("role" => "assistant", "content" => [...]), + Dict("role" => "tool", "tool_call_id" => "...", "content" => [...]), +]) +``` +""" +function formatMsgForLLM(ctx::agentContext)::Dict{String, Any} + """ openai message format example msg = Dict( "model" => "gemma-4-E4B-it-UD-Q4_K_XL", @@ -140,7 +172,7 @@ function formatMsgForLLM(preparedContext::Vector{agentMessage})::Dict{String, An Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"), Dict( "type" => "image_url", - "image_url" => Dict("url" => data1_uri) + "image_url" => Dict("url" => data_uri) ) ] ), @@ -161,9 +193,18 @@ function formatMsgForLLM(preparedContext::Vector{agentMessage})::Dict{String, An ) """ - messages = Dict{String, Any}[] + messages = Vector{Dict{String, Any}}() - for msg in preparedContext + # System prompt as system message + if !isempty(ctx.systemPrompt) + push!(messages, Dict( + "role" => "system", + "content" => [Dict("type" => "text", "text" => ctx.systemPrompt)] + )) + end + + # Conversation messages + for msg in ctx.messages if msg isa userMessage push!(messages, _userMessageToOpenAI(msg)) elseif msg isa assistantMessage -- 2.52.0 From b08121ee508cd7c2fb753143eaa2c4fbe91bdab4 Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 09:37:27 +0700 Subject: [PATCH 35/50] update --- src/agentCore.jl | 3 +++ src/utils.jl | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 311d00e..d19313d 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -176,6 +176,9 @@ function _process_message(agent::yiemAgent)::assistantMessage # loop until llmCall() response didn't use tool calls final_response = nothing while true + #WORKING check + + # call agent.prepareContext() preparedContext = agent.prepareContext(agent._state) diff --git a/src/utils.jl b/src/utils.jl index a958535..fda9814 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,6 +1,7 @@ module utils -export clearhistory, availableWineToText, prepareContext +export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, _userMessageToOpenAI, + _assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks using UUIDs, Dates, DataStructures, HTTP, JSON using GeneralUtils -- 2.52.0 From 2de4fa07c12d5c055dd564c94133dbd37bb21a4d Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 10:19:55 +0700 Subject: [PATCH 36/50] update --- src/agentCore.jl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index d19313d..21c7dfa 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -176,7 +176,22 @@ function _process_message(agent::yiemAgent)::assistantMessage # loop until llmCall() response didn't use tool calls final_response = nothing while true - #WORKING check + + """ example message in agent.inputChannel + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data_uri) + ) + ] + ), + """ + + # check agent.inputChannel if there are, use OpenAiToUserMessage() to convert them into + # userMessage type and push to agent._state.messages. # call agent.prepareContext() -- 2.52.0 From 14bac755ea87596ca1e486f93e60b0abde415a9b Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 13:28:09 +0700 Subject: [PATCH 37/50] update --- src/agentCore.jl | 79 +++++++++++++++++++++++++++++++++++++++++++++--- src/type.jl | 40 ------------------------ 2 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 21c7dfa..4de1215 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -1,6 +1,6 @@ module agentCore -export _agent_loop +export _agent_loop, OpenAiToUserMessage using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, Serde @@ -184,14 +184,23 @@ function _process_message(agent::yiemAgent)::assistantMessage Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"), Dict( "type" => "image_url", - "image_url" => Dict("url" => data_uri) + "image_url" => Dict("url" => "data:mime_type;base64,image2_base64_string") ) ] ), """ - # check agent.inputChannel if there are, use OpenAiToUserMessage() to convert them into - # userMessage type and push to agent._state.messages. + # Drain inputChannel and convert OpenAI-format messages to userMessage type + while isready(agent.inputChannel) + raw_msg = take!(agent.inputChannel) + if raw_msg === :shutdown + # Re-emit shutdown signal for the loop to handle + put!(agent.inputChannel, :shutdown) + break + end + user_msg = OpenAiToUserMessage(raw_msg) + push!(agent._state.messages, user_msg) + end # call agent.prepareContext() @@ -1008,14 +1017,66 @@ end +""" + OpenAiToUserMessage(msg::Dict) -> userMessage +Converts an OpenAI-format message dictionary into a `userMessage` type. +Parses `content` blocks: `text` blocks become `textContent`, `image_url` blocks +have their data URI (`data:;base64,`) parsed via regex to extract the +base64 data and MIME type as separate `imageContent` fields. +The OpenAI-format dictionary: +``` +Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "..."), + Dict("type" => "image_url", "image_url" => Dict("url" => "data:image/png;base64,...")) + ] +) +``` +# Arguments +- `msg`: A dictionary with `"role"` and `"content"` keys in OpenAI format +# Returns +- `userMessage`: Instance with `content` as `Vector{messageContent}` +# Examples +```julia +msg = Dict("role" => "user", "content" => [Dict("type" => "text", "text" => "Hello")]) +OpenAiToUserMessage(msg) +# => userMessage("user", [textContent("Hello")], DateTime(...)) +``` +""" +function OpenAiToUserMessage(msg::Dict)::userMessage + content_blocks = Vector{messageContent}() + raw_content = get(msg, "content", Any[]) + if raw_content isa Vector + for block in raw_content + if block isa Dict + block_type = get(block, "type", "") + if block_type == "text" + text = get(block, "text", "") + push!(content_blocks, textContent(text)) + elseif block_type == "image_url" + image_url = get(block, "image_url", Dict()) + url = get(image_url, "url", "") + m = match(r"^data:([a-z0-9/_-]+);base64,(.+)$", url) + if m !== nothing + push!(content_blocks, imageContent(m.captures[2], m.captures[1])) + else + push!(content_blocks, imageContent(url, "image/png")) + end + end + end + end + end + return userMessage(content=content_blocks) +end @@ -1087,4 +1148,12 @@ end -end # end of module \ No newline at end of file + + + + + + + + +end # end of module diff --git a/src/type.jl b/src/type.jl index 264cdd7..f573e67 100644 --- a/src/type.jl +++ b/src/type.jl @@ -139,51 +139,11 @@ struct textContent <: messageContent # Plain text message content text::String # The text content end -""" -Create plain text message content. - -# Arguments -- `text::String`: The text content - -# Returns -- A new `textContent` instance - -# Examples -```julia -julia> content = textContent("Hello, world!") -textContent("Hello, world!") -``` -""" -function textContent(; text="") - return textContent(text) -end - struct imageContent <: messageContent # Image message content data::String # Base64-encoded image data mimeType::String # MIME type (e.g., "image/png") end -""" -Create image message content. - -# Arguments -- `data::String`: Base64-encoded image data -- `mimeType::String`: MIME type (e.g., "image/png") - -# Returns -- A new `imageContent` instance - -# Examples -```julia -julia> img = imageContent(data="base64data...", mimeType="image/png") -imageContent("base64data...", "image/png") -``` -""" -function imageContent(; data="", mimeType="") - return imageContent(data, mimeType) -end - - # ------------------------------------------------------------------------------------------------ # # Tool types # # ------------------------------------------------------------------------------------------------ # -- 2.52.0 From 197a7a0cb7a3dec3cd241b36faa92526a1f88853 Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 13:59:13 +0700 Subject: [PATCH 38/50] update --- src/agentCore.jl | 3 +-- src/type.jl | 46 +++++++++++++++++----------------------------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 4de1215..7031295 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -202,7 +202,6 @@ function _process_message(agent::yiemAgent)::assistantMessage push!(agent._state.messages, user_msg) end - # call agent.prepareContext() preparedContext = agent.prepareContext(agent._state) @@ -998,7 +997,7 @@ function executeToolCalls( hasSequential = false for tc in toolCalls for t in context.tools - if t.name == tc.name && get(t.executionMode, "parallel") == "sequential" + if t.name == tc.name && !t.parallelToolExecute hasSequential = true break end diff --git a/src/type.jl b/src/type.jl index f573e67..0da023f 100644 --- a/src/type.jl +++ b/src/type.jl @@ -13,6 +13,21 @@ struct Usage outputTokens::Int64 end +# ------------------------------------------------------------------------------------------------ # +# Message content types # +# ------------------------------------------------------------------------------------------------ # + +abstract type messageContent end # Base type for message content + +struct textContent <: messageContent # Plain text message content + text::String # The text content +end + +struct imageContent <: messageContent # Image message content + data::String # Base64-encoded image data + mimeType::String # MIME type (e.g., "image/png") +end + # ------------------------------------------------------------------------------------------------ # # Message types # @@ -128,22 +143,6 @@ function toolResultMessage(; role="tool", toolCallId="", toolName="", return toolResultMessage(role, toolCallId, toolName, content, details, usage, addedToolNames, isError, timestamp) end - -# ------------------------------------------------------------------------------------------------ # -# Message content types # -# ------------------------------------------------------------------------------------------------ # - -abstract type messageContent end # Base type for message content - -struct textContent <: messageContent # Plain text message content - text::String # The text content -end - -struct imageContent <: messageContent # Image message content - data::String # Base64-encoded image data - mimeType::String # MIME type (e.g., "image/png") -end - # ------------------------------------------------------------------------------------------------ # # Tool types # # ------------------------------------------------------------------------------------------------ # @@ -158,7 +157,7 @@ A tool available to the agent. - `parameters::TParameters`: Tool parameters schema (JSON schema) - `execute::Function`: Tool execution function - `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback -- `parallelExecute::Union{toolparallelExecute, Nothing}`: Override: run tool calls sequentially or in parallel +- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel # Returns - A new `agentTool` instance @@ -170,7 +169,7 @@ struct agentTool{TParameters, TDetails} # A tool available to the agent parameters::TParameters # Tool parameters schema (JSON schema) execute::Function # Tool execution function prepareArguments::Union{Function, Nothing} # Optional argument preparation callback - parallelExecute::Union{toolparallelExecute, Nothing} # Override: run tool calls sequentially or in parallel + parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel end @@ -346,17 +345,6 @@ struct agentToolResult terminate::Bool end -""" -Function type for parallel execution override on a tool. - -# Arguments -- Context for parallel execution - -# Returns -- `agentToolCallBatch`: The result batch from parallel execution -""" -const toolparallelExecute = Function - """ Context passed to the `beforeToolCall` hook. -- 2.52.0 From 9e637f92bd99492d233a8f227a430d735938e09f Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 14:07:40 +0700 Subject: [PATCH 39/50] update --- src/type.jl | 61 +++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/type.jl b/src/type.jl index 0da023f..e659ca1 100644 --- a/src/type.jl +++ b/src/type.jl @@ -8,11 +8,36 @@ using GeneralUtils const Timestamp = DateTime -struct Usage +# ------------------------------------------------------------------------------------------------ # +# LLM model info # +# ------------------------------------------------------------------------------------------------ # + +struct modelCost # Model pricing per 1M tokens + input::Float64 # Price per 1M input tokens + output::Float64 # Price per 1M output tokens + cache_read::Float64 # Price per 1M cached read tokens + cache_write::Float64 # Price per 1M cache write tokens +end + +struct llmModel # LLM model configuration + id::String # Unique model identifier + name::String # Human-readable model name + api::Api # API type (parametric type) + provider::String # Provider name (e.g., "anthropic", "openai") + baseUrl::String # API endpoint base URL + reasoning::Bool # Whether the model supports chain-of-thought + input::Vector{String} # Supported input modalities (e.g., "text", "image") + cost::modelCost # Pricing information + contextWindow::Int64 # Maximum context length in tokens + maxTokens::Int64 # Maximum output tokens per completion +end + +struct llmUsage inputTokens::Int64 outputTokens::Int64 end + # ------------------------------------------------------------------------------------------------ # # Message content types # # ------------------------------------------------------------------------------------------------ # @@ -67,7 +92,7 @@ struct assistantMessage <: agentMessage # Message from the AI assistant api::String # API name used (e.g., "openai") provider::String # Provider name (e.g., "anthropic") model::String # Model identifier - usage::Usage # Token usage for this message + usage::llmUsage # Token usage for this message stopReason::String # Why generation stopped (e.g., "end_turn") errorMessage::Union{String, Nothing} # Error if generation failed timestamp::Timestamp # When the message was received @@ -82,7 +107,7 @@ Create a new assistant message. - `api::String`: API name used (e.g., "openai") - `provider::String`: Provider name (e.g., "anthropic") - `model::String`: Model identifier -- `usage::Usage`: Token usage for this message +- `usage::llmUsage`: Token usage for this message - `stopReason::String`: Why generation stopped (e.g., "end_turn") - `errorMessage::Union{String, Nothing}`: Error if generation failed - `timestamp::Timestamp`: When the message was received @@ -97,7 +122,7 @@ assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "en ``` """ function assistantMessage(; role="assistant", content=Vector{messageContent}(), - api="", provider="", model="", usage=Usage(0, 0), stopReason="end_turn", + api="", provider="", model="", usage=llmUsage(0, 0), stopReason="end_turn", errorMessage=nothing, timestamp=now()) return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp) end @@ -108,7 +133,7 @@ struct toolResultMessage <: agentMessage # Result returned from a tool execut toolName::String # Name of the executed tool content::Vector{messageContent} # Tool output content details::Any # Additional tool-specific details - usage::Union{Usage, Nothing} # Token usage if applicable + usage::Union{llmUsage, Nothing} # Token usage if applicable addedToolNames::Union{Vector{String}, Nothing} # Tools added during execution isError::Bool # Whether the tool call resulted in an error timestamp::Timestamp # When the result was recorded @@ -123,7 +148,7 @@ Create a new tool result message. - `toolName::String`: Name of the executed tool - `content::Vector{messageContent}`: Tool output content - `details::Any`: Additional tool-specific details -- `usage::Union{Usage, Nothing}`: Token usage if applicable +- `usage::Union{llmUsage, Nothing}`: Token usage if applicable - `addedToolNames::Union{Vector{String}, Nothing}`: Tools added during execution - `isError::Bool`: Whether the tool call resulted in an error - `timestamp::Timestamp`: When the result was recorded @@ -279,26 +304,6 @@ struct prepareNextTurnContext # Context for preparing the end -struct modelCost # Model pricing per 1M tokens - input::Float64 # Price per 1M input tokens - output::Float64 # Price per 1M output tokens - cache_read::Float64 # Price per 1M cached read tokens - cache_write::Float64 # Price per 1M cache write tokens -end - -struct llmModel{Api} # LLM model configuration - id::String # Unique model identifier - name::String # Human-readable model name - api::Api # API type (parametric type) - provider::String # Provider name (e.g., "anthropic", "openai") - baseUrl::String # API endpoint base URL - reasoning::Bool # Whether the model supports chain-of-thought - input::Vector{String} # Supported input modalities (e.g., "text", "image") - cost::modelCost # Pricing information - contextWindow::Int64 # Maximum context length in tokens - maxTokens::Int64 # Maximum output tokens per completion -end - # ------------------------------------------------------------------------------------------------ # # Agent loop configuration & tool execution types # # ------------------------------------------------------------------------------------------------ # @@ -335,13 +340,13 @@ Result returned by tool execution before the `afterToolCall` hook. # Arguments - `content::Vector{messageContent}`: Tool output content - `details::Dict{Any,Any}`: Tool-specific details -- `usage::Union{Usage, Nothing}`: Token usage if applicable +- `usage::Union{llmUsage, Nothing}`: Token usage if applicable - `terminate::Bool`: Whether tool requests termination of the agent loop """ struct agentToolResult content::Vector{messageContent} details::Dict{Any,Any} - usage::Union{Usage, Nothing} + usage::Union{llmUsage, Nothing} terminate::Bool end -- 2.52.0 From 7b465baddd88d4f9bc1da409eb7e01dc7f9a31ea Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 14:31:33 +0700 Subject: [PATCH 40/50] update --- src/YiemAgent.jl | 4 ++-- src/agentCore.jl | 26 +++++++++++++------------- src/type.jl | 1 - 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl index e021f3c..764574c 100644 --- a/src/YiemAgent.jl +++ b/src/YiemAgent.jl @@ -13,8 +13,8 @@ module YiemAgent include("utils.jl") using .utils - include("llmfunction.jl") - using .llmfunction + # include("llmfunction.jl") + # using .llmfunction include("agentCore.jl") using .agentCore diff --git a/src/agentCore.jl b/src/agentCore.jl index 7031295..6b96c57 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -3,7 +3,7 @@ module agentCore export _agent_loop, OpenAiToUserMessage using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, - DataFrames, Serde + DataFrames, Serde, Base.Threads using GeneralUtils using ..type, ..utils, ..llmfunction @@ -34,41 +34,41 @@ julia> # Called automatically by yiemAgent constructor """ function _agent_loop(agent::yiemAgent) try - processing_task = nothing + processingTask = nothing """ cases: 1) agent -> idle, user msg -> nothing - typeof(processing_task) == Nothing + typeof(processingTask) == Nothing agent._state.activeRun -> false agent.inputChannel -> nothing agent.followUpChannel -> nothing 2) agent -> idle, user msg -> new msg - typeof(processing_task) == Nothing + typeof(processingTask) == Nothing agent._state.activeRun -> false agent.inputChannel -> new msg agent.followUpChannel -> nothing 3) agent -> running, user msg -> nothing - typeof(processing_task) == Task, istaskdone(processing_task) -> false + typeof(processingTask) == Task, istaskdone(processingTask) -> false agent._state.activeRun -> true agent.inputChannel -> nothing agent.followUpChannel -> nothing 4) agent -> running, user msg -> new msg - typeof(processing_task) == Task, istaskdone(processing_task) -> false + typeof(processingTask) == Task, istaskdone(processingTask) -> false agent._state.activeRun -> true agent.inputChannel -> new msg agent.followUpChannel -> nothing 5) agent -> running, user msg -> nothing, user msg follow up -> new msg - typeof(processing_task) == Task, istaskdone(processing_task) -> false + typeof(processingTask) == Task, istaskdone(processingTask) -> false agent._state.activeRun -> true agent.inputChannel -> nothing agent.followUpChannel -> new msg 6) agent -> idle, user msg -> nothing - typeof(processing_task) == Task, istaskdone(processing_task) -> true + typeof(processingTask) == Task, istaskdone(processingTask) -> true agent._state.activeRun -> false agent.inputChannel -> nothing agent.followUpChannel -> nothing @@ -108,12 +108,12 @@ function _agent_loop(agent::yiemAgent) # start _process_message loop if agent._state.activeRun == false # Dispatch message through the processing pipeline - processing_task = @spawn _process_message(agent) + processingTask = Threads.@spawn _process_message(agent) agent._state.activeRun = true end # during agent runs, check followUp message after _process_message() is done - if typeof(processing_task) == Task && istaskdone(processing_task) == false + if typeof(processingTask) == Task && istaskdone(processingTask) == false # if followUp message available, add them all to agent.inputChannel if isready(agent.followUpChannel) while isready(agent.followUpChannel) @@ -123,7 +123,7 @@ function _agent_loop(agent::yiemAgent) end continue # continue to process user message in the next loop - elseif typeof(processing_task) == Task && istaskdone(processing_task) == true + elseif typeof(processingTask) == Task && istaskdone(processingTask) == true # if agent runs is done but followUpChannel has messages, discard all message in it. # when agent work is done it should not accept follow up msg. # user should put new message in inputChannel instead @@ -132,10 +132,10 @@ function _agent_loop(agent::yiemAgent) _ = take!(agent.followUpChannel) end end - result = fetch(processing_task) + result = fetch(processingTask) put!(agent.outputChannel, result) agent._state.activeRun = false # reset - processing_task = nothing # reset + processingTask = nothing # reset end end catch e diff --git a/src/type.jl b/src/type.jl index e659ca1..c803289 100644 --- a/src/type.jl +++ b/src/type.jl @@ -22,7 +22,6 @@ end struct llmModel # LLM model configuration id::String # Unique model identifier name::String # Human-readable model name - api::Api # API type (parametric type) provider::String # Provider name (e.g., "anthropic", "openai") baseUrl::String # API endpoint base URL reasoning::Bool # Whether the model supports chain-of-thought -- 2.52.0 From 4df10430326f016e1792bf2e684af30b8ddff34b Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 15:18:42 +0700 Subject: [PATCH 41/50] update --- src/type.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/type.jl b/src/type.jl index c803289..4f987c4 100644 --- a/src/type.jl +++ b/src/type.jl @@ -3,7 +3,7 @@ module type run_agent, take_response, follow_up, stop_agent -using Dates, UUIDs, DataStructures, JSON, NATS +using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads using GeneralUtils const Timestamp = DateTime @@ -587,7 +587,7 @@ carrying the full context through the call chain. # Examples ```julia # After prepareToolCall succeeds, the agent holds a preparedToolCall -prep = preparedToolCall( + prep = preparedToolCall( tool, # agentTool found in context.tools toolCall, # {id: "call_1", name: "search_wine", arguments: "{\"query\": \"red wine\"}"} validatedArgs # Dict("query" => "red wine") -- 2.52.0 From 02e93cae57609587938a0ceee5a762a1b3eedc7e Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 15:47:27 +0700 Subject: [PATCH 42/50] update --- src/type.jl | 47 +++++++++++++++++++++++++++++++++++------------ src/utils.jl | 8 ++++---- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/type.jl b/src/type.jl index 4f987c4..46f95bc 100644 --- a/src/type.jl +++ b/src/type.jl @@ -1,6 +1,29 @@ -module type - export agentContext, yiemAgent, - run_agent, take_response, follow_up, stop_agent + module type + export Timestamp, + # Abstract types + messageContent, agentMessage, agent, + # Model types + modelCost, llmModel, llmUsage, + # Message content types + textContent, imageContent, + # Message types + userMessage, assistantMessage, toolResultMessage, + # Tool types + agentTool, + # Context types + agentContext, agentState, agentToolCall, prepareNextTurnContext, + # Loop & execution types + agentLoopConfig, abortSignal, agentToolResult, + assistantMsgCtx, afterCtx, + # Event types + toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent, + # Agent + yiemAgent, + # Tool call lifecycle types + preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome, + agentToolCallBatch, + # Functions (defined elsewhere) + run_agent, take_response, follow_up, stop_agent using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads @@ -597,7 +620,7 @@ carrying the full context through the call chain. struct preparedToolCall tool::agentTool # The resolved tool definition from the context toolCall::agentToolCall # The original tool call from the assistant - args::any # Validated (and coerced) argument values + args::Any # Validated (and coerced) argument values end """ @@ -615,7 +638,7 @@ corrected arguments after a validation failure. # Fields - `result::agentToolResult`: The pre-computed tool result -- `isError::bool`: Whether this outcome represents an error +- `isError::Bool`: Whether this outcome represents an error # Examples ```julia @@ -634,7 +657,7 @@ immediateOutcome( """ struct immediateOutcome result::agentToolResult # The pre-computed tool result - isError::bool # Whether this outcome represents an error + isError::Bool # Whether this outcome represents an error end """ @@ -649,7 +672,7 @@ whether to transform it or replace it entirely. # Fields - `result::agentToolResult`: The tool's execution result -- `isError::bool`: Whether execution raised an error +- `isError::Bool`: Whether execution raised an error # Examples ```julia @@ -668,7 +691,7 @@ executedOutcome( """ struct executedOutcome result::agentToolResult # The tool's execution result - isError::bool # Whether execution raised an error + isError::Bool # Whether execution raised an error end """ @@ -689,7 +712,7 @@ and swappable. # Fields - `toolCall::agentToolCall`: The original tool call reference - `result::agentToolResult`: The final tool result (post-afterToolCall) -- `isError::bool`: Whether the call failed or was blocked +- `isError::Bool`: Whether the call failed or was blocked # Examples ```julia @@ -703,7 +726,7 @@ finalizedOutcome(tc, agentToolResult(content, details, usage, true), false) struct finalizedOutcome toolCall::agentToolCall # The original tool call reference result::agentToolResult # The final tool result (post-afterToolCall) - isError::bool # Whether the call failed or was blocked + isError::Bool # Whether the call failed or was blocked end """ @@ -749,8 +772,8 @@ agentToolCallBatch(toolResultMessage[], false) ``` """ struct agentToolCallBatch - messages::vector{toolResultMessage} # Tool result messages for this batch - terminate::bool # Whether the batch should terminate the loop + messages::Vector{toolResultMessage} # Tool result messages for this batch + terminate::Bool # Whether the batch should terminate the loop end diff --git a/src/utils.jl b/src/utils.jl index fda9814..0c92f43 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -29,10 +29,10 @@ julia> YiemAgent.clearhistory(agent) ``` """ function clearhistory(a::T) where {T<:agent} - empty!(a.chathistory) - empty!(a.memory["shortmem"]) - empty!(a.memory["events"]) - a.memory["chatbox"] = "" + # empty!(a.chathistory) + # empty!(a.memory["shortmem"]) + # empty!(a.memory["events"]) + # a.memory["chatbox"] = "" end -- 2.52.0 From 0f16e045b9c0365454fe5360e27e1a95408917df Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 15:54:36 +0700 Subject: [PATCH 43/50] update --- src/agentCore.jl | 24 ++++++++++++------------ src/api.jl | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/agentCore.jl b/src/agentCore.jl index 6b96c57..0ee5916 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -5,7 +5,7 @@ export _agent_loop, OpenAiToUserMessage using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, Serde, Base.Threads using GeneralUtils -using ..type, ..utils, ..llmfunction +using ..type, ..utils # ---------------------------------------------- 100 --------------------------------------------- # @@ -406,7 +406,7 @@ julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], dict{any,any}() true ``` """ -function shouldTerminate(batches::vector{finalizedOutcome})::bool +function shouldTerminate(batches::Vector{finalizedOutcome})::Bool return !isempty(batches) && all(b -> b.result.terminate, batches) end @@ -513,8 +513,8 @@ function prepareToolCall( assistantMsg::assistantMessage, toolCall::agentToolCall, config::agentLoopConfig, - signal::union{nothing,abortSignal}, -)::union{preparedToolCall,immediateOutcome} + signal::Union{Nothing, abortSignal}, +)::Union{preparedToolCall,immediateOutcome} tool = find(t -> t.name == toolCall.name, context.tools) if tool === nothing @@ -588,7 +588,7 @@ executePreparedToolCall(prep, nothing, emit) """ function executePreparedToolCall( prep::preparedToolCall, - signal::union{nothing,abortSignal}, + signal::Union{Nothing,abortSignal}, emit::Function, )::executedOutcome @@ -678,7 +678,7 @@ function finalizeExecutedToolCall( prep::preparedToolCall, executed::executedOutcome, config::agentLoopConfig, - signal::union{nothing,abortSignal}, + signal::Union{Nothing,abortSignal}, )::finalizedOutcome result = executed.result @@ -795,9 +795,9 @@ executeToolCallsSequential(ctx, msg, [deployTc], config, nothing, emit) function executeToolCallsSequential( context::agentContext, assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, + toolCalls::Vector{agentToolCall}, config::agentLoopConfig, - signal::union{nothing,abortSignal}, + signal::Union{Nothing,abortSignal}, emit::Function, )::agentToolCallBatch @@ -888,9 +888,9 @@ executeToolCallsParallel(ctx, msg, [tc1, tc2, tc3], config, abortedSignal, emit) function executeToolCallsParallel( context::agentContext, assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, + toolCalls::Vector{agentToolCall}, config::agentLoopConfig, - signal::union{nothing,abortSignal}, + signal::Union{Nothing,abortSignal}, emit::Function, )::agentToolCallBatch @@ -988,9 +988,9 @@ executeToolCalls(ctx, msg, [searchTc, fetchTc], configSequential, nothing, emit) function executeToolCalls( context::agentContext, assistantMsg::assistantMessage, - toolCalls::vector{agentToolCall}, + toolCalls::Vector{agentToolCall}, config::agentLoopConfig, - signal::union{nothing,abortSignal}, + signal::Union{Nothing,abortSignal}, emit::Function, )::agentToolCallBatch diff --git a/src/api.jl b/src/api.jl index 9412301..2ec3c6a 100644 --- a/src/api.jl +++ b/src/api.jl @@ -5,7 +5,7 @@ export prompt using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, Serde using GeneralUtils -using ..type, ..util, ..llmfunction +using ..type, ..utils, ..llmfunction # ---------------------------------------------- 100 --------------------------------------------- # -- 2.52.0 From 131973edd4ece08cad46f36e4b6cd7edd596e793 Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 15:55:37 +0700 Subject: [PATCH 44/50] update --- src/api.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.jl b/src/api.jl index 2ec3c6a..9db183f 100644 --- a/src/api.jl +++ b/src/api.jl @@ -5,7 +5,7 @@ export prompt using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, Serde using GeneralUtils -using ..type, ..utils, ..llmfunction +using ..type, ..utils # ---------------------------------------------- 100 --------------------------------------------- # -- 2.52.0 From 8e356f06bcc58b287bae2bf6378ec7e4d682d11e Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 17:48:50 +0700 Subject: [PATCH 45/50] update --- src/api.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.jl b/src/api.jl index 9db183f..ec8ece2 100644 --- a/src/api.jl +++ b/src/api.jl @@ -117,7 +117,7 @@ julia> stop_agent(agent) function stop_agent(agent::yiemAgent) put!(agent.inputChannel, :shutdown) try - fetch(agent._task) + fetch(agent._agent_loop) catch e if e isa TaskFailedException rethrow(e) -- 2.52.0 From e60cbc67c4edc09f52f482d9da3e233e70675cbe Mon Sep 17 00:00:00 2001 From: narawat Date: Fri, 7 Aug 2026 23:13:13 +0700 Subject: [PATCH 46/50] update --- docs/loadtools.md | 92 +++++++++++++++++++++++++ src/YiemAgent.jl | 3 + src/tools/get_weather.jl | 59 ++++++++++++++++ src/tools/registry.jl | 142 +++++++++++++++++++++++++++++++++++++++ src/type.jl | 58 ++++++++++++++-- 5 files changed, 347 insertions(+), 7 deletions(-) create mode 100644 docs/loadtools.md create mode 100644 src/tools/get_weather.jl create mode 100644 src/tools/registry.jl diff --git a/docs/loadtools.md b/docs/loadtools.md new file mode 100644 index 0000000..c222248 --- /dev/null +++ b/docs/loadtools.md @@ -0,0 +1,92 @@ +# Dynamic Tool Loading + +Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory without hardcoding filenames in the main module. + +## How It Works + +1. `src/tools/registry.jl` defines a `load_tools(dir::String)` function that scans a directory for `.jl` files +2. Each tool file must define a single function: `get_tool() :: agentTool` +3. `load_tools()` sorts files alphabetically, includes each one, calls `get_tool()`, and registers the result +4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent` + +## Directory Structure + +``` +src/ +├── tools/ +│ ├── registry.jl # Tool loader (do not edit) +│ ├── get_weather.jl # Your tool +│ └── query_db.jl # Another tool +├── type.jl +├── utils.jl +├── agentCore.jl +├── api.jl +└── YiemAgent.jl +``` + +## Creating a Tool + +Each `.jl` file in `src/tools/` must define `get_tool()` returning an `agentTool`: + +```julia +# src/tools/get_weather.jl +function get_tool() :: agentTool + return agentTool( + name = "get_weather", + label = "Weather Lookup", + description = "Fetch current weather and forecast for a given city.", + inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict( + "city" => Dict("type" => "string", "description" => "City and country"), + "units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius") + ), + "required" => ["city"] + ), + execute = (toolCallId, args, signal, onPartialResult) -> begin + city = args["city"] + return agentToolResult( + [textContent("Weather in $(city): Sunny, 22C")], + Dict{Any,Any}(), nothing, false + ) + end, + prepareArguments = nothing, + parallelToolExecute = false + ) +end +``` + +No `module` wrapper needed — the registry includes each file in the current module scope so all types (`agentTool`, `textContent`, `agentToolResult`, etc.) resolve correctly. + +## Loading Tools + +```julia +using .YiemAgent +using .YiemAgent: toolRegistry + +# Load all tool files from src/tools/ +tools = load_tools(joinpath(@__DIR__, "src", "tools")) + +# Create agent with loaded tools +agent = yiemAgent( + systemPrompt = "You are a helpful assistant.", + model = my_model, + tools = tools, + llmCall = my_llm_call, + agentEventSink = my_event_sink +) +``` + +## Available Functions + +| Function | Description | +|----------|-------------| +| `load_tools(dir::String)` | Scan directory and load all `.jl` tool files | +| `register_tool(tool::agentTool)` | Register a single tool into the global registry | +| `get_tools()` | Get deep copy of all registered tools | +| `list_tools()` | List all registered tools as `(name, label)` pairs | +| `clear_tools()` | Clear the global registry | + +## File Loading Order + +Files are sorted alphabetically before loading, so `01_database.jl` loads before `02_weather.jl`. This ensures deterministic registration order. diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl index 764574c..765807e 100644 --- a/src/YiemAgent.jl +++ b/src/YiemAgent.jl @@ -13,6 +13,9 @@ module YiemAgent include("utils.jl") using .utils + include("tools/registry.jl") + using .toolRegistry + # include("llmfunction.jl") # using .llmfunction diff --git a/src/tools/get_weather.jl b/src/tools/get_weather.jl new file mode 100644 index 0000000..808586a --- /dev/null +++ b/src/tools/get_weather.jl @@ -0,0 +1,59 @@ +""" +Execute the get_weather tool. + +# Arguments +- `toolCallId::String`: Unique identifier for this tool call +- `args::Dict{String,Any}`: Parsed arguments from the LLM +- `signal::Union{Nothing,abortSignal}`: Optional abort signal +- `onPartialResult::Function`: Callback for streaming partial results + +# Returns +- `agentToolResult`: Result content with weather data +""" +function execute_tool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) :: agentToolResult + city = get(args, "city", "") + units = get(args, "units", "celsius") + + # Validate required arguments + if isempty(city) + return agentToolResult( + [textContent("Error: 'city' argument is required.")], + Dict{Any,Any}(), nothing, false + ) + end + + # Simulate weather fetch — replace with actual API call + # You can call onPartialResult() here for streaming progress updates: + # onPartialResult(Dict("status" => "Fetching weather data...")) + # onPartialResult(Dict("status" => "Processing...")) + + temp = units == "fahrenheit" ? "72" : "22" + unit_symbol = units == "celsius" ? "°C" : "°F" + + return agentToolResult( + [textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")], + Dict{Any,Any}(), nothing, false + ) +end + +""" +Define and return the get_weather agentTool. +""" +function get_tool() :: agentTool + return agentTool( + name = "get_weather", + label = "Weather Lookup", + description = "Fetch current weather and forecast for a given city.", + inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict( + "city" => Dict("type" => "string", "description" => "City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'"), + "units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius", "description" => "Temperature scale") + ), + "required" => ["city"] + ), + execute = execute_tool, # reference the function defined above + prepareArguments = nothing, + parallelToolExecute = false + ) +end diff --git a/src/tools/registry.jl b/src/tools/registry.jl new file mode 100644 index 0000000..de121fd --- /dev/null +++ b/src/tools/registry.jl @@ -0,0 +1,142 @@ +module toolRegistry + +export load_tools, register_tool, get_tools, list_tools, clear_tools + +using ..type + +# Global registry — populated at runtime by load_tools() or register_tool() +const _registry = Vector{agentTool}() + +""" +Load all tool modules from a directory. + +Scans `dir` for `.jl` files. Each file must define a function named +`get_tool() :: agentTool`. Files are sorted alphabetically so tool +registration order is deterministic. + +# Tool file format +Each `.jl` file defines one function `get_tool()` that returns an `agentTool`: + +```julia +# src/tools/get_weather.jl +function get_tool() :: agentTool + return agentTool( + name = "get_weather", + label = "Weather Lookup", + description = "Fetch current weather and forecast for a given city.", + inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict( + "city" => Dict("type" => "string", "description" => "City and country"), + "units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius") + ), + "required" => ["city"] + ), + execute = (toolCallId, args, signal, onPartialResult) -> begin + city = args["city"] + return agentToolResult( + [textContent("Sunny, 22C in $(city)")], + Dict{Any,Any}(), nothing, false + ) + end, + prepareArguments = nothing, + parallelToolExecute = false + ) +end +``` + +# Arguments +- `dir::String`: Directory path to scan for `.jl` tool files + +# Returns +- `Vector{agentTool}`: All loaded tools + +# Errors +- Throws `ArgumentError` if a tool file does not define a `get_tool` function +""" +function load_tools(dir::String)::Vector{agentTool} + if !isdir(dir) + throw(ArgumentError("Tool directory does not exist: $dir")) + end + + tools = agentTool[] + jl_files = filter(f -> endswith(f, ".jl"), readdir(dir)) + sort!(jl_files) + + for filename in jl_files + filepath = joinpath(dir, filename) + println("[toolRegistry] Loading tool from: $filepath") + + # Include the file in the current module scope so all types resolve + # (agentTool, textContent, agentToolResult, etc. are all available) + include(filepath) + + # Validate that get_tool was defined (include() places it in current module scope) + if !isdefined(:get_tool) + throw(ArgumentError( + "Tool file $(filepath) does not define a `get_tool()` function. " * + "Each tool file must define: function get_tool() :: agentTool ... end" + )) + end + + # Call get_tool() — it runs in current scope where types are visible + tool = get_tool() + if !(tool isa agentTool) + throw(ArgumentError( + "get_tool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))" + )) + end + + push!(_registry, tool) + push!(tools, tool) + println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)") + end + + return tools +end + +""" +Register a single agentTool into the global registry. + +# Arguments +- `tool::agentTool`: The tool to register + +# Returns +- `Vector{agentTool}`: Updated registry +""" +function register_tool(tool::agentTool)::Vector{agentTool} + push!(_registry, tool) + println("[toolRegistry] Registered tool: $(tool.name)") + return _registry +end + +""" +Get all registered tools. + +# Returns +- `Vector{agentTool}`: Copy of the registry +""" +function get_tools()::Vector{agentTool} + return deepcopy(_registry) +end + +""" +List all registered tool names and labels. + +# Returns +- `Vector{Tuple{String,String}}`: Pairs of (name, label) +""" +function list_tools()::Vector{Tuple{String,String}} + return [(t.name, t.label) for t in _registry] +end + +""" +Clear all registered tools from the global registry. +""" +function clear_tools()::Nothing + empty!(_registry) + println("[toolRegistry] Registry cleared") + return nothing +end + +end # module diff --git a/src/type.jl b/src/type.jl index 46f95bc..66d4cda 100644 --- a/src/type.jl +++ b/src/type.jl @@ -197,23 +197,67 @@ end """ A tool available to the agent. +Maps MCP server tool definitions to an executable Julia tool. + # 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 +- `name::String`: Tool identifier (from MCP `name`) +- `label::String`: Human-readable tool name (from MCP `title`) +- `description::String`: What the tool does (from MCP `description`) +- `inputSchema::Any`: Tool parameters schema (from MCP `inputSchema`, JSON Schema format) +- `execute::Function`: Tool execution function, signature: + `execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)` - `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback - `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel # Returns - A new `agentTool` instance + +# MCP Tool Example +``` +{ + "name": "get_weather", + "title": "Weather Lookup", + "description": "Fetch current weather and forecast for a given city.", + "inputSchema": { + "type": "object", + "properties": { + "city": { "type": "string", "description": "City and state/country" }, + "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" } + }, + "required": ["city"] + } +} +``` + +# Example +```julia +tool = agentTool( + name="get_weather", + label="Weather Lookup", + description="Fetch current weather and forecast for a given city.", + inputSchema=Dict( + "type" => "object", + "properties" => Dict( + "city" => Dict("type" => "string", "description" => "City name"), + "units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"]) + ), + "required" => ["city"] + ), + execute=(toolCallId, args, signal, onPartialResult) -> begin + city = args["city"] + return agentToolResult( + [textContent("Sunny, 22C in $(city)")], + Dict{Any,Any}(), nothing, false + ) + end +) +``` """ -struct agentTool{TParameters, TDetails} # A tool available to the agent +struct agentTool # A tool available to the agent name::String # Tool identifier label::String # Human-readable tool name description::String # What the tool does - parameters::TParameters # Tool parameters schema (JSON schema) + inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format) execute::Function # Tool execution function prepareArguments::Union{Function, Nothing} # Optional argument preparation callback parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel -- 2.52.0 From c1ef544734828909463a0640dfa730b478399ac2 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 8 Aug 2026 08:04:08 +0700 Subject: [PATCH 47/50] update --- docs/loadtools.md | 22 +- example/agent_chat_virtualCustomer.jl | 585 --------------------- example/config.json | 76 --- example/main.jl | 706 -------------------------- src/tools/get_weather.jl | 6 +- src/tools/registry.jl | 36 +- 6 files changed, 32 insertions(+), 1399 deletions(-) delete mode 100644 example/agent_chat_virtualCustomer.jl delete mode 100644 example/config.json delete mode 100644 example/main.jl diff --git a/docs/loadtools.md b/docs/loadtools.md index c222248..8dfe6be 100644 --- a/docs/loadtools.md +++ b/docs/loadtools.md @@ -4,9 +4,9 @@ Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory w ## How It Works -1. `src/tools/registry.jl` defines a `load_tools(dir::String)` function that scans a directory for `.jl` files -2. Each tool file must define a single function: `get_tool() :: agentTool` -3. `load_tools()` sorts files alphabetically, includes each one, calls `get_tool()`, and registers the result +1. `src/tools/registry.jl` defines a `loadTools(dir::String)` function that scans a directory for `.jl` files +2. Each tool file must define a single function: `getTool() :: agentTool` +3. `loadTools()` sorts files alphabetically, includes each one, calls `getTool()`, and registers the result 4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent` ## Directory Structure @@ -26,11 +26,11 @@ src/ ## Creating a Tool -Each `.jl` file in `src/tools/` must define `get_tool()` returning an `agentTool`: +Each `.jl` file in `src/tools/` must define `getTool()` returning an `agentTool`: ```julia # src/tools/get_weather.jl -function get_tool() :: agentTool +function getTool() :: agentTool return agentTool( name = "get_weather", label = "Weather Lookup", @@ -65,7 +65,7 @@ using .YiemAgent using .YiemAgent: toolRegistry # Load all tool files from src/tools/ -tools = load_tools(joinpath(@__DIR__, "src", "tools")) +tools = YiemAgent.loadTools(joinpath(@__DIR__, "src", "tools")) # Create agent with loaded tools agent = yiemAgent( @@ -81,11 +81,11 @@ agent = yiemAgent( | Function | Description | |----------|-------------| -| `load_tools(dir::String)` | Scan directory and load all `.jl` tool files | -| `register_tool(tool::agentTool)` | Register a single tool into the global registry | -| `get_tools()` | Get deep copy of all registered tools | -| `list_tools()` | List all registered tools as `(name, label)` pairs | -| `clear_tools()` | Clear the global registry | +| `loadTools(dir::String)` | Scan directory and load all `.jl` tool files | +| `registerTool(tool::agentTool)` | Register a single tool into the global registry | +| `getTools()` | Get deep copy of all registered tools | +| `listTools()` | List all registered tools as `(name, label)` pairs | +| `clearTools()` | Clear the global registry | ## File Loading Order diff --git a/example/agent_chat_virtualCustomer.jl b/example/agent_chat_virtualCustomer.jl deleted file mode 100644 index b880599..0000000 --- a/example/agent_chat_virtualCustomer.jl +++ /dev/null @@ -1,585 +0,0 @@ -using Revise -using JSON, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures -using YiemAgent, GeneralUtils -using Base.Threads - -# ---------------------------------------------- 100 --------------------------------------------- # - - - -# load config -config = JSON.parsefile("/appfolder/app/dev/YiemAgent/test/config.json") -# config = copy(JSON.parsefile("../mountvolume/config.json")) - - -function executeSQL(sql::T) where {T<:AbstractString} - host = config[:externalservice][:wineDB][:host] - port = config[:externalservice][:wineDB][:port] - dbname = config[:externalservice][:wineDB][:dbname] - user = config[:externalservice][:wineDB][:user] - password = config[:externalservice][:wineDB][:password] - DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") - result = LibPQ.execute(DBconnection, sql) - close(DBconnection) - return result -end - -function executeSQLVectorDB(sql) - host = config[:externalservice][:SQLVectorDB][:host] - port = config[:externalservice][:SQLVectorDB][:port] - dbname = config[:externalservice][:SQLVectorDB][:dbname] - user = config[:externalservice][:SQLVectorDB][:user] - password = config[:externalservice][:SQLVectorDB][:password] - DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") - result = LibPQ.execute(DBconnection, sql) - close(DBconnection) - return result -end - -function text2textInstructLLM(prompt::String; maxattempt::Integer=10, modelsize::String="medium", - senderId=GeneralUtils.uuid4snakecase(), timeout=90, - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) - ) - msgMeta = GeneralUtils.generate_msgMeta( - config[:externalservice][:loadbalancer][:mqtttopic]; - msgPurpose="inference", - senderName="yiemagent", - senderId=senderId, - receiverName="text2textinstruct_$modelsize", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :text => prompt, - :kwargs => llmkwargs - ) - ) - - response = nothing - for attempts in 1:maxattempt - _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; responsetimeout=timeout, responsemaxattempt=maxattempt) - payload = _response[:response] - if _response[:success] && payload[:text] !== nothing - response = _response[:response][:text] - break - else - println("\n attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(outgoingMsg) - println(" attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - sleep(3) - end - end - - return response -end - -# get text embedding from a LLM service -function getEmbedding(text::T) where {T<:AbstractString} - msgMeta = GeneralUtils.generate_msgMeta( - config[:externalservice][:loadbalancer][:mqtttopic]; - msgPurpose="embedding", - senderName="yiemagent", - senderId=sessionId, - receiverName="textembedding", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :text => [text] # must be a vector of string - ) - ) - - response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; responsetimeout=120, responsemaxattempt=3) - embedding = response[:response][:embeddings] - return embedding -end - -function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3, - vectorDB::Function; limit::Integer=1 - )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} - # get embedding from LLM service - embedding = getEmbedding(text)[1] - # check whether there is close enough vector already store in vectorDB. if no, add, else skip - sql = """ - SELECT *, $embeddingColumnName <-> '$embedding' as distance - FROM $tablename - ORDER BY distance LIMIT $limit; - """ - response = vectorDB(sql) - df = DataFrame(response) - return df -end - -function similarSQLVectorDB(query; maxdistance::Integer=100) - tablename = "sqlllm_decision_repository" - # get embedding of the query - df = findSimilarTextFromVectorDB(query, tablename, - "function_input_embedding", executeSQLVectorDB) - # println(df[1, [:id, :function_output]]) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - # distance = 100 # CHANGE this is for testing only - if row != 0 && distance < maxdistance - # if there is usable SQL, return it. - output_b64 = df[1, :function_output_base64] # pick the closest match - output_str = String(base64decode(output_b64)) - rowid = df[1, :id] - println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - return (dict=output_str, distance=distance) - else - println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - return (dict=nothing, distance=nothing) - end -end - -function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} - tablename = "sqlllm_decision_repository" - # get embedding of the query - # query = state[:thoughtHistory][:question] - df = findSimilarTextFromVectorDB(query, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row == 0 || distance > maxdistance # no close enough SQL stored in the database - query_embedding = getEmbedding(query)[1] - query = replace(query, "'" => "") - sql_base64 = base64encode(SQL) - sql_ = replace(SQL, "'" => "") - - sql = """ - INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); - """ - # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # println(sql) - _ = executeSQLVectorDB(sql) - end -end - - -function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3 - )::Union{AbstractDict, Nothing} where {T1<:AbstractString} - tablename = "sommelier_decision_repository" - # find similar - println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__) - df = findSimilarTextFromVectorDB(recentevents, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row != 0 && distance < maxdistance - # if there is usable decision, return it. - rowid = df[1, :id] - println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) - output_b64 = df[1, :function_output_base64] # pick the closest match - _output_str = String(base64decode(output_b64)) - output = copy(JSON.parsefile(_output_str)) - return output - else - println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) - return nothing - end -end - - -function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5 - ) where {T1<:AbstractString, T2<:AbstractDict} - tablename = "sommelier_decision_repository" - # find similar - df = findSimilarTextFromVectorDB(recentevents, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row == 0 || distance > maxdistance # no close enough SQL stored in the database - recentevents_embedding = getEmbedding(recentevents)[1] - recentevents = replace(recentevents, "'" => "") - decision_json = JSON.json(decision) - decision_base64 = base64encode(decision_json) - decision = replace(decision_json, "'" => "") - - sql = """ - INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding'); - """ - println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) - println(sql) - _ = executeSQLVectorDB(sql) - else - println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__) - end -end - - -sessionId = GeneralUtils.uuid4snakecase() - -externalFunction = ( - getEmbedding=getEmbedding, - text2textInstructLLM=text2textInstructLLM, - executeSQL=executeSQL, - similarSQLVectorDB=similarSQLVectorDB, - insertSQLVectorDB=insertSQLVectorDB, - similarSommelierDecision=similarSommelierDecision, - insertSommelierDecision=insertSommelierDecision, - ) - - -# s = "full-bodied red wine, budget 1500 USD" -# r = YiemAgent.extractWineAttributes_1(agent, s) -# println(r) - - -# --------------------------- generating scenario and customer profile --------------------------- # - -function rolegenerator() - rolegenerator_systemmsg = - """ - Your role: - - You are a helpful assistant - Your mission: - - Create one random role of a potential customer of an internet wine store. - You must follow the following guidelines: - - the user only need the role, do not add your own words. - - the role should be detailed and realistic. - You should then respond to the user with: - Name: a name of the potential customer - Situation: a situation that the potential customer may be facing - Mission: a mission of the potential customer - Profile: a profile of the potential customer, including their age, gender, occupation, and other relevant information - You should only respond in format as described below: - Name: ... - Situation: ... - Mission: ... - Profile: ... - Additional_information: ... - - Here are some examples: - Name: Jimmy - Situation: - - Your relationship with your boss is not that good. You need to improve your relationship with your boss. - - Your boss's wedding anniversary is coming up. - - You are at a wine store and start talking with the store's sommelier. - Mission: - - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. - Profile: - - You are a young professional in a big company. - - You are avid party goer - - You like beer. - - You know nothing about wine. - - You have a budget of 1500usd. - Additional_information: - - your boss like spicy food. - - your boss is a middle-aged man. - - your boss likes Australian wine. - - Name: Kate - Situation: - - Your husband asked you to get him a bottle of wine. He will gift the wine to his business client while dining at a German restaurant. - - Your husband is a business client and he will gift the wine to his business - - You are at a wine store and start talking with the store's sommelier. - Mission: - - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. - Profile: - - You are a CEO in a startup company. - - You are a nerd - - You don't like alcohol. - - You have a budget of 150usd. - - You don't care about organic, sulfite, gluten-free, or sustainability certified wines - Additional_information: - - your husband like spicy food. - - your husband is a middle-aged man. - - Name: John - Situation: - - A local newspaper club wants to have a scoop about wine with local food in the U.S. - - You are at a wine store and start talking with the store's sommelier. - Mission: - - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. - Profile: - - I'm a young guy. - - I prefer to express my ideas in a succinct and clear manner. - Additional_information: - - N/A - - Name: Jane - Situation: - - You have catering a dinner party with French cuisine. - - You want to serve wine with your guests. - - You are at a wine store and start talking with the store's sommelier. - Mission: - - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. - Profile: - - You are a young French restaurant owner. - - You like dry, full-bodied red wine with high tannin - - You don't care about organic, sulfite, gluten-free, or sustainability certified wines. - - You have a budget of 200 usd. - Additional_information: - - N/A - - Let's begin! - """ - - header = ["Name:", "Situation:", "Mission:", "Profile:", "Additional_information:"] - dictkey = ["name", "situation", "mission", "profile", "additional_information"] - errornote = "N/A" - - for attempt in 1:10 - _prompt = - [ - Dict(:name => "system", :text => rolegenerator_systemmsg), - ] - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") - - response = text2textInstructLLM(prompt) # generated role - response = GeneralUtils.deFormatLLMtext(response, "qwen3") - think, response = GeneralUtils.extractthink(response) - - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - responsedict[:id] = GeneralUtils.uuid4snakecase() - - responsedict[:systemmsg] = - """ - You are role playing as a CUSTOMER of a wine store and you are currently talking with a sommelier of a wine store. - Your profile is as follows: - Situation: $(responsedict[:situation]) - Mission: $(responsedict[:mission]) - Profile: $(responsedict[:profile]) - Additional_information: $(responsedict[:additional_information]) - - You should follow the following guidelines: - - Focus on the lastest conversation - - Your like to be short and concise - - If you don't know an answer to sommelier's question, you should say: I don't know. - - If you think the store can't provide what you seek, you can leave. - - You should then respond to the user with: - Dialogue: what you want to say to the user - Role: Verify that the dialogue is intended for the customer of a wine store. Can be "yes" or "no" - You should only respond in format as described below: - Dialogue: ... - Role: ... - - Let's begin! - """ - - println("\nrolegenerator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - println(responsedict) - return responsedict - end - error("ERROR rolegenerator() failed to generate customer role: ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -end - - -# Define the external functions for the customer agent in named tuple format -customer_externalFunction = ( - text2textInstructLLM=text2textInstructLLM, - ) - - - - -function main() - agent = YiemAgent.sommelier( - externalFunction; - name="Jane", - id=sessionId, # agent instance id - retailername="Yiem", - llmFormatName="qwen3" - ) - - customerDict = rolegenerator() - customer = YiemAgent.virtualcustomer( - customer_externalFunction; - systemmsg=customerDict[:systemmsg], - name=customerDict[:name], - id=sessionId, # agent instance id - llmFormatName="qwen3" - ) - - # customer_chat = "hello" - - # YiemAgent.addNewMessage(customer, "assistant", customer_chat) - # # add user activity to events memory - # push!(customer.memory[:events], - # YiemAgent.eventdict(; - # event_description="the assistant talks to the user.", - # timestamp=Dates.now(), - # subject="assistant", - # action_name="CHAT_BOX", - # action_input=customer_chat, - # ) - # ) - # println("\ncustomer respond:\n $customer_chat") - agent_response = YiemAgent.conversation(agent; maximumMsg=50) - println("\nagent respond:\n $agent_response") - while true - customer_chat = nothing - while customer_chat === nothing - customer_response = YiemAgent.conversation(customer, Dict(:text=> agent_response); - converPartnerName=agent.name, - maximumMsg=50) - customer_response = GeneralUtils.deFormatLLMtext(customer_response, customer.llmFormatName) - customer_chat = customer_response - - #[WORKING] check whether customer response the same before - end - - println("\ncustomer respond:\n $customer_chat") - - agent_response = YiemAgent.conversation(agent; - userinput=Dict(:text=> customer_chat), - maximumMsg=50) - println("\nagent respond:\n $agent_response") - - if haskey(agent.memory[:events][end], :thought) - lastAssistantAction = agent.memory[:events][end][:thought][:action_name] - if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict - - # save a.memory[:shortmem][:decisionlog] to disk using JSON - println("\nsaving agent.memory[:shortmem][:decisionlog] to disk") - date = "$(Dates.now())" - date = replace(date, ':'=>'.') - filename = "agent_decision_log_$(date)_$(agent.id).json" - filepath = "/appfolder/mountvolume/appdata/log/$filename" - open(filepath, "w") do io - JSON.pretty(io, agent.memory[:shortmem][:decisionlog]) - end - - # check how many file in /appfolder/mountvolume/appdata/log/ folder now - logfilesnumber = length(readdir("/appfolder/mountvolume/appdata/log/")) - println("\nCaching conversation process done. Total $logfilesnumber files in /appfolder/mountvolume/appdata/log/ folder now.\n") - break - end - end - end -end - -for i in 1:100 - main() - println("\n Round $i/100 done.") -end - -println("done") - -# prompt = -# """ -# <|im_start|>system -# You are a role playing agent acting as: -# Name: Emily -# Situation: - Emily is planning her upcoming birthday party and wants to make it extra special. She has invited close friends and family, and she's looking for a unique wine that will impress them. -# Mission: - Emily needs to find a rare and high-quality wine that matches the theme of her party, which is a mix of classic and modern flavors. She also wants to ensure that the wine is not too expensive so that it won't break her budget. -# Profile: - Emily is in her late 20s, works as a marketing executive for a tech company, and has a passion for trying new things. She's organized and detail-oriented but can be spontaneous when it comes to planning events. -# Additional_information: - Emily loves experimenting with different types of food and wine pairings. - -# Your are currently talking with a sommelier. - -# You should follow the following guidelines: -# - Focus on the lastest conversation -# - If you satisfy with the sommelier's recommendation for bottle of wine(s), you should say: Thanks for you help. I will buy the wine you recommended. -# - If you don't satisfy with the sommelier's questions or can't get a good wine recommendation, you can continue the conversation. - -# Let's begin! - -# <|im_end|> -# <|im_start|>Jane -# Hello! Welcome to Yiem's Wine Store. I'm Jane, your friendly sommelier. How can I assist you today? What type of wine are you in the mood for, and is there a special occasion or event on your mind? -# <|im_end|> -# <|im_start|>Emily -# Hi Jane! Thank you so much for welcoming me. For my birthday party, I'm looking for something that combines classic and modern flavors. It's a mix of guests who enjoy both traditional tastes and more contemporary ones. Also, I want to make sure it won't break the bank. Any suggestions? -# <|im_end|> -# <|im_start|>Jane -# Thank you for sharing your preferences, Jane! To better assist you, could you please let me know if there are any specific characteristics of wine you're looking for, such as tannin, sweetness, intensity, or acidity? Additionally, do you have any food items in mind that this wine should pair well with? -# <|im_end|> -# <|im_start|>Emily -# """ - -# llmkwargs=Dict( -# :num_ctx => 32768, -# :temperature => 0.3, -# ) -# r = text2textInstructLLM(prompt, llmkwargs=llmkwargs) -# println(r) -# println(555) - -# response = YiemAgent.conversation(agent, Dict(:text=> "I want to get a French red wine under 100.")) - - -# while true -# println("your respond: ") -# user_answer = readline() -# response = YiemAgent.conversation(agent, Dict(:text=> user_answer)) -# println("\n$response") -# end - - - - - - - - - - - - -# """ -# Hello - -# I would like to get a bottle of wine for my boss but I don't know much about wine. Can you help me? - -# well actually, my boss is going to offer the wine to his client as a gift in a business meeting. All I know is his client like spicy food and French wine. I have a budget about 1000. - -# """ - -# input = "French wine, bordeaux, under USD100, pairs with spicy food" -# r = YiemAgent.extractWineAttributes_1(a, input) - -# inventory_order = "French Syrah, Viognier, full bodied, under 100" -# r = YiemAgent.extractWineAttributes_2(a, inventory_order) -# pprintln(r) - - -# cron job -# @reboot sleep 50 && nvidia-smi -pm 1 -# @reboot sleep 51 && nvidia-smi -i 0 -pl 150 -# @reboot sleep 52 && nvidia-smi -i 1 -pl 150 -# @reboot sleep 53 && nvidia-smi -i 2 -pl 150 -# @reboot sleep 54 && nvidia-smi -i 3 -pl 150 - -# @reboot sleep 55 && julia -t 2 /home/ton/work/restartContainer/main.jl - -# using GeneralUtils -# msgMeta = GeneralUtils.generate_msgMeta( -# "/tonpc_containerServices", -# senderName= "somename", -# senderId= "1230", -# mqttBrokerAddress= "mqtt.yiem.cc", -# mqttBrokerPort= 1883, -# ) -# outgoingMsg = Dict( -# :msgMeta=> msgMeta, -# :payload=> "docker container restart playground-app", -# ) -# GeneralUtils.sendMqttMsg(outgoingMsg) - diff --git a/example/config.json b/example/config.json deleted file mode 100644 index 13ca00f..0000000 --- a/example/config.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "mqttServerInfo": { - "description": "mqtt server info", - "port": 1883, - "broker": "mqtt.yiem.cc" - }, - "testingOrProduction": { - "value": "testing", - "description": "agent status, couldbe testing or production" - }, - "agentid": { - "value": "2b74b87a-5413-4fe2-a4d3-405891051680", - "description": "a unique id for this agent" - }, - "agentCentralConfigTopic": { - "mqtttopic": "/yiem_branch_1/agent/sommelier/backend/config/api/v1.1", - "description": "a central agent server's topic to get this agent config" - }, - "servicetopic": { - "mqtttopic": [ - "/yiem/hq/agent/sommelier/backend/prompt/api_v1/testing" - ], - "description": "a topic this agent are waiting for service request" - }, - "role": { - "value": "sommelier", - "description": "agent role" - }, - "organization": { - "value": "yiem_branch_1", - "description": "organization name" - }, - "externalservice": { - "loadbalancer": { - "mqtttopic": "/loadbalancer/requestingservice", - "description": "text to text service with instruct LLM" - }, - "text2textinstruct": { - "mqtttopic": "/loadbalancer/requestingservice", - "description": "text to text service with instruct LLM", - "llminfo": { - "name": "llama3instruct" - } - }, - "virtualWineCustomer_1": { - "mqtttopic": "/virtualenvironment/winecustomer", - "description": "text to text service with instruct LLM that act as wine customer", - "llminfo": { - "name": "llama3instruct" - } - }, - "text2textchat": { - "mqtttopic": "/loadbalancer/requestingservice", - "description": "text to text service with instruct LLM", - "llminfo": { - "name": "llama3instruct" - } - }, - "wineDB" : { - "description": "A wine database connection info for LibPQ client", - "host": "192.168.88.12", - "port": 10201, - "dbname": "wineDB", - "user": "yiemtechnologies", - "password": "yiemtechnologies@Postgres_0.0" - }, - "SQLVectorDB" : { - "description": "A wine database connection info for LibPQ client", - "host": "192.168.88.12", - "port": 10203, - "dbname": "SQLVectorDB", - "user": "yiemtechnologies", - "password": "yiemtechnologies@Postgres_0.0" - } - } -} \ No newline at end of file diff --git a/example/main.jl b/example/main.jl deleted file mode 100644 index c28785a..0000000 --- a/example/main.jl +++ /dev/null @@ -1,706 +0,0 @@ -using JSON, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures -using YiemAgent, GeneralUtils -using Base.Threads - -# ---------------------------------------------- 100 --------------------------------------------- # - - -""" Expected incomming MQTT message format for this service: - { - "msgMeta": { - "msgPurpose": "updateStatus", - "requestresponse": "request", - "timestamp": "2024-03-29T05:8:48.362", - "replyToMsgId": null, - "receiverId": null, - "getpost": "get", - "msgId": "e5c09bd8-7100-4e4e-bb43-05bee589a22c", - "acknowledgestatus": null, - "sendTopic": "/agent/wine/backend/chat/api/v1/prompt", - "receiverName": "agent-wine-backend", - "replyTopic": "/agent/wine/frontend/chat/api/v1/txt/receive", - "senderName": "agent-wine-frontend-chat", - "senderId": "0938a757-e0ee-40a9-8355-5e24906a87cd" - }, - "payload" : { - "text": "hello" - } - - } -""" - - - - - -# load config -config = copy(JSON.parsefile("../mountvolume/config/config.json")) - -""" Instantiate an agent. One need to specify startmessage and one of gpu location info, - Mqtt or Rest. start message must be comply with GeneralUtils's message format - - Arguments\n - ----- - channel::Channel - communication channel - sessionId::String - sesstion ID of the agent - agentName::String - Name of the agent - mqttBroker::String - mqtt broker e.g. "tcp://127.0.0.1:1883" - agentConfigTopic::String - main communication topic for an agent to ask for config - timeout::Int64 - inactivity timeout in minutes. If timeout is reached, an agent will be terminated. - - Return\n - ----- - a task represent an agent - - Example\n - ----- - ```jldoctest - julia> using YiemAgent, GeneralUtils - julia> msg = GeneralUtils.generate_msgMeta("/agent") - julia> incoming_msg = msg # assuming 1st msg was sent from other app - julia> agentConfigTopic = "/agent/wine/backend/config" - julia> task = runAgentInstance(incoming_msg, mqttBroker, agentConfigTopic, 60) - ``` - - TODO\n - ----- - [] update docstringLAMA_CONTEXT_LENGTH=40960 since the default size is 2048 as you can see in your debug log: - [] change how to get result of YiemAgent from let YiemAgent send msg directly to frontend, - to - response = YiemAgent.conversation() - then send response to frontend - - Signature\n - ----- -""" -function runAgentInstance( - receiveUserMsgChannel::Channel, - outputchannel::Channel, - sessionId::String, - config::Dict, - timeout::Int64, - ) - - function executeSQL(sql::T) where {T<:AbstractString} - host = config[:externalservice][:wineDB][:host] - port = config[:externalservice][:wineDB][:port] - dbname = config[:externalservice][:wineDB][:dbname] - user = config[:externalservice][:wineDB][:user] - password = config[:externalservice][:wineDB][:password] - DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") - result = LibPQ.execute(DBconnection, sql) - close(DBconnection) - return result - end - - function executeSQLVectorDB(sql) - host = config[:externalservice][:SQLVectorDB][:host] - port = config[:externalservice][:SQLVectorDB][:port] - dbname = config[:externalservice][:SQLVectorDB][:dbname] - user = config[:externalservice][:SQLVectorDB][:user] - password = config[:externalservice][:SQLVectorDB][:password] - DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") - result = LibPQ.execute(DBconnection, sql) - close(DBconnection) - return result - end - - function text2textInstructLLM(prompt::String; maxattempt::Integer=3, modelsize::String="medium", - senderId=GeneralUtils.uuid4snakecase(), timeout=180, - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - )) - msgMeta = GeneralUtils.generate_msgMeta( - config[:externalservice][:loadbalancer][:mqtttopic]; - msgPurpose="inference", - senderName="yiemagent", - senderId=senderId, - receiverName="text2textinstruct_$modelsize", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :text => prompt, - :kwargs => llmkwargs - ) - ) - - response = nothing - for attempts in 1:maxattempt - _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=timeout, maxattempt=maxattempt) - payload = _response[:response] - if _response[:success] && payload[:text] !== nothing - response = _response[:response][:text] - break - else - println("\n attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(outgoingMsg) - println(" attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - sleep(3) - end - end - - return response - end - - # get text embedding from a LLM service - function getEmbedding(text::T) where {T<:AbstractString} - msgMeta = GeneralUtils.generate_msgMeta( - config[:externalservice][:loadbalancer][:mqtttopic]; - msgPurpose="embedding", - senderName="yiemagent", - senderId=sessionId, - receiverName="textembedding", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :text => [text] # must be a vector of string - ) - ) - - response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120, maxattempt=3) - embedding = response[:response][:embeddings] - return embedding - end - - function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3, - vectorDB::Function; limit::Integer=1 - )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} - # get embedding from LLM service - embedding = getEmbedding(text)[1] - # check whether there is close enough vector already store in vectorDB. if no, add, else skip - sql = """ - SELECT *, $embeddingColumnName <-> '$embedding' as distance - FROM $tablename - ORDER BY distance LIMIT $limit; - """ - response = vectorDB(sql) - df = DataFrame(response) - return df - end - - function similarSQLVectorDB(query; maxdistance::Integer=100) - tablename = "sqlllm_decision_repository" - # get embedding of the query - df = findSimilarTextFromVectorDB(query, tablename, - "function_input_embedding", executeSQLVectorDB) - # println(df[1, [:id, :function_output]]) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - # distance = 100 # CHANGE this is for testing only - if row != 0 && distance < maxdistance - # if there is usable SQL, return it. - output_b64 = df[1, :function_output_base64] # pick the closest match - output_str = String(base64decode(output_b64)) - rowid = df[1, :id] - println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - return (dict=output_str, distance=distance) - else - println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - return (dict=nothing, distance=nothing) - end - end - - function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} - tablename = "sqlllm_decision_repository" - # get embedding of the query - # query = state[:thoughtHistory][:question] - df = findSimilarTextFromVectorDB(query, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row == 0 || distance > maxdistance # no close enough SQL stored in the database - query_embedding = getEmbedding(query)[1] - query = replace(query, "'" => "") - sql_base64 = base64encode(SQL) - sql_ = replace(SQL, "'" => "") - - sql = """ - INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); - """ - # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # println(sql) - _ = executeSQLVectorDB(sql) - end - end - - - function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3 - )::Union{AbstractDict, Nothing} where {T1<:AbstractString} - tablename = "sommelier_decision_repository" - # find similar - println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__) - df = findSimilarTextFromVectorDB(recentevents, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row != 0 && distance < maxdistance - # if there is usable decision, return it. - rowid = df[1, :id] - println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) - output_b64 = df[1, :function_output_base64] # pick the closest match - _output_str = String(base64decode(output_b64)) - output = copy(JSON.parsefile(_output_str)) - return output - else - println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) - return nothing - end - end - - - function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5 - ) where {T1<:AbstractString, T2<:AbstractDict} - tablename = "sommelier_decision_repository" - # find similar - df = findSimilarTextFromVectorDB(recentevents, tablename, - "function_input_embedding", executeSQLVectorDB) - row, col = size(df) - distance = row == 0 ? Inf : df[1, :distance] - if row == 0 || distance > maxdistance # no close enough SQL stored in the database - recentevents_embedding = getEmbedding(recentevents)[1] - recentevents = replace(recentevents, "'" => "") - decision_json = JSON.json(decision) - decision_base64 = base64encode(decision_json) - decision = replace(decision_json, "'" => "") - - sql = - """ - INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding'); - """ - println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) - println(sql) - _ = executeSQLVectorDB(sql) - else - println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__) - end - end - - # keepaliveChannel_2::Channel{Dict} = Channel{Dict}(8) - latestUserMsgTimeStamp::DateTime = Dates.now() - - externalFunction = ( - getEmbedding=getEmbedding, - text2textInstructLLM=text2textInstructLLM, - executeSQL=executeSQL, - similarSQLVectorDB=similarSQLVectorDB, - insertSQLVectorDB=insertSQLVectorDB, - similarSommelierDecision=similarSommelierDecision, - insertSommelierDecision=insertSommelierDecision, - ) - - agent = YiemAgent.sommelier( - externalFunction; - name="Jane", - id=sessionId, # agent instance id - retailername="Yiem", - llmFormatName="qwen3" - ) - - # user chat loop - while true - # check for new user message - if isready(receiveUserMsgChannel) - incomingMsg = take!(receiveUserMsgChannel) - incoming_msgMeta = incomingMsg[:msgMeta] - incomingPayload = incomingMsg[:payload] - latestUserMsgTimeStamp = Dates.now() - - # make sure the message has :text key because YiemAgent use this key for incoming user msg - if haskey(incomingPayload, :text) - # skip, msg already has correct key name - elseif haskey(incomingPayload, :txt) - # change key name to text - incomingPayload[:text] = incomingPayload[:txt] - else - error("\n no :txt or :text key in the message.") - end - - # reset agent - if occursin("newtopic", incomingPayload[:text]) || - occursin("Newtopic", incomingPayload[:text]) || - occursin("New topic", incomingPayload[:text]) || - occursin("new topic", incomingPayload[:text]) - # YiemAgent.clearhistory(agent) - - agent = YiemAgent.sommelier( - externalFunction; - name="Janie", - id=sessionId, # agent instance id - retailername="Yiem", - ) - - # sending msg back to sender i.e. LINE - msgMeta = GeneralUtils.generate_msgMeta( - incomingMsg[:msgMeta][:replyTopic]; - senderName="wine_assistant_backend", - senderId=sessionId, - replyToMsgId=incomingMsg[:msgMeta][:msgId], - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :alias => agent.name, # will be shown in frontend as agent name - :text => "Okay. What shall we talk about?" - ) - ) - _ = GeneralUtils.sendMqttMsg(outgoingMsg) - println("--> outgoingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(outgoingMsg) - else - usermsg = incomingPayload - - if incoming_msgMeta[:msgPurpose] == "initialize" - println("\n-- Initializing... ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - # send prompt - result = YiemAgent.conversation(agent; - userinput=usermsg, - maximumMsg=50) - # Ken's bot use [br] for newline character '\n' - # result = replace(result, '\n'=>"[br]") - - if incoming_msgMeta[:msgPurpose] == "initialize" - println("\n-- Initialized. Ready! waiting for request at:\n$(config[:servicetopic][:mqtttopic]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - msgMeta = GeneralUtils.generate_msgMeta( - incomingMsg[:msgMeta][:replyTopic]; - senderName="wine_assistant_backend", - senderId=string(uuid4()), - replyToMsgId=incomingMsg[:msgMeta][:msgId], - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( - :alias => agent.name, # will be shown in frontend as agent name - :text => result - ) - ) - _ = GeneralUtils.sendMqttMsg(outgoingMsg) - println("\n--> outgoingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(outgoingMsg) - - - # jpg_as_juliaStr = nothing - # prompt = nothing - - # if haskey(payload, "img") - # url_or_base64 = payload["img"] - - # if startswith(url_or_base64, "http") - # # img in http - # julia_rgb_img, cv2_bgr_img = ImageUtils.url_to_cv2_image(url_or_base64) - # _, buffer = cv2.imencode(".jpg", cv2_bgr_img) - # jpg_as_pyStr = base64.b64encode(buffer).decode("utf-8") - # jpg_as_juliaStr = pyconvert(String, jpg_as_pyStr) - # else - # # img in base64 - # cv2_bgr_img = payload["img"] - # jpg_as_juliaStr = pyconvert(String, jpg_as_pyStr) - # end - # end - - end - else - # println("\n no msg") - end - - if haskey(agent.memory[:events][end], :thought) - lastAssistantAction = agent.memory[:events][end][:thought][:action_name] - if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict - - # save a.memory[:shortmem][:decisionlog] to disk using JSON - println("\nsaving agent.memory[:shortmem][:decisionlog] to disk") - filename = "agent_decision_log_$(Dates.now())_$(agent.id).json" - filepath = "/appfolder/app/log/$filename" - open(filepath, "w") do io - JSON.pretty(io, agent.memory[:shortmem][:decisionlog]) - end - - # for (i, event) in enumerate(agent.memory[:events]) - # if event[:subject] == "assistant" - # # create timeline of the last 3 conversation except the last one. - # # The former will be used as caching key and the latter will be the caching target - # # in vector database - # all_recapkeys = keys(agent.memory[:recap]) #[TESTING] recap as caching - # all_recapkeys_vec = [r for r in all_recapkeys] # convert to a vector - - # # select from 1 to 2nd-to-lase event (i.e. excluding the latest which is assistant's response) - # _recapkeys_vec = all_recapkeys_vec[1:i-1] - - # # select only previous 3 recaps - # recapkeys_vec = - # if length(_recapkeys_vec) <= 3 # 1st message is a user's hello msg - # _recapkeys_vec # choose all - # else - # _recapkeys_vec[end-2:end] - # end - # #[PENDING] if there is specific data such as number, donot store in database - # tempmem = DataStructures.OrderedDict() - # for k in recapkeys_vec - # tempmem[k] = agent.memory[:recap][k] - # end - - # recap = GeneralUtils.dictToString_noKey(tempmem) - # thoughtDict = agent.memory[:events][i][:thought] # latest assistant thoughtDict - # insertSommelierDecision(recap, thoughtDict) - # else - # # skip - # end - # end - println("\nCaching conversation process done") - break - end - end - - # self terminate if too long inactivity - timediff = GeneralUtils.timedifference(latestUserMsgTimeStamp, Dates.now(), "minutes") - if timediff > timeout - - result = Dict(:exitreason => "timeout", :timestamp => Dates.now()) - put!(outputchannel, result) - println("Agent ID $(agent.id) timeout has been reached $timediff/$timeout minutes Send delete session msg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - - # send "delete session" message to inform the main loop that this session can be deleted - sendto = - if typeof(config[:servicetopic][:mqtttopic]) <: Array - config[:servicetopic][:mqtttopic][1] - else - config[:servicetopic][:mqtttopic] - end - - msgMeta = GeneralUtils.generate_msgMeta( - sendto; - senderName="session", - senderId=sessionId, - msgPurpose="delete session", - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => nothing - ) - _ = GeneralUtils.sendMqttMsg(outgoingMsg) - - try disconnect(agent.mqttClient) catch end - break - end - sleep(1) # allowing on_msg_2, asyncmove above and other process to run - end -end - -sessionDict = Dict{String,Any}() -incomingMsgChannel = (ch1=Channel(8),) # store msg that coming into servicetopic -# incommingInternalMsg = [] # st ore msg that coming into servicetopic internal management -keepaliveChannel::Channel{Dict} = Channel{Dict}(8) - -# Define the callback for receiving messages. -function onMsgCallback_1(topic, payload) - jobj = JSON.parsefile(String(payload)) - incomingMqttMsg = copy(jobj) # convert json object into julia dictionary recursively - - if occursin("keepalive", topic) - put!(keepaliveChannel, incomingMqttMsg) - else - put!(incomingMsgChannel[:ch1], incomingMqttMsg) - end -end - -mqttInstance = GeneralUtils.mqttClientInstance_v2( - config[:mqttServerInfo][:broker], - config[:servicetopic][:mqtttopic], - incomingMsgChannel, - keepaliveChannel, - onMsgCallback_1 -) - -# ------------------------------------------------------------------------------------------------ # -# this service main loop # -# ------------------------------------------------------------------------------------------------ # - -function main() - sessiontimeout = 1 * 1 * 60 # timeout in minute for each instance (day * hour * minute) - initializing = false - while true - # check if mqtt connection is still up - _ = GeneralUtils.checkMqttConnection!(mqttInstance; keepaliveCheckInterval=30) - - # initialize session 0 - if initializing == false # send init msg - sendto = - if typeof(config[:servicetopic][:mqtttopic]) <: Array - config[:servicetopic][:mqtttopic][1] - else - config[:servicetopic][:mqtttopic] - end - - msgMeta = GeneralUtils.generate_msgMeta( - sendto; - msgPurpose="initialize", - senderName="initializer", - senderId="0", - msgId= "initMsg", - replyTopic=sendto, - mqttBrokerAddress=config[:mqttServerInfo][:broker], - mqttBrokerPort=config[:mqttServerInfo][:port], - ) - - outgoingMsg = Dict( - :msgMeta => msgMeta, - :payload => Dict( # will be shown in frontend as agent name - :text => "Do you have full-bodied red wines under 100 USD. I don't have any other preferences." - ) - ) - _ = GeneralUtils.sendMqttMsg(outgoingMsg) - initializing = true - println("\n--> Initializing msg sent ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - # check for new message - if !isempty(incomingMsgChannel[:ch1]) - msg = popfirst!(incomingMsgChannel[:ch1]) - println("\n<-- incomingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(msg) - - # @spawn new runAgentInstance and store it in sessionDict - # use agent's frontend id because 1 backend agent per 1 frontend session - sessionId = msg[:msgMeta][:senderId] - sessionId = replace(sessionId, "-" => "_") # julia can't use "-" in a dict key - - # check for delete session msg - if msg[:msgMeta][:msgPurpose] == "delete session" - delete!(sessionDict, sessionId) - println("sessionId $(sessionId) has been terminated ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - - # no session yet, create new session - elseif sessionId ∉ keys(sessionDict) - inputch = Channel{Dict}(8) - outputch = Channel{Dict}(8) - - process = @spawn runAgentInstance(inputch, outputch, sessionId, config, sessiontimeout) - # process = runAgentInstance(inputch, outputch, sessionId, config, sessiontimeout) #XXX use spawn version - - println("\ninstantiate agent success ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - - # call runAgentInstance() and store it in sessionDict to be able to check on it later - sessionDict[sessionId] = Dict( - :inputchannel => inputch, - :outputchannel => outputch, - :process => process, - ) - put!(sessionDict[sessionId][:inputchannel], msg) - # ongoing session - else - println("sessionId $(sessionId) existing session ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - put!(sessionDict[sessionId][:inputchannel], msg) - end - end - - # sleep is needed because MQTTClient use async. "while true" loop leave no - # chance for control to switch to on_msg() - sleep(1) - end -end - -main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/tools/get_weather.jl b/src/tools/get_weather.jl index 808586a..9b23aef 100644 --- a/src/tools/get_weather.jl +++ b/src/tools/get_weather.jl @@ -10,7 +10,7 @@ Execute the get_weather tool. # Returns - `agentToolResult`: Result content with weather data """ -function execute_tool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) :: agentToolResult +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) :: agentToolResult city = get(args, "city", "") units = get(args, "units", "celsius") @@ -39,7 +39,7 @@ end """ Define and return the get_weather agentTool. """ -function get_tool() :: agentTool +function getTool() :: agentTool return agentTool( name = "get_weather", label = "Weather Lookup", @@ -52,7 +52,7 @@ function get_tool() :: agentTool ), "required" => ["city"] ), - execute = execute_tool, # reference the function defined above + execute = executeTool, # reference the function defined above prepareArguments = nothing, parallelToolExecute = false ) diff --git a/src/tools/registry.jl b/src/tools/registry.jl index de121fd..b4cb46a 100644 --- a/src/tools/registry.jl +++ b/src/tools/registry.jl @@ -1,25 +1,25 @@ module toolRegistry -export load_tools, register_tool, get_tools, list_tools, clear_tools +export loadTools, registerTool, getTools, listTools, clearTools using ..type -# Global registry — populated at runtime by load_tools() or register_tool() +# Global registry — populated at runtime by loadTools() or registerTool() const _registry = Vector{agentTool}() """ Load all tool modules from a directory. Scans `dir` for `.jl` files. Each file must define a function named -`get_tool() :: agentTool`. Files are sorted alphabetically so tool +`getTool() :: agentTool`. Files are sorted alphabetically so tool registration order is deterministic. # Tool file format -Each `.jl` file defines one function `get_tool()` that returns an `agentTool`: +Each `.jl` file defines one function `getTool()` that returns an `agentTool`: ```julia # src/tools/get_weather.jl -function get_tool() :: agentTool +function getTool() :: agentTool return agentTool( name = "get_weather", label = "Weather Lookup", @@ -52,9 +52,9 @@ end - `Vector{agentTool}`: All loaded tools # Errors -- Throws `ArgumentError` if a tool file does not define a `get_tool` function +- Throws `ArgumentError` if a tool file does not define a `getTool` function """ -function load_tools(dir::String)::Vector{agentTool} +function loadTools(dir::String)::Vector{agentTool} if !isdir(dir) throw(ArgumentError("Tool directory does not exist: $dir")) end @@ -71,19 +71,19 @@ function load_tools(dir::String)::Vector{agentTool} # (agentTool, textContent, agentToolResult, etc. are all available) include(filepath) - # Validate that get_tool was defined (include() places it in current module scope) - if !isdefined(:get_tool) + # Validate that getTool was defined (include() places it in current module scope) + if !isdefined(:getTool) throw(ArgumentError( - "Tool file $(filepath) does not define a `get_tool()` function. " * - "Each tool file must define: function get_tool() :: agentTool ... end" + "Tool file $(filepath) does not define a `getTool()` function. " * + "Each tool file must define: function getTool() :: agentTool ... end" )) end - # Call get_tool() — it runs in current scope where types are visible - tool = get_tool() + # Call getTool() — it runs in current scope where types are visible + tool = getTool() if !(tool isa agentTool) throw(ArgumentError( - "get_tool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))" + "getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))" )) end @@ -104,7 +104,7 @@ Register a single agentTool into the global registry. # Returns - `Vector{agentTool}`: Updated registry """ -function register_tool(tool::agentTool)::Vector{agentTool} +function registerTool(tool::agentTool)::Vector{agentTool} push!(_registry, tool) println("[toolRegistry] Registered tool: $(tool.name)") return _registry @@ -116,7 +116,7 @@ Get all registered tools. # Returns - `Vector{agentTool}`: Copy of the registry """ -function get_tools()::Vector{agentTool} +function getTools()::Vector{agentTool} return deepcopy(_registry) end @@ -126,14 +126,14 @@ List all registered tool names and labels. # Returns - `Vector{Tuple{String,String}}`: Pairs of (name, label) """ -function list_tools()::Vector{Tuple{String,String}} +function listTools()::Vector{Tuple{String,String}} return [(t.name, t.label) for t in _registry] end """ Clear all registered tools from the global registry. """ -function clear_tools()::Nothing +function clearTools()::Nothing empty!(_registry) println("[toolRegistry] Registry cleared") return nothing -- 2.52.0 From 04e75e61b8c7fd403d200d59a60abee3b9cf4823 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 8 Aug 2026 08:51:07 +0700 Subject: [PATCH 48/50] update --- docs/loadtools.md | 10 ++--- src/tools/getTime.jl | 49 +++++++++++++++++++++ src/tools/{get_weather.jl => getWeather.jl} | 10 ++--- src/tools/registry.jl | 12 ++--- src/type.jl | 4 +- src_OLD/OLD_interface.jl | 4 +- 6 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 src/tools/getTime.jl rename src/tools/{get_weather.jl => getWeather.jl} (89%) diff --git a/docs/loadtools.md b/docs/loadtools.md index 8dfe6be..42f446c 100644 --- a/docs/loadtools.md +++ b/docs/loadtools.md @@ -5,7 +5,7 @@ Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory w ## How It Works 1. `src/tools/registry.jl` defines a `loadTools(dir::String)` function that scans a directory for `.jl` files -2. Each tool file must define a single function: `getTool() :: agentTool` +2. Each tool file must define a single function: `getTool()::agentTool` 3. `loadTools()` sorts files alphabetically, includes each one, calls `getTool()`, and registers the result 4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent` @@ -15,7 +15,7 @@ Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory w src/ ├── tools/ │ ├── registry.jl # Tool loader (do not edit) -│ ├── get_weather.jl # Your tool +│ ├── getWeather.jl # Your tool │ └── query_db.jl # Another tool ├── type.jl ├── utils.jl @@ -29,10 +29,10 @@ src/ Each `.jl` file in `src/tools/` must define `getTool()` returning an `agentTool`: ```julia -# src/tools/get_weather.jl -function getTool() :: agentTool +# src/tools/getWeather.jl +function getTool()::agentTool return agentTool( - name = "get_weather", + name = "getWeather", label = "Weather Lookup", description = "Fetch current weather and forecast for a given city.", inputSchema = Dict{String,Any}( diff --git a/src/tools/getTime.jl b/src/tools/getTime.jl new file mode 100644 index 0000000..d100f9b --- /dev/null +++ b/src/tools/getTime.jl @@ -0,0 +1,49 @@ +""" +Execute the get_time tool. + +# Arguments +- `toolCallId::String`: Unique identifier for this tool call +- `args::Dict{String,Any}`: Parsed arguments from the LLM +- `signal::Union{Nothing,abortSignal}`: Optional abort signal +- `onPartialResult::Function`: Callback for streaming partial results + +# Returns +- `agentToolResult`: Result content with current time +""" +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + tz = get(args, "timezone", "local") + + t = now() + + if tz == "local" + timeStr = string(t) + else + timeStr = string(t) + end + + return agentToolResult( + [textContent("Current time: $(timeStr)")], + Dict{Any,Any}(), nothing, false + ) +end + +""" +Define and return the get_time agentTool. +""" +function getTool()::agentTool + return agentTool( + name = "get_time", + label = "Get Current Time", + description = "Get the current date and time.", + inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict( + "timezone" => Dict("type" => "string", "description" => "Timezone (currently only 'local' is supported)") + ), + "required" => [] + ), + execute = executeTool, + prepareArguments = nothing, + parallelToolExecute = false + ) +end diff --git a/src/tools/get_weather.jl b/src/tools/getWeather.jl similarity index 89% rename from src/tools/get_weather.jl rename to src/tools/getWeather.jl index 9b23aef..f62cb80 100644 --- a/src/tools/get_weather.jl +++ b/src/tools/getWeather.jl @@ -1,5 +1,5 @@ """ -Execute the get_weather tool. +Execute the getWeather tool. # Arguments - `toolCallId::String`: Unique identifier for this tool call @@ -10,7 +10,7 @@ Execute the get_weather tool. # Returns - `agentToolResult`: Result content with weather data """ -function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) :: agentToolResult +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult city = get(args, "city", "") units = get(args, "units", "celsius") @@ -37,11 +37,11 @@ function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{N end """ -Define and return the get_weather agentTool. +Define and return the getWeather agentTool. """ -function getTool() :: agentTool +function getTool()::agentTool return agentTool( - name = "get_weather", + name = "getWeather", label = "Weather Lookup", description = "Fetch current weather and forecast for a given city.", inputSchema = Dict{String,Any}( diff --git a/src/tools/registry.jl b/src/tools/registry.jl index b4cb46a..5ce045f 100644 --- a/src/tools/registry.jl +++ b/src/tools/registry.jl @@ -11,17 +11,17 @@ const _registry = Vector{agentTool}() Load all tool modules from a directory. Scans `dir` for `.jl` files. Each file must define a function named -`getTool() :: agentTool`. Files are sorted alphabetically so tool +`getTool()::agentTool`. Files are sorted alphabetically so tool registration order is deterministic. # Tool file format Each `.jl` file defines one function `getTool()` that returns an `agentTool`: ```julia -# src/tools/get_weather.jl -function getTool() :: agentTool +# src/tools/getWeather.jl +function getTool()::agentTool return agentTool( - name = "get_weather", + name = "getWeather", label = "Weather Lookup", description = "Fetch current weather and forecast for a given city.", inputSchema = Dict{String,Any}( @@ -72,10 +72,10 @@ function loadTools(dir::String)::Vector{agentTool} include(filepath) # Validate that getTool was defined (include() places it in current module scope) - if !isdefined(:getTool) + if !isdefined(@__MODULE__, :getTool) throw(ArgumentError( "Tool file $(filepath) does not define a `getTool()` function. " * - "Each tool file must define: function getTool() :: agentTool ... end" + "Each tool file must define: function getTool()::agentTool ... end" )) end diff --git a/src/type.jl b/src/type.jl index 66d4cda..5146870 100644 --- a/src/type.jl +++ b/src/type.jl @@ -215,7 +215,7 @@ Maps MCP server tool definitions to an executable Julia tool. # MCP Tool Example ``` { - "name": "get_weather", + "name": "getWeather", "title": "Weather Lookup", "description": "Fetch current weather and forecast for a given city.", "inputSchema": { @@ -232,7 +232,7 @@ Maps MCP server tool definitions to an executable Julia tool. # Example ```julia tool = agentTool( - name="get_weather", + name="getWeather", label="Weather Lookup", description="Fetch current weather and forecast for a given city.", inputSchema=Dict( diff --git a/src_OLD/OLD_interface.jl b/src_OLD/OLD_interface.jl index 8c1ca5c..5e375d6 100644 --- a/src_OLD/OLD_interface.jl +++ b/src_OLD/OLD_interface.jl @@ -140,14 +140,14 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3 }, "action_name": { "type": "string", - "enum": ["search_web", "get_weather", "calculate_math"], + "enum": ["search_web", "getWeather", "calculate_math"], "description": "The exact name of the tool to execute." }, "action_input": { "type": "object", "properties": { "query": { "type": ["string", "null"], "description": "For search_web" }, - "location": { "type": ["string", "null"], "description": "For get_weather" }, + "location": { "type": ["string", "null"], "description": "For getWeather" }, "equation": { "type": ["string", "null"], "description": "For calculate_math" } }, "required": ["query", "location", "equation"], -- 2.52.0 From 0ddbcf9ca1b2ca2bf1784bd81d845c7b97c82325 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 8 Aug 2026 09:38:05 +0700 Subject: [PATCH 49/50] update --- src/tools/README.md | 383 ++++++++++++++++++++++++++++++++++++++++ src/tools/getTime.jl | 68 +++++-- src/tools/getWeather.jl | 9 +- src/type.jl | 12 +- src/utils.jl | 103 ++++++++++- 5 files changed, 548 insertions(+), 27 deletions(-) create mode 100644 src/tools/README.md diff --git a/src/tools/README.md b/src/tools/README.md new file mode 100644 index 0000000..d7c6380 --- /dev/null +++ b/src/tools/README.md @@ -0,0 +1,383 @@ +# Tools + +Tools allow the agent to perform actions and fetch data. Each tool defines a **schema** (what arguments it accepts) and an **execution function** (what it does). + +## Quick Start + +Add a new tool by creating a `.jl` file in `src/tools/`. The file must define a `getTool()` function that returns an `agentTool`: + +```julia +# src/tools/my_tool.jl + +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + city = args["city"] + return agentToolResult( + [textContent("Hello from $(city)!")], + Dict{Any,Any}(), nothing, false + ) +end + +function getTool()::agentTool + return agentTool( + name = "my_tool", + label = "My Tool", + description = "Says hello to a city.", + inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict( + "city" => Dict("type" => "string", "description" => "City name") + ), + "required" => ["city"] + ), + execute = executeTool, + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = false + ) +end +``` + +When `loadTools()` or `registerTool()` is called, the tool becomes available to the agent. + +## Tool Anatomy + +Each tool has 3 main parts: + +### 1. Schema (`inputSchema`) + +JSON Schema (MCP format) describing the tool's arguments. The `"required"` array lists mandatory fields: + +```julia +inputSchema = Dict{String,Any}( + "type" => "object", + "properties" => Dict( + "city" => Dict("type" => "string", "description" => "City name"), + "units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius") + ), + "required" => ["city"] +) +``` + +### 2. Execution Function (`execute`) + +A function with the signature: + +```julia +execute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult +``` + +- **`toolCallId`** — unique ID for this invocation (from the LLM's tool call) +- **`args`** — validated arguments provided by the LLM +- **`signal`** — abort signal for cancellable operations +- **`onPartialResult`** — callback for streaming progress updates +- **Returns** — `agentToolResult` with content, details, usage, and termination flag + +```julia +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + # Optional: stream progress updates + onPartialResult(Dict("status" => "Fetching data...")) + + # Do work + result = "Weather in $(args["city"]): Sunny, 22°C" + + # Return result + return agentToolResult( + [textContent(result)], + Dict{Any,Any}(), # details + nothing, # usage + false # terminate (true to stop agent loop) + ) +end +``` + +### 3. Tool Definition (`getTool()`) + +Returns an `agentTool` struct: + +| Field | Type | Description | +|---|---|---| +| `name` | `String` | Unique identifier (e.g. `"getWeather"`) | +| `label` | `String` | Human-readable name (e.g. `"Weather Lookup"`) | +| `description` | `String` | What the tool does (shown to the LLM) | +| `inputSchema` | `Any` | JSON Schema (MCP format) | +| `execute` | `Function` | The execution function | +| `prepareArguments` | `Union{Function,Nothing}` | Optional argument transform before validation | +| `validateRequiredArgs` | `Union{Function,Nothing}` | Optional custom validation | +| `parallelToolExecute` | `Bool` | Run this tool in parallel with others | + +## Argument Validation + +Validation happens **before** tool execution, in the `prepareToolCall` phase. Invalid calls return an error immediately without invoking `execute`, `beforeToolCall`, or logging `toolExecutionStart`. + +### Default: JSON Schema Required Fields + +Set `validateRequiredArgs = nothing` to use the default validator, which checks that all fields in `inputSchema["required"]` are present: + +```julia +# src/tools/getWeather.jl — uses default validation +function getTool()::agentTool + return agentTool( + name = "getWeather", + # ... + validateRequiredArgs = nothing, # uses default + ) +end +``` + +### Custom Validation Hook + +Override `validateRequiredArgs` when you need: +- **Cross-field constraints** (e.g. "at least one of X or Y") +- **Format validation** (e.g. regex patterns, date parsing) +- **Domain rules** (e.g. value ranges, business logic) + +The hook signature takes only `args`: + +```julia +function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String} + tz = get(args, "timezone", nothing) + city = get(args, "city", "") + + if !haskey(args, "timezone") && isempty(city) + return "Missing required argument: provide at least one of 'timezone' or 'city'" + end + + if tz !== nothing + tz_str = string(tz) + if !occursin(r"^[A-Za-z]+\/[A-Za-z]+(/[A-Za-z]+)*$", tz_str) + return "Invalid timezone format: '$tz_str'. Use IANA format, e.g. 'America/New_York'" + end + end + + return nothing +end +``` + +Return `nothing` to pass, or an error `String` to fail. The error is fed back to the LLM so it can retry with corrected arguments. + +## Tool Call Lifecycle + +``` +LLM requests tool call + └── prepareToolCall (agentCore.jl:511) + ├── Tool lookup by name + ├── prepareArguments (tool-specific transform, if defined) + ├── validateToolArguments (validateRequiredArgs hook or default) + │ └── on failure → immediateOutcome (no execution) + ├── beforeToolCall hook (if defined) + │ └── on block → immediateOutcome (no execution) + └── returns preparedToolCall + +executed by executeToolCallsSequential or executeToolCallsParallel + └── executePreparedToolCall (agentCore.jl:589) + ├── emit toolExecutionStart + ├── call tool.execute() + │ └── on error → executedOutcome(isError=true) + └── returns executedOutcome + +finalizeExecutedToolCall (agentCore.jl:675) + ├── afterToolCall hook (if defined) + │ └── can mutate result content, usage, terminate, isError + └── returns finalizedOutcome + +emit toolExecutionEnd + └── createToolResultMessage → added to conversation history +``` + +## Execution Modes + +### Sequential + +Tools execute one at a time in order. Required when: +- Tools have implicit dependencies +- Tools share state (e.g. writing to the same file) +- Tools have `parallelToolExecute = false` + +Set globally via `agentLoopConfig.toolExecution = "sequential"`, or per-tool via `parallelToolExecute = false`. + +### Parallel + +Tools execute concurrently when all are independent. Reduces wall-clock time. Set `parallelToolExecute = true` on individual tools, or set `agentLoopConfig.toolExecution = "parallel"`. + +## Streaming Partial Results + +For long-running tools (API calls, file uploads, training), use `onPartialResult` to stream progress: + +```julia +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + onPartialResult(Dict("status" => "Step 1: Fetching data...")) + sleep(1) + + onPartialResult(Dict("status" => "Step 2: Processing...")) + sleep(1) + + return agentToolResult( + [textContent("Done!")], + Dict{Any,Any}(), nothing, false + ) +end +``` + +UI listeners and the TUI consume these events in real time via `toolExecutionUpdate`. + +## Loading Tools + +### Auto-load from Directory + +```julia +using .toolRegistry + +tools = loadTools("src/tools") # scans for *.jl files with getTool() +``` + +Files are loaded alphabetically for deterministic registration order. + +### Manual Registration + +```julia +tool = getTool() # from your tool module +registerTool(tool) +``` + +## Using Tools with an Agent + +Loading tools only registers them — you must pass them to the `yiemAgent` and provide an `llmCall` function. Here is the complete flow: + +```julia +using .YiemAgent +using .toolRegistry + +# 1. Load tools from the tools directory +tools = loadTools("src/tools") +# [toolRegistry] Loading tool from: src/tools/getTime.jl +# [toolRegistry] Loaded tool: getTime — Time Lookup +# [toolRegistry] Loading tool from: src/tools/getWeather.jl +# [toolRegistry] Loaded tool: getWeather — Weather Lookup + +# 2. Define your LLM call function +function my_llm_call(messages::Dict)::assistantMessage + # Call your LLM API here (OpenAI, Anthropic, local model, etc.) + # Return an assistantMessage with the response content + # If the LLM wants to call a tool, include tool_call content blocks + ... +end + +# 3. Define your event sink (optional, for logging/debugging) +function my_event_sink(event) + if event isa toolExecStartEvent + println("[EVENT] Tool start: $(event.toolName)") + elseif event isa toolExecEndEvent + status = event.isError ? "ERROR" : "OK" + println("[EVENT] Tool end: $(event.toolName) — $status") + end +end + +# 4. Create the agent with tools +agent = yiemAgent( + systemPrompt = "You are a helpful assistant that can check weather and time.", + model = my_model, + tools = tools, # pass loaded tools + llmCall = my_llm_call, # your LLM function + agentEventSink = my_event_sink, # event handler +) + +# 5. Send a message and get a response +run_agent(agent, "What's the weather in Tokyo?") +response = take_response(agent) + +# response.content contains the LLM's reply (with tool results if applicable) +println(response.content) + +# 6. When done, stop the agent +stop_agent(agent) +``` + +### How It Works + +1. **User sends a message** via `run_agent(agent, "What's the weather in Tokyo?")`. The message goes into `inputChannel`. + +2. **Agent loop** (`_agent_loop`) picks it up, converts it to a `userMessage`, and adds it to `agent._state.messages`. + +3. **LLM is called** via `agent.llmCall(formatted_messages)`. The LLM sees the system prompt, conversation history, and the tool definitions in the prompt (via `formatMsgForLLM`). + +4. **If the LLM uses a tool**, it returns a response with `tool_call` content blocks. The agent: + - Extracts each tool call (name, arguments) + - Runs validation (`validateRequiredArgs` or default) + - Executes the tool (or returns an error if validation fails) + - Feeds the result back as a `toolResultMessage` in the conversation + +5. **LLM is called again** with the tool results. This repeats until the LLM returns a text response with no tool calls. + +6. **Final response** is sent to `outputChannel` — retrieve it with `take_response(agent)`. + +### Minimal Working Example + +```julia +using .YiemAgent +using .toolRegistry + +# Load tools +tools = loadTools("src/tools") + +# Mock LLM that echoes back a tool call, then a text response +call_count = 0 +function mock_llm_call(messages::Dict)::assistantMessage + global call_count += 1 + if call_count == 1 + # First call: LLM decides to use getWeather + return assistantMessage( + content=[ + Dict("type" => "tool_calls", + "tool_calls" => [Dict("id" => "call_1", "name" => "getWeather", + "arguments" => Dict("city" => "Tokyo"))]) + ], + model = "mock", + usage = llmUsage(0, 0) + ) + else + # Second call: LLM returns text (after tool result) + return assistantMessage( + content = [textContent("The weather in Tokyo is sunny, 22°C.")], + model = "mock", + usage = llmUsage(0, 0) + ) + end +end + +# Create agent +agent = yiemAgent( + systemPrompt = "You are a helpful assistant.", + tools = tools, + llmCall = mock_llm_call, + agentEventSink = e -> nothing, # no events +) + +# Run +run_agent(agent, "What's the weather in Tokyo?") +response = take_response(agent) + +stop_agent(agent) +``` + +## Available Tools + +| Tool | Description | Validation | +|---|---|---| +| `getWeather` | Fetch weather for a city | Default (JSON Schema required) | +| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) | + +## Example: Error Flow + +When the LLM calls a tool with invalid arguments: + +``` +User: "What's the weather?" + └── LLM: call getWeather() with no arguments + └── prepareToolCall → validateRequiredArgs → "Missing required arguments: city" + └── immediateOutcome → error tool result + └── LLM sees: "Missing required arguments: city" + └── LLM retries: call getWeather(city="Tokyo") + └── executeTool → "Weather in Tokyo: Sunny, 22°C" +``` + +The agent feeds the error back to the LLM as a tool result message, allowing it to self-correct. diff --git a/src/tools/getTime.jl b/src/tools/getTime.jl index d100f9b..97636ab 100644 --- a/src/tools/getTime.jl +++ b/src/tools/getTime.jl @@ -1,5 +1,43 @@ """ -Execute the get_time tool. +Validate required arguments for the getTime tool. + +Demonstrates custom validation beyond simple required-field checking: +- Ensures at least one time source (timezone or city) is provided +- Validates timezone is in IANA format if specified +- Validates city name is not empty if specified + +# Arguments +- `args::Dict{String,Any}`: Arguments from the LLM + +# Returns +- `nothing` if validation passes +- `String` error message if validation fails +""" +function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String} + tz = get(args, "timezone", nothing) + city = get(args, "city", "") + + hasTz = tz !== nothing && !isempty(tz) + hasCity = !isempty(city) + + # At least one of timezone or city is required + if !hasTz && !hasCity + return "Missing required argument: provide at least one of 'timezone' or 'city'" + end + + # Validate timezone format (IANA tz database: "Continent/City" or "Continent/City/SubCity") + if hasTz + tz_str = string(tz) + if !occursin(r"^[A-Za-z]+\/[A-Za-z]+(/[A-Za-z]+)*$", tz_str) + return "Invalid timezone format: '$tz_str'. Use IANA format, e.g. 'America/New_York' or 'Asia/Tokyo'" + end + end + + return nothing +end + +""" +Execute the getTime tool. # Arguments - `toolCallId::String`: Unique identifier for this tool call @@ -8,42 +46,44 @@ Execute the get_time tool. - `onPartialResult::Function`: Callback for streaming partial results # Returns -- `agentToolResult`: Result content with current time +- `agentToolResult`: Result content with current time data """ function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult - tz = get(args, "timezone", "local") + tz = get(args, "timezone", nothing) + city = get(args, "city", "") - t = now() - - if tz == "local" - timeStr = string(t) + # Simulate time lookup — replace with actual timezone API call + if tz !== nothing + result = "Current time in $(tz): $(now())" else - timeStr = string(t) + result = "Current time in $(city): $(now())" end return agentToolResult( - [textContent("Current time: $(timeStr)")], + [textContent(result)], Dict{Any,Any}(), nothing, false ) end """ -Define and return the get_time agentTool. +Define and return the getTime agentTool. """ function getTool()::agentTool return agentTool( - name = "get_time", - label = "Get Current Time", - description = "Get the current date and time.", + name = "getTime", + label = "Time Lookup", + description = "Get current local time for a timezone or city.", inputSchema = Dict{String,Any}( "type" => "object", "properties" => Dict( - "timezone" => Dict("type" => "string", "description" => "Timezone (currently only 'local' is supported)") + "timezone" => Dict("type" => "string", "description", "IANA timezone, e.g. 'America/New_York'"), + "city" => Dict("type" => "string", "description", "City name as fallback") ), "required" => [] ), execute = executeTool, prepareArguments = nothing, + validateRequiredArgs = validateRequiredArgs, parallelToolExecute = false ) end diff --git a/src/tools/getWeather.jl b/src/tools/getWeather.jl index f62cb80..0411841 100644 --- a/src/tools/getWeather.jl +++ b/src/tools/getWeather.jl @@ -14,14 +14,6 @@ function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{N city = get(args, "city", "") units = get(args, "units", "celsius") - # Validate required arguments - if isempty(city) - return agentToolResult( - [textContent("Error: 'city' argument is required.")], - Dict{Any,Any}(), nothing, false - ) - end - # Simulate weather fetch — replace with actual API call # You can call onPartialResult() here for streaming progress updates: # onPartialResult(Dict("status" => "Fetching weather data...")) @@ -54,6 +46,7 @@ function getTool()::agentTool ), execute = executeTool, # reference the function defined above prepareArguments = nothing, + validateRequiredArgs = nothing, parallelToolExecute = false ) end diff --git a/src/type.jl b/src/type.jl index 5146870..60ae3c5 100644 --- a/src/type.jl +++ b/src/type.jl @@ -8,8 +8,8 @@ textContent, imageContent, # Message types userMessage, assistantMessage, toolResultMessage, - # Tool types - agentTool, + # Tool types + agentTool, validateRequiredArgs # Context types agentContext, agentState, agentToolCall, prepareNextTurnContext, # Loop & execution types @@ -207,6 +207,8 @@ Maps MCP server tool definitions to an executable Julia tool. - `execute::Function`: Tool execution function, signature: `execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)` - `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback +- `validateRequiredArgs::Union{Function, Nothing}`: Optional validation hook, signature: + `validateRequiredArgs(args::Dict) -> Union{Nothing, String}` where `String` is an error message - `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel # Returns @@ -249,7 +251,10 @@ tool = agentTool( [textContent("Sunny, 22C in $(city)")], Dict{Any,Any}(), nothing, false ) - end + end, + prepareArguments = nothing, + validateRequiredArgs = nothing, + parallelToolExecute = false ) ``` """ @@ -260,6 +265,7 @@ struct agentTool # A tool available to the agent inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format) execute::Function # Tool execution function prepareArguments::Union{Function, Nothing} # Optional argument preparation callback + validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel end diff --git a/src/utils.jl b/src/utils.jl index 0c92f43..db3e51d 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,6 +1,6 @@ module utils -export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, _userMessageToOpenAI, +export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI, _assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks using UUIDs, Dates, DataStructures, HTTP, JSON @@ -275,10 +275,109 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{ end end - return blocks + return blocks end +""" + validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String} + +Validates that all required fields listed in the tool's JSON Schema are present +in `args`. Returns `nothing` if validation passes, or a descriptive error string +listing the missing required fields. + +This is the default `validateRequiredArgs` hook. Tool authors can override it +with a custom validation function that performs additional checks (e.g. type +coercion, format validation, cross-field constraints). + +# Arguments +- `args::Dict{String,Any}`: The arguments provided by the LLM +- `inputSchema::Dict{String,Any}`: The tool's `inputSchema` (JSON Schema format) + +# Returns +- `nothing` if all required args are present +- `String` error message listing missing fields otherwise + +# Examples +```julia +schema = Dict("required" => ["city"]) +args = Dict{String,Any}() +validateRequiredArgs(args, schema) # => "Missing required arguments: city" + +args2 = Dict("city" => "Tokyo") +validateRequiredArgs(args2, schema) # => nothing +``` +""" +function validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any})::Union{Nothing,String} + required = get(inputSchema, "required", Any[]) + if isempty(required) + return nothing + end + + missing = String[] + for field in required + if !(field in keys(args)) + push!(missing, field) + end + end + + if !isempty(missing) + return "Missing required arguments: $(join(missing, ", "))" + end + + return nothing +end + + +""" + validateToolArguments(tool::agentTool, prepared::agentToolCall) -> Dict{String,Any} + +Validates the prepared tool call arguments by calling the tool's +`validateRequiredArgs` hook (or the default implementation). If validation +fails, returns a modified `agentToolCall` with an empty arguments dict +so downstream code can detect the failure. If the hook exists on the tool +and returns an error string, that error is returned. + +This runs **before** the `beforeToolCall` hook, allowing the agent to +reject invalid calls without invoking lifecycle callbacks or logging +false `toolExecutionStart` events. + +# Arguments +- `tool::agentTool`: The resolved tool definition +- `prepared::agentToolCall`: The prepared tool call with potentially transformed arguments + +# Returns +- `Dict{String,Any}`: The validated arguments if successful + +# Errors +- Throws `ArgumentError` if validation fails — this is caught by `prepareToolCall` + and converted to an `immediateOutcome` + +# Examples +```julia +# With validateRequiredArgs hook set on the tool +validateToolArguments(toolWithHook, tc) # => validated args or throws + +# With default validation (nothing on tool) +validateToolArguments(toolDefault, tc) # => args or throws +``` +""" +function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{String,Any} + # Use default (2-arg: args + schema) or tool-specific hook (1-arg: args only) + if isnothing(tool.validateRequiredArgs) + result = validateRequiredArgs(prepared.arguments, tool.inputSchema) + else + result = tool.validateRequiredArgs(prepared.arguments) + end + + if result !== nothing + throw(ArgumentError(result)) + end + + return prepared.arguments +end + + -- 2.52.0 From d29a413159f006ea55b7c05ef7d8ead80fe44cde Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 8 Aug 2026 11:09:00 +0700 Subject: [PATCH 50/50] update --- src/tools/README.md | 229 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 207 insertions(+), 22 deletions(-) diff --git a/src/tools/README.md b/src/tools/README.md index d7c6380..7262450 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -155,33 +155,218 @@ end Return `nothing` to pass, or an error `String` to fail. The error is fed back to the LLM so it can retry with corrected arguments. -## Tool Call Lifecycle +## Tool Lifecycle — Framework Internals + +This section traces the full code path from the moment the LLM returns tool calls to the final result being fed back into the conversation. All code references are to `agentCore.jl`. + +### Phase 1: Detect Tool Calls in LLM Response + +After the LLM returns an `assistantMessage`, the loop at `agentCore.jl:220-244` inspects each `content` block: + +```julia +# agentCore.jl:217-244 +has_tool_calls = false +tool_call_list = agentToolCall[] + +for content_block in response.content + if content_block isa Dict + # OpenAI-style: type == "tool_calls" with array of tool calls + if get(content_block, :type, "") == "tool_calls" + for tc_data in get(content_block, :tool_calls, []) + tc = agentToolCall( + type="function", + id=get(tc_data, :id, string(uuid4())), + name=get(tc_data, :function, Dict{String,Any}())[:name], + arguments=get(tc_data, :function, Dict{String,Any}())[:arguments], + ) + push!(tool_call_list, tc) + end + # Alternative style: type == "tool_call" single dict per block + elseif get(content_block, :type, "") == "tool_call" + tc = agentToolCall( + type="function", + id=get(tc_data, :id, string(uuid4())), + name=get(tc_data, :name, ""), + arguments=get(tc_data, :arguments, Dict{String,Any}()), + ) + push!(tool_call_list, tc) + end + end +end +``` + +Each content block with `type == "tool_calls"` or `type == "tool_call"` extracts an `agentToolCall` (id, name, arguments dict) and collects them into a `Vector{agentToolCall}`. + +### Phase 2: Dispatch to Sequential or Parallel Execution + +At `agentCore.jl:247`, the framework checks if any tool calls exist and decides execution mode: + +```julia +# agentCore.jl:247-265 +context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools) +config = agentLoopConfig( + agent._state.tools, + agent.beforeToolCall, + agent.afterToolCall, + agent.parallelToolExecute ? "parallel" : "sequential", +) +batch = executeToolCalls(context, response, tool_call_list, config, signal, emit) +``` + +`executeToolCalls` (`agentCore.jl:988-1015`) checks: +- `config.toolExecution == "sequential"` → sequential mode +- Any tool has `parallelToolExecute == false` → sequential mode +- Otherwise → parallel mode + +### Phase 3: Per-Call Preparation (`prepareToolCall`) + +Each tool call goes through `prepareToolCall` (`agentCore.jl:511-547`): ``` -LLM requests tool call - └── prepareToolCall (agentCore.jl:511) - ├── Tool lookup by name - ├── prepareArguments (tool-specific transform, if defined) - ├── validateToolArguments (validateRequiredArgs hook or default) - │ └── on failure → immediateOutcome (no execution) - ├── beforeToolCall hook (if defined) - │ └── on block → immediateOutcome (no execution) - └── returns preparedToolCall +1. Look up tool by name: find(t -> t.name == tc.name, context.tools) +2. If not found → immediateOutcome("Tool X not found", true) +3. Run tool.prepareArguments (if defined) → transforms raw LLM args +4. Run validateToolArguments → validateRequiredArgs (hook or default) + → if fails → throws ArgumentError → caught below +5. Run beforeToolCall hook (if defined) → can block execution + → if blocked → immediateOutcome("Tool execution was blocked", true) +6. Return preparedToolCall(tool, tc, validatedArgs) +``` -executed by executeToolCallsSequential or executeToolCallsParallel - └── executePreparedToolCall (agentCore.jl:589) - ├── emit toolExecutionStart - ├── call tool.execute() - │ └── on error → executedOutcome(isError=true) - └── returns executedOutcome +If any step throws (validation, prepareArguments, beforeToolCall), the catch block at `agentCore.jl:545` converts it to an `immediateOutcome`: -finalizeExecutedToolCall (agentCore.jl:675) - ├── afterToolCall hook (if defined) - │ └── can mutate result content, usage, terminate, isError - └── returns finalizedOutcome +```julia +catch err + return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true) +end +``` -emit toolExecutionEnd - └── createToolResultMessage → added to conversation history +### Phase 4: Execution (`executePreparedToolCall`) + +For each `preparedToolCall`, `executePreparedToolCall` (`agentCore.jl:589-617`) runs: + +```julia +function executePreparedToolCall(prep::preparedToolCall, signal, emit)::executedOutcome + updateEvents = promise[] + accepting = true + + try + result = prep.tool.execute( + prep.toolCall.id, prep.args, signal, + partialResult -> begin + if accepting + push!(updateEvents, emit(toolExecUpdateEvent(..., partialResult))) + end + end + ) + accepting = false + wait.(updateEvents) + return executedOutcome(result, false) + catch err + accepting = false + wait.(updateEvents) + return executedOutcome(createErrorToolResult(sprint(showerror, err)), true) + end +end +``` + +Key behaviors: +- Calls `tool.execute(id, args, signal, onPartialResult)` — your tool's `executeTool` function +- `signal` can be checked inside `executeTool` for cancellation +- `onPartialResult` is called for streaming updates, which are emitted as `toolExecutionUpdate` events +- `accepting` guard prevents emitting updates after the result is already captured +- `wait.(updateEvents)` ensures all streaming updates are delivered before returning +- Execution errors are caught and returned as `executedOutcome(isError=true)` — never thrown + +### Phase 5: Finalization (`finalizeExecutedToolCall`) + +After execution, `finalizeExecutedToolCall` (`agentCore.jl:675-706`) runs the `afterToolCall` hook: + +```julia +function finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)::finalizedOutcome + result = executed.result + isError = executed.isError + + if config.afterToolCall !== nothing + try + after = config.afterToolCall(afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal) + if after !== nothing + # Hook can mutate: content, details, usage, terminate, isError + result = merge(result, dict(...)) + isError = get(after, :isError, isError) + end + catch err + result = createErrorToolResult(sprint(showerror, err)) + isError = true + end + end + + return finalizedOutcome(prep.toolCall, result, isError) +end +``` + +The hook can: +- Mask sensitive data from result content +- Normalize usage tracking +- Flip `terminate: true` based on business logic +- Wrap errors in friendlier messages for the LLM + +If the hook itself throws, the error is caught and converted to an error outcome. + +### Phase 6: Emit Events and Create Result Message + +Each call emits `toolExecutionEnd`: + +```julia +function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function) + emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, finalized.result, finalized.isError)) +end +``` + +Then creates the `toolResultMessage` for conversation history (`agentCore.jl:373-379`): + +```julia +function createToolResultMessage(f::finalizedOutcome)::toolResultMessage + return toolResultMessage( + "toolResult", f.toolCall.id, f.toolCall.name, + f.result.content, f.result.details, f.result.usage, + get(f.result, :addedToolNames, string[]), f.isError, nowMillis() + ) +end +``` + +### Phase 7: Batch Assembly and Loop Control + +In `executeToolCallsSequential` (`agentCore.jl:795-829`) or `executeToolCallsParallel` (`agentCore.jl:888-936`), all results are collected: + +```julia +messages = toolResultMessage[] +for finalized in finalizedCalls + push!(messages, createToolResultMessage(finalized)) +end +return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) +``` + +`shouldTerminate` (`agentCore.jl:409`) returns `true` only if ALL tools in the batch set `result.terminate == true`. If `false`, the agent loop at `agentCore.jl:176-308` feeds the tool results back to the LLM for another turn. + +### Data Flow Summary + +``` +response.content (Vector{Any}) + └── phase 1: parse content blocks + └── tool_call_list :: Vector{agentToolCall} + └── phase 2: dispatch to sequential/parallel + └── phase 3: prepareToolCall + └── preparedToolCall or immediateOutcome + └── phase 4: executePreparedToolCall + └── executedOutcome + └── phase 5: finalizeExecutedToolCall + └── finalizedOutcome + └── phase 6: createToolResultMessage + └── toolResultMessage + └── phase 7: agentToolCallBatch + └── pushed to agent._state.messages + └── loop back to LLM ``` ## Execution Modes -- 2.52.0