464 lines
16 KiB
Julia
464 lines
16 KiB
Julia
module type
|
|
export agent, sommelier, companion, virtualcustomer, 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
|
|
inputTokens::Int64
|
|
outputTokens::Int64
|
|
end
|
|
|
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
|
|
|
|
|
# ============================================================================
|
|
# Message types
|
|
# ============================================================================
|
|
abstract type agentMessage end # Base type for all agent messages
|
|
|
|
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
|
|
|
|
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
|
|
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
|
|
|
|
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
|
|
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
|
|
|
|
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
|
|
# ============================================================================
|
|
|
|
abstract type messageContent end # Base type for message content
|
|
|
|
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
|
|
# ============================================================================
|
|
|
|
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
|
|
|
|
|
|
# ============================================================================
|
|
# Agent context
|
|
# ============================================================================
|
|
|
|
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
|
|
|
|
|
|
# ============================================================================
|
|
# 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
|
|
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
|
|
errorMessage::Union{String, Nothing} # Last error message
|
|
end
|
|
|
|
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,
|
|
)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Tool call types
|
|
# ============================================================================
|
|
|
|
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
|
|
end
|
|
|
|
|
|
# ============================================================================
|
|
# Next turn context
|
|
# ============================================================================
|
|
|
|
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
|
|
# ============================================================================
|
|
|
|
abstract type agent end
|
|
|
|
"""
|
|
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.
|
|
# 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
|
|
|
|
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
|
|
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
|
|
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
|
|
|
|
"""
|
|
docstring
|
|
"""
|
|
function yiemAgent(
|
|
; systemPrompt::String="",
|
|
model=nothing,
|
|
tools::Vector{agentTool}=agentTool[],
|
|
messages::Vector{agentMessage}=agentMessage[],
|
|
formatMsgForLLM::Function=defaultformatMsgForLLM,
|
|
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=nothing,
|
|
)
|
|
# 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),
|
|
input_ch,
|
|
followUp,
|
|
output_ch,
|
|
nothing, # placeholder — replaced below
|
|
formatMsgForLLM,
|
|
preprocessMessages,
|
|
beforeToolCall,
|
|
afterToolCall,
|
|
prepareNextTurn,
|
|
prepareNextTurnWithContext,
|
|
sessionId,
|
|
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) #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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end # module type
|