435 lines
11 KiB
Julia
435 lines
11 KiB
Julia
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 filtered tools to the current system prompt / modify systemPrompt here
|
|
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 |