V0.8.0 async think loop #40
+2
-2
@@ -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
|
||||
|
||||
+102
-84
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+175
-39
@@ -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
|
||||
|
||||
-505
@@ -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
|
||||
+179
@@ -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
|
||||
Reference in New Issue
Block a user