diff --git a/src/agentCore.jl b/src/agentCore.jl index e438904..3d3d357 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -3,7 +3,7 @@ module agentCore export yiemAgent, _agentLoop, OpenAiToUserMessage using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, - DataFrames, Base.Threads + DataFrames, Base.Threads, NATS using GeneralUtils using ..type, ..utils, ..toolRegistry @@ -342,20 +342,23 @@ function _processMessage( agentEventSink("_processMessage 1") # loop until llmCall() response didn't use tool calls final_response = nothing + + """ example message in inputChannel + 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:mime_type;base64,image2_base64_string") + ), + ] + ) + """ + while true - """ example message in inputChannel - 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:mime_type;base64,image2_base64_string") - ), - ] - ) - """ + # Drain inputChannel and convert OpenAI-format messages to userMessage type while isready(inputChannel) @@ -377,22 +380,26 @@ function _processMessage( # call prepareContext() state = agentState(systemPrompt, nothing, tools, messages) agentEventSink("_processMessage 8") - preparedContext = prepareContext(state) + preparedContext = prepareContext(state, agentEventSink) agentEventSink("_processMessage 8") # Call formatMessagesForLLM() to format for LLM - formattedMessages = formatMessagesForLLM(preparedContext) + formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink) agentEventSink("_processMessage 10") # Call llmCall() (blocking — the task waits here) response = llmCall(formattedMessages) - agentEventSink(response) + + """ response example + response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")])) + """ + + agentEventSink(string(response)) agentEventSink("_processMessage 11") - error("debug marker") - # Check if LLM used tool calls (inspect content for tool_call blocks) + #WORKING Check if LLM used tool calls (inspect content for tool_call blocks) hasToolCalls = false toolCallList = agentToolCall[] - for content_block in response.content + for content_block in response.content # extract response if content_block isa Dict if get(content_block, :type, "") == "tool_calls" hasToolCalls = true diff --git a/src/type.jl b/src/type.jl index 0db7744..9b8d9bd 100644 --- a/src/type.jl +++ b/src/type.jl @@ -291,7 +291,7 @@ Snapshot of the agent's conversation context. # Arguments - `systemPrompt::String`: System prompt for the agent - `messages::Vector{agentMessage}`: Conversation messages -- `tools::Union{Dict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup +- `tools::Union{OrderedDict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup # Returns - A new `agentContext` instance @@ -299,7 +299,7 @@ Snapshot of the agent's conversation context. struct agentContext # Snapshot of the agent's conversation context systemPrompt::String # System prompt for the agent messages::Vector{agentMessage} # Conversation messages - tools::Union{Dict{String, agentTool}, Nothing} # Available tools keyed by name + tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name end diff --git a/src/utils.jl b/src/utils.jl index 67c4a1b..18e5147 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -2,10 +2,10 @@ module utils export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI, - _assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, + _assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI, beforeToolCall, afterToolCall, agentEventSink -using UUIDs, Dates, DataStructures, HTTP, JSON +using UUIDs, Dates, DataStructures, HTTP, JSON, NATS using GeneralUtils using ..type @@ -75,7 +75,6 @@ function availableWineToText(vecd::Vector)::String end - """ prepareContext(state::agentState) -> agentContext @@ -110,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages) # end ``` """ -function prepareContext(state::agentState)::agentContext +function prepareContext(state::agentState, agentEventSink)::agentContext #TODO filter tools from state.tools based on user intend in user message and tool description filteredTools = state.tools @@ -157,7 +156,7 @@ formatMsgForLLm(ctx) == Dict("messages" => [ ]) ``` """ -function formatMsgForLLM(ctx::agentContext)::Dict{String, Any} +function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any} """ openai message format example msg = Dict( @@ -185,18 +184,12 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any} Dict("type" => "text", "text" => "let me check."), ] ), - Dict( - "role" => "toolResult", - "content" => [ - Dict("type" => "text", "text" => "name: Chateau Montelena ..."), - ] - ), ], "tools"=> [ Dict( "type" => "function", "function" => Dict( - "name" => "get_weather", + "name" => "getWeather", "description" => "Get current weather", "parameters" => Dict( "type" => "object", @@ -212,8 +205,10 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any} ) """ + openaiReadyMsg = Dict{String, Any}() + # openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL" messages = Vector{Dict{String, Any}}() - + agentEventSink("formatMsgForLLM 1") # System prompt as system message if !isempty(ctx.systemPrompt) push!(messages, Dict( @@ -221,7 +216,7 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any} "content" => [Dict("type" => "text", "text" => ctx.systemPrompt)] )) end - + agentEventSink("formatMsgForLLM 2") # Conversation messages for msg in ctx.messages if msg isa userMessage @@ -232,14 +227,18 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any} push!(messages, _toolResultMessageToOpenAI(msg)) end end + agentEventSink("formatMsgForLLM 3") + # Convert ctx.tools into OpenAI tools format + tools_array = _toolsToOpenAI(ctx.tools, agentEventSink) + agentEventSink("formatMsgForLLM 4") + openaiReadyMsg["messages"] = messages + openaiReadyMsg["temperature"] = 0.7 - #WORKING convert ctx.tools into openai's tools format. - # ctx.tools has the following format - # tools = OrderedDict{String, YiemAgent.type.agentTool}("getTime" => YiemAgent.type.agentTool("getTime", "Time Lookup", "Get current local time for a timezone or city.", Dict{String, Any}("properties" => Dict("city" => Dict("type" => "string", "description" => "City name as fallback"), "timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'")), "required" => Any[], "type" => "object"), YiemAgent.toolRegistry._tool_getTime.executeTool, nothing, YiemAgent.toolRegistry._tool_getTime.validateRequiredArgs, false), "getWeather" => YiemAgent.type.agentTool("getWeather", "Weather Lookup", "Fetch current weather and forecast for a given city.", Dict{String, Any}("properties" => Dict{String, Dict{String}}("units" => Dict{String, Any}("default" => "celsius", "type" => "string", "description" => "Temperature scale", "enum" => ["celsius", "fahrenheit"]), "city" => Dict("type" => "string", "description" => "City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'")), "required" => ["city"], "type" => "object"), YiemAgent.toolRegistry._tool_getWeather.executeTool, nothing, nothing, false), "listTools" => YiemAgent.type.agentTool("listTools", "List Tools", "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.", Dict{String, Any}("properties" => Dict{String, Any}(), "required" => Any[], "type" => "object"), YiemAgent.toolRegistry.var"#listTool##0#listTool##1"{YiemAgent.toolRegistry.toolStore}(YiemAgent.toolRegistry.toolStore(OrderedDict{String, YiemAgent.type.agentTool}(#= circular reference @-4 =#), "myagent")), nothing, nothing, false)) - + if !isempty(tools_array) + openaiReadyMsg["tools"] = tools_array + end - - return Dict("messages" => messages) + return openaiReadyMsg end #TODO @@ -331,6 +330,40 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{ end +""" + _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}) -> Vector{Dict{String, Any}} + +Convert an OrderedDict of agentTool definitions into OpenAI function tool format. + +Returns an empty vector when `tools` is `nothing` or empty. + +# Examples +```julia +_toolsToOpenAI(nothing) # => Dict{String, Any}[] +_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))] +``` +""" +function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, agentEventSink)::Vector{Dict{String, Any}} + tools_array = Vector{Dict{String, Any}}() + agentEventSink("_toolsToOpenAI 1") + agentEventSink(string(typeof(tools))) + if tools !== nothing + for (_, tool) in tools + push!(tools_array, Dict( + "type" => "function", + "function" => Dict( + "name" => tool.name, + "description" => tool.description, + "parameters" => tool.inputSchema + ) + )) + end + end + agentEventSink("_toolsToOpenAI 2") + return tools_array +end + + """ validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String}