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