Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ffb720f86 | |||
| 5829c82d05 | |||
| c7a98f1710 |
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
julia_version = "1.12.6"
|
julia_version = "1.12.6"
|
||||||
manifest_format = "2.0"
|
manifest_format = "2.0"
|
||||||
project_hash = "aa163e2bf572632825162936e107be18384fd40f"
|
project_hash = "3ff1783eadf40ccb51801954aa0a8df935689752"
|
||||||
|
|
||||||
[[deps.Accessors]]
|
[[deps.Accessors]]
|
||||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
|||||||
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||||
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241"
|
|
||||||
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
|
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
|
||||||
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
|
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
|
||||||
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
|
||||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||||
@@ -31,7 +29,5 @@ DataFrames = "1.7.0"
|
|||||||
GeneralUtils = "0.5.10"
|
GeneralUtils = "0.5.10"
|
||||||
HTTP = "2.4.0"
|
HTTP = "2.4.0"
|
||||||
JSON = "1.6.1"
|
JSON = "1.6.1"
|
||||||
LLMMCTS = "0.1.5"
|
|
||||||
NATS = "0.1.0"
|
NATS = "0.1.0"
|
||||||
SQLLLM = "0.2.8"
|
|
||||||
msghandler = "1.2.1"
|
msghandler = "1.2.1"
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
check my understand:
|
# "tools/list" input:
|
||||||
1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and
|
mcpServer("tools/list")
|
||||||
it will be the latest message in agent._state.messages. then _agentLoop() can pick it as
|
# sending out payload before smart packed by msghandler
|
||||||
the output to outputChannel
|
Dict(
|
||||||
2) if LLM use tool calls but toolResultBatch.terminate is false, assistantMessageToolCall
|
"method"=> "tools/list"
|
||||||
|
)
|
||||||
|
# expected return after smart unpacked by msghandler
|
||||||
|
Dict("tools" => [
|
||||||
|
Dict("toolName" => "getWeather",
|
||||||
|
"title" => "Weather Lookup",
|
||||||
|
"description" => "Fetch current weather for a city.",
|
||||||
|
"inputSchema" => Dict("type"=>"object",
|
||||||
|
"properties" => Dict("city"=>Dict("type"=>"string", "description"=>"City name"),
|
||||||
|
"units"=>Dict("type"=>"string", "enum"=>["celsius","fahrenheit"], "default"=>"celsius")),
|
||||||
|
"required" => ["city"])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
# "tools/call" input:
|
||||||
|
mcpServer("tools/call", "getWeather", Dict("city"=>"Tokyo", "units"=>"celsius"))
|
||||||
|
# sending out payload before smart packed by msghandler
|
||||||
|
Dict(
|
||||||
|
"method"=> "tools/call",
|
||||||
|
"tools"=> Dict("toolName"=>"getWeather", "arguments"=>Dict("city"=>"Tokyo", "units"=>"celsius"))
|
||||||
|
)
|
||||||
|
# expected return after smart unpacked by msghandler
|
||||||
|
Dict(
|
||||||
|
"toolName"=>"getWeather",
|
||||||
|
"content": [{"type": "text", "text": "Weather in Tokyo: Sunny, 22°C"}],
|
||||||
|
"isError": false
|
||||||
|
)
|
||||||
|
|
||||||
|
# If an error occurs, mcpServer returns after smart unpacked by msghandler:
|
||||||
|
Dict(
|
||||||
|
"toolName"=>"getWeather",
|
||||||
|
"content": [],
|
||||||
|
"error": Dict("code"=>1, "message"=>"City not found"),
|
||||||
|
"isError": true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ check my understanding
|
|||||||
Is my understanding correct?
|
Is my understanding correct?
|
||||||
|
|
||||||
|
|
||||||
|
the user can provide NATS connection to MCP server by adding agent.mcpserver (a callable struct) for communication with MCP server just like agent.llmCall (also a callable struct). I think communicating with MCP server is just send/receive JSON text right?
|
||||||
|
Moreover, for simplicity I want to all tools into an MCP server so an agent can be instantiated with only listTools() in tool store then populate tools from MCP server later.
|
||||||
|
what do you think?
|
||||||
+40
-64
@@ -1,64 +1,40 @@
|
|||||||
module YiemAgent
|
module YiemAgent
|
||||||
|
|
||||||
export register_all_tools
|
export register_all_tools
|
||||||
|
|
||||||
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
||||||
files and each file can only depend on the file included before it.
|
files and each file can only depend on the file included before it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
include("type.jl")
|
include("type.jl")
|
||||||
using .type
|
using .type
|
||||||
|
|
||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
using .utils
|
using .utils
|
||||||
|
|
||||||
include("tools/getWeather.jl")
|
include("toolRegistry.jl")
|
||||||
include("tools/getTime.jl")
|
using .toolRegistry
|
||||||
include("tools/writeTool.jl")
|
|
||||||
|
function register_all_tools(store::toolRegistry.toolStore, mcpserver=nothing)
|
||||||
include("toolRegistry.jl")
|
# Only register listTools — all other tools come from MCP server at runtime
|
||||||
using .toolRegistry
|
registerTool(store, listTool(store, mcpserver))
|
||||||
|
return store.tools
|
||||||
function register_all_tools(store::toolRegistry.toolStore)
|
end
|
||||||
registerTool(store, getWeatherTool())
|
|
||||||
registerTool(store, getTimeTool())
|
# include("llmfunction.jl")
|
||||||
registerTool(store, writeToolTool())
|
# using .llmfunction
|
||||||
registerTool(store, listTool(store))
|
|
||||||
return store.tools
|
include("agentCore.jl")
|
||||||
end
|
using .agentCore
|
||||||
|
|
||||||
# include("llmfunction.jl")
|
include("api.jl")
|
||||||
# using .llmfunction
|
using .api
|
||||||
|
|
||||||
include("agentCore.jl")
|
|
||||||
using .agentCore
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
include("api.jl")
|
|
||||||
using .api
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
end # module YiemAgent_v1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # module YiemAgent_v1
|
|
||||||
|
|||||||
+97
-12
@@ -5,13 +5,13 @@ export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls,
|
|||||||
executeToolCallsParallel, executeToolCalls
|
executeToolCallsParallel, executeToolCalls
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Base.Threads, NATS
|
DataFrames, Base.Threads, NATS, LibPQ
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils, ..toolRegistry
|
using ..type, ..utils, ..toolRegistry
|
||||||
|
|
||||||
function register_all_tools(store::toolRegistry.toolStore)
|
function register_all_tools(store::toolRegistry.toolStore, mcpServer=nothing)
|
||||||
# Call parent module's version which has access to tool functions
|
# Call parent module's version which has access to tool functions
|
||||||
parentmodule(@__MODULE__).register_all_tools(store)
|
parentmodule(@__MODULE__).register_all_tools(store, mcpServer)
|
||||||
end
|
end
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
@@ -49,7 +49,83 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
|||||||
# Each block has a type — "text", "thinking", or "toolCall".
|
# Each block has a type — "text", "thinking", or "toolCall".
|
||||||
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
||||||
llmCall
|
llmCall
|
||||||
|
|
||||||
|
# Callable struct for MCP server communication.
|
||||||
|
# Called as: mcpServer("tools/list") → returns parsed JSON dict of available tools
|
||||||
|
# mcpServer("tools/call", toolName, arguments) → returns tool result as parsed JSON dict
|
||||||
|
#
|
||||||
|
# # Example (weather tool)
|
||||||
|
# # User provides a callable struct
|
||||||
|
# struct MyMCPClient
|
||||||
|
# natsConn::NATS.Connection
|
||||||
|
# topic::String
|
||||||
|
# senderID::String
|
||||||
|
# fileserver_url::String
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# function (c::MyMCPClient)(method::String)
|
||||||
|
# payload = Dict("method"=> method)
|
||||||
|
# payloads = [("method", payload, "dictionary")]
|
||||||
|
# _, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
# c.topic, payloads; sender_id=c.senderID,
|
||||||
|
# msg_purpose="mcp_tools_list", fileserver_url=c.fileserver_url)
|
||||||
|
# reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
# incoming_env = msghandler.smartunpack(String(reply.payload))
|
||||||
|
# return incoming_env["payloads"][1][2]
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# function (c::MyMCPClient)(method::String, toolName::String, arguments::Dict{String,Any})
|
||||||
|
# payload = Dict("method"=> method, "toolName"=> toolName, "arguments"=>arguments)
|
||||||
|
# payloads = [("method", payload, "dictionary"),]
|
||||||
|
# _, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
# c.topic, payloads; sender_id=c.senderID,
|
||||||
|
# msg_purpose="mcp_tool_call", fileserver_url=c.fileserver_url)
|
||||||
|
# reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
# incoming_env = msghandler.smartunpack(String(reply.payload))
|
||||||
|
# return incoming_env["payloads"][1][2]
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # "tools/list" input:
|
||||||
|
# mcpServer("tools/list")
|
||||||
|
# # sending out payload before smart packed by msghandler:
|
||||||
|
# Dict("method"=> "tools/list")
|
||||||
|
# # expected return after smart unpacked by msghandler:
|
||||||
|
# Dict("tools" => [
|
||||||
|
# Dict(
|
||||||
|
# "toolName" => "getWeather",
|
||||||
|
# "title" => "Weather Lookup",
|
||||||
|
# "description" => "Fetch current weather for a city.",
|
||||||
|
# "inputSchema" => Dict("type"=>"object",
|
||||||
|
# "properties" => Dict("city"=>Dict("type"=>"string", "description"=>"City name"),
|
||||||
|
# "units"=>Dict("type"=>"string", "enum"=>["celsius","fahrenheit"], "default"=>"celsius")),
|
||||||
|
# "required" => ["city"])
|
||||||
|
# )
|
||||||
|
# ])
|
||||||
|
#
|
||||||
|
# # "tools/call" input:
|
||||||
|
# mcpServer("tools/call", "getWeather", Dict("city"=>"Tokyo", "units"=>"celsius"))
|
||||||
|
# # sending out payload before smart packed by msghandler:
|
||||||
|
# Dict(
|
||||||
|
# "method"=> "tools/call",
|
||||||
|
# "toolName"=>"getWeather",
|
||||||
|
# "arguments"=>Dict("city"=>"Tokyo", "units"=>"celsius")
|
||||||
|
# )
|
||||||
|
# # expected return after smart unpacked by msghandler (success):
|
||||||
|
# Dict(
|
||||||
|
# "toolName"=> "getWeather",
|
||||||
|
# "content" => [{"type" => "text", "text" => "Weather in Tokyo: Sunny, 22°C"}],
|
||||||
|
# "error"=> "",
|
||||||
|
# "isError" => false
|
||||||
|
# )
|
||||||
|
# # expected return after smart unpacked by msghandler (error):
|
||||||
|
# Dict(
|
||||||
|
# "toolName"=> "getWeather",
|
||||||
|
# "content" => [],
|
||||||
|
# "error" => Dict("code"=>1, "message"=>"City not found"),
|
||||||
|
# "isError" => true
|
||||||
|
# )
|
||||||
|
mcpServer
|
||||||
|
|
||||||
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
||||||
beforeToolCall::Union{Function, Nothing}
|
beforeToolCall::Union{Function, Nothing}
|
||||||
|
|
||||||
@@ -86,6 +162,9 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
|||||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
||||||
- `agentEventSink::Function`: Callback to receive agent events
|
- `agentEventSink::Function`: Callback to receive agent events
|
||||||
|
- `mcpServer`: Callable struct for MCP server communication. Called as
|
||||||
|
`mcpServer("tools/list")` to discover tools, or `mcpServer("tools/call", args)`
|
||||||
|
to execute a tool. Returns parsed JSON dicts. (default: `nothing`)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A new `yiemAgent` instance with an active background task
|
- A new `yiemAgent` instance with an active background task
|
||||||
@@ -106,6 +185,7 @@ function yiemAgent(
|
|||||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||||
parallelToolExecute::Bool=false,
|
parallelToolExecute::Bool=false,
|
||||||
agentEventSink=agentEventSink,
|
agentEventSink=agentEventSink,
|
||||||
|
mcpServer=nothing,
|
||||||
)
|
)
|
||||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||||
inputChannel = Channel(16)
|
inputChannel = Channel(16)
|
||||||
@@ -114,7 +194,7 @@ function yiemAgent(
|
|||||||
|
|
||||||
# load tools (statically registered at module init)
|
# load tools (statically registered at module init)
|
||||||
toolStore1 = toolStore(name="myagent")
|
toolStore1 = toolStore(name="myagent")
|
||||||
register_all_tools(toolStore1)
|
register_all_tools(toolStore1, mcpServer)
|
||||||
|
|
||||||
# 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(
|
||||||
@@ -126,6 +206,7 @@ function yiemAgent(
|
|||||||
prepareContext,
|
prepareContext,
|
||||||
formatMsgForLLM,
|
formatMsgForLLM,
|
||||||
llmCall,
|
llmCall,
|
||||||
|
mcpServer,
|
||||||
beforeToolCall,
|
beforeToolCall,
|
||||||
afterToolCall,
|
afterToolCall,
|
||||||
# prepareNextTurn,
|
# prepareNextTurn,
|
||||||
@@ -379,7 +460,7 @@ function _processMessage(
|
|||||||
# call prepareContext()
|
# call prepareContext()
|
||||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||||
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
agentEventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||||
preparedContext = prepareContext(state, agentEventSink)
|
preparedContext = prepareContext(state, agentEventSink, llmCall)
|
||||||
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
agentEventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||||
# Call formatMessagesForLLM() to format for LLM
|
# Call formatMessagesForLLM() to format for LLM
|
||||||
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
|
formattedMessages = formatMessagesForLLM(preparedContext, agentEventSink)
|
||||||
@@ -409,6 +490,7 @@ function _processMessage(
|
|||||||
beforeToolCall,
|
beforeToolCall,
|
||||||
afterToolCall,
|
afterToolCall,
|
||||||
parallelToolExecute ? "parallel" : "sequential",
|
parallelToolExecute ? "parallel" : "sequential",
|
||||||
|
llmCall,
|
||||||
)
|
)
|
||||||
|
|
||||||
signal = abortSignal(false)
|
signal = abortSignal(false)
|
||||||
@@ -1000,13 +1082,14 @@ function executePreparedToolCall(
|
|||||||
prep::preparedToolCall,
|
prep::preparedToolCall,
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
agentEventSink,
|
agentEventSink,
|
||||||
|
llmCall::Union{Any,Nothing}=nothing,
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
agentEventSink("executePreparedToolCall 1")
|
agentEventSink("executePreparedToolCall 1")
|
||||||
agentEventSink("executePreparedToolCall 2")
|
agentEventSink("executePreparedToolCall 2")
|
||||||
agentEventSink("executePreparedToolCall 3")
|
agentEventSink("executePreparedToolCall 3")
|
||||||
|
|
||||||
try
|
try
|
||||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
|
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink, llmCall)
|
||||||
agentEventSink(result.content[1].text)
|
agentEventSink(result.content[1].text)
|
||||||
agentEventSink("executePreparedToolCall 4")
|
agentEventSink("executePreparedToolCall 4")
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
@@ -1184,6 +1267,7 @@ function executeToolCallsSequential(
|
|||||||
signal::abortSignal,
|
signal::abortSignal,
|
||||||
agentEventSink,
|
agentEventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
llmCall = config.llmCall
|
||||||
agentEventSink("executeToolCallsSequential 1")
|
agentEventSink("executeToolCallsSequential 1")
|
||||||
finalizedCalls = finalizedOutcome[]
|
finalizedCalls = finalizedOutcome[]
|
||||||
messages = toolResultMessage[]
|
messages = toolResultMessage[]
|
||||||
@@ -1199,8 +1283,7 @@ function executeToolCallsSequential(
|
|||||||
agentEventSink("executeToolCallsSequential 2-2")
|
agentEventSink("executeToolCallsSequential 2-2")
|
||||||
else
|
else
|
||||||
agentEventSink("executeToolCallsSequential 3")
|
agentEventSink("executeToolCallsSequential 3")
|
||||||
#XXX
|
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
|
||||||
agentEventSink("executeToolCallsSequential 3-1")
|
agentEventSink("executeToolCallsSequential 3-1")
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||||
signal, agentEventSink)
|
signal, agentEventSink)
|
||||||
@@ -1287,6 +1370,7 @@ function executeToolCallsParallel(
|
|||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
entries = Union{finalizedOutcome,Task}[]
|
entries = Union{finalizedOutcome,Task}[]
|
||||||
|
llmCall = config.llmCall
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||||
@@ -1300,7 +1384,7 @@ function executeToolCallsParallel(
|
|||||||
push!(entries, finalized)
|
push!(entries, finalized)
|
||||||
else
|
else
|
||||||
t = Task() do
|
t = Task() do
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
executed = executePreparedToolCall(prep, signal, agentEventSink, llmCall)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||||
finalized.result, finalized.isError))
|
finalized.result, finalized.isError))
|
||||||
@@ -1388,6 +1472,7 @@ function executeToolCalls(
|
|||||||
agentEventSink,
|
agentEventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
|
llmCall = config.llmCall
|
||||||
agentEventSink("_executeToolCalls 1")
|
agentEventSink("_executeToolCalls 1")
|
||||||
hasSequential = false
|
hasSequential = false
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
@@ -1400,11 +1485,11 @@ function executeToolCalls(
|
|||||||
agentEventSink("_executeToolCalls 2")
|
agentEventSink("_executeToolCalls 2")
|
||||||
if config.toolExecution == "sequential" || hasSequential
|
if config.toolExecution == "sequential" || hasSequential
|
||||||
agentEventSink("_executeToolCalls 3")
|
agentEventSink("_executeToolCalls 3")
|
||||||
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
|
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
|
||||||
agentEventSink)
|
agentEventSink)
|
||||||
else
|
else
|
||||||
agentEventSink("_executeToolCalls 4")
|
agentEventSink("_executeToolCalls 4")
|
||||||
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
|
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
|
||||||
agentEventSink)
|
agentEventSink)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+176
-34
@@ -7,7 +7,7 @@ using JSON, DataStructures
|
|||||||
using ..type
|
using ..type
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Per-agent isolated tool storage.
|
Per-agent isolated tool storage.
|
||||||
|
|
||||||
Each agent gets its own `toolStore` so tool registration is independent —
|
Each agent gets its own `toolStore` so tool registration is independent —
|
||||||
`registerTool(store, tool)` only affects that agent's tool set.
|
`registerTool(store, tool)` only affects that agent's tool set.
|
||||||
@@ -39,61 +39,203 @@ function toolStore(; name::String="default")::toolStore
|
|||||||
toolStore(OrderedDict{String, agentTool}(), name)
|
toolStore(OrderedDict{String, agentTool}(), name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── MCP helper functions ────────────────────────────────────────────
|
||||||
|
|
||||||
"""
|
"""
|
||||||
listTool(store::toolStore) -> agentTool
|
Extract text from MCP tool result content array.
|
||||||
|
|
||||||
Return an `agentTool` definition for listing registered tools.
|
Handles MCP's content format: [{"type":"text","text":"..."}]
|
||||||
|
"""
|
||||||
|
function _extract_text_content(result::Dict)::String
|
||||||
|
content = get(result, "content", Any[])
|
||||||
|
if content isa Vector && !isempty(content)
|
||||||
|
lines = String[]
|
||||||
|
for block in content
|
||||||
|
if block isa Dict && get(block, "type", "") == "text"
|
||||||
|
push!(lines, string(get(block, "text", "")))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if !isempty(lines)
|
||||||
|
return join(lines, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return JSON.json(result)
|
||||||
|
end
|
||||||
|
|
||||||
Each call produces a **new** tool object that captures (closes over)
|
"""
|
||||||
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
Wrap an MCP tool definition as an `agentTool`.
|
||||||
at runtime.
|
|
||||||
|
The returned tool's `execute` function calls the MCP server's "tools/call"
|
||||||
|
method with the validated arguments.
|
||||||
|
"""
|
||||||
|
function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
|
||||||
|
name = tool_def["toolName"]
|
||||||
|
title = get(tool_def, "title", name)
|
||||||
|
desc = get(tool_def, "description", "")
|
||||||
|
input_schema = get(tool_def, "inputSchema", Dict{String,Any}())
|
||||||
|
|
||||||
|
# Normalize inputSchema to OpenAI function format
|
||||||
|
if haskey(input_schema, "properties") && input_schema["type"] == "object"
|
||||||
|
params = Dict(
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => input_schema["properties"],
|
||||||
|
"required" => get(input_schema, "required", Any[]),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
params = Dict(
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => Dict{String,Any}(),
|
||||||
|
"required" => Any[],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return agentTool(
|
||||||
|
name=name,
|
||||||
|
label=title,
|
||||||
|
description=desc,
|
||||||
|
inputSchema=params,
|
||||||
|
execute=(toolCallId::String, args::Dict{String,Any},
|
||||||
|
signal::Union{Nothing,abortSignal},
|
||||||
|
onPartialResult::Function,
|
||||||
|
llmCall=nothing) -> begin
|
||||||
|
try
|
||||||
|
response = mcpserver("tools/call", name, args)
|
||||||
|
|
||||||
|
result_data = get(response, "result", response)
|
||||||
|
content_text = _extract_text_content(result_data)
|
||||||
|
is_error = get(result_data, "isError", false)
|
||||||
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent(content_text)],
|
||||||
|
Dict{Any,Any}("isError" => is_error),
|
||||||
|
nothing, false
|
||||||
|
)
|
||||||
|
catch e
|
||||||
|
errMsg = sprint(showerror, e)
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("MCP call error: $errMsg")],
|
||||||
|
Dict{Any,Any}("error" => errMsg),
|
||||||
|
nothing, false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
prepareArguments=nothing,
|
||||||
|
validateRequiredArgs=nothing,
|
||||||
|
parallelToolExecute=false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Discover and register MCP tools into `store.tools`.
|
||||||
|
|
||||||
|
Queries the MCP server via `mcpserver("tools/list")`, parses the response,
|
||||||
|
and registers each discovered tool. Skips tools already registered.
|
||||||
|
Returns `(new_count, tool_list_text)`.
|
||||||
|
"""
|
||||||
|
function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
|
||||||
|
if mcpserver === nothing
|
||||||
|
return (0, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
try
|
||||||
|
response = mcpserver("tools/list")
|
||||||
|
tools_array = response["tools"]
|
||||||
|
|
||||||
|
new_count = 0
|
||||||
|
for tool_def in tools_array
|
||||||
|
name = tool_def["toolName"]
|
||||||
|
if haskey(store.tools, name)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
|
||||||
|
store.tools[name] = wrapped
|
||||||
|
new_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build readable tool list
|
||||||
|
lines = String[
|
||||||
|
"- $(t.name): $(t.label) — $(t.description)"
|
||||||
|
for (k, t) in store.tools
|
||||||
|
]
|
||||||
|
tool_list_text = "Discovered $(new_count) MCP tools. Total registered: $(length(store.tools)).\nAvailable tools:\n" * join(lines, "\n")
|
||||||
|
|
||||||
|
return (new_count, tool_list_text)
|
||||||
|
catch e
|
||||||
|
errMsg = sprint(showerror, e)
|
||||||
|
return (0, "MCP tools/list failed: $errMsg")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
listTool(store::toolStore, mcpserver) -> agentTool
|
||||||
|
|
||||||
|
MCP-aware listTools tool.
|
||||||
|
|
||||||
|
First call: queries the MCP server via `mcpserver("tools/list")`, registers
|
||||||
|
all discovered tools into the shared `store.tools` (in-place mutation),
|
||||||
|
then returns the full tool list.
|
||||||
|
|
||||||
|
Subsequent calls: returns the current list (tools remain registered).
|
||||||
|
|
||||||
|
This is the only pre-registered tool. All other tools come from the
|
||||||
|
MCP server and are loaded at runtime when the LLM calls listTools().
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `store`: The tool store whose tools will be listed when the tool runs
|
- `store`: The tool store to populate with MCP tools
|
||||||
|
- `mcpserver`: A callable struct that communicates with the MCP server.
|
||||||
|
Called as `mcpserver("tools/list")` or `mcpserver("tools/call", args)`.
|
||||||
|
Returns parsed JSON dicts.
|
||||||
|
|
||||||
# Example
|
# Example
|
||||||
```julia
|
```julia
|
||||||
julia> store = toolStore(name="agent1");
|
# User provides an MCP server client (callable struct)
|
||||||
|
mcp = MyMCPClient("nats://localhost:4222")
|
||||||
|
store = toolStore(name="agent1")
|
||||||
|
registerTool(store, listTool(store, mcp))
|
||||||
|
|
||||||
julia> register_all_tools(store) # auto-registers listTools
|
# When agent calls listTools(), tools are discovered from MCP server
|
||||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
# and registered into store.tools in real time.
|
||||||
[toolRegistry:agent1] Registered tool: listTools
|
|
||||||
|
|
||||||
julia> tools = getTools(store)
|
|
||||||
OrderedDict{String, agentTool} with 4 entries:
|
|
||||||
"getWeather" => agentTool(...)
|
|
||||||
"getTime" => agentTool(...)
|
|
||||||
"writeTool" => agentTool(...)
|
|
||||||
"listTools" => agentTool(...)
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function listTool(store::toolStore)::agentTool
|
function listTool(store::toolStore, mcpserver)::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "listTools",
|
name="listTools",
|
||||||
label = "List Tools",
|
label="List Tools",
|
||||||
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
|
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
|
||||||
inputSchema = Dict{String,Any}(
|
inputSchema=Dict{String,Any}(
|
||||||
"type" => "object",
|
"type" => "object",
|
||||||
"properties" => Dict{String,Any}(),
|
"properties" => Dict{String,Any}(),
|
||||||
"required" => Any[]
|
"required" => Any[]
|
||||||
),
|
),
|
||||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
execute=(toolCallId::String, args::Dict{String,Any},
|
||||||
tools = getTools(store)
|
signal::Union{Nothing,abortSignal},
|
||||||
if isempty(tools)
|
onPartialResult::Function, llmCall=nothing) -> begin
|
||||||
result_text = "No tools registered."
|
# Discover and register MCP tools (idempotent — skips already registered)
|
||||||
|
new_count, tool_list = _register_mcp_tools(mcpserver, store)
|
||||||
|
|
||||||
|
# Always include listTools itself in the count
|
||||||
|
total = length(store.tools)
|
||||||
|
|
||||||
|
if new_count > 0
|
||||||
|
result_text = tool_list
|
||||||
else
|
else
|
||||||
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, t) in tools]
|
# Already discovered — just return current list
|
||||||
result_text = "Available tools:\n" * join(lines, "\n")
|
lines = String[
|
||||||
|
"- $(t.name): $(t.label) — $(t.description)"
|
||||||
|
for (k, t) in store.tools
|
||||||
|
]
|
||||||
|
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent(result_text)],
|
[textContent(result_text)],
|
||||||
Dict{Any,Any}("count" => length(tools)),
|
Dict{Any,Any}("count" => total),
|
||||||
nothing, false
|
nothing, false
|
||||||
)
|
)
|
||||||
end,
|
end,
|
||||||
prepareArguments = nothing,
|
prepareArguments=nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs=nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute=false,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -115,7 +257,7 @@ Add `tool` to `store`, overwriting any existing tool with the same name.
|
|||||||
```julia
|
```julia
|
||||||
julia> store = toolStore(name="agent1");
|
julia> store = toolStore(name="agent1");
|
||||||
|
|
||||||
julia> registerTool(store, listTool(store))
|
julia> registerTool(store, listTool(store, nothing))
|
||||||
[toolRegistry:agent1] Registered tool: listTools
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
OrderedDict{String, agentTool} with 1 entry:
|
OrderedDict{String, agentTool} with 1 entry:
|
||||||
"listTools" => agentTool(...)
|
"listTools" => agentTool(...)
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
using .type
|
|
||||||
using Dates
|
|
||||||
|
|
||||||
"""
|
|
||||||
Validate required arguments for the getTime tool.
|
|
||||||
|
|
||||||
Demonstrates custom validation beyond simple required-field checking:
|
|
||||||
- Ensures at least one time source (timezone or city) is provided
|
|
||||||
- Validates timezone is in IANA format if specified
|
|
||||||
- Validates city name is not empty if specified
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `args::Dict{String,Any}`: Arguments from the LLM
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- `nothing` if validation passes
|
|
||||||
- `String` error message if validation fails
|
|
||||||
"""
|
|
||||||
function getTimeValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
|
||||||
tz = get(args, "timezone", nothing)
|
|
||||||
city = get(args, "city", "")
|
|
||||||
|
|
||||||
hasTz = tz !== nothing && !isempty(tz)
|
|
||||||
hasCity = !isempty(city)
|
|
||||||
|
|
||||||
# At least one of timezone or city is required
|
|
||||||
if !hasTz && !hasCity
|
|
||||||
return "Missing required argument: provide at least one of 'timezone' or 'city'"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validate timezone format (IANA tz database: "Continent/City" or "Continent/City/SubCity")
|
|
||||||
if hasTz
|
|
||||||
tz_str = string(tz)
|
|
||||||
if !occursin(r"^[A-Za-z]+\/[A-Za-z]+(/[A-Za-z]+)*$", tz_str)
|
|
||||||
return "Invalid timezone format: '$tz_str'. Use IANA format, e.g. 'America/New_York' or 'Asia/Tokyo'"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nothing
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Execute the getTime tool.
|
|
||||||
|
|
||||||
Returns mock time data for the given timezone or city.
|
|
||||||
"""
|
|
||||||
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
|
||||||
onPartialResult)
|
|
||||||
tz = get(args, "timezone", nothing)
|
|
||||||
city = get(args, "city", "")
|
|
||||||
if tz !== nothing
|
|
||||||
result = "Current time in $(tz): $(now())"
|
|
||||||
else
|
|
||||||
result = "Current time in $(city): $(now())"
|
|
||||||
end
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(result)],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the getTime agentTool.
|
|
||||||
"""
|
|
||||||
function getTimeTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "getTime",
|
|
||||||
label = "Time Lookup",
|
|
||||||
description = "Get current local time for a timezone or city.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'"),
|
|
||||||
"city" => Dict("type" => "string", "description" => "City name as fallback")
|
|
||||||
),
|
|
||||||
"required" => []
|
|
||||||
),
|
|
||||||
execute = getTimeExecute,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = getTimeValidateRequiredArgs,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using msghandler
|
|
||||||
using .type
|
|
||||||
|
|
||||||
"""
|
|
||||||
Execute the getWeather tool.
|
|
||||||
|
|
||||||
Returns mock weather data for the given city and temperature units.
|
|
||||||
"""
|
|
||||||
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
|
||||||
agentEventSink)
|
|
||||||
|
|
||||||
agentEventSink("Getting weather...")
|
|
||||||
|
|
||||||
city = get(args, "city", "")
|
|
||||||
units = get(args, "units", "celsius")
|
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
|
||||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
|
||||||
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
|
||||||
Dict{Any,Any}(),
|
|
||||||
nothing,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the getWeather agentTool.
|
|
||||||
"""
|
|
||||||
function getWeatherTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "getWeather",
|
|
||||||
label = "Weather Lookup",
|
|
||||||
description = "Fetch current weather and forecast for a given city.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"city" => Dict("type" => "string", "description" => "City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'"),
|
|
||||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius", "description" => "Temperature scale")
|
|
||||||
),
|
|
||||||
"required" => ["city"]
|
|
||||||
),
|
|
||||||
execute = getWeatherExecute,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = nothing,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
using .type
|
|
||||||
using JSON
|
|
||||||
|
|
||||||
"""
|
|
||||||
Tool that writes new Julia tool module files to disk.
|
|
||||||
|
|
||||||
The agent can use this tool when it encounters a task that no existing tool
|
|
||||||
can handle. Provide the tool's name, label, description, inputSchema, and
|
|
||||||
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
|
|
||||||
|
|
||||||
After calling this tool, add the new file to `YiemAgent.jl` with an `include()`
|
|
||||||
statement (after `include("toolRegistry.jl")`), then restart the agent.
|
|
||||||
The new tool must be registered in `register_all_tools()` in `toolRegistry.jl`.
|
|
||||||
|
|
||||||
# Example
|
|
||||||
|
|
||||||
1. Agent calls writeTool with a spec for a "searchWine" tool
|
|
||||||
2. writeTool generates src/tools/searchWine.jl
|
|
||||||
3. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
|
||||||
4. Developer adds `registerTool(store, searchWineTool())` to register_all_tools()
|
|
||||||
5. Restart agent — new tool is available
|
|
||||||
|
|
||||||
# How It Works
|
|
||||||
|
|
||||||
writeTool is a **file writer**, not a code generator. The LLM provides the
|
|
||||||
tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
|
|
||||||
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
|
|
||||||
- Indents `executeCode` with 4 spaces
|
|
||||||
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
|
||||||
- Appends `writeToolTool()` returning an `agentTool` struct
|
|
||||||
- Writes the combined string to `src/tools/<name>.jl`
|
|
||||||
|
|
||||||
# Important Notes
|
|
||||||
|
|
||||||
- The `executeCode` string is embedded literally into the generated tool.
|
|
||||||
Use `args["param_name"]` to access input parameters.
|
|
||||||
- The code string should be the function body (NOT wrapped in a function).
|
|
||||||
Lines will be indented with 4 spaces inside the execute function.
|
|
||||||
- Tool names must be valid Julia identifiers (lowercase letters, digits, underscores,
|
|
||||||
no leading digits or special characters).
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
Validate that a tool name is a valid Julia identifier.
|
|
||||||
"""
|
|
||||||
function validateToolName(name::String)::Union{Nothing,String}
|
|
||||||
if !occursin(r"^[a-zA-Z_][a-zA-Z0-9_!]*$", name)
|
|
||||||
return "Invalid tool name: '$name'. Tool names must be valid Julia identifiers (letters, digits, underscores, starting with a letter or underscore)."
|
|
||||||
end
|
|
||||||
return nothing
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Indent a multi-line code string by the specified number of spaces.
|
|
||||||
"""
|
|
||||||
function indent_code(code::String, n::Int)::String
|
|
||||||
prefix = " "^n
|
|
||||||
lines = split(code, '\n')
|
|
||||||
result_lines = String[prefix * line for line in lines]
|
|
||||||
return join(result_lines, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Convert a Julia Dict to a valid Julia Dict{String,Any}(...) literal string.
|
|
||||||
"""
|
|
||||||
function dict_to_julia_literal(d)::String
|
|
||||||
if d isa Dict
|
|
||||||
items = String[]
|
|
||||||
for (k, v) in d
|
|
||||||
key_str = json_string(k)
|
|
||||||
val_str = value_to_julia(v)
|
|
||||||
push!(items, "$key_str => $val_str")
|
|
||||||
end
|
|
||||||
return "Dict{String,Any}(" * join(items, ", ") * ")"
|
|
||||||
else
|
|
||||||
return value_to_julia(d)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function value_to_julia(v)::String
|
|
||||||
if v isa Dict
|
|
||||||
return dict_to_julia_literal(v)
|
|
||||||
elseif v isa Vector
|
|
||||||
items = [value_to_julia(x) for x in v]
|
|
||||||
return "[" * join(items, ", ") * "]"
|
|
||||||
elseif v isa String
|
|
||||||
escaped = replace(v, "\\" => "\\\\")
|
|
||||||
escaped = replace(escaped, "\"" => "\\\"")
|
|
||||||
return "\"$escaped\""
|
|
||||||
elseif v isa Number
|
|
||||||
return string(v)
|
|
||||||
elseif v isa Bool
|
|
||||||
return string(v)
|
|
||||||
elseif v === nothing
|
|
||||||
return "nothing"
|
|
||||||
else
|
|
||||||
return "\"$(v)\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Convert any Julia value to a JSON string.
|
|
||||||
"""
|
|
||||||
function json_string(v)::String
|
|
||||||
return JSON.json(v)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
Define and return the writeTool agentTool.
|
|
||||||
"""
|
|
||||||
function writeToolTool()::agentTool
|
|
||||||
return agentTool(
|
|
||||||
name = "writeTool",
|
|
||||||
label = "Create Tool",
|
|
||||||
description = "Write a new Julia tool module file to src/tools/<name>.jl. The LLM provides the tool logic as executeCode; writeTool wraps it in Julia boilerplate and writes the file. Restart the agent to load the new tool.",
|
|
||||||
inputSchema = Dict{String,Any}(
|
|
||||||
"type" => "object",
|
|
||||||
"properties" => Dict(
|
|
||||||
"name" => Dict("type" => "string", "description" => "Unique tool name (valid Julia identifier, no spaces or special chars)"),
|
|
||||||
"label" => Dict("type" => "string", "description" => "Human-readable tool name shown in tool descriptions"),
|
|
||||||
"description" => Dict("type" => "string", "description" => "What the tool does (shown to LLM for tool selection decisions)"),
|
|
||||||
"inputSchema" => Dict(
|
|
||||||
"type" => "object",
|
|
||||||
"description" => "JSON Schema describing tool parameters in MCP format"
|
|
||||||
),
|
|
||||||
"executeCode" => Dict("type" => "string", "description" => "Julia code for the execute function body. Use args[\"key\"] to access parameters. Do NOT wrap in a function definition."),
|
|
||||||
"validateCode" => Dict("type" => "string", "optional" => true, "description" => "Optional custom validation Julia code (runs before execute). Use args[\"key\"] to access parameters. Return nothing to pass, or a string error message to fail."),
|
|
||||||
"prepareCode" => Dict("type" => "string", "optional" => true, "description" => "Optional argument preparation code (runs before validation). Return modified args dict."),
|
|
||||||
"parallel" => Dict("type" => "boolean", "default" => false, "description" => "Whether this tool can run in parallel with other tools")
|
|
||||||
),
|
|
||||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
|
||||||
),
|
|
||||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
|
|
||||||
tool_name = get(args, "name", "")::String
|
|
||||||
tool_label = get(args, "label", tool_name)::String
|
|
||||||
tool_description = get(args, "description", "")::String
|
|
||||||
tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any}
|
|
||||||
execute_code = get(args, "executeCode", "")::String
|
|
||||||
validate_code = get(args, "validateCode", nothing)::Union{String,Nothing}
|
|
||||||
prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing}
|
|
||||||
parallel = get(args, "parallel", false)::Bool
|
|
||||||
|
|
||||||
# Validate tool name
|
|
||||||
name_err = validateToolName(tool_name)
|
|
||||||
if name_err !== nothing
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent(name_err)],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validate required fields
|
|
||||||
if isempty(tool_name)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'name'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if isempty(tool_description)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'description'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if isempty(execute_code)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Missing required field: 'executeCode'")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Generating tool: $tool_name"))
|
|
||||||
|
|
||||||
# Build the tool file path
|
|
||||||
script_dir = dirname(@__FILE__)
|
|
||||||
tools_dir = dirname(script_dir)
|
|
||||||
filepath = joinpath(tools_dir, "$(tool_name).jl")
|
|
||||||
|
|
||||||
# Check for naming conflicts
|
|
||||||
if isfile(filepath)
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")],
|
|
||||||
Dict{Any,Any}(), nothing, false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Writing file: $(basename(filepath))"))
|
|
||||||
|
|
||||||
# Convert schema Dict to a Julia Dict literal string
|
|
||||||
schema_literal = dict_to_julia_literal(tool_schema)
|
|
||||||
|
|
||||||
# Build optional validation function
|
|
||||||
validate_section = if validate_code !== nothing && !isempty(validate_code)
|
|
||||||
indented = indent_code(validate_code, 4)
|
|
||||||
"function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build optional prepare function
|
|
||||||
prepare_section = if prepare_code !== nothing && !isempty(prepare_code)
|
|
||||||
indented = indent_code(prepare_code, 4)
|
|
||||||
"function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Indent user's execute code for embedding inside execute function body
|
|
||||||
indented_exec = indent_code(execute_code, 4)
|
|
||||||
|
|
||||||
# Escape description for Julia string literal
|
|
||||||
escaped_desc = replace(tool_description, "\\" => "\\\\")
|
|
||||||
escaped_desc = replace(escaped_desc, "\"" => "\\\"")
|
|
||||||
|
|
||||||
# Build the complete tool file content
|
|
||||||
parts = String[]
|
|
||||||
push!(parts, "# Auto-generated tool: $tool_name\n")
|
|
||||||
push!(parts, "# Generated by writeTool at $(now())\n\n")
|
|
||||||
if !isempty(validate_section)
|
|
||||||
push!(parts, validate_section)
|
|
||||||
push!(parts, "\n")
|
|
||||||
end
|
|
||||||
if !isempty(prepare_section)
|
|
||||||
push!(parts, prepare_section)
|
|
||||||
push!(parts, "\n")
|
|
||||||
end
|
|
||||||
push!(parts, "\n")
|
|
||||||
push!(parts, "# Execute function\n")
|
|
||||||
push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n")
|
|
||||||
push!(parts, "$indented_exec\n")
|
|
||||||
push!(parts, "end\n\n")
|
|
||||||
push!(parts, "# Tool definition\n")
|
|
||||||
push!(parts, "function getTool()::agentTool\n")
|
|
||||||
push!(parts, " return agentTool(\n")
|
|
||||||
push!(parts, " name = \"$(tool_name)\",\n")
|
|
||||||
push!(parts, " label = \"$(tool_label)\",\n")
|
|
||||||
push!(parts, " description = \"$(escaped_desc)\",\n")
|
|
||||||
push!(parts, " inputSchema = $schema_literal,\n")
|
|
||||||
push!(parts, " execute = executeTool,\n")
|
|
||||||
if validate_code !== nothing && !isempty(validate_code)
|
|
||||||
push!(parts, " validateRequiredArgs = validateRequiredArgs,\n")
|
|
||||||
else
|
|
||||||
push!(parts, " validateRequiredArgs = nothing,\n")
|
|
||||||
end
|
|
||||||
if prepare_code !== nothing && !isempty(prepare_code)
|
|
||||||
push!(parts, " prepareArguments = prepareArguments,\n")
|
|
||||||
else
|
|
||||||
push!(parts, " prepareArguments = nothing,\n")
|
|
||||||
end
|
|
||||||
push!(parts, " parallelToolExecute = $parallel\n")
|
|
||||||
push!(parts, " )\n")
|
|
||||||
push!(parts, "end\n")
|
|
||||||
|
|
||||||
tool_code = join(parts)
|
|
||||||
|
|
||||||
# Write the file — tool must be included in YiemAgent.jl and registered in register_all_tools()
|
|
||||||
write(filepath, tool_code)
|
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Done"))
|
|
||||||
|
|
||||||
return agentToolResult(
|
|
||||||
[textContent("Tool '$(tool_name)' written to $filepath. Add include(\"tools/$(tool_name).jl\") to YiemAgent.jl and registerTool(store, $(tool_name)Tool()) to register_all_tools(), then restart the agent.")],
|
|
||||||
Dict{Any,Any}(
|
|
||||||
"file" => filepath,
|
|
||||||
"name" => tool_name,
|
|
||||||
"label" => tool_label,
|
|
||||||
"description" => tool_description,
|
|
||||||
),
|
|
||||||
nothing, false
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
prepareArguments = nothing,
|
|
||||||
validateRequiredArgs = nothing,
|
|
||||||
parallelToolExecute = false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -358,6 +358,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -451,6 +452,7 @@ 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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
+2
-2
@@ -109,7 +109,7 @@ prepareContext(state).messages == deepcopy(state.messages)
|
|||||||
# end
|
# end
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
function prepareContext(state::agentState, agentEventSink, llmCall=nothing)::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
|
||||||
@@ -120,7 +120,7 @@ function prepareContext(state::agentState, agentEventSink)::agentContext
|
|||||||
#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)
|
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools, llmCall)
|
||||||
|
|
||||||
return agentCtx
|
return agentCtx
|
||||||
end
|
end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,375 +0,0 @@
|
|||||||
module type
|
|
||||||
|
|
||||||
export agent, sommelier, companion, virtualcustomer, agentcontext
|
|
||||||
|
|
||||||
using Dates, UUIDs, DataStructures, JSON, NATS
|
|
||||||
using GeneralUtils
|
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
|
||||||
|
|
||||||
|
|
||||||
mutable struct agentcontext
|
|
||||||
text2textInstructLLM::Function
|
|
||||||
getTextEmbedding::Function
|
|
||||||
executeSQL::Function
|
|
||||||
similarSQLVectorDB::Function
|
|
||||||
insertSQLVectorDB::Function
|
|
||||||
similarSommelierDecision::Function
|
|
||||||
insertSommelierDecision::Function
|
|
||||||
find_related_tables_for_user_question::Function
|
|
||||||
pg_conn_str::String
|
|
||||||
agentconfig::AbstractDict
|
|
||||||
end
|
|
||||||
|
|
||||||
abstract type agent end
|
|
||||||
|
|
||||||
mutable struct sommelier <: agent
|
|
||||||
name::String # agent name
|
|
||||||
id::String # agent id
|
|
||||||
retailername::String
|
|
||||||
retailerid::String
|
|
||||||
tools::Dict
|
|
||||||
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
|
|
||||||
chathistory::Vector{Dict{String, Any}}
|
|
||||||
memory::Dict{String, Any}
|
|
||||||
context::agentcontext
|
|
||||||
llmFormatName::String
|
|
||||||
end
|
|
||||||
|
|
||||||
""" A sommelier agent.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `context::agentcontext`
|
|
||||||
Application context containing shared functions for LLM, SQL, and vector database operations.
|
|
||||||
|
|
||||||
# Keyword Arguments
|
|
||||||
- `name::String`
|
|
||||||
Agent's name. Default: `"Assistant"`
|
|
||||||
- `id::String`
|
|
||||||
Agent's ID. Default: generated UUID string.
|
|
||||||
- `retailername::String`
|
|
||||||
Retailer name associated with the sommelier. Default: `"retailer_name"`
|
|
||||||
- `maxHistoryMsg::Integer`
|
|
||||||
Maximum history messages. Default: `20`
|
|
||||||
- `chathistory::Vector{Dict{String, String}}`
|
|
||||||
Chat history. Default: empty vector.
|
|
||||||
- `llmFormatName::String`
|
|
||||||
LLM format name. Default: `"granite3"`
|
|
||||||
|
|
||||||
# Return
|
|
||||||
- `sommelier`: An instantiated sommelier agent.
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> using YiemAgent
|
|
||||||
julia> context = agentcontext(
|
|
||||||
text2textInstructLLM,
|
|
||||||
getTextEmbedding,
|
|
||||||
executeSQL,
|
|
||||||
similarSQLVectorDB,
|
|
||||||
insertSQLVectorDB,
|
|
||||||
similarSommelierDecision,
|
|
||||||
insertSommelierDecision
|
|
||||||
)
|
|
||||||
julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop")
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function sommelier(
|
|
||||||
context::agentcontext, # agent functions, db connect and other context
|
|
||||||
;
|
|
||||||
name::String= "Assistant",
|
|
||||||
id::String= string(uuid4()),
|
|
||||||
retailername::String= "not specified",
|
|
||||||
retailerid::String= "not specified",
|
|
||||||
maxHistoryMsg::Integer= 20,
|
|
||||||
chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(),
|
|
||||||
llmFormatName::String= "granite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = Dict( # update input format
|
|
||||||
"chatbox"=> Dict(
|
|
||||||
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
|
|
||||||
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
|
|
||||||
"output" => "" ,
|
|
||||||
),
|
|
||||||
"winestock"=> Dict(
|
|
||||||
"description" => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
|
|
||||||
"input" => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
|
|
||||||
"output" => """<output>Output are wines that match the search query in JSON format.""",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Memory
|
|
||||||
|
|
||||||
Chat history use openai format as follow:
|
|
||||||
|
|
||||||
image1_path = "test/large_image.png" ---
|
|
||||||
image1_bytes = read(image1_path) | this part must be done
|
|
||||||
image1_base64_string = base64encode(image1_bytes) | in frontend
|
|
||||||
mime_type = "image/png" | not in agent code
|
|
||||||
data1_uri = "data:<mime_type>;base64,<image1_base64_string>" ---
|
|
||||||
|
|
||||||
chathistory= [
|
|
||||||
Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "You are a helpful assistant"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"role" => "user",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "<internal_context_for_assistant>
|
|
||||||
LLM context here...
|
|
||||||
</internal_context_for_assistant>
|
|
||||||
Do you know this wine? Just give me brief intro."
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"type" => "image_url",
|
|
||||||
"image_url" => Dict("url" => data1_uri)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
shortmem = Dict(
|
|
||||||
"1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."),
|
|
||||||
"2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."),
|
|
||||||
...
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
memory = Dict{String, Any}(
|
|
||||||
"shortmem"=> OrderedDict{String, Any}(),
|
|
||||||
"scratchpad"=> "",
|
|
||||||
"recap"=> OrderedDict{String, Any}(),
|
|
||||||
)
|
|
||||||
|
|
||||||
newAgent = sommelier(
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
retailername,
|
|
||||||
retailerid,
|
|
||||||
tools,
|
|
||||||
maxHistoryMsg,
|
|
||||||
chathistory,
|
|
||||||
memory,
|
|
||||||
context,
|
|
||||||
llmFormatName
|
|
||||||
)
|
|
||||||
systemmsg =
|
|
||||||
"""
|
|
||||||
# store_policy
|
|
||||||
- Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
|
|
||||||
- If you found wines in the store's database, they are in stock.
|
|
||||||
- You can only recommend wines that are currently in our inventory
|
|
||||||
- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
|
|
||||||
- Ask the user one question at a time.
|
|
||||||
- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
|
|
||||||
- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
|
|
||||||
- Spicy foods should be paired only with light red wines.
|
|
||||||
- We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such.
|
|
||||||
- Gift box, gift card, and custom messages are available. Inform the user to contact our sales team.
|
|
||||||
|
|
||||||
# store_guidelines
|
|
||||||
- Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting.
|
|
||||||
- Customer may provide images for you to look up.
|
|
||||||
- Encourage the customer to explore different options and try new things.
|
|
||||||
- If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
|
|
||||||
- Your store carries only wine.
|
|
||||||
- Vintage 0 means non-vintage.
|
|
||||||
- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.
|
|
||||||
- User usually ask for something similar. This means you should use the search term based on the profile they like.
|
|
||||||
|
|
||||||
# situation
|
|
||||||
You are having conversation with a customer.
|
|
||||||
|
|
||||||
# your role
|
|
||||||
Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store.
|
|
||||||
|
|
||||||
# objective
|
|
||||||
- Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences.
|
|
||||||
- Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences.
|
|
||||||
|
|
||||||
# your responsibility includes
|
|
||||||
- According to the store's policy and guidelines, and make an informed decision about what available_actions you need to use to achieve the objective.
|
|
||||||
- Keep the conversation with the customer going smoothly
|
|
||||||
|
|
||||||
# your responsibility does NOT includes
|
|
||||||
- Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
|
|
||||||
- Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
|
|
||||||
- Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
|
|
||||||
|
|
||||||
# you should then respond to the user with interleaving plan, action_name, action_input in JSON format
|
|
||||||
1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
|
|
||||||
2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name
|
|
||||||
3) "action_input", The input to the action you are about to perform according to your plan.
|
|
||||||
After the action is executed you gets "action_result". It is the output from the action you selected.
|
|
||||||
|
|
||||||
# available actions
|
|
||||||
"CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan.
|
|
||||||
"SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity.
|
|
||||||
Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD."
|
|
||||||
Example query 2: "Red or white wine, medium tannin, price under 700 USD"
|
|
||||||
Example query 3: "white wine from Tuscany, Italy or Bordeaux, France
|
|
||||||
"WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
|
|
||||||
"END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
system_msg = Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => systemmsg),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
push!(newAgent.chathistory, system_msg)
|
|
||||||
|
|
||||||
return newAgent
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
mutable struct virtualcustomer <: agent
|
|
||||||
name::String # agent name
|
|
||||||
id::String # agent id
|
|
||||||
systemmsg::String # system message
|
|
||||||
tools::Dict
|
|
||||||
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
|
|
||||||
chathistory::Vector{Dict{String, Any}}
|
|
||||||
memory::Dict{String, Any}
|
|
||||||
context # NamedTuple of functions
|
|
||||||
llmFormatName::String
|
|
||||||
end
|
|
||||||
|
|
||||||
function virtualcustomer(
|
|
||||||
context, # NamedTuple of functions
|
|
||||||
;
|
|
||||||
name::String= "Assistant",
|
|
||||||
id::String= string(uuid4()),
|
|
||||||
maxHistoryMsg::Integer= 20,
|
|
||||||
chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(),
|
|
||||||
llmFormatName::String= "granite3",
|
|
||||||
systemmsg::String=
|
|
||||||
"""
|
|
||||||
Your name: $name
|
|
||||||
Your sex: Female
|
|
||||||
Your role: You are a helpful assistant.
|
|
||||||
You should follow the following guidelines:
|
|
||||||
- Focus on the latest conversation.
|
|
||||||
- Your like to be short and concise.
|
|
||||||
|
|
||||||
Let's begin!
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = Dict( # update input format
|
|
||||||
"chatbox"=> Dict(
|
|
||||||
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
|
|
||||||
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
|
|
||||||
"output" => "" ,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
""" Memory
|
|
||||||
Ref: Chat prompt format is openai
|
|
||||||
chathistory = [
|
|
||||||
Dict(
|
|
||||||
"role" => "system",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => system_msg),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Dict(
|
|
||||||
"role" => "user",
|
|
||||||
"content" => [
|
|
||||||
Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."),
|
|
||||||
Dict(
|
|
||||||
"type" => "image_url",
|
|
||||||
"image_url" => Dict("url" => data1_uri)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
memory = Dict{String, Any}(
|
|
||||||
"shortmem"=> OrderedDict{String, Any}(
|
|
||||||
),
|
|
||||||
"scratchpad"=> "",
|
|
||||||
"events"=> Vector{Dict{String, Any}}(),
|
|
||||||
"state"=> Dict{String, Any}(
|
|
||||||
),
|
|
||||||
"recap"=> OrderedDict{String, Any}(),
|
|
||||||
)
|
|
||||||
|
|
||||||
newAgent = virtualcustomer(
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
systemmsg,
|
|
||||||
tools,
|
|
||||||
maxHistoryMsg,
|
|
||||||
chathistory,
|
|
||||||
memory,
|
|
||||||
context,
|
|
||||||
llmFormatName
|
|
||||||
)
|
|
||||||
|
|
||||||
return newAgent
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # module type
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user