module utils export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI, _assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks 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) -> agentContext 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`. 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, system prompt, tools, and other configuration # Returns - `agentContext`: An `agentContext` containing the prepared system prompt, messages, and tools to be sent to the LLM # Examples ```julia # Default: returns an agentContext with deep copies of system prompt, messages, and tools prepareContext(state).messages == deepcopy(state.messages) # Override to filter tools and inject system context: # function prepareContext(state::agentState) # msgs = deepcopy(state.messages) # sysPrompt = state.systemPrompt * "\\nCurrent time: $(now())" # tools = filter(t -> contains(t.description, "wine"), state.tools) # return agentContext(sysPrompt, msgs, tools) # end ``` """ function prepareContext(state::agentState)::agentContext #TODO filter tools from state.tools based on user intend in user message and tool description filteredTools = state.tools #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 """ 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", "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" => data_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 ) """ messages = Vector{Dict{String, Any}}() # 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 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 """ 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 end # module util