mcp works
This commit is contained in:
+109
-101
@@ -187,13 +187,13 @@ function yiemAgent(
|
|||||||
followUp = Channel(32)
|
followUp = Channel(32)
|
||||||
outputChannel = Channel(16)
|
outputChannel = Channel(16)
|
||||||
|
|
||||||
# load tools (statically registered at module init)
|
|
||||||
toolStore1 = toolStore(name="myagent")
|
tools = OrderedDict{String, agentTool}()
|
||||||
registerAllTools(toolStore1, mcpServer; eventSink=eventSink)
|
registerAllTools(tools, mcpServer; eventSink=eventSink)
|
||||||
|
|
||||||
# Create struct with a placeholder task, then spawn and replace it
|
# Create struct with a placeholder task, then spawn and replace it
|
||||||
agent = yiemAgent(
|
agent = yiemAgent(
|
||||||
agentState(systemPrompt, model, getTools(toolStore1), messages),
|
agentState(systemPrompt, model, tools, messages),
|
||||||
inputChannel,
|
inputChannel,
|
||||||
followUp,
|
followUp,
|
||||||
outputChannel,
|
outputChannel,
|
||||||
@@ -442,112 +442,123 @@ function _processMessage(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
while true
|
while true
|
||||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
try
|
||||||
while isready(inputChannel)
|
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||||
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
while isready(inputChannel)
|
||||||
newUserMsg_openai = take!(inputChannel)
|
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||||
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
newUserMsg_openai = take!(inputChannel)
|
||||||
if newUserMsg_openai === :shutdown
|
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||||
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
if newUserMsg_openai === :shutdown
|
||||||
# Re-emit shutdown signal for the loop to handle
|
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
||||||
put!(inputChannel, :shutdown)
|
# Re-emit shutdown signal for the loop to handle
|
||||||
break
|
put!(inputChannel, :shutdown)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
||||||
|
push!(agentMsgHistory, newUserMsg)
|
||||||
|
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||||
end
|
end
|
||||||
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||||
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
# call prepareContext()
|
||||||
push!(agentMsgHistory, newUserMsg)
|
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||||
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||||
end
|
preparedContext = prepareContext(state, eventSink)
|
||||||
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||||
# call prepareContext()
|
# Call formatMessagesForLLM() to format for LLM
|
||||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
|
||||||
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
preparedContext = prepareContext(state, eventSink, llmCall)
|
eventSink("_processMessage 10 formattedMessages $formattedMessages")
|
||||||
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
# Call formatMessagesForLLM() to format for LLM
|
|
||||||
formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
|
|
||||||
|
|
||||||
eventSink("_processMessage 10 formattedMessages $formattedMessages")
|
|
||||||
|
|
||||||
""" response example
|
""" 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")]))
|
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")]))
|
||||||
"""
|
"""
|
||||||
response = llmCall(formattedMessages)
|
response = llmCall(formattedMessages)
|
||||||
eventSink(" llmCall " * string(response))
|
eventSink("LLM response " * string(response))
|
||||||
|
|
||||||
eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
|
||||||
# Extract tool calls from LLM response content blocks
|
# Extract tool calls from LLM response content blocks
|
||||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||||
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
||||||
eventSink("assistant_msg " * string(assistant_msg))
|
eventSink("assistant_msg " * string(assistant_msg))
|
||||||
eventSink("_processMessage 11-1")
|
eventSink("_processMessage 11-1")
|
||||||
|
|
||||||
# Add assistant message (tool calls or text) to history for next LLM turn
|
# Add assistant message (tool calls or text) to history for next LLM turn
|
||||||
push!(agentMsgHistory, assistant_msg)
|
push!(agentMsgHistory, assistant_msg)
|
||||||
|
|
||||||
if hasToolCalls && length(toolCallList) > 0
|
if hasToolCalls && length(toolCallList) > 0
|
||||||
# Build context and config for executeToolCalls
|
# Build context and config for executeToolCalls
|
||||||
|
|
||||||
config = agentLoopConfig(
|
config = agentLoopConfig(
|
||||||
beforeToolCall,
|
beforeToolCall,
|
||||||
afterToolCall,
|
afterToolCall,
|
||||||
parallelToolExecute ? "parallel" : "sequential",
|
parallelToolExecute ? "parallel" : "sequential",
|
||||||
llmCall,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
signal = abortSignal(false)
|
signal = abortSignal(false)
|
||||||
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
|
||||||
# call executeToolCalls()
|
# call executeToolCalls()
|
||||||
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||||
signal, eventSink)
|
signal, eventSink)
|
||||||
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
|
||||||
# save toolResults to messages
|
# save toolResults to messages
|
||||||
for toolResult in toolResultBatch.messages
|
|
||||||
eventSink("toolResult " * string(toolResult))
|
|
||||||
push!(agentMsgHistory, toolResult)
|
|
||||||
end
|
|
||||||
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
if toolResultBatch.terminate
|
|
||||||
eventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
|
||||||
# If toolResultBatch requested termination, build a final response
|
|
||||||
final_content = [textContent("Tool execution completed.")]
|
|
||||||
for toolResult in toolResultBatch.messages
|
for toolResult in toolResultBatch.messages
|
||||||
for content_block in toolResult.content
|
eventSink("toolResult " * string(toolResult))
|
||||||
if content_block isa textContent
|
push!(agentMsgHistory, toolResult)
|
||||||
append!(final_content, [content_block])
|
end
|
||||||
elseif content_block isa Dict
|
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||||
if haskey(content_block, :text)
|
if toolResultBatch.terminate
|
||||||
push!(final_content, textContent(content_block[:text]))
|
eventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
# If toolResultBatch requested termination, build a final response
|
||||||
|
final_content = [textContent("Tool execution completed.")]
|
||||||
|
for toolResult in toolResultBatch.messages
|
||||||
|
for content_block in toolResult.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
|
end
|
||||||
end
|
end
|
||||||
|
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||||
|
final_response = assistantMessage(
|
||||||
|
role="assistant",
|
||||||
|
content=final_content,
|
||||||
|
api=assistant_msg.api,
|
||||||
|
model=assistant_msg.model,
|
||||||
|
usage=assistant_msg.usage,
|
||||||
|
stopReason="tool_use_terminated",
|
||||||
|
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
|
||||||
|
"One or more tool calls failed"
|
||||||
|
else
|
||||||
|
nothing
|
||||||
|
end,
|
||||||
|
timestamp=now(),
|
||||||
|
)
|
||||||
|
push!(agentMsgHistory, final_response)
|
||||||
|
break
|
||||||
end
|
end
|
||||||
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
else
|
||||||
final_response = assistantMessage(
|
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||||
role="assistant",
|
# LLM did not use tool calls —
|
||||||
content=final_content,
|
|
||||||
api=assistant_msg.api,
|
|
||||||
model=assistant_msg.model,
|
|
||||||
usage=assistant_msg.usage,
|
|
||||||
stopReason="tool_use_terminated",
|
|
||||||
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
|
|
||||||
"One or more tool calls failed"
|
|
||||||
else
|
|
||||||
nothing
|
|
||||||
end,
|
|
||||||
timestamp=now(),
|
|
||||||
)
|
|
||||||
push!(agentMsgHistory, final_response)
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
else
|
catch e
|
||||||
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
bt = catch_backtrace()
|
||||||
# LLM did not use tool calls —
|
err_msg = sprint() do io
|
||||||
break
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
eventSink(err_msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
@@ -1087,14 +1098,13 @@ function executePreparedToolCall(
|
|||||||
prep::preparedToolCall,
|
prep::preparedToolCall,
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
eventSink,
|
eventSink,
|
||||||
llmCall::Union{Any,Nothing}=nothing,
|
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
eventSink("executePreparedToolCall 1")
|
eventSink("executePreparedToolCall 1")
|
||||||
|
|
||||||
try #WORKING
|
try #WORKING
|
||||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink, llmCall)
|
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink)
|
||||||
eventSink("executePreparedToolCall 2")
|
eventSink("executePreparedToolCall 2")
|
||||||
eventSink(result.content[1].text)
|
eventSink("tool execute raw result: " * sprint(show, result))
|
||||||
eventSink("executePreparedToolCall 3")
|
eventSink("executePreparedToolCall 3")
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
catch e
|
catch e
|
||||||
@@ -1271,7 +1281,6 @@ function executeToolCallsSequential(
|
|||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
eventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
llmCall = config.llmCall
|
|
||||||
eventSink("executeToolCallsSequential 1")
|
eventSink("executeToolCallsSequential 1")
|
||||||
finalizedCalls = finalizedOutcome[]
|
finalizedCalls = finalizedOutcome[]
|
||||||
messages = toolResultMessage[]
|
messages = toolResultMessage[]
|
||||||
@@ -1287,9 +1296,10 @@ function executeToolCallsSequential(
|
|||||||
eventSink("executeToolCallsSequential 2-2")
|
eventSink("executeToolCallsSequential 2-2")
|
||||||
else
|
else
|
||||||
eventSink("executeToolCallsSequential 3")
|
eventSink("executeToolCallsSequential 3")
|
||||||
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
|
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||||
|
|
||||||
eventSink("executeToolCallsSequential 3-1")
|
eventSink("executeToolCallsSequential 3-1")
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||||
signal, eventSink)
|
signal, eventSink)
|
||||||
eventSink("executeToolCallsSequential 3-2")
|
eventSink("executeToolCallsSequential 3-2")
|
||||||
end
|
end
|
||||||
@@ -1374,7 +1384,6 @@ function executeToolCallsParallel(
|
|||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
entries = Union{finalizedOutcome,Task}[]
|
entries = Union{finalizedOutcome,Task}[]
|
||||||
llmCall = config.llmCall
|
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||||
@@ -1388,7 +1397,7 @@ function executeToolCallsParallel(
|
|||||||
push!(entries, finalized)
|
push!(entries, finalized)
|
||||||
else
|
else
|
||||||
t = Task() do
|
t = Task() do
|
||||||
executed = executePreparedToolCall(prep, signal, eventSink, llmCall)
|
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
eventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||||
finalized.result, finalized.isError))
|
finalized.result, finalized.isError))
|
||||||
@@ -1476,7 +1485,6 @@ function executeToolCalls(
|
|||||||
eventSink,
|
eventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
llmCall = config.llmCall
|
|
||||||
eventSink("_executeToolCalls 1")
|
eventSink("_executeToolCalls 1")
|
||||||
hasSequential = false
|
hasSequential = false
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
|
|||||||
+6
-6
@@ -18,7 +18,7 @@ The agent processes messages from `inputChannel` in the background task.
|
|||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `agent::yiemAgent`: The agent instance to send a message to
|
- `agent::yiemAgent`: The agent instance to send a message to
|
||||||
- `msg`: The message to send (any type accepted by the agent's processing pipeline)
|
- `msg`: The message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Hello!")`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
@@ -29,11 +29,11 @@ The agent processes messages from `inputChannel` in the background task.
|
|||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> runAgent(agent, "Hello!")
|
julia> runAgent(agent, Dict("role" => "user", "content" => "Hello!"))
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function runAgent(agent::yiemAgent, msg)
|
function runAgent(agent::yiemAgent, msg::AbstractDict{String, Any})
|
||||||
put!(agent.inputChannel, msg)
|
put!(agent.inputChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
@@ -70,7 +70,7 @@ and before any tool call results are sent.
|
|||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `agent::yiemAgent`: The agent instance to send a follow-up message to
|
- `agent::yiemAgent`: The agent instance to send a follow-up message to
|
||||||
- `msg`: The follow-up message to send
|
- `msg`: The follow-up message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Also consider red wines")`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
@@ -82,11 +82,11 @@ and before any tool call results are sent.
|
|||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> followUp(agent, "Also consider red wines")
|
julia> followUp(agent, Dict("role" => "user", "content" => "Also consider red wines"))
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function followUp(agent::yiemAgent, msg)
|
function followUp(agent::yiemAgent, msg::AbstractDict{String, Any})
|
||||||
put!(agent.followUpChannel, msg)
|
put!(agent.followUpChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
|
|||||||
+40
-82
@@ -1,36 +1,12 @@
|
|||||||
module toolRegistry
|
module toolRegistry
|
||||||
|
|
||||||
export toolStore, registerTool, registerAllTools, getTools, clearTools, listTool
|
export registerTool, registerAllTools, clearTools, listTool
|
||||||
|
|
||||||
using Dates
|
using Dates
|
||||||
using JSON, DataStructures
|
using JSON, DataStructures
|
||||||
using ..type
|
using ..type
|
||||||
|
|
||||||
"""
|
|
||||||
Per-agent isolated tool storage.
|
|
||||||
|
|
||||||
Each agent gets its own `toolStore` so tool registration is independent.
|
|
||||||
|
|
||||||
# Fields
|
|
||||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
|
||||||
- `name::String` — identifier for debugging/logs
|
|
||||||
"""
|
|
||||||
struct toolStore
|
|
||||||
tools::OrderedDict{String, agentTool}
|
|
||||||
name::String
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
toolStore(; name="default") -> toolStore
|
|
||||||
|
|
||||||
Create a new empty tool store.
|
|
||||||
|
|
||||||
# Keyword Arguments
|
|
||||||
- `name::String`: Display name for logging (default: `"default"`)
|
|
||||||
"""
|
|
||||||
function toolStore(; name::String="default")::toolStore
|
|
||||||
toolStore(OrderedDict{String, agentTool}(), name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# ── MCP helper functions ────────────────────────────────────────────
|
# ── MCP helper functions ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -40,7 +16,7 @@ Extract text from MCP tool result content array.
|
|||||||
Handles JSON-RPC 2.0 result content format:
|
Handles JSON-RPC 2.0 result content format:
|
||||||
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
||||||
"""
|
"""
|
||||||
function _extract_text_content(result::Dict)::String
|
function _extract_text_content(result::AbstractDict)::String
|
||||||
content = get(result, "content", Any[])
|
content = get(result, "content", Any[])
|
||||||
if content isa Vector && !isempty(content)
|
if content isa Vector && !isempty(content)
|
||||||
lines = String[]
|
lines = String[]
|
||||||
@@ -90,8 +66,7 @@ function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSin
|
|||||||
inputSchema=params,
|
inputSchema=params,
|
||||||
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
eventSink,
|
eventSink) -> begin
|
||||||
llmCall=nothing) -> begin
|
|
||||||
try
|
try
|
||||||
response = mcpserver("tools/call", name, args)
|
response = mcpserver("tools/call", name, args)
|
||||||
|
|
||||||
@@ -140,7 +115,7 @@ Handles pagination via `nextCursor`. Skips tools already registered.
|
|||||||
# Returns
|
# Returns
|
||||||
- `Int`: number of new tools registered
|
- `Int`: number of new tools registered
|
||||||
"""
|
"""
|
||||||
function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing)::Int
|
||||||
if mcpserver === nothing
|
if mcpserver === nothing
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
@@ -148,13 +123,14 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
|||||||
new_count = 0
|
new_count = 0
|
||||||
|
|
||||||
try
|
try
|
||||||
|
eventSink("register_mcp_tools 1")
|
||||||
response = mcpserver("tools/list")
|
response = mcpserver("tools/list")
|
||||||
|
eventSink("register_mcp_tools 2")
|
||||||
# Parse JSON-RPC 2.0 response envelope
|
# Parse JSON-RPC 2.0 response envelope
|
||||||
if haskey(response, "error")
|
if haskey(response, "error")
|
||||||
rpc_error = response["error"]
|
rpc_error = response["error"]
|
||||||
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
||||||
println("[toolRegistry:$(store.name)] MCP tools/list failed: $err_msg")
|
println("[toolRegistry] MCP tools/list failed: $err_msg")
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
if haskey(response, "result")
|
if haskey(response, "result")
|
||||||
@@ -166,12 +142,12 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
|||||||
|
|
||||||
for tool_def in tools_array
|
for tool_def in tools_array
|
||||||
name = tool_def["name"]
|
name = tool_def["name"]
|
||||||
if haskey(store.tools, name)
|
if haskey(tools, name)
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
@show "tool_def $(typeof(tool_def))"
|
|
||||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
||||||
store.tools[name] = wrapped
|
tools[name] = wrapped
|
||||||
new_count += 1
|
new_count += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -185,16 +161,16 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
|||||||
cursor = get(response, "nextCursor", nothing)
|
cursor = get(response, "nextCursor", nothing)
|
||||||
for tool_def in tools_array
|
for tool_def in tools_array
|
||||||
name = tool_def["name"]
|
name = tool_def["name"]
|
||||||
if haskey(store.tools, name)
|
if haskey(tools, name)
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
|
||||||
store.tools[name] = wrapped
|
tools[name] = wrapped
|
||||||
new_count += 1
|
new_count += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
println("[toolRegistry:$(store.name)] Discovered $new_count MCP tools. Total: $(length(store.tools))")
|
println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(tools))")
|
||||||
catch e
|
catch e
|
||||||
bt = catch_backtrace()
|
bt = catch_backtrace()
|
||||||
err_msg = sprint() do io
|
err_msg = sprint() do io
|
||||||
@@ -205,75 +181,60 @@ function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
|
|||||||
eventSink(err_msg)
|
eventSink(err_msg)
|
||||||
|
|
||||||
errMsg = sprint(showerror, e)
|
errMsg = sprint(showerror, e)
|
||||||
println("[toolRegistry:$(store.name)] MCP tools/list failed: $errMsg")
|
println("[toolRegistry] MCP tools/list failed: $errMsg")
|
||||||
end
|
end
|
||||||
|
|
||||||
return new_count
|
return new_count
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||||
|
|
||||||
Add `tool` to `store`, overwriting any existing tool with the same name.
|
Add `tool` to `tools`, overwriting any existing tool with the same name.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store`: Tool store to modify
|
- `tools`: Tool dict to modify
|
||||||
- `tool`: The `agentTool` to register
|
- `tool`: The `agentTool` to register
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- The same `store.tools` dict (modified in place)
|
- The same `tools` dict (modified in place)
|
||||||
"""
|
"""
|
||||||
function registerTool(store::toolStore, tool::agentTool; eventSink=nothing
|
function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
|
||||||
)::OrderedDict{String, agentTool}
|
)::OrderedDict{String, agentTool}
|
||||||
store.tools[tool.name] = tool
|
tools[tool.name] = tool
|
||||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
println("[toolRegistry] Registered tool: $(tool.name)")
|
||||||
return store.tools
|
return tools
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Return the tools registered in `store`.
|
Remove all tools from the dict.
|
||||||
|
|
||||||
The returned dict is the **same object** stored inside `store`.
|
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store`: Tool store to query
|
- `tools`: Tool dict to clear
|
||||||
|
|
||||||
# Returns
|
|
||||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
|
||||||
"""
|
|
||||||
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
|
||||||
return store.tools
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Remove all tools from `store`.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `store`: Tool store to clear
|
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- `nothing`
|
- `nothing`
|
||||||
"""
|
"""
|
||||||
function clearTools(store::toolStore)::Nothing
|
function clearTools(tools::OrderedDict{String, agentTool})::Nothing
|
||||||
empty!(store.tools)
|
empty!(tools)
|
||||||
println("[$(store.name)] Registry cleared")
|
println("[toolRegistry] Registry cleared")
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
|
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
|
||||||
|
|
||||||
"""
|
"""
|
||||||
listTool(store::toolStore, mcpserver) -> agentTool
|
listTool(tools::OrderedDict{String, agentTool}, mcpserver) -> agentTool
|
||||||
|
|
||||||
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
|
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
|
||||||
|
|
||||||
First call: queries the MCP server via `mcpserver("tools/list")`, registers
|
First call: queries the MCP server via `mcpserver("tools/list")`, registers
|
||||||
all discovered tools into the shared `store.tools` (in-place mutation, with
|
all discovered tools into `tools` (in-place mutation, with
|
||||||
pagination via nextCursor), then returns the full tool list.
|
pagination via nextCursor), then returns the full tool list.
|
||||||
|
|
||||||
Subsequent calls: returns the current list (tools remain registered).
|
Subsequent calls: returns the current list (tools remain registered).
|
||||||
"""
|
"""
|
||||||
function listTool(store::toolStore, mcpserver)::agentTool
|
function listTool(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing)::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name="listTools",
|
name="listTools",
|
||||||
label="List Tools",
|
label="List Tools",
|
||||||
@@ -285,16 +246,17 @@ function listTool(store::toolStore, mcpserver)::agentTool
|
|||||||
),
|
),
|
||||||
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
eventSink, llmCall=nothing) -> begin
|
eventSink) -> begin
|
||||||
# Discover and register MCP tools (idempotent — skips already registered)
|
# Discover and register MCP tools (idempotent — skips already registered)
|
||||||
new_count = register_mcp_tools(mcpserver, store)
|
new_count = register_mcp_tools(tools, mcpserver; eventSink=eventSink)
|
||||||
|
eventSink("tools dump: " * sprint(show, tools))
|
||||||
|
|
||||||
# Always include listTools itself in the count
|
# Always include listTools itself in the count
|
||||||
total = length(store.tools)
|
total = length(tools)
|
||||||
|
|
||||||
lines = String[
|
lines = String[
|
||||||
"- $(t.name): $(t.label) — $(t.description)"
|
"- $(t.name): $(t.label) — $(t.description)"
|
||||||
for (k, t) in store.tools
|
for (k, t) in tools
|
||||||
]
|
]
|
||||||
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
||||||
|
|
||||||
@@ -313,23 +275,19 @@ end
|
|||||||
# ── High-level API ──────────────────────────────────────────────────
|
# ── High-level API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
"""
|
"""
|
||||||
registerAllTools(store::toolStore, mcpserver) -> toolStore
|
registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver)
|
||||||
|
|
||||||
Register all tools for an agent:
|
Register all tools for an agent:
|
||||||
1. Auto-discover existing tools from the MCP server
|
1. Auto-discover existing tools from the MCP server
|
||||||
2. Register the `listTools` tool so the agent can discover new tools at runtime
|
2. Register the `listTools` tool so the agent can discover new tools at runtime
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store`: The tool store to populate
|
- `tools`: The tool dict to populate
|
||||||
- `mcpserver`: A callable struct that communicates with the MCP server
|
- `mcpserver`: A callable struct that communicates with the MCP server
|
||||||
|
|
||||||
# Returns
|
|
||||||
- The populated `toolStore`
|
|
||||||
"""
|
"""
|
||||||
function registerAllTools(store::toolStore, mcpserver=nothing; eventSink=nothing)::toolStore
|
function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing)
|
||||||
register_mcp_tools(mcpserver, store; eventSink=eventSink)
|
list_t = listTool(tools, mcpserver; eventSink=eventSink)
|
||||||
registerTool(store, listTool(store, mcpserver); eventSink=eventSink)
|
registerTool(tools, list_t; eventSink=eventSink)
|
||||||
return store
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end # module
|
end # module
|
||||||
|
|||||||
+2
-4
@@ -358,7 +358,6 @@ struct agentContext # Snapshot of the agent's conversa
|
|||||||
systemPrompt::String # System prompt for the agent
|
systemPrompt::String # System prompt for the agent
|
||||||
messages::Vector{agentMessage} # Conversation messages
|
messages::Vector{agentMessage} # Conversation messages
|
||||||
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||||
llmCall::Union{Any, Nothing} # LLM call function (for tools that need it)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -408,7 +407,7 @@ function agentState(
|
|||||||
agentState(
|
agentState(
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
model,
|
model,
|
||||||
deepcopy(tools),
|
tools,
|
||||||
deepcopy(messages),
|
deepcopy(messages),
|
||||||
Vector{String}(),
|
Vector{String}(),
|
||||||
nothing,
|
nothing,
|
||||||
@@ -443,7 +442,7 @@ end
|
|||||||
"""
|
"""
|
||||||
Configuration for the agent tool execution loop.
|
Configuration for the agent tool execution loop.
|
||||||
|
|
||||||
# Arguments
|
# Fields
|
||||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||||
@@ -452,7 +451,6 @@ struct agentLoopConfig
|
|||||||
beforeToolCall::Union{Function, Nothing}
|
beforeToolCall::Union{Function, Nothing}
|
||||||
afterToolCall::Union{Function, Nothing}
|
afterToolCall::Union{Function, Nothing}
|
||||||
toolExecution::String
|
toolExecution::String
|
||||||
llmCall::Union{Any, Nothing} # LLM call function (for tools like searchWine)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
+4
-4
@@ -109,18 +109,18 @@ prepareContext(state).messages == deepcopy(state.messages)
|
|||||||
# end
|
# end
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function prepareContext(state::agentState, eventSink, llmCall=nothing)::agentContext
|
function prepareContext(state::agentState, eventSink)::agentContext
|
||||||
|
|
||||||
#TODO filter tools from state.tools based on user intend in user message and tool description
|
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||||
filteredTools = state.tools
|
filteredTools = state.tools
|
||||||
|
|
||||||
#TODO add filtered tools to the current system prompt / modify systemPrompt here
|
#TODO add filtered tools to the current system prompt / modify systemPrompt here
|
||||||
preparedSystemPrompt = state.systemPrompt
|
preparedSystemPrompt = state.systemPrompt
|
||||||
|
|
||||||
#TODO add system prompt, adjust/modify and inject additional context into messages
|
#TODO add system prompt, adjust/modify and inject additional context into messages
|
||||||
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
|
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
|
||||||
|
|
||||||
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools, llmCall)
|
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
|
||||||
|
|
||||||
return agentCtx
|
return agentCtx
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user