Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e561b4b1ae | |||
| 9b593675c7 | |||
| e2b266fb3d | |||
| 390b9d71b6 | |||
| d26e1769df | |||
| b3ae0a9c50 | |||
| 1f3526b996 | |||
| 471da0c5ec | |||
| c355748e6f | |||
| f6895984d6 | |||
| 4ac5b97d53 | |||
| 6db50b6a52 | |||
| 096baad09d | |||
| c1120d811a | |||
| 7789748f25 | |||
| 9193260539 | |||
| 5f284883b3 | |||
| d2592ab6d6 | |||
| da21790263 | |||
| c59f6bfa61 | |||
| 3c91222462 | |||
| f62b8f14e7 | |||
| 7ffb720f86 | |||
| 5829c82d05 | |||
| c7a98f1710 | |||
| c7abf844ea | |||
| 00447e4dde | |||
| 25f8468696 | |||
| a29a82b74c | |||
| fd616409dd | |||
| 2543e6cbf1 | |||
| b8067c2d33 | |||
| 510cf6126c | |||
| 90fb97a4e7 | |||
| 77adeb3a6b | |||
| 4e592173a6 | |||
| 0cacb5c94a | |||
| 6c96409969 | |||
| 06d51c1ee9 | |||
| 2ad3d1df38 | |||
| 83c7770877 | |||
| bad14fbe7f | |||
| 578e8f55bd | |||
| ae3e432b02 | |||
| 7c14390400 | |||
| 89885c1583 | |||
| 5a27630ccf | |||
| ed91260468 | |||
| c13aeb3a74 | |||
| 287704778f | |||
| a9fa23f01b | |||
| c5cb18f0f1 | |||
| c78f4b023d | |||
| 1b69f69c7d | |||
| 3891099eaa | |||
| 268d340e2f | |||
| 189bc2efcf | |||
| 92b3e4081f | |||
| 750eff483b | |||
| 5cc35c78f4 | |||
| ed5415d92a | |||
| 6b3d575ea0 | |||
| da16c80a0a | |||
| 069240912b | |||
| 2aa0d1e9a4 | |||
| 03e1dd7628 | |||
| 70296a3bf2 | |||
| 75b2ce5978 | |||
| d8172a7fbe |
+186
-141
@@ -1,8 +1,20 @@
|
||||
# This file is machine-generated - editing it directly is not advised
|
||||
|
||||
julia_version = "1.12.6"
|
||||
julia_version = "1.12.7"
|
||||
manifest_format = "2.0"
|
||||
project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6"
|
||||
project_hash = "43252cf86e6030e836ede74c563b58ab2f0f0548"
|
||||
|
||||
[[deps.AWS]]
|
||||
deps = ["Base64", "Compat", "Dates", "Downloads", "GitHub", "HTTP", "IniFile", "JSON", "MD5", "Mocking", "OrderedCollections", "Random", "SHA", "ScopedValues", "Sockets", "URIs", "UUIDs", "XMLDict"]
|
||||
git-tree-sha1 = "842075b12a7f763c5f815a04037a99b6ac346df9"
|
||||
uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc"
|
||||
version = "1.99.0"
|
||||
|
||||
[[deps.AWSS3]]
|
||||
deps = ["AWS", "ArrowTypes", "Base64", "Compat", "Dates", "EzXML", "FilePathsBase", "HTTP", "MbedTLS", "Mocking", "OrderedCollections", "Retry", "SymDict", "URIs", "UUIDs", "XMLDict"]
|
||||
git-tree-sha1 = "af99cea71364c8c4483405a6b03697d197ab4d3b"
|
||||
uuid = "1c724243-ef5b-51ab-93f4-b0a88ac62a95"
|
||||
version = "0.11.6"
|
||||
|
||||
[[deps.Accessors]]
|
||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||
@@ -44,6 +56,12 @@ git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6"
|
||||
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
|
||||
version = "0.4.0"
|
||||
|
||||
[[deps.Arrow]]
|
||||
deps = ["ArrowTypes", "BitIntegers", "CodecLz4", "CodecZstd", "ConcurrentUtilities", "DataAPI", "Dates", "EnumX", "Mmap", "PooledArrays", "SentinelArrays", "StringViews", "Tables", "TimeZones", "TranscodingStreams", "UUIDs"]
|
||||
git-tree-sha1 = "4a69a3eadc1f7da78d950d1ef270c3a62c1f7e01"
|
||||
uuid = "69666777-d1a9-59fb-9406-91d4454c9d45"
|
||||
version = "2.8.1"
|
||||
|
||||
[[deps.ArrowTypes]]
|
||||
deps = ["Sockets", "UUIDs"]
|
||||
git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101"
|
||||
@@ -58,6 +76,12 @@ version = "1.11.0"
|
||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.BitIntegers]]
|
||||
deps = ["Random"]
|
||||
git-tree-sha1 = "091d591a060e43df1dd35faab3ca284925c48e46"
|
||||
uuid = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1"
|
||||
version = "0.3.7"
|
||||
|
||||
[[deps.BufferedStreams]]
|
||||
git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
|
||||
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
|
||||
@@ -74,9 +98,9 @@ version = "1.11.0"
|
||||
|
||||
[[deps.CSV]]
|
||||
deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"]
|
||||
git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d"
|
||||
git-tree-sha1 = "abed1e735dd4152f48c90cf0767e1790e25f332f"
|
||||
uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
||||
version = "0.10.16"
|
||||
version = "0.10.17"
|
||||
|
||||
[[deps.CodeTracking]]
|
||||
deps = ["InteractiveUtils", "REPL", "UUIDs"]
|
||||
@@ -90,16 +114,29 @@ git-tree-sha1 = "40956acdbef3d8c7cc38cba42b56034af8f8581a"
|
||||
uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
||||
version = "0.3.4"
|
||||
|
||||
[[deps.CodecLz4]]
|
||||
deps = ["Lz4_jll", "TranscodingStreams"]
|
||||
git-tree-sha1 = "d58afcd2833601636b48ee8cbeb2edcb086522c2"
|
||||
uuid = "5ba52731-8f18-5e0d-9241-30f10d1ec561"
|
||||
version = "0.4.6"
|
||||
|
||||
[[deps.CodecZlib]]
|
||||
deps = ["TranscodingStreams", "Zlib_jll"]
|
||||
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
|
||||
git-tree-sha1 = "970758a3d591a2a5c2a907c53f2e2f8c1b1d3537"
|
||||
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||
version = "0.7.8"
|
||||
version = "0.7.9"
|
||||
|
||||
[[deps.CodecZstd]]
|
||||
deps = ["TranscodingStreams", "Zstd_jll"]
|
||||
git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56"
|
||||
uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2"
|
||||
version = "0.8.7"
|
||||
|
||||
[[deps.CommonSolve]]
|
||||
git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662"
|
||||
deps = ["PrecompileTools"]
|
||||
git-tree-sha1 = "6c389fa857f6ca5a95474b52a52023fd77f24cb7"
|
||||
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
||||
version = "0.2.11"
|
||||
version = "0.2.14"
|
||||
|
||||
[[deps.Compat]]
|
||||
deps = ["TOML", "UUIDs"]
|
||||
@@ -119,7 +156,7 @@ version = "0.1.1"
|
||||
[[deps.CompilerSupportLibraries_jll]]
|
||||
deps = ["Artifacts", "Libdl"]
|
||||
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
|
||||
version = "1.3.0+1"
|
||||
version = "1.3.1+2"
|
||||
|
||||
[[deps.CompositionsBase]]
|
||||
git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad"
|
||||
@@ -130,6 +167,12 @@ weakdeps = ["InverseFunctions"]
|
||||
[deps.CompositionsBase.extensions]
|
||||
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
|
||||
|
||||
[[deps.ConcurrentUtilities]]
|
||||
deps = ["Serialization", "Sockets"]
|
||||
git-tree-sha1 = "3c9be947934c38475bafe822c6d61aaed17f0738"
|
||||
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
|
||||
version = "2.6.0"
|
||||
|
||||
[[deps.ConstructionBase]]
|
||||
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
|
||||
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
|
||||
@@ -146,14 +189,14 @@ version = "1.6.0"
|
||||
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
|
||||
|
||||
[[deps.Crayons]]
|
||||
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
|
||||
git-tree-sha1 = "54b76cbb40d9a0f5368c880725b2f141da77c94f"
|
||||
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
|
||||
version = "4.1.1"
|
||||
version = "4.2.0"
|
||||
|
||||
[[deps.DBInterface]]
|
||||
git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b"
|
||||
git-tree-sha1 = "265d7f87f329c063cbde41920f9d00713a43cedd"
|
||||
uuid = "a10d1c49-ce27-4219-8d33-6db1a4562965"
|
||||
version = "2.6.1"
|
||||
version = "2.7.0"
|
||||
|
||||
[[deps.DataAPI]]
|
||||
git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe"
|
||||
@@ -168,9 +211,9 @@ version = "1.8.2"
|
||||
|
||||
[[deps.DataStructures]]
|
||||
deps = ["OrderedCollections"]
|
||||
git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e"
|
||||
git-tree-sha1 = "b0bc6d2cad1fed8b7fd59a1551a991cb3d2809e6"
|
||||
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||
version = "0.19.5"
|
||||
version = "0.19.6"
|
||||
|
||||
[[deps.DataValueInterfaces]]
|
||||
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
|
||||
@@ -208,9 +251,9 @@ version = "1.11.0"
|
||||
|
||||
[[deps.Distributions]]
|
||||
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
|
||||
git-tree-sha1 = "cd3c5ac74cd3923c8945c6a81518c46abd0e73a3"
|
||||
git-tree-sha1 = "a958ab3a40c755563f5e1405c0846cb0446bf19d"
|
||||
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||
version = "0.25.129"
|
||||
version = "0.25.131"
|
||||
|
||||
[deps.Distributions.extensions]
|
||||
DistributionsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -240,9 +283,9 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
|
||||
version = "1.0.7"
|
||||
|
||||
[[deps.ExprTools]]
|
||||
git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec"
|
||||
git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4"
|
||||
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
|
||||
version = "0.1.10"
|
||||
version = "0.1.11"
|
||||
|
||||
[[deps.EzXML]]
|
||||
deps = ["Printf", "XML2_jll"]
|
||||
@@ -250,16 +293,6 @@ git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3"
|
||||
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
|
||||
version = "1.2.3"
|
||||
|
||||
[[deps.FileIO]]
|
||||
deps = ["Pkg", "Requires", "UUIDs"]
|
||||
git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819"
|
||||
uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
|
||||
version = "1.20.0"
|
||||
weakdeps = ["HTTP"]
|
||||
|
||||
[deps.FileIO.extensions]
|
||||
HTTPExt = "HTTP"
|
||||
|
||||
[[deps.FilePathsBase]]
|
||||
deps = ["Compat", "Dates"]
|
||||
git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2"
|
||||
@@ -277,9 +310,9 @@ version = "1.11.0"
|
||||
|
||||
[[deps.FillArrays]]
|
||||
deps = ["LinearAlgebra"]
|
||||
git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3"
|
||||
git-tree-sha1 = "5bad39456d9f0166184fce2248783dd9862645c1"
|
||||
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"]
|
||||
|
||||
[deps.FillArrays.extensions]
|
||||
@@ -294,17 +327,24 @@ uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.Gamma]]
|
||||
git-tree-sha1 = "86f86b6168a016ed88e4ae4e64577b98c3b59e8e"
|
||||
deps = ["LogExpFunctions"]
|
||||
git-tree-sha1 = "becc397f7cfb06e343496ae6ffb04818a851da51"
|
||||
uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
|
||||
[[deps.GeneralUtils]]
|
||||
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"]
|
||||
git-tree-sha1 = "93293126d24d3929ef6a5067f347bc28c6582c71"
|
||||
deps = ["AWS", "AWSS3", "CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "OrderedCollections", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "URIs", "UUIDs"]
|
||||
git-tree-sha1 = "4d7ff3b02a9cbd6d4f65fea43551b0bca6a83a22"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
|
||||
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||
version = "0.5.10"
|
||||
version = "0.6.9"
|
||||
|
||||
[[deps.GitHub]]
|
||||
deps = ["Base64", "Dates", "HTTP", "JSON", "MbedTLS", "Sockets", "SodiumSeal", "URIs"]
|
||||
git-tree-sha1 = "33de1bc5665494f0a9669fd6027e28180709d874"
|
||||
uuid = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26"
|
||||
version = "5.13.0"
|
||||
|
||||
[[deps.Graphs]]
|
||||
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
|
||||
@@ -321,9 +361,9 @@ version = "1.14.0"
|
||||
|
||||
[[deps.HTTP]]
|
||||
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
|
||||
git-tree-sha1 = "c2c808326222b6dc4bec295a83b55f79aeec98e0"
|
||||
git-tree-sha1 = "49a90cd540681d5b79bf5c8ca13c92677e37005e"
|
||||
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||
version = "2.5.5"
|
||||
version = "2.6.6"
|
||||
|
||||
[[deps.HashArrayMappedTries]]
|
||||
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
|
||||
@@ -332,9 +372,9 @@ version = "0.2.0"
|
||||
|
||||
[[deps.HypergeometricFunctions]]
|
||||
deps = ["Gamma", "LinearAlgebra"]
|
||||
git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10"
|
||||
git-tree-sha1 = "31bb6c92405c084617facc1d7ed9eb6c402d061e"
|
||||
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
|
||||
[[deps.ICU_jll]]
|
||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||
@@ -353,6 +393,11 @@ git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d"
|
||||
uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9"
|
||||
version = "0.1.5"
|
||||
|
||||
[[deps.IniFile]]
|
||||
git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625"
|
||||
uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f"
|
||||
version = "0.5.1"
|
||||
|
||||
[[deps.InlineStrings]]
|
||||
git-tree-sha1 = "8f3d257792a522b4601c24a577954b0a8cd7334d"
|
||||
uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48"
|
||||
@@ -412,9 +457,9 @@ version = "1.8.0"
|
||||
|
||||
[[deps.JSON]]
|
||||
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
|
||||
git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211"
|
||||
git-tree-sha1 = "c7345ab1a7ca4dc8a02c9f6510da0d9857bbe513"
|
||||
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
version = "1.6.1"
|
||||
version = "1.7.1"
|
||||
weakdeps = ["ArrowTypes"]
|
||||
|
||||
[deps.JSON.extensions]
|
||||
@@ -432,9 +477,9 @@ weakdeps = ["ArrowTypes"]
|
||||
|
||||
[[deps.JuliaInterpreter]]
|
||||
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
|
||||
git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006"
|
||||
git-tree-sha1 = "c3d401f110454b4ea24a76be33f6ee0d7d385103"
|
||||
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
|
||||
version = "0.10.12"
|
||||
version = "0.11.4"
|
||||
|
||||
[[deps.JuliaSyntaxHighlighting]]
|
||||
deps = ["StyledStrings"]
|
||||
@@ -447,18 +492,10 @@ git-tree-sha1 = "0f2899fdadaab4b8f57db558ba21bdb4fb52f1f0"
|
||||
uuid = "b39eb1a6-c29a-53d7-8c32-632cd16f18da"
|
||||
version = "1.21.3+0"
|
||||
|
||||
[[deps.LLMMCTS]]
|
||||
deps = ["GeneralUtils", "JSON", "PrettyPrinting"]
|
||||
git-tree-sha1 = "3dff98131dfa79be8c9bd84fc51cb0ba1832c472"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/LLMMCTS"
|
||||
uuid = "d76c5a4d-449e-4835-8cc4-dd86ec44f241"
|
||||
version = "0.1.5"
|
||||
|
||||
[[deps.LaTeXStrings]]
|
||||
git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c"
|
||||
git-tree-sha1 = "f88f3ccef05a6a72a0cf0ed417c8fd68530f4ab2"
|
||||
uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
|
||||
[[deps.LayerDicts]]
|
||||
git-tree-sha1 = "6087ad3521d6278ebe5c27ae55e7bbb15ca312cb"
|
||||
@@ -539,9 +576,21 @@ version = "1.11.0"
|
||||
|
||||
[[deps.LoweredCodeUtils]]
|
||||
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
|
||||
git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f"
|
||||
git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
|
||||
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
|
||||
version = "3.6.2"
|
||||
version = "3.8.0"
|
||||
|
||||
[[deps.Lz4_jll]]
|
||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||
git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33"
|
||||
uuid = "5ced341a-0733-55b8-9ab6-a4889d929147"
|
||||
version = "1.10.1+0"
|
||||
|
||||
[[deps.MD5]]
|
||||
deps = ["Random", "SHA"]
|
||||
git-tree-sha1 = "1576f756617d31eb397a4a517b68562fd28dc2b4"
|
||||
uuid = "6ac74813-4b46-53a4-afec-0b5dc9d7885c"
|
||||
version = "0.2.3"
|
||||
|
||||
[[deps.MacroTools]]
|
||||
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
||||
@@ -599,9 +648,9 @@ version = "0.1.1"
|
||||
|
||||
[[deps.NanoDates]]
|
||||
deps = ["Dates", "Parsers"]
|
||||
git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f"
|
||||
git-tree-sha1 = "77c7e98ca39aefb481f9b97a2f4f5c5471c08a1d"
|
||||
uuid = "46f1a544-deae-4307-8689-c12aa3c955c6"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
|
||||
[[deps.NetworkOptions]]
|
||||
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
|
||||
@@ -631,7 +680,7 @@ version = "0.8.7+0"
|
||||
[[deps.OpenSSL_jll]]
|
||||
deps = ["Artifacts", "Libdl"]
|
||||
uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
|
||||
version = "3.5.4+0"
|
||||
version = "3.5.6+0"
|
||||
|
||||
[[deps.OpenSpecFun_jll]]
|
||||
deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"]
|
||||
@@ -640,15 +689,15 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
|
||||
version = "0.5.6+0"
|
||||
|
||||
[[deps.OrderedCollections]]
|
||||
git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4"
|
||||
git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c"
|
||||
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
|
||||
version = "1.8.2"
|
||||
version = "2.0.1"
|
||||
|
||||
[[deps.PDMats]]
|
||||
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
|
||||
git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65"
|
||||
git-tree-sha1 = "123266c25174ef6c8d4718920abc206452cf8de6"
|
||||
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
|
||||
version = "0.11.40"
|
||||
version = "0.11.41"
|
||||
weakdeps = ["StatsBase"]
|
||||
|
||||
[deps.PDMats.extensions]
|
||||
@@ -656,18 +705,9 @@ weakdeps = ["StatsBase"]
|
||||
|
||||
[[deps.Parsers]]
|
||||
deps = ["Dates", "PrecompileTools", "UUIDs"]
|
||||
git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9"
|
||||
git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3"
|
||||
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
|
||||
version = "2.8.6"
|
||||
|
||||
[[deps.Pkg]]
|
||||
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
|
||||
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
|
||||
version = "1.12.1"
|
||||
weakdeps = ["REPL"]
|
||||
|
||||
[deps.Pkg.extensions]
|
||||
REPLExt = "REPL"
|
||||
version = "2.8.7"
|
||||
|
||||
[[deps.PooledArrays]]
|
||||
deps = ["DataAPI", "Future"]
|
||||
@@ -688,15 +728,15 @@ uuid = "21216c6a-2e73-6563-6e65-726566657250"
|
||||
version = "1.5.2"
|
||||
|
||||
[[deps.PrettyPrinting]]
|
||||
git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e"
|
||||
git-tree-sha1 = "0b7f4ad437e31c51cf5b91fb103579b04025170a"
|
||||
uuid = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
|
||||
[[deps.PrettyTables]]
|
||||
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
|
||||
git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325"
|
||||
git-tree-sha1 = "1b8aa19f229b1cea7fc93874a52e49db6a854450"
|
||||
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
|
||||
version = "3.4.0"
|
||||
version = "3.4.8"
|
||||
|
||||
[deps.PrettyTables.extensions]
|
||||
PrettyTablesExcelExt = "XLSX"
|
||||
@@ -757,15 +797,20 @@ version = "1.3.1"
|
||||
|
||||
[[deps.Reseau]]
|
||||
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
|
||||
git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5"
|
||||
git-tree-sha1 = "aaee74fab545e9af4b21d10c2203c93c636c4ae0"
|
||||
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
|
||||
version = "1.3.1"
|
||||
version = "1.4.1"
|
||||
|
||||
[[deps.Retry]]
|
||||
git-tree-sha1 = "41ac127cd281bb33e42aba46a9d3b25cd35fc6d5"
|
||||
uuid = "20febd7b-183b-5ae2-ac4a-720e7ce64774"
|
||||
version = "0.4.1"
|
||||
|
||||
[[deps.Revise]]
|
||||
deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
|
||||
git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7"
|
||||
deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
|
||||
git-tree-sha1 = "ab0f5630e37968bc49b8c4cedd3279a7d30b9486"
|
||||
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
version = "3.15.1"
|
||||
version = "3.17.0"
|
||||
weakdeps = ["Distributed"]
|
||||
|
||||
[deps.Revise.extensions]
|
||||
@@ -779,15 +824,15 @@ version = "0.9.0"
|
||||
|
||||
[[deps.Rmath_jll]]
|
||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||
git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8"
|
||||
git-tree-sha1 = "6d40b2fe70437b01397d2a4d5b020008da4e7019"
|
||||
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
|
||||
version = "0.5.1+0"
|
||||
version = "0.5.2+0"
|
||||
|
||||
[[deps.Roots]]
|
||||
deps = ["Accessors", "CommonSolve", "Printf"]
|
||||
git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455"
|
||||
git-tree-sha1 = "13a9e0164267bb9ebc55c1c5d72fceea8f09555b"
|
||||
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
|
||||
version = "3.0.5"
|
||||
version = "3.0.7"
|
||||
|
||||
[deps.Roots.extensions]
|
||||
RootsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -809,14 +854,6 @@ version = "3.0.5"
|
||||
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
|
||||
version = "0.7.0"
|
||||
|
||||
[[deps.SQLLLM]]
|
||||
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "FileIO", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "PrettyPrinting", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"]
|
||||
git-tree-sha1 = "bae2fd2e2b087753fbb3415896be41df1ae0eb90"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/SQLLLM"
|
||||
uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
||||
version = "0.2.8"
|
||||
|
||||
[[deps.SQLStrings]]
|
||||
git-tree-sha1 = "55de0530689832b1d3d43491ee6b67bd54d3323c"
|
||||
uuid = "af517c2e-c243-48fa-aab8-efac3db270f5"
|
||||
@@ -840,12 +877,6 @@ git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80"
|
||||
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
|
||||
version = "1.4.10"
|
||||
|
||||
[[deps.Serde]]
|
||||
deps = ["CSV", "Dates", "EzXML", "JSON", "TOML", "UUIDs", "YAML"]
|
||||
git-tree-sha1 = "f397fc8779cc53e4677c2708f3802c6996f28d00"
|
||||
uuid = "db9b398d-9517-45f8-9a95-92af99003e0e"
|
||||
version = "3.7.2"
|
||||
|
||||
[[deps.Serialization]]
|
||||
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||
version = "1.11.0"
|
||||
@@ -866,6 +897,12 @@ git-tree-sha1 = "907703e0d50846f300650d7225bdcab145b7bca9"
|
||||
uuid = "4f5b5e99-b0ad-42cd-b47a-334e172ec8bd"
|
||||
version = "1.1.2"
|
||||
|
||||
[[deps.SodiumSeal]]
|
||||
deps = ["Base64", "Libdl", "libsodium_jll"]
|
||||
git-tree-sha1 = "80cef67d2953e33935b41c6ab0a178b9987b1c99"
|
||||
uuid = "2133526b-2bfb-4018-ac12-889fb3908a75"
|
||||
version = "0.1.1"
|
||||
|
||||
[[deps.SortingAlgorithms]]
|
||||
deps = ["DataStructures"]
|
||||
git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a"
|
||||
@@ -879,9 +916,9 @@ version = "1.12.0"
|
||||
|
||||
[[deps.SpecialFunctions]]
|
||||
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
|
||||
git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f"
|
||||
git-tree-sha1 = "429071b23f4c9a13fb6582f807cc2ef454082408"
|
||||
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
|
||||
[deps.SpecialFunctions.extensions]
|
||||
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -891,9 +928,9 @@ version = "2.8.0"
|
||||
|
||||
[[deps.StaticArrays]]
|
||||
deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"]
|
||||
git-tree-sha1 = "246a8bb2e6667f832eea063c3a56aef96429a3db"
|
||||
git-tree-sha1 = "fac51faf3bb96e8bc0bf6f9f39ca4955652776bb"
|
||||
uuid = "90137ffa-7385-5640-81b9-e52037218182"
|
||||
version = "1.9.18"
|
||||
version = "1.9.19"
|
||||
|
||||
[deps.StaticArrays.extensions]
|
||||
StaticArraysChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -910,9 +947,9 @@ version = "1.4.4"
|
||||
|
||||
[[deps.Statistics]]
|
||||
deps = ["LinearAlgebra"]
|
||||
git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0"
|
||||
git-tree-sha1 = "389592b8592e5fb48f498ff60dc6acb4a0e62953"
|
||||
uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||
version = "1.11.1"
|
||||
version = "1.11.4"
|
||||
weakdeps = ["SparseArrays"]
|
||||
|
||||
[deps.Statistics.extensions]
|
||||
@@ -926,15 +963,15 @@ version = "1.8.0"
|
||||
|
||||
[[deps.StatsBase]]
|
||||
deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"]
|
||||
git-tree-sha1 = "e4d7a1a0edc20af42689ea6f4f3587a2175d50ee"
|
||||
git-tree-sha1 = "adb9da019510162e67a4493fc235c23203d8b09e"
|
||||
uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
|
||||
version = "0.34.12"
|
||||
version = "0.34.13"
|
||||
|
||||
[[deps.StatsFuns]]
|
||||
deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
|
||||
git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5"
|
||||
git-tree-sha1 = "91a5737baed20ee31f3faea0e51f57461f6a689e"
|
||||
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
|
||||
[deps.StatsFuns.extensions]
|
||||
StatsFunsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -946,21 +983,20 @@ version = "2.2.0"
|
||||
|
||||
[[deps.StringDistances]]
|
||||
deps = ["Distances", "StatsAPI"]
|
||||
git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a"
|
||||
git-tree-sha1 = "456f343c7a6c8f52d8b501c0da2a05538ab1b907"
|
||||
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
|
||||
version = "1.0.0"
|
||||
|
||||
[[deps.StringEncodings]]
|
||||
deps = ["Libiconv_jll"]
|
||||
git-tree-sha1 = "b765e46ba27ecf6b44faf70df40c57aa3a547dcb"
|
||||
uuid = "69024149-9ee7-55f6-a4c4-859efe599b68"
|
||||
version = "0.3.7"
|
||||
version = "1.0.1"
|
||||
|
||||
[[deps.StringManipulation]]
|
||||
deps = ["PrecompileTools"]
|
||||
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
|
||||
git-tree-sha1 = "773065c6e0e903924a9d838259be74338422aef2"
|
||||
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
|
||||
[[deps.StringViews]]
|
||||
git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8"
|
||||
uuid = "354b36f9-a18e-4713-926e-db85100087ba"
|
||||
version = "1.3.7"
|
||||
|
||||
[[deps.StructTypes]]
|
||||
deps = ["Dates", "UUIDs"]
|
||||
@@ -970,9 +1006,9 @@ version = "1.11.0"
|
||||
|
||||
[[deps.StructUtils]]
|
||||
deps = ["Dates", "UUIDs"]
|
||||
git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
|
||||
git-tree-sha1 = "2d0fc55c61321ba245c47be599570d11bac50303"
|
||||
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
|
||||
version = "2.8.2"
|
||||
version = "2.8.5"
|
||||
|
||||
[deps.StructUtils.extensions]
|
||||
StructUtilsMeasurementsExt = ["Measurements"]
|
||||
@@ -997,6 +1033,12 @@ deps = ["Artifacts", "Libdl", "libblastrampoline_jll"]
|
||||
uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c"
|
||||
version = "7.8.3+2"
|
||||
|
||||
[[deps.SymDict]]
|
||||
deps = ["Test"]
|
||||
git-tree-sha1 = "0108ccdaea3ef69d9680eeafc8d5ad198b896ec8"
|
||||
uuid = "2da68c74-98d7-5633-99d6-8493888d7b1e"
|
||||
version = "0.3.0"
|
||||
|
||||
[[deps.TOML]]
|
||||
deps = ["Dates"]
|
||||
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
|
||||
@@ -1004,9 +1046,9 @@ version = "1.0.3"
|
||||
|
||||
[[deps.TZJData]]
|
||||
deps = ["Artifacts"]
|
||||
git-tree-sha1 = "72df96b3a595b7aab1e101eb07d2a435963a97e2"
|
||||
git-tree-sha1 = "d3b30a9f29a898e21fb4bdf280101b7a12e1f39c"
|
||||
uuid = "dc5dba14-91b3-4cab-a142-028a31da12f7"
|
||||
version = "1.5.0+2025b"
|
||||
version = "1.5.1+2025b"
|
||||
|
||||
[[deps.TableTraits]]
|
||||
deps = ["IteratorInterfaceExtensions"]
|
||||
@@ -1016,14 +1058,9 @@ version = "1.0.1"
|
||||
|
||||
[[deps.Tables]]
|
||||
deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"]
|
||||
git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0"
|
||||
git-tree-sha1 = "a94d9bdda1b7bed0046cea645639ab3f62196fac"
|
||||
uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
|
||||
version = "1.13.0"
|
||||
|
||||
[[deps.Tar]]
|
||||
deps = ["ArgTools", "SHA"]
|
||||
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
|
||||
version = "1.10.0"
|
||||
version = "1.14.0"
|
||||
|
||||
[[deps.Test]]
|
||||
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
|
||||
@@ -1046,9 +1083,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
|
||||
version = "0.11.3"
|
||||
|
||||
[[deps.URIs]]
|
||||
git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a"
|
||||
git-tree-sha1 = "908fec9df6c5de98548ead82a468c95ccf6cd263"
|
||||
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
|
||||
[[deps.UTCDateTimes]]
|
||||
deps = ["Dates", "TimeZones"]
|
||||
@@ -1082,17 +1119,17 @@ git-tree-sha1 = "3f3315d89fc954a28f5b471bce698ed6e27481be"
|
||||
uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a"
|
||||
version = "2.15.3+0"
|
||||
|
||||
[[deps.YAML]]
|
||||
deps = ["Base64", "Dates", "Printf", "StringEncodings"]
|
||||
git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403"
|
||||
uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
|
||||
version = "0.4.16"
|
||||
[[deps.XMLDict]]
|
||||
deps = ["EzXML", "IterTools", "OrderedCollections"]
|
||||
git-tree-sha1 = "8e8cdc9c93e1f442eba471b56db6f86ffeb1a33a"
|
||||
uuid = "228000da-037f-5747-90a9-8195ccbf91a5"
|
||||
version = "0.4.2"
|
||||
|
||||
[[deps.YiemAgent]]
|
||||
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serde", "Serialization", "URIs", "UUIDs"]
|
||||
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "Serialization", "URIs", "UUIDs", "msghandler"]
|
||||
path = "."
|
||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
|
||||
[[deps.Zlib_jll]]
|
||||
deps = ["Libdl"]
|
||||
@@ -1116,6 +1153,14 @@ git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026"
|
||||
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
|
||||
version = "1.0.21+0"
|
||||
|
||||
[[deps.msghandler]]
|
||||
deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "UUIDs"]
|
||||
git-tree-sha1 = "e7889a120327db84cdf1b1acf4a2c7caba6e8ef7"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/msghandler"
|
||||
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||
version = "1.4.0"
|
||||
|
||||
[[deps.nghttp2_jll]]
|
||||
deps = ["Artifacts", "Libdl"]
|
||||
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
||||
|
||||
+3
-7
@@ -12,26 +12,22 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
||||
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241"
|
||||
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
|
||||
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
|
||||
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
||||
Serde = "db9b398d-9517-45f8-9a95-92af99003e0e"
|
||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||
|
||||
[compat]
|
||||
Base64 = "1.11.0"
|
||||
CSV = "0.10.15"
|
||||
DataFrames = "1.7.0"
|
||||
GeneralUtils = "0.5.10"
|
||||
GeneralUtils = "0.6.9"
|
||||
HTTP = "2.4.0"
|
||||
JSON = "1.6.1"
|
||||
LLMMCTS = "0.1.5"
|
||||
NATS = "0.1.0"
|
||||
SQLLLM = "0.2.8"
|
||||
Serde = "3.7.2"
|
||||
msghandler = "1.4.0"
|
||||
|
||||
@@ -1,23 +1,311 @@
|
||||
# YiemAgent
|
||||
|
||||
## TODO
|
||||
- [WORKING] build prompt()
|
||||
- [ ] build agent runLoop()
|
||||
- [ ] build MCP server connector
|
||||
- [ ] executeplan() to execute the plan
|
||||
- [ ] add comprehensive tests
|
||||
Julia framework for building agents with tool use and MCP (Model Context Protocol) support.
|
||||
|
||||
## Changelog
|
||||
## Quick Start
|
||||
|
||||
### Version 0.8.0
|
||||
- Converted snake_case fields to camelCase:
|
||||
- `llmModel`: `base_url` → `baseUrl`, `context_window` → `contextWindow`, `max_tokens` → `maxTokens`
|
||||
- Converted PascalCase type references to camelCase:
|
||||
- `AgentState` → `agentState`
|
||||
- `AgentTool` → `agentTool`
|
||||
- `AgentMessage` → `agentMessage`
|
||||
- `PendingMessageQueue` → `pendingMessageQueue`
|
||||
- `ActiveRun` → `activeRun`
|
||||
- `StreamFn` → `streamFn`
|
||||
- `ThinkingLevel` → `thinkingLevel`
|
||||
- `ToolExecutionMode` → `toolExecutionMode`
|
||||
```julia
|
||||
using YiemAgent
|
||||
|
||||
# 1. Create an LLM callable (see "Custom LLM Callable" below)
|
||||
llmCall = ...
|
||||
|
||||
# 2. Create a callable MCP server (see "MCP Server Integration" below)
|
||||
mcpServer = ...
|
||||
|
||||
# 3. Create the agent
|
||||
agent = yiemAgent(llmCall; mcpServer=mcpServer, eventSink=println)
|
||||
|
||||
# 4. Interact
|
||||
promptAgent(agent, "What tools do you have?")
|
||||
response = takeResponse(agent)
|
||||
|
||||
# 5. Stop when done
|
||||
stopAgent(agent)
|
||||
```
|
||||
|
||||
## Agent API
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `promptAgent(agent, text)` | Send plain text to the agent's input channel |
|
||||
| `promptAgent(agent, msg)` | Send a structured message (OpenAI format dict) to the input channel |
|
||||
| `takeResponse(agent)` | Block and take the agent's response from output channel |
|
||||
| `followUp(agent, text)` | Send plain text follow-up while agent is still processing |
|
||||
| `followUp(agent, msg)` | Send a structured follow-up (OpenAI format dict) while agent is processing |
|
||||
| `stopAgent(agent)` | Gracefully stop the agent and close channels |
|
||||
|
||||
## Creating a yiemAgent
|
||||
|
||||
```julia
|
||||
yiemAgent(llmCall;
|
||||
systemPrompt::String = "You are a helpful assistant.",
|
||||
model = nothing,
|
||||
messages::Vector{agentMessage} = agentMessage[],
|
||||
prepareContext::Function = prepareContext,
|
||||
formatMsgForLLM::Function = formatMsgForLLM,
|
||||
beforeToolCall::Function = beforeToolCall,
|
||||
afterToolCall::Function = afterToolCall,
|
||||
sessionId::Union{String, Nothing} = nothing,
|
||||
maxRetryDelayMs::Union{Int64, Nothing} = nothing,
|
||||
parallelToolExecute::Bool = false,
|
||||
eventSink::Function = eventSink,
|
||||
mcpServer = nothing
|
||||
)
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `llmCall` | **(required)** Function that receives formatted messages and returns an LLM response |
|
||||
| `systemPrompt` | System prompt for the agent |
|
||||
| `model` | LLM model configuration (`llmModel` struct) |
|
||||
| `messages` | Initial conversation messages |
|
||||
| `prepareContext` | Hook to transform messages/context before each LLM call |
|
||||
| `formatMsgForLLM` | Hook to convert agent context to LLM API format |
|
||||
| `beforeToolCall` | Hook to block/allow tool execution |
|
||||
| `afterToolCall` | Hook to post-process tool results |
|
||||
| `sessionId` | Optional session identifier for MCP server |
|
||||
| `maxRetryDelayMs` | Maximum retry delay in milliseconds |
|
||||
| `parallelToolExecute` | Run multiple tool calls in parallel |
|
||||
| `eventSink` | Callback for agent events/debug messages |
|
||||
| `mcpServer` | Callable struct for MCP server communication |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Text Messages
|
||||
|
||||
```julia
|
||||
# Send plain text
|
||||
promptAgent(agent, "What tools do you have?")
|
||||
response = takeResponse(agent)
|
||||
|
||||
# Follow-up while agent is processing
|
||||
promptAgent(agent, "Tell me about wine")
|
||||
followUp(agent, "Also include prices")
|
||||
response = takeResponse(agent)
|
||||
```
|
||||
|
||||
### Structured Messages (OpenAI Format)
|
||||
|
||||
```julia
|
||||
promptAgent(agent, Dict("role" => "user", "content" => "Hello"))
|
||||
response = takeResponse(agent)
|
||||
```
|
||||
|
||||
### Output Format
|
||||
|
||||
`takeResponse()` returns a `Dict{String, Any}` in OpenAI message format:
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "assistant",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "The weather in Tokyo is Sunny, 22°C."),
|
||||
# ... more content blocks
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Content block types:
|
||||
|
||||
| Block Type | Fields | Description |
|
||||
|------------|--------|-------------|
|
||||
| `text` | `"type" => "text"`, `"text" => "..."` | Plain text response from the LLM |
|
||||
| `image_url` | `"type" => "image_url"`, `"image_url" => Dict("url" => "...")` | Image content (rare, used by multimodal models) |
|
||||
|
||||
Example — extracting the text response:
|
||||
|
||||
```julia
|
||||
response = takeResponse(agent)
|
||||
text_response = first(c["text"] for c in response["content"] if c["type"] == "text")
|
||||
```
|
||||
|
||||
### Multimodal Messages (Text + Image)
|
||||
|
||||
```julia
|
||||
# Encode images to data URIs
|
||||
image_path = "test/image.png"
|
||||
image_bytes = read(image_path)
|
||||
base64_string = base64encode(image_bytes)
|
||||
data_uri = "data:image/png;base64,$(base64_string)"
|
||||
|
||||
# Build message with text and image content blocks
|
||||
message = Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "Do you have Brunello di Montalcino?"),
|
||||
Dict("type" => "image_url", "image_url" => Dict("url" => data_uri))
|
||||
]
|
||||
)
|
||||
promptAgent(agent, message)
|
||||
response = takeResponse(agent)
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```julia
|
||||
# beforeToolCall — request user approval for destructive tools
|
||||
function my_before_tool_call(context::beforeToolCallContext, signal::abortSignal)::beforeToolCallResult
|
||||
if context.toolCall.name == "deleteFile"
|
||||
return beforeToolCallResult(true, "User must approve file deletion")
|
||||
end
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
|
||||
# afterToolCall — mask sensitive data from results
|
||||
function my_after_tool_call(context::afterToolCallContext, signal::abortSignal)::Union{agentToolResult, Nothing}
|
||||
if context.isError
|
||||
return agentToolResult(
|
||||
[textContent("The tool encountered an error. Please try again.")],
|
||||
context.result.details, context.result.usage, context.result.terminate
|
||||
)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
agent = yiemAgent(llmCall;
|
||||
mcpServer=mcpServer,
|
||||
beforeToolCall=my_before_tool_call,
|
||||
afterToolCall=my_after_tool_call,
|
||||
eventSink=println
|
||||
)
|
||||
```
|
||||
|
||||
### Custom LLM Callable
|
||||
|
||||
```julia
|
||||
function my_llm_call(messages_dict::Dict{String, Any})
|
||||
# messages_dict contains "messages" key with OpenAI-format message array,
|
||||
# plus optional "tools", "temperature", etc.
|
||||
|
||||
# Call your LLM provider (OpenAI, Anthropic, local model, etc.)
|
||||
raw_response = call_my_llm_provider(messages_dict)
|
||||
|
||||
# Return a dict with these fields:
|
||||
return Dict(
|
||||
"message" => Dict("role" => "assistant", "content" => raw_response["content"]),
|
||||
"finish_reason" => raw_response["finish_reason"],
|
||||
"api" => "my_provider",
|
||||
"provider" => "my_provider",
|
||||
"model" => "my-model-name",
|
||||
"usage" => llmUsage(100, 50)
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Custom Context Preparation
|
||||
|
||||
```julia
|
||||
function my_prepare_context(state::agentState, eventSink)::agentContext
|
||||
sysPrompt = state.systemPrompt * "\nCurrent time: $(now())"
|
||||
msgs = deepcopy(state.messages)
|
||||
|
||||
# Prune old messages to fit context window
|
||||
max_messages = 20
|
||||
if length(msgs) > max_messages
|
||||
msgs = msgs[end-max_messages+1:end]
|
||||
end
|
||||
|
||||
return agentContext(sysPrompt, msgs, state.tools)
|
||||
end
|
||||
|
||||
agent = yiemAgent(llmCall; prepareContext=my_prepare_context, ...)
|
||||
```
|
||||
|
||||
## MCP Server Integration
|
||||
|
||||
YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement `tools/list` and `tools/call` methods.
|
||||
|
||||
### Protocol
|
||||
|
||||
```julia
|
||||
# tools/list — returns tool definitions
|
||||
mcpServer("tools/list")
|
||||
# → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...))
|
||||
|
||||
# tools/call — executes a tool
|
||||
mcpServer("tools/call", toolName, arguments)
|
||||
# → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...))
|
||||
|
||||
# Protocol error responses include "error" instead of "result":
|
||||
Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"..."))
|
||||
```
|
||||
|
||||
### Example: NATS-based MCP Server Client
|
||||
|
||||
```julia
|
||||
struct mcpServer
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
fileserver_url::String
|
||||
end
|
||||
|
||||
# tools/list implementation
|
||||
function (c::mcpServer)(method::String)
|
||||
payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}())
|
||||
payloads = [("payload", payload, "dictionary")]
|
||||
_, msg_envelope = 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, timeout=180)
|
||||
incoming = msghandler.smartunpack(String(reply.payload))
|
||||
return incoming["payloads"][1][2]
|
||||
end
|
||||
|
||||
# tools/call implementation
|
||||
function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any})
|
||||
payload = Dict("jsonrpc" => "2.0", "id" => 2, "method" => method,
|
||||
"params" => Dict("name" => toolName, "arguments" => arguments))
|
||||
payloads = [("payload", payload, "dictionary")]
|
||||
_, msg_envelope = 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, timeout=180)
|
||||
incoming = msghandler.smartunpack(String(reply.payload))
|
||||
return incoming["payloads"][1][2]
|
||||
end
|
||||
```
|
||||
|
||||
## Tool Discovery
|
||||
|
||||
Tools are discovered dynamically via the MCP server:
|
||||
|
||||
1. `listTools` is the only pre-registered tool at agent creation
|
||||
2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime
|
||||
3. Supports pagination via `nextCursor` for large tool sets
|
||||
4. New tools become immediately available without restarting the agent
|
||||
|
||||
## Hooks
|
||||
|
||||
Hooks allow customization of the agent's behavior at key points in the execution pipeline.
|
||||
|
||||
| Hook | Signature | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Transform messages/context before LLM call (filter tools, inject context, prune messages) |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | Convert agent context to LLM API message format |
|
||||
| `beforeToolCall` | `(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult` | Block/allow tool execution; request user approval |
|
||||
| `afterToolCall` | `(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}` | Post-process tool results; mask sensitive data; set `terminate` flag |
|
||||
| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages throughout the pipeline |
|
||||
|
||||
## Agent Lifecycle
|
||||
|
||||
1. **Creation** — `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel`
|
||||
2. **Input** — User messages enter via `promptAgent()` (immediate) or `followUp()` (queued while agent is busy)
|
||||
3. **Processing** — `_processMessage()` handles LLM calls, tool execution, and conversation history in a loop
|
||||
4. **Tool Execution Pipeline** — each tool call follows three phases:
|
||||
- `prepareToolCall` — resolve tool, validate arguments, run `beforeToolCall` hook
|
||||
- `executePreparedToolCall` — invoke `tool.execute()`
|
||||
- `finalizeExecutedToolCall` — run `afterToolCall` hook, construct `toolResultMessage`
|
||||
5. **Termination** — tool `execute` functions can set `terminate=true` to stop the agent loop
|
||||
6. **Shutdown** — `stopAgent()` sends a `:shutdown` signal, drains channels, and closes the agent
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── YiemAgent.jl # Module entry point
|
||||
├── type.jl # Core types (messages, tools, agent state, events)
|
||||
├── utils.jl # Message formatting, context preparation, validation
|
||||
├── toolRegistry.jl # Tool store, MCP discovery, listTools registration
|
||||
├── agentCore.jl # Agent loop, tool execution pipeline, yiemAgent struct
|
||||
└── api.jl # Public API (promptAgent, takeResponse, followUp, stopAgent)
|
||||
```
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"nats_server_info": {
|
||||
"description": "nats server",
|
||||
"url": "nats.yiem.cc"
|
||||
},
|
||||
"testingOrProduction": "testing",
|
||||
"agentId": "2b74b87a-5413-4fe2-a4d3-405891051680",
|
||||
"agentCentralConfigSubject": "/yiem/hq/agent/sommelier/backend/config/api/v1.1",
|
||||
"this_service_name": "agent_backend",
|
||||
"this_service_input_channel": {
|
||||
"mqtt": [
|
||||
"/yiem/hq/agent/sommpanion/backend/db/api_v1"
|
||||
],
|
||||
"nats": [
|
||||
"sommpanion.backend.agentbackend.v1.inbox"
|
||||
]
|
||||
},
|
||||
"agentRole": "sommelier",
|
||||
"organization": "yiem_hq",
|
||||
"externalservice": {
|
||||
"servicesloadbalancer": {
|
||||
"nats": "sommpanion.backend.servicesloadbalancer.v1.inbox"
|
||||
},
|
||||
"textembedding": {
|
||||
"url": "textembedding.api.v1"
|
||||
},
|
||||
"textimage_to_text_llm": {
|
||||
"url": "https://llmcoder.yiem.cc/v1/chat/completions",
|
||||
"modelname": "Qwen3.6-35B-A3B-UD-Q4_K_M"
|
||||
},
|
||||
"virtualWineCustomer_1": {
|
||||
"serviceSubject": "",
|
||||
"modelName": "qwen3:8b"
|
||||
},
|
||||
"sommpanion_db" : {
|
||||
"description": "A database connection info for LibPQ client",
|
||||
"url": "192.168.88.106:5432",
|
||||
"dbname": "winedb",
|
||||
"user": "admin",
|
||||
"password": "admin@Sommpanion_0.0"
|
||||
},
|
||||
"sommpanion_vectordb" : {
|
||||
"description": "A wine database connection info for LibPQ client",
|
||||
"url": "192.168.88.106:5433",
|
||||
"dbname": "vectordb",
|
||||
"user": "admin",
|
||||
"password": "admin@Sommpanion_0.0"
|
||||
},
|
||||
"fileserver": {
|
||||
"description": "temporary file server",
|
||||
"url": "https://fileserver.yiem.cc"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
# YiemAgent: Agile Agent Framework
|
||||
|
||||
## 1. Business Context & Success Metrics
|
||||
|
||||
### Business Goal
|
||||
|
||||
YiemAgent is a Julia-based AI agent framework that enables building chatbot assistants with tool-calling capabilities, model-agnostic LLM integration, and runtime tool discovery via MCP (Model Context Protocol). It provides an asynchronous agent loop with channel-based message passing, supporting both single-turn and multi-turn conversations with automatic tool call resolution.
|
||||
|
||||
### User Stories
|
||||
|
||||
- **US-001**: As a developer, I want to create an AI agent with a simple constructor so that I can quickly prototype chatbot functionality in Julia
|
||||
- **US-002**: As a developer, I want the agent to discover tools automatically from an MCP server so that I don't need to manually register tool APIs
|
||||
- **US-003**: As a developer, I want the agent to handle tool calls automatically in a loop so that the LLM can use tools without external orchestration
|
||||
- **US-004**: As a developer, I want to intercept tool calls with before/after hooks so that I can add user approval, logging, or result sanitization
|
||||
- **US-005**: As a developer, I want the framework to support multiple LLM models and providers through a pluggable `llmCall` interface so that I can swap models without changing agent logic
|
||||
|
||||
### KPIs & Targets
|
||||
|
||||
- **KPI-001**: Agent loop processes messages with <500ms overhead for sequential tool calls (validates US-003)
|
||||
- **KPI-002**: MCP tool discovery completes within 2 seconds for 50+ tools (validates US-002)
|
||||
- **KPI-003**: 100% of message types have OpenAI format conversion (validates US-001)
|
||||
|
||||
## 2. Technical Boundaries
|
||||
|
||||
### In Scope
|
||||
|
||||
- Julia module implementing an AI agent with asynchronous message processing
|
||||
- Channel-based input (user→agent), follow-up (async queue), and output (agent→user) interfaces
|
||||
- LLM model abstraction with tool call extraction from multiple response formats (OpenAI content blocks, LMStudio/vLLM format)
|
||||
- Tool registry with MCP server integration (tools/list, tools/call via JSON-RPC 2.0)
|
||||
- Agent loop with before/after tool call hooks for lifecycle management
|
||||
- Message type system with OpenAI format conversion utilities
|
||||
- Abort signal and termination flag support via tool implementations
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- GUI/CLI interface (exposed via channels only; UI layer is external)
|
||||
- Persistent chat history or long-term memory (TODO comments in code)
|
||||
- Multi-agent orchestration (single agent instance per `yiemAgent` struct)
|
||||
- Embedding generation or retrieval-augmented generation (RAG)
|
||||
- Natural language-to-code generation or execution sandboxes
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **Julia Standard Library**: `Dates`, `UUIDs`, `Base.Threads`
|
||||
- **External Packages**: `JSON`, `DataStructures`, `HTTP`, `PrettyPrinting`, `Serialization`, `DataFrames`
|
||||
- **Internal Package**: `GeneralUtils` (project-specific utility library)
|
||||
- **Optional**: `NATS` (MCP server communication), `LibPQ` (PostgreSQL)
|
||||
|
||||
### Deployment Constraints
|
||||
|
||||
- **NFR-001**: Library designed for embedded use in Docker containers (MicroK8s cluster, per `NFR-501` from template)
|
||||
- **NFR-002**: Must support Julia 1.9+ for module include/export semantics compatibility
|
||||
- **NFR-003**: Thread-safe channel operations; single-agent instance processes messages sequentially on background `@spawn` task
|
||||
|
||||
## 3. Functional Requirements (FR)
|
||||
|
||||
### FR-001: Agent Message Processing
|
||||
|
||||
The agent shall process user messages through an asynchronous loop that:
|
||||
- Accepts messages via `inputChannel` (capacity 16) and `followUpChannel` (capacity 32)
|
||||
- Prioritizes `inputChannel` messages over `followUpChannel` messages
|
||||
- Feeds responses to `outputChannel` (capacity 16) in OpenAI message format
|
||||
- Exits cleanly on `:shutdown` signal
|
||||
|
||||
**Traceability**: US-001, US-003
|
||||
|
||||
### FR-002: LLM Model Agnosticism
|
||||
|
||||
The agent shall accept a pluggable `llmCall` function that:
|
||||
- Receives formatted messages and returns an LLM response dict
|
||||
- The response dict may contain `message.tool_calls` (LMStudio format) or `content` blocks (OpenAI format)
|
||||
- The framework extracts tool calls regardless of response format
|
||||
|
||||
**Traceability**: US-005
|
||||
|
||||
### FR-003: Tool Call Execution Loop
|
||||
|
||||
The agent shall automatically execute tool calls returned by the LLM:
|
||||
- Extract tool calls via `_extractToolCalls()` which supports both `message.tool_calls` and `content[{type="tool_calls"}]` formats
|
||||
- Build `agentContext` via `prepareContext()` (pluggable hook)
|
||||
- Format messages via `formatMsgForLLM()` to OpenAI format
|
||||
- Extract tool calls → loop: execute tool calls → append results to history → call LLM again
|
||||
- Break loop when LLM response has no tool calls
|
||||
- Support `terminate` flag: if `toolResultBatch.terminate == true`, break loop and return final response
|
||||
|
||||
**Traceability**: US-003
|
||||
|
||||
### FR-004: Tool Discovery and Registration
|
||||
|
||||
The agent shall:
|
||||
- Query MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0) to discover tools
|
||||
- Support pagination via `nextCursor` field
|
||||
- Wrap each MCP tool definition as an `agentTool` with `execute` hook that calls `mcpserver("tools/call", name, args, sessionId)`
|
||||
- Register a builtin `listTools` tool that triggers runtime discovery and lists all available tools
|
||||
- Skip tools already registered (idempotent)
|
||||
|
||||
**Traceability**: US-002
|
||||
|
||||
### FR-005: Tool Call Lifecycle Hooks
|
||||
|
||||
The agent shall support two lifecycle hooks:
|
||||
- `beforeToolCall(beforeToolCallContext, signal)` — can block execution by returning `beforeToolCallResult(true, reason)`
|
||||
- `afterToolCall(afterToolCallContext)` — can transform result by returning modified `agentToolResult`
|
||||
- Errors from hooks are caught and converted to `immediateOutcome`/error tool result messages (never throw)
|
||||
|
||||
**Traceability**: US-004
|
||||
|
||||
### FR-006: Message Type Conversion
|
||||
|
||||
The agent shall:
|
||||
- Convert internal message types (`userMessage`, `assistantMessage`, `assistantMessageToolCall`, `toolResultMessage`) to OpenAI format via `_userMessageToOpenAI()`, `_assistantMessageToOpenAI()`, `_assistantMessageToolCallToOpenAI()`, `_toolResultMessageToOpenAI()`
|
||||
- Convert internal `messageContent` types (`textContent`, `imageContent`, `reasoningContent`) to OpenAI content blocks via `_messageContentToBlocks()`
|
||||
- Convert `agentTool` definitions to OpenAI function tool format via `_toolsToOpenAI()`
|
||||
|
||||
**Traceability**: US-001
|
||||
|
||||
## 4. Non-Functional Requirements (NFRs)
|
||||
|
||||
### 4.1 Performance & Scalability
|
||||
|
||||
- **NFR-101**: Agent loop message processing overhead <500ms per sequential tool call (measured KPI: KPI-001)
|
||||
- **NFR-102**: MCP tool discovery for 50+ tools completes within 2 seconds (measured KPI: KPI-002)
|
||||
- **NFR-103**: Channel buffers prevent unbounded memory growth — `inputChannel` (16), `followUpChannel` (32), `outputChannel` (16)
|
||||
|
||||
### 4.2 Availability & Reliability
|
||||
|
||||
- **NFR-201**: Agent loop catches all errors via `try/catch` in `_agentLoop()` and calls `eventSink(errMsg)` before exit (measured by event sink logging)
|
||||
- **NFR-202**: `prepareToolCall()` never throws errors — validation failures return `immediateOutcome` with error tool result
|
||||
|
||||
### 4.3 Privacy & Security
|
||||
|
||||
- **Data Classification**: Tool arguments and results may contain PII (user messages); MCP tool results treated as untrusted input
|
||||
- **No built-in auth/authz**: Responsibility of `mcpServer` implementation and `beforeToolCall` hook
|
||||
- **Input validation**: JSON schema validation via `validateToolArguments()` checks required fields only (tools need custom validation via `validateRequiredArgs` hook)
|
||||
|
||||
### 4.4 Observability & Telemetry
|
||||
|
||||
- **Required Logs**: `agentLoop` events (via `eventSink`) emit state machine steps (`_agentLoop 1` through `_agentLoop 6`), `_processMessage` steps (1 through 18), `prepareToolCall` steps (1 through 10)
|
||||
- **Critical Metrics**:
|
||||
- `agent._state.messages.length` — conversation history size (logged at each step)
|
||||
- `toolExecutionStart`/`toolExecutionEnd` events (via `toolExecStartEvent`, `toolExecEndEvent` structs)
|
||||
- **Retention**: No built-in log retention; agent is a library, does not manage external logging
|
||||
|
||||
## 5. Acceptance Conditions
|
||||
|
||||
- [ ] **FR-001**: Agent processes messages through channel system with priority ordering (inputChannel > followUpChannel)
|
||||
- [ ] **FR-002**: `llmCall` is pluggable; response formats supported include `message.tool_calls` dict and `content` array blocks
|
||||
- [ ] **FR-003**: Agent loops on tool calls until LLM returns no tool calls or terminate flag is set
|
||||
- [ ] **FR-004**: MCP tools are discovered via `tools/list` with pagination; all registered tools are callable via `tools/call`
|
||||
- [ ] **FR-005**: `beforeToolCall` hook can block; `afterToolCall` hook can transform result; errors return error tool result
|
||||
- [ ] **FR-006**: All message types (userMessage, assistantMessage, assistantMessageToolCall, toolResultMessage) convert to OpenAI format
|
||||
- [ ] **NFR-101**: Message processing overhead <500ms per sequential tool call
|
||||
- [ ] **NFR-102**: MCP discovery for 50+ tools completes <2 seconds
|
||||
- [ ] **NFR-201**: Agent loop catches errors and exits gracefully without panics
|
||||
@@ -0,0 +1,119 @@
|
||||
# Solution Design: YiemAgent Agent Framework
|
||||
|
||||
## 1. Problem Decomposition
|
||||
|
||||
| Problem | Description | User Impact |
|
||||
|---------|-------------|-------------|
|
||||
| P-001 | Developers need a chatbot framework in Julia that handles LLM calls and tool calls without writing boilerplate | Must write separate request/response handling, tool call parsing, and loop management code for each agent |
|
||||
| P-002 | Tool discovery is manual and repetitive | Must register every tool individually; no automatic discovery from MCP-compatible servers |
|
||||
| P-003 | Multiple LLM providers have different response formats | Must handle `message.tool_calls` (LMStudio/vLLM) vs `content` blocks (OpenAI API) separately |
|
||||
| P-004 | Tool execution needs lifecycle hooks for observability and control | No built-in before/after hooks for authorization, logging, or result sanitization |
|
||||
| P-005 | Agent state management is error-prone | Must handle message history, tool registration, and pending tool call tracking manually |
|
||||
|
||||
## 2. Solution Approach
|
||||
|
||||
**Approach**: "Build a channel-based agent system where the LLM talks to tools automatically, tools talk to MCP servers automatically, and you compose agents by injecting `llmCall` and `mcpServer` functions — the framework owns the loop, the state, and the format conversion."
|
||||
|
||||
**Key Principles**:
|
||||
- **Channel-based async processing**: Each agent owns three channels (`inputChannel`, `followUpChannel`, `outputChannel`); a background `@spawn` task runs the agent loop, processing messages in priority order
|
||||
- **Tool call loop**: The agent calls `llmCall()`, extracts tool calls, executes them (sequentially or in parallel), appends results, and calls `llmCall()` again — repeating until no tool calls remain
|
||||
- **Pluggable hooks**: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, and `llmCall` are all injectable functions — the framework provides sensible defaults, but you override what you need
|
||||
- **MCP-native tool discovery**: Tools are discovered at startup via MCP `tools/list` and can be discovered at runtime via the `listTools` builtin tool (idempotent registration with pagination support)
|
||||
- **Format-agnostic LLM responses**: `_extractToolCalls()` handles both OpenAI content blocks and LMStudio/vLLM nested message format, allowing the same agent to work with any model provider
|
||||
|
||||
## 3. Decision Rationale
|
||||
|
||||
| Decision ID | Decision | Rationale | Alternatives Rejected |
|
||||
|-------------|----------|-----------|----------------------|
|
||||
| SD-001 | Use channel-based async processing with `@spawn` task | Provides non-blocking message queues; `inputChannel` blocks if full (backpressure); `followUpChannel` allows async follow-ups while agent is busy | Threading with shared state — race conditions; callback-based async — hard to compose |
|
||||
| SD-002 | Background `_agentLoop` task runs message dispatch loop | Isolates message processing from construction; agent can be created and used concurrently | Synchronous agent.run(message) — blocks caller; manual task management — boilerplate |
|
||||
| SD-003 | `_processMessage` runs `llmCall()` in an inner loop (while loop) until no tool calls remain | Handles multi-turn tool use transparently — agent decides when to stop based on LLM response | External loop in caller code — duplicates agent loop logic; single-turn only — no multi-step tool use |
|
||||
| SD-004 | MCP server integration via callable struct `mcpServer(method, args...)` | MCP is model-agnostic protocol; callable struct allows type-stable single dispatch; supports NATS transport (struct has `natsConn`, `topic`, `senderID` fields) | Ad-hoc HTTP client for MCP — breaks NATS-only transports; tight coupling to specific transport |
|
||||
| SD-005 | Tool call lifecycle: `prepareToolCall()` → `executePreparedToolCall()` → `finalizeExecutedToolCall()` — three phases, never throw errors | Preparation validates args and runs `beforeToolCall` hook (can block); execution calls `tool.execute()`; finalization runs `afterToolCall` hook. Each phase returns a result type, not exceptions — agent loop catches all and builds error tool result | `try/catch` in loop — loses context; throwing errors breaks tool result message contract |
|
||||
| SD-006 | Message types are Julia structs with keyword constructors (`userMessage(content=[...])`) | Type stable; keyword constructors provide defaults (`role="user"`, `timestamp=now()`); supports both positional and keyword usage | `Dict{String, Any}` for messages — lose type safety, docgen, IDE support |
|
||||
| SD-007 | `eventSink` function passed to all deep lifecycle calls | Provides hooks for external monitoring (logging, metrics, NATS pub/sub) without modifying core logic | Built-in `println` — can't disable or redirect; no metrics integration |
|
||||
| SD-008 | `abortSignal` struct with `.aborted::Bool` field passed to `prepareToolCall()` and hooks | Simple mutable state for interrupting long-running tool calls (no async cancellation — just flag check) | Threading with `Task.cancel()` — Julia thread model doesn't support it; `@async` spawn — complex cleanup |
|
||||
| SD-009 | `_extractToolCalls()` supports both `message.tool_calls` and `content[{type="tool_calls"}]` formats | LMStudio.jl/vLLM vs OpenAI API have different response shapes; framework must work with both | Separate parser per provider — maintenance burden; require provider wrapper — adds indirection |
|
||||
|
||||
## 4. High-Level Component Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User[External Caller] -->|promptAgent / followUp| InputCh[inputChannel]
|
||||
|
||||
subagent AgentLoop [Agent Loop Task]
|
||||
InputCh -->|take!| Disp[Message Dispatcher]
|
||||
Disp -->|spawn| Proc[_processMessage]
|
||||
Proc -->|Channel| PrCh[processMessageInputCh]
|
||||
end
|
||||
|
||||
subagent Message Pipeline
|
||||
PrCh -->|put!| DMsg[Drain input, convert to userMessage]
|
||||
DMsg --> PC[prepareContext hook]
|
||||
PC --> FM[formatMsgForLLM hook]
|
||||
FM --> LLM[llmCall]
|
||||
LLM --> EC[_extractToolCalls]
|
||||
EC -->|hasToolCalls| TC[executeToolCalls]
|
||||
TC -->|toolResultBatch| TM[toolResultMessage]
|
||||
TM -->|push!| MH[messageHistory]
|
||||
TC -->|no tool calls break| BRK[break loop]
|
||||
BRK --> End[exit]
|
||||
end
|
||||
|
||||
subagent Tool System
|
||||
TC --> Prep[prepareToolCall]
|
||||
Prep --> Exec[executePreparedToolCall]
|
||||
Exec --> Term[finalizeExecutedToolCall]
|
||||
Term --> CRM[createToolResultMessage]
|
||||
CRM --> TM
|
||||
end
|
||||
|
||||
subagent MCP Integration
|
||||
Prep --> ToolLookup[context.tools lookup]
|
||||
ToolLookup --> MCP[MCP tools/list + tools/call]
|
||||
MCP --> Wr[_wrap_mcp_tool]
|
||||
Wr --> Reg[toolRegistry.registerAllTools]
|
||||
end
|
||||
|
||||
OutputCh[outputChannel] --> User
|
||||
Proc -->|final response| OutputCh
|
||||
```
|
||||
|
||||
**Component Descriptions**:
|
||||
- **Agent Loop Task** (`SD-001`, `SD-002`): Runs `@spawn _agentLoop()`; manages three channels; dispatches messages to `_processMessage`
|
||||
- **Message Pipeline** (`SD-003`, `SD-004`): `prepareContext` → `formatMsgForLLM` → `llmCall` → extract tool calls → loop
|
||||
- **Tool System** (`SD-005`): Three-phase lifecycle (prepare → execute → finalize); never throws; returns error tool result on failure
|
||||
- **MCP Integration** (`SD-004`): `tools/list` discovery with pagination; `_wrap_mcp_tool` creates executable `agentTool` with MCP JSON-RPC calls
|
||||
|
||||
## 5. Alternatives Considered
|
||||
|
||||
| Alternative | Pros | Cons | Decision |
|
||||
|-------------|------|------|----------|
|
||||
| Synchronous `agent.process(message)` | Simpler API; no channels or tasks | Blocks caller; no follow-up support; harder to test concurrency | **Rejected** — async channels needed for real-time chat UX |
|
||||
| JSON dicts for messages everywhere | Flexible; no type definitions | No static checking; no docgen; no IDE autocomplete; harder to serialize/deserialize | **Rejected** — Julia advantages of structs (type-stable, documented) outweigh cost |
|
||||
| Single `onToolCall` hook (pre-execute only) | Simpler hook interface | Can't sanitize results post-execution; can't set terminate flag after tool completes | **Rejected** — `afterToolCall` needed for masking secrets, normalizing usage, inspecting error status |
|
||||
| Direct HTTP calls to MCP servers (no NATS) | Simpler dependency (no NATS) | Breaks if MCP uses NATS (common in microservices); no single transport | **Rejected** — callable struct abstracts transport; NATS is one implementation option |
|
||||
| External orchestrator runs tool call loop | Caller has full control | Duplicates logic; every caller must implement same loop; harder to maintain | **Rejected** — loop is agent framework responsibility (single source of truth) |
|
||||
|
||||
## 6. Risk Assessment
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| `llmCall` throws (network timeout, API error) | Agent loop exits with error in `eventSink` | Medium | `try/catch` in `_processMessage` and `_agentLoop`; eventSink receives error string |
|
||||
| Tool `prepareArguments` or `validateRequiredArgs` hook throws | `prepareToolCall` catches and returns `immediateOutcome` error | Low | Hook errors caught; error tool result sent to LLM for recovery |
|
||||
| MCP server unreachable | Tool discovery returns 0 tools; `listTools` still available | Medium | `register_mcp_tools` returns 0 on error; tool execution calls catch errors |
|
||||
| Long-running tool call hangs | Caller blocks on `outputChannel`; no built-in timeout | Medium | `eventSink` passes abort signal; caller sets `signal.aborted = true` externally |
|
||||
| Multiple agents share same `mcpServer` struct | `mcpServer` must be thread-safe for concurrent tool calls | Low | Each thread gets own `mcpServer` instance; NATS handles concurrency |
|
||||
| Message history grows unbounded | Memory exhaustion in long sessions | Medium | No built-in pruning; `prepareContext` can filter/prune messages (TODO in code) |
|
||||
|
||||
## 7. Requirements Traceability
|
||||
|
||||
| Solution Component | Requirement ID | Decision ID | Description |
|
||||
|-------------------|----------------|-------------|-------------|
|
||||
| Channel-based async processing | FR-001 | SD-001, SD-002 | `inputChannel`, `followUpChannel`, `outputChannel` with priority dispatch |
|
||||
| LLM call loop | FR-003 | SD-003 | `_processMessage` while loop; `llmCall()` → extract → execute → append → repeat |
|
||||
| MCP tool discovery | FR-004 | SD-004, SD-009 | `tools/list` with pagination; `tools/call` execution |
|
||||
| Lifecycle hooks | FR-005 | SD-005 | `beforeToolCall` can block; `afterToolCall` transforms; never throw |
|
||||
| Message type conversion | FR-006 | SD-006 | Julia structs with keyword constructors; `_userMessageToOpenAI`, `_messageContentToBlocks` |
|
||||
| Format-agnostic extraction | FR-002 | SD-009 | `_extractToolCalls` handles both `message.tool_calls` and `content` blocks |
|
||||
| Event sink observability | NFR-201, NFR-103 | SD-007 | `eventSink` passed to all lifecycle steps; logs state machine transitions |
|
||||
@@ -0,0 +1,318 @@
|
||||
# Walkthrough: YiemAgent Agent Framework
|
||||
|
||||
## Structural Guidance
|
||||
|
||||
- One **overview sequence diagram** per participant (high-level flow)
|
||||
- **Detailed sub-diagrams** for complex interaction points (tool call loop, MCP discovery)
|
||||
- Use sequence diagrams for request/response patterns; lifecycle states for multi-step operations
|
||||
|
||||
---
|
||||
|
||||
## External Caller
|
||||
|
||||
_The Developer calling `YiemAgent.jl` via `promptAgent`, `takeResponse`, `followUp`, and `stopAgent`._
|
||||
|
||||
### Session Start: Create Agent
|
||||
|
||||
**WT-001**: Developer creates an `yiemAgent` instance with `llmCall` and `mcpServer`
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-001` (channel-based async), `SD-004` (MCP struct)
|
||||
|
||||
**Details**:
|
||||
```julia
|
||||
mcp = mcpServer(conn, topic, senderID)
|
||||
agent = yiemAgent(llmCall, mcpServer=mcp, systemPrompt="You are a helpful assistant")
|
||||
```
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent YiemAgent Constructor
|
||||
Dev->>agent: yiemAgent(llmCall, ; mcpServer=mcp)
|
||||
agent->>agent: create channels (input=16, followUp=32, output=16)
|
||||
agent->>agent: registerAllTools(tools, mcp)
|
||||
agent->>mcp: mcp("tools/list", sessionId)
|
||||
mcp-->>agent: response (tools array + nextCursor)
|
||||
agent->>agent: _wrap_mcp_tool for each tool
|
||||
agent->>agent: registerTool(tools, listTools_tool)
|
||||
agent->>agent: println("[toolRegistry] Discovered N MCP tools")
|
||||
agent->>agent: yiemAgent struct with placeholder task
|
||||
agent->>agent: @spawn _agentLoop(agent)
|
||||
agent-->>Dev: yiemAgent instance
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (tool discovery), US-001 (agent creation)
|
||||
|
||||
### Send Message: User Prompt
|
||||
|
||||
**WT-002**: Developer sends a user message to the agent
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-002` (background task dispatch)
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: promptAgent(agent, text="What's the weather?")
|
||||
agent->>agent: put!(inputChannel, msg_dict)
|
||||
Dev->>agent: takeResponse(agent) # blocks
|
||||
agent->>agent: inputChannel isready
|
||||
agent->>agent: take!(inputChannel)
|
||||
agent->>Dev: (returns agent for chaining)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001 (channel processing), PR-001, SD-001, SD-002
|
||||
|
||||
### Receive Response
|
||||
|
||||
**WT-003**: Agent returns response on `outputChannel`
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
AgentLoop->>AgentLoop: _processMessage runs
|
||||
AgentLoop->>AgentLoop: while loop, llmCall, tool calls...
|
||||
AgentLoop->>AgentLoop: no more tool calls → exit loop
|
||||
AgentLoop->>AgentLoop: filter reasoningContent from final result
|
||||
AgentLoop->>AgentLoop: _assistantMessageToOpenAI(result)
|
||||
AgentLoop->>AgentLoop: put!(outputChannel, respondToUI)
|
||||
AgentLoop->>Dev: take!(outputChannel) # unblocks
|
||||
Dev->>Dev: receive OpenAI dict response
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001, FR-003, FR-006, PR-001
|
||||
|
||||
### Send Follow-up While Agent Busy
|
||||
|
||||
**WT-004**: Developer sends a follow-up message while agent is processing a tool call
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: promptAgent(text="What's the weather in Tokyo?") # agent starts processing
|
||||
AgentLoop->>AgentLoop: _processMessage started (processingTask = Task)
|
||||
Dev->>agent: followUp(text="Also check Seoul") # while busy
|
||||
agent->>agent: put!(followUpChannel, msg_dict)
|
||||
AgentLoop->>AgentLoop: _processMessage completes (tool calls done)
|
||||
AgentLoop->>AgentLoop: outputChannel has message → put!(outputChannel, response), take!(next msg)
|
||||
AgentLoop->>AgentLoop: no inputChannel → check followUpChannel
|
||||
AgentLoop->>AgentLoop: isready(followUpChannel) → take!(followUpMsg), put!(inputChannel, followUpMsg)
|
||||
AgentLoop->>AgentLoop: processingTask = nothing → reset
|
||||
AgentLoop->>AgentLoop: loop back → take!(inputChannel) from followUp
|
||||
Dev->>agent: takeResponse() → blocks
|
||||
AgentLoop->>Dev: next response via outputChannel
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001 (follow-up priority), PR-001, SD-001, SD-002
|
||||
|
||||
### Stop Agent
|
||||
|
||||
**WT-005**: Developer stops the agent gracefully
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: stopAgent(agent)
|
||||
agent->>agent: put!(inputChannel, :shutdown)
|
||||
AgentLoop->>AgentLoop: check newUserMsg === :shutdown
|
||||
AgentLoop->>AgentLoop: drain inputChannel (while isready)
|
||||
AgentLoop->>AgentLoop: drain followUpChannel (while isready)
|
||||
AgentLoop->>AgentLoop: break, catch, fetch(_agentLoop)
|
||||
Dev->>agent: close(inputChannel, outputChannel, followUpChannel)
|
||||
Dev-->>Dev: nothing (agent destroyed)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001, NFR-201 (graceful exit), PR-001
|
||||
|
||||
---
|
||||
|
||||
## LLM Provider
|
||||
|
||||
_The external LLM API (e.g. Anthropic, OpenAI, LMStudio) receiving formatted messages and returning responses._
|
||||
|
||||
### Receive Formatted Message
|
||||
|
||||
**WT-010**: Agent sends LLM request in OpenAI format
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-003` (formatMsgForLLM)
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
part agent AgentLoop
|
||||
AgentLoop->>AgentLoop: preparedContext = prepareContext(state)
|
||||
AgentLoop->>AgentLoop: formattedMessages = formatMsgForLLM(preparedContext)
|
||||
Note over AgentLoop: messages: [system, user, assistant, tool, ...]<br/>tools: [{"function": {"name": "getWeather", "parameters": {...}}}]
|
||||
agentLoop->>llmCall: llmCall(formattedMessages, eventSink=eventSink)
|
||||
llmCall->>Provider: HTTP POST /v1/chat/completions (JSON)
|
||||
Provider-->>llmCall: JSON response
|
||||
llmCall-->>AgentLoop: response = JSON.Object{String, Any}(...)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-002, SD-003, SD-009
|
||||
|
||||
### Receive Response: No Tool Calls
|
||||
|
||||
**WT-11**: Agent receives text-only response
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
llmCall-->>AgentLoop: response = {finish_reason=> "stop", message=> {role=> "assistant", content=> "It's 22°C in Bangkok"}}
|
||||
AgentLoop->>AgentLoop: _extractToolCalls(response)
|
||||
AgentLoop->>AgentLoop: hasToolCalls = false
|
||||
AgentLoop->>AgentLoop: assistant_msg = assistantMessage(content=[textContent("It's 22°C")], ...)
|
||||
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
|
||||
AgentLoop->>AgentLoop: break (no tool calls)
|
||||
AgentLoop->>AgentLoop: filter reasoningContent (if any)
|
||||
AgentLoop->>AgentLoop: create final assistantMessage
|
||||
AgentLoop->>AgentLoop: put!(outputChannel, OpenAI format dict)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-003 (loop exit), FR-006 (OpenAI conversion), PR-003
|
||||
|
||||
### Receive Response: With Tool Calls
|
||||
|
||||
**WT-012**: Agent receives tool call response
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
llmCall-->>AgentLoop: response = {finish_reason=> "tool_calls",<br/>message=> {role=> "assistant",<br/>reasoning_content=> "I need to call getWeather...",<br/>tool_calls=> [{"id"=>"call_1", "function"=>{name=>"getWeather", arguments=>"{\"city\":\"Bangkok\"}"}}, ...]}}
|
||||
AgentLoop->>AgentLoop: _extractToolCalls(response)
|
||||
AgentLoop->>AgentLoop: hasToolCalls = true<br/>toolCallList = [agentToolCall("getWeather", ...)]
|
||||
AgentLoop->>AgentLoop: assistant_msg = assistantMessageToolCall(toolCalls, reasoningContent, ...)
|
||||
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
|
||||
AgentLoop->>AgentLoop: config = agentLoopConfig(beforeToolCall, afterToolCall, parallel="sequential")
|
||||
AgentLoop->>AgentLoop: toolResultBatch = executeToolCalls(...)
|
||||
...
|
||||
```
|
||||
|
||||
> **Traceability**: FR-002, FR-003, PR-003, SD-003, SD-005, SD-009
|
||||
|
||||
---
|
||||
|
||||
## MCP Server
|
||||
|
||||
_The Model Context Protocol server providing tools via JSON-RPC 2.0 (transport via NATS or HTTP).
|
||||
|
||||
### Receive tools/list Request
|
||||
|
||||
**WT-020**: Agent discovers tools on startup
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-004` (MCP integration)
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
part agent AgentLoop
|
||||
agent->>mcp: mcp("tools/list", sessionId; eventSink=eventSink)
|
||||
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/list", "id"=>..., "params"=>{...}}
|
||||
MCP_server-->>agent: response = {<br/> "result" => {<br/> "tools" => [name, label, description, inputSchema], ...<br/> , "nextCursor" => 123<br/> }<br/>}
|
||||
agent->>agent: for tool_def in tools_array<br/> _wrap_mcp_tool(mcp, tool_def)
|
||||
agent->>agent: while cursor !== nothing → paginate
|
||||
agent->>agent: println("[toolRegistry] Discovered N MCP tools. Total: $(length(tools))")
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (MCP discovery), PR-004
|
||||
|
||||
### Receive tools/call Request
|
||||
|
||||
**WT-021**: Agent executes tool via MCP
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
part agent Tool System
|
||||
agent->>mcp: mcp("tools/call", "getWeather", args={"city" => "Bangkok"}, sessionId; eventSink=eventSink)
|
||||
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/call", "params"=>{name, arguments, sessionId}}
|
||||
MCP_server-->>agent: response = {<br/> "result" => {<br/> "content" => [{"type"=>"text", "text"=>"Sunny, 22°C"}],<br/> "isError" => false<br/> }<br/>}
|
||||
agent->>agent: check haskey(response, "error") → no
|
||||
agent->>agent: result_data = response["result"]
|
||||
agent->>agent: content_text = _extract_text_content(result_data)
|
||||
agent->>agent: return agentToolResult([textContent(content_text)], details={isError=>false}, nothing, false)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (tool execution), PR-004, SD-004, SD-005
|
||||
|
||||
### Receive Error from MCP Server
|
||||
|
||||
**WT-022**: MCP server returns error
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
MCP_server-->>agent: response = {<br/> "error" => {<br/> "code" => -32601,<br/> "message" => "Method not found"<br/> }<br/>}
|
||||
agent->>agent: haskey(response, "error") → true
|
||||
agent->>agent: return agentToolResult(<br/> [textContent("MCP error: Method not found")],<br/> details={"error" => "Method not found"},<br/> nothing, false<br/>)
|
||||
agent->>AgentLoop: finalOutcome(error=true)
|
||||
agent->>AgentLoop: push!(agentMsgHistory, toolResultMessage(...))
|
||||
agent->>AgentLoop: next LLM turn receives error result
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (error handling), NFR-202 (never throw), PR-004, SD-005
|
||||
|
||||
---
|
||||
|
||||
## Error Paths and Edge Cases
|
||||
|
||||
**WT-030**: LLM times out (network error)
|
||||
|
||||
> Same participant POV — from caller's perspective, they block on `takeResponse()` until task exits
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: takeResponse(agent) # blocks
|
||||
agent->>llmCall: llmCall(messages) # calls external API
|
||||
lltimout
|
||||
LLM provider-->>AgentLoop: ERROR (timeout)
|
||||
AgentLoop->>AgentLoop: @error "Agent loop failed"
|
||||
AgentLoop->>AgentLoop: eventSink("error: ...")
|
||||
AgentLoop->>Dev: fetch(_agentLoop) throws TaskFailedException
|
||||
Dev->>agent: stopAgent(agent) # cleanup channels
|
||||
```
|
||||
|
||||
**Traceability**: PR-001, NFR-201 (catch error and exit), SD-001
|
||||
|
||||
**WT-031**: Tool execution throws exception
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
agent->>agent: executePreparedToolCall
|
||||
agent->>tool: tool.execute(toolCallId, args, signal, eventSink)
|
||||
note over tool: tool raises exception (e.g., network error, invalid JSON)
|
||||
tool-->>agent: throw Exception
|
||||
agent->>agent: catch e
|
||||
agent->>agent: executedOutcome(createErrorToolResult("error message"), true)
|
||||
agent->>AgentLoop: finalizeExecutedToolCall
|
||||
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
|
||||
AgentLoop->>AgentLoop: push!(history, toolResultMessage)
|
||||
AgentLoop->>AgentLoop: next llmCall with error in message history
|
||||
```
|
||||
|
||||
**Traceability**: FR-005 (hook errors caught), NFR-202 (never throw), SD-005
|
||||
|
||||
**WT-032**: beforeToolCall hook blocks execution
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
agent->>agent: beforeToolCall(beforeCtx, signal, eventSink)
|
||||
agent->>agent: returns beforeToolCallResult(true, "User must approve")
|
||||
agent->>agent: return immediateOutcome(createErrorToolResult("Tool blocked"), true)
|
||||
agent->>AgentLoop: executeToolCalls → immediateOutcome
|
||||
AgentLoop->>AgentLoop: finalizeExecutedToolCall
|
||||
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
|
||||
AgentLoop->>AgentLoop: push! to history
|
||||
```
|
||||
|
||||
**Traceability**: FR-005 (beforeToolCall can block), SD-005
|
||||
@@ -0,0 +1,518 @@
|
||||
# Specification: YiemAgent Agent Framework
|
||||
|
||||
## 1. Message Type System (`src/type.jl`)
|
||||
|
||||
### SP-001: userMessage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"user"` |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | No | Plain text and/or image content |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was sent |
|
||||
|
||||
**Keyword Constructor**: `userMessage(; role="user", content=[...], timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006 (message type conversion)
|
||||
- Solution Design: SD-006 (Julia structs for messages)
|
||||
|
||||
### SP-002: assistantMessage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"assistant"` |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Text and/or image content |
|
||||
| `api::String` | `String` | Yes | API name used (e.g., `"openai"`) |
|
||||
| `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`) |
|
||||
| `model::String` | `String` | Yes | Model identifier |
|
||||
| `usage::llmUsage` | `llmUsage` | Yes | Token usage (in/out) |
|
||||
| `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"end_turn"`) |
|
||||
| `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was received |
|
||||
|
||||
**Keyword Constructor**: `assistantMessage(; role="assistant", content=[...], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="end_turn", errorMessage=nothing, timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006 (message type conversion)
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-003: assistantMessageToolCall
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"assistant"` |
|
||||
| `toolCalls::Vector{agentToolCall}` | `Vector{agentToolCall}` | Yes | Tool calls to execute |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Reasoning/thinking content blocks |
|
||||
| `api::String` | `String` | Yes | API name used |
|
||||
| `provider::String` | `String` | Yes | Provider name |
|
||||
| `model::String` | `String` | Yes | Model identifier |
|
||||
| `usage::llmUsage` | `llmUsage` | Yes | Token usage |
|
||||
| `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"tool_calls"`) |
|
||||
| `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was received |
|
||||
|
||||
**Keyword Constructor**: `assistantMessageToolCall(; role="assistant", toolCalls=[...], content=[], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="tool_calls", errorMessage=nothing, timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003 (tool call loop)
|
||||
- Solution Design: SD-005 (lifecycle phases), SD-009 (format-agnostic LLM responses)
|
||||
|
||||
### SP-004: toolResultMessage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"toolResult"` (note: `"tool"` in example) |
|
||||
| `toolCallId::String` | `String` | Yes | ID matching the tool call |
|
||||
| `toolName::String` | `String` | Yes | Name of the executed tool |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Tool output content |
|
||||
| `details::Any` | `Any` | Yes | Additional tool-specific details |
|
||||
| `usage::Union{llmUsage, Nothing}` | `Union{llmUsage, Nothing}` | Yes | Token usage if applicable |
|
||||
| `addedToolNames::Union{Vector{String}, Nothing}` | `Union{Vector{String}, Nothing}` | Yes | Tools added during execution |
|
||||
| `isError::Bool` | `Bool` | Yes | Whether the tool call resulted in an error |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the result was recorded |
|
||||
|
||||
**Keyword Constructor**: `toolResultMessage(; role="tool", toolCallId="", toolName="", content=[], details=nothing, usage=nothing, addedToolNames=nothing, isError=false, timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005 (tool lifecycle)
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-005: agentToolCall
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `type::String` | `String` | Yes | Always `"function"` |
|
||||
| `id::String` | `String` | Yes | Unique tool call identifier |
|
||||
| `name::String` | `String` | Yes | Tool name |
|
||||
| `arguments::Dict{String, Any}` | `Dict{String, Any}` | Yes | Parsed tool arguments |
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003 (tool call extraction)
|
||||
- Solution Design: SD-009 (extract tool calls from LLM response)
|
||||
|
||||
### SP-006: messageContent Types
|
||||
|
||||
**textContent** (`SP-006.1`):
|
||||
```julia
|
||||
struct textContent <: messageContent
|
||||
text::String
|
||||
end
|
||||
```
|
||||
|
||||
**imageContent** (`SP-006.2`):
|
||||
```julia
|
||||
struct imageContent <: messageContent
|
||||
data::String # Base64-encoded image data
|
||||
mimeType::String # MIME type (e.g., "image/png")
|
||||
end
|
||||
```
|
||||
|
||||
**reasoningContent** (`SP-006.3`):
|
||||
```julia
|
||||
struct reasoningContent <: messageContent
|
||||
text::String
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006 (message content conversion)
|
||||
- Solution Design: SD-006
|
||||
|
||||
## 2. LLM Model Configuration (`src/type.jl`)
|
||||
|
||||
### SP-007: llmModel
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id::String` | `String` | Yes | Unique model identifier |
|
||||
| `name::String` | `String` | Yes | Human-readable model name |
|
||||
| `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`, `"openai"`) |
|
||||
| `baseUrl::String` | `String` | Yes | API endpoint base URL |
|
||||
| `reasoning::Bool` | `Bool` | Yes | Whether the model supports chain-of-thought |
|
||||
| `input::Vector{String}` | `Vector{String}` | Yes | Supported input modalities (e.g., `"text"`, `"image"`) |
|
||||
| `cost::modelCost` | `modelCost` | Yes | Pricing information |
|
||||
| `contextWindow::Int64` | `Int64` | Yes | Maximum context length in tokens |
|
||||
| `maxTokens::Int64` | `Int64` | Yes | Maximum output tokens per completion |
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-002 (LLM agnosticism)
|
||||
- Solution Design: SD-003
|
||||
|
||||
### SP-008: modelCost
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `input::Float64` | `Float64` | Yes | Price per 1M input tokens |
|
||||
| `output::Float64` | `Float64` | Yes | Price per 1M output tokens |
|
||||
| `cache_read::Float64` | `Float64` | Yes | Price per 1M cached read tokens |
|
||||
| `cache_write::Float64` | `Float64` | Yes | Price per 1M cache write tokens |
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: N/A (internal tracking data)
|
||||
- Solution Design: SD-003
|
||||
|
||||
## 3. MCP Tool Integration (`src/toolRegistry.jl`)
|
||||
|
||||
### SP-009: tools/list Response Format
|
||||
|
||||
**Request**: `mcp("tools/list", sessionId; eventSink=eventSink)`
|
||||
|
||||
**Response** (JSON-RPC 2.0):
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool_name",
|
||||
"title"?: "Human-readable title",
|
||||
"label"?: "Fallback title",
|
||||
"description": "What the tool does",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string", "description": "..." }
|
||||
},
|
||||
"required": ["field"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"nextCursor"?: "pagination_cursor_or_empty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response error**:
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Method not found"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-004 (MCP discovery)
|
||||
- Solution Design: SD-004
|
||||
|
||||
### SP-010: tools/call Request/Response Format
|
||||
|
||||
**Request**: `mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)`
|
||||
|
||||
**Response** (JSON-RPC 2.0):
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Tool output text"}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response error**:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Error message"}
|
||||
],
|
||||
"isError": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-004 (tool execution)
|
||||
- Solution Design: SD-004 (`_wrap_mcp_tool`)
|
||||
|
||||
## 4. OpenAI Message Format (`src/utils.jl`)
|
||||
|
||||
### SP-011: system message format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "system",
|
||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-012: userMessage → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "user",
|
||||
"content" => _messageContentToBlocks(msg.content)
|
||||
)
|
||||
```
|
||||
|
||||
Where `_messageContentToBlocks` converts:
|
||||
- `textContent(t)` → `Dict("type" => "text", "text" => t)`
|
||||
- `imageContent(data, mime)` → `Dict("type" => "image_url", "image_url" => Dict("url" => "data:$mime;base64,$data"))`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-013: assistantMessageToolCall → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "assistant",
|
||||
"content" => nothing,
|
||||
"tool_calls" => [
|
||||
Dict(
|
||||
"id" => tc.id,
|
||||
"type" => tc.type,
|
||||
"function" => Dict(
|
||||
"name" => tc.name,
|
||||
"arguments" => JSON.json(tc.arguments) # serialized as JSON string
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003
|
||||
- Solution Design: SD-009
|
||||
|
||||
### SP-014: assistantMessage → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "assistant",
|
||||
"content" => _messageContentToBlocks(msg.content)
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-015: toolResultMessage → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "tool",
|
||||
"tool_call_id" => msg.toolCallId,
|
||||
"content" => _messageContentToBlocks(msg.content)
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005, FR-006
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-016: agentTool → OpenAI tools format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => tool.name,
|
||||
"description" => tool.description,
|
||||
"parameters" => tool.inputSchema
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-004
|
||||
- Solution Design: SD-004, SD-006
|
||||
|
||||
## 5. LLM Response Format Agnostic Extraction (`src/agentCore.jl`)
|
||||
|
||||
### SP-017: format 1 — LMStudio.jl / vLLM style (dict with nested `message.tool_calls`)
|
||||
|
||||
**Response dict** (key: `message::Dict`):
|
||||
```json
|
||||
{
|
||||
"finish_reason": "tool_calls",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning_content": "I need to call getWeather...",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getWeather",
|
||||
"arguments": "{\"city\":\"Bangkok\"}" // JSON string
|
||||
},
|
||||
"id": "call_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When no tool calls**:
|
||||
```json
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "It's 22°C in Bangkok"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-002
|
||||
- Solution Design: SD-009
|
||||
|
||||
### SP-018: format 2 — OpenAI API style (content blocks array)
|
||||
|
||||
**Content blocks with tool calls** (`SP-018.1`):
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_calls",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getWeather",
|
||||
"arguments": {"city": "Bangkok"} // already parsed dict
|
||||
},
|
||||
"id": "call_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Content blocks with tool_call (already parsed)** (`SP-018.2`):
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": "getWeather",
|
||||
"arguments": {"city": "Bangkok"},
|
||||
"id": "call_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Content blocks with text only** (`SP-018.3`):
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{"type": "text", "text": "It's 22°C in Bangkok"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response dict extras** (common across all formats, `SP-018.4`):
|
||||
```json
|
||||
{
|
||||
"api": "openai",
|
||||
"provider": "anthropic",
|
||||
"model": "gpt-4",
|
||||
"usage": {"inputTokens": 100, "outputTokens": 50},
|
||||
"error_message": "Optional error"
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-002
|
||||
- Solution Design: SD-009
|
||||
|
||||
## 6. Agent Loop Configuration (`src/type.jl` + `src/agentCore.jl`)
|
||||
|
||||
### SP-019: agentContext
|
||||
|
||||
```julia
|
||||
struct agentContext
|
||||
systemPrompt::String
|
||||
messages::Vector{agentMessage}
|
||||
tools::Union{OrderedDict{String, agentTool}, Nothing} # O(1) lookup by name
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003 (context preparation)
|
||||
- Solution Design: SD-003, SD-004
|
||||
|
||||
### SP-020: agentState
|
||||
|
||||
```julia
|
||||
mutable struct agentState
|
||||
systemPrompt::String
|
||||
model::Union{llmModel, Nothing}
|
||||
tools::OrderedDict{String, agentTool} # insertion-ordered
|
||||
messages::Vector{agentMessage} # history (no system prompt)
|
||||
pendingToolCalls::Vector{String} # tool call IDs waiting for results
|
||||
errorMessage::Union{String, Nothing} # last error message
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-001 (agent state management)
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-021: agentLoopConfig
|
||||
|
||||
```julia
|
||||
struct agentLoopConfig
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
toolExecution::String # "sequential" or "parallel"
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005 (lifecycle hooks)
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-022: abortSignal
|
||||
|
||||
```julia
|
||||
struct abortSignal
|
||||
aborted::Bool
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: NFR-101 (timeout handling)
|
||||
- Solution Design: SD-008
|
||||
|
||||
## 7. Tool Validation Hooks (`src/utils.jl`)
|
||||
|
||||
### SP-023: validateRequiredArgs (default hook)
|
||||
|
||||
**Signature**: `validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing, String}`
|
||||
|
||||
**Behavior**:
|
||||
- Extract `required = get(inputSchema, "required", Any[])`
|
||||
- Check each required field exists in `keys(args)`
|
||||
- Return `nothing` if all present
|
||||
- Return `"Missing required arguments: $(join(missing, ", "))"` if any missing
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: NFR-301 (input validation)
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-024: validateToolArguments (wrapper hook)
|
||||
|
||||
**Signature**: `validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}`
|
||||
|
||||
**Behavior**:
|
||||
- If `tool.validateRequiredArgs !== nothing`: call `tool.validateRequiredArgs(prepared.arguments)`
|
||||
- Else: call `validateRequiredArgs(prepared.arguments, tool.inputSchema)`
|
||||
- If returns `!nothing`: throw `ArgumentError(result)`
|
||||
- Caller (`prepareToolCall`) catches and returns `immediateOutcome`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005 (tool validation)
|
||||
- Solution Design: SD-005
|
||||
@@ -0,0 +1,192 @@
|
||||
# Validation: YiemAgent Agent Framework
|
||||
|
||||
## Test Architecture Overview
|
||||
|
||||
Because YiemAgent is a Julia library (not a web service), validation tests focus on:
|
||||
- **Unit tests**: message type conversion, tool execution logic, format extraction
|
||||
- **Integration tests**: full agent loop with stubbed LLM and MCP
|
||||
- **Contract tests**: response format compatibility with OpenAI/LMStudio specs
|
||||
- **Performance tests**: agent loop overhead for sequential tool calls
|
||||
|
||||
Every specification rule (SP-XXX), walkthrough journey (WT-XXX), and requirement (FR-XXX) must have a corresponding test.
|
||||
|
||||
---
|
||||
|
||||
## Test Types Mapping to Source Documents
|
||||
|
||||
| Test Type | Source Document | Example Coverage |
|
||||
|-----------|-----------------|------------------|
|
||||
| **Unit tests** | `4-specification.md` (SP-001..SP-024), `src/utils.jl` (converters) | Message type → OpenAI format conversion; tool argument validation |
|
||||
| **Integration tests** | `3-walkthrough.md` (WT-001..WT-032) | Full agent loop: prompt → llmCall → tool extract → execute → response |
|
||||
| **Contract tests** | `4-specification.md` SP-017, SP-018 | Response format 1 (LMStudio/vLLM) and format 2 (OpenAI) parsed correctly |
|
||||
| **Performance tests** | `1-requirements.md` FR-003, NFR-101 | Agent loop overhead <500ms per sequential tool call |
|
||||
| **Penetration tests** | `1-requirements.md` NFR-301 | Tool argument injection; MCP spoofing |
|
||||
| **Schema validation** | `4-specification.md` SP-023, SP-024 | Required fields, type checking on tool arguments |
|
||||
|
||||
---
|
||||
|
||||
## Specification-to-Test Execution Trace
|
||||
|
||||
Every traceable item — specification rules (SP-XXX), walkthrough journeys (WT-XXX), and requirements (FR-XXX) — must have a runnable test. This trace maps each item to its corresponding test file and test function.
|
||||
|
||||
**Format**:
|
||||
|
||||
| Requirement/Spec ID | Participant | Source Path | Unit Test File | Test Function | Additional Test File(s) | CI Gate | Status |
|
||||
|---------------------|-------------|-------------|----------------|---------------|------------------------|---------|--------|
|
||||
| FR-001 | — | `1-requirements.md:3.1` | `tests/unit/channel-handling.jl` | `test_input_channel_priority` | `tests/integration/full-agent-loop.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-002 | — | `1-requirements.md:3.2` | `tests/unit/llm-format-extraction.jl` | `test_format1_tool_calls` | `tests/contract/response-format.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-003 | — | `1-requirements.md:3.3` | `tests/integration/tool-call-loop.jl` | `test_loop_exits_on_no_tool_calls` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| FR-004 | — | `1-requirements.md:3.4` | `tests/unit/mcp-tool-discovery.jl` | `test_tools_list_with_pagination` | `tests/contract/mcp-json-rpc.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-005 | — | `1-requirements.md:3.5` | `tests/unit/tool-hooks.jl` | `test_beforeToolCall_blocks` | `tests/integration/agent-loop.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-006 | — | `1-requirements.md:3.6` | `tests/unit/message-conversion.jl` | `test_userMessage_to_OpenAI` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-001 | Caller | `3-walkthrough.md:#agent-creation` | `integration/agent-creation.jl` | `test_agent_construction_and_lifecycle` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-002 | Caller | `3-walkthrough.md:#send-message` | `integration/message-flow.jl` | `test_promptAgent_and_takeResponse` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-003 | Caller | `3-walkthrough.md:#receive-response` | `integration/response-format.jl` | `test_output_channel_delivery` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-004 | Caller | `3-walkthrough.md:#follow-up` | `integration/followup.jl` | `test_followUp_while_busy` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-005 | Caller | `3-walkthrough.md:#stop-agent` | `integration/shutdown.jl` | `test_stopAgent_channel_cleanup` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-010 | LLM Provider | `3-walkthrough.md:#receive-formatted` | `integration/llm-call.jl` | `test_formatMsgForLLM_output_structure` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-011 | LLM Provider | `3-walkthrough.md:#response-no-tool-calls` | `integration/non-tool-response.jl` | `test_lmm_response_no_tool_calls_breaks_loop` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-012 | LLM Provider | `3-walkthrough.md:#response-with-tool` | `integration/tool-response.jl` | `test_lmm_response_with_tool_calls` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-020 | MCP Server | `3-walkthrough.md:#discover-tools` | `unit/mcp-discovery.jl` | `test_mcp_tools_list_parsed` | `contract/mcp-json-rpc.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-021 | MCP Server | `3-walkthrough.md:#execute-tool` | `unit/mcp-tool-execute.jl` | `test_mcp_tools_call_response_parsed` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-022 | MCP Server | `3-walkthrough.md:#mcp-error` | `unit/mcp-error-handling.jl` | `test_mcp_response_error_returned` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-030 | Error | `3-walkthrough.md:#llm-timeout` | `integration/llm-timeout.jl` | `test_LLM_timeout_agent_loop_exits` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-031 | Error | `3-walkthrough.md:#tool-throw` | `integration/tool-error.jl` | `test_tool_execute_throws_returns_error_result` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-032 | Error | `3-walkthrough.md:#beforeToolCall-block` | `unit/beforeToolCall-block.jl` | `test_beforeToolCall_blocks_execution` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-001 | — | `4-specification.md:#SP-001:userMessage` | `unit/message-types.jl` | `test_userMessage_struct_default_roles` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-002 | — | `4-specification.md:#SP-002:assistantMessage` | `unit/message-types.jl` | `test_assistantMessage_struct_default_stopReason_end_turn` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-003 | — | `4-specification.md:#SP-003:assistantMessageToolCall` | `unit/message-types.jl` | `test_assistantMessageToolCall_struct_default_stopReason_tool_calls` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-004 | — | `4-specification.md:#SP-004:toolResultMessage` | `unit/message-types.jl` | `test_toolResultMessage_struct_defaults` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-005 | — | `4-specification.md:#SP-005:agentToolCall` | `unit/message-types.jl` | `test_agentToolCall_type_is_always_function` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-006.1,2,3 | — | `4-specification.md:#SP-006:content-types` | `unit/content-types.jl` | `test_textContent_imageContent_reasoningContent` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-017 | — | `4-specification.md:#SP-017` | `contract/response-format.jl` | `test_lmStudio_vLLM_format_parsed` | — | `ci/julia-contract.yaml` | **TODO** |
|
||||
| SP-018.1,2,3,4 | — | `4-specification.md:#SP-018` | `contract/response-format.jl` | `test_openAI_format_blocks_parsed` | — | `ci/julia-contract.yaml` | **TODO** |
|
||||
| SP-023 | — | `4-specification.md:#SP-023` | `unit/tool-validation.jl` | `test_validateRequiredArgs_missing_fields` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-024 | — | `4-specification.md:#SP-024` | `unit/tool-validation.jl` | `test_validateToolArguments_throws_on_missing` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| NFR-101 | — | `1-requirements.md:4.1` | `perf/agent-loop.jl` | `perf_agent_loop_overhead_<500ms` | `tests/perf/concurrency.jl` | `ci/julia-perf.yaml` | **TODO** |
|
||||
| NFR-102 | — | `1-requirements.md:4.1` | `perf/mcp-discovery.jl` | `perf_mcp_discovery_50_tools_<2s` | — | `ci/julia-perf.yaml` | **TODO** |
|
||||
| NFR-201 | — | `1-requirements.md:4.2` | `unit/error-handling.jl` | `test_agent_loop_catches_and_logs_errors` | `integration/llm-timeout.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
|
||||
---
|
||||
|
||||
## Trace Rules
|
||||
|
||||
### Unit Test Rules
|
||||
|
||||
- **Every specification struct** (SP-001..SP-008, SP-019..SP-022) must have a unit test validating:
|
||||
- Default values for keyword arguments are set correctly
|
||||
- Type field matches spec (e.g., `agentToolCall.type` is always `"function"`)
|
||||
- Constructor works with both positional and keyword args
|
||||
- **Every converter function** (`_userMessageToOpenAI`, `_assistantMessageToOpenAI`, etc. SP-011..SP-016) must have a unit test:
|
||||
- Input Julia struct → output `Dict{String, Any}` matching OpenAI spec
|
||||
- Round-trip conversion preserves data (e.g., `textContent` text is preserved)
|
||||
- **Every tool validation function** (SP-023, SP-024) must have unit tests:
|
||||
- Missing required field → returns error string (not throws)
|
||||
- All required present → returns `nothing`
|
||||
- Custom `validateRequiredArgs` hook on tool takes precedence over default
|
||||
|
||||
### Integration Test Rules
|
||||
|
||||
- **Every walkthrough journey** (WT-XXX) must have at least one integration test:
|
||||
- `_agentLoop` processes messages through the full pipeline with stubbed LLM/MCP
|
||||
- `promptAgent` → `takeResponse` round-trip works
|
||||
- `followUp` while busy queues to `followUpChannel`, processes after current work
|
||||
- `stopAgent` sends `:shutdown`, drains channels, closes all 3 channels
|
||||
- **Tool call lifecycle** (FR-003, FR-005):
|
||||
- `llmCall` returns tool calls → loop calls `executeToolCalls` → results appended
|
||||
- Tool calls → no tool calls on next `llmCall` → loop exits
|
||||
- `toolResultBatch.terminate == true` → loop exits immediately
|
||||
- **Hook tests**:
|
||||
- `beforeToolCall` returning `beforeToolCallResult(true, reason)` blocks execution → error tool result
|
||||
- `afterToolCall` returning modified `agentToolResult` replaces result
|
||||
|
||||
### Contract Test Rules
|
||||
|
||||
- **Every response format** (SP-017, SP-018) must be parsed:
|
||||
- Format 1: `_extractToolCalls({message => {tool_calls => [...]}})` extracts `assistantMessageToolCall`
|
||||
- Format 2: `_extractToolCalls({content => [{type=>"tool_calls", tool_calls=[...]}}])` extracts `assistantMessageToolCall`
|
||||
- Format 2 (no tools): `_extractToolCalls({content => [{type=>"text", text=>...}]})` extracts `assistantMessage`
|
||||
- `reasoning_content` field → added to `assistantMessageToolCall.content`
|
||||
- `api`, `provider`, `model`, `usage` → copied to message fields
|
||||
|
||||
### Performance Test Rules
|
||||
|
||||
- **NFR-101**: Agent loop overhead measurement:
|
||||
- Stub LLM returns tool call → execute tool (1ms) → stub returns no tool calls → measure elapsed
|
||||
- Subtract stub costs (llm call 1ms + tool 1ms)
|
||||
- Overhead = elapsed - stub costs
|
||||
- Must be <500ms per tool call (p95)
|
||||
- **NFR-102**: MCP discovery performance:
|
||||
- Mock MCP server returns 200 tools (simulated, no real network)
|
||||
- Measure `_wrap_mcp_tool` + `registerTool` for all 200 tools
|
||||
- Must complete <2 seconds
|
||||
|
||||
### Security/Validation Rules
|
||||
|
||||
- **NFR-301 (input validation)**:
|
||||
- Tool arguments with injection patterns (e.g., `{"city" => "; rm -rf /"}`) → passed through to MCP
|
||||
- `beforeToolCall` can block dangerous tools (e.g., `toolCall.name == "delete_file"`)
|
||||
- **Penetration test**: MCP server spoofing
|
||||
- Attacker sends `{"result" => {"tools" => [{"name" => "malicious", ...}]}}` from untrusted MCP endpoint
|
||||
- Agent executes tool via MCP `tools/call` with attacker-controlled arguments
|
||||
- Mitigation: `beforeToolCall` hook can block unknown tools; app should validate MCP server URL
|
||||
|
||||
---
|
||||
|
||||
## Gap Check (Validation Review)
|
||||
|
||||
Per ASG Framework §6, verify:
|
||||
|
||||
- ✓ Every `SP-XXX` specification item has at least one unit test (listed in trace table above)
|
||||
- ✓ Every `WT-XXX` walkthrough step has at least one integration test (listed in trace table above)
|
||||
- ✓ Every NFR threshold has at least one performance test (NFR-101, NFR-102 in trace table)
|
||||
- ✓ Security NFRs have penetration test scenarios (NFR-301 in trace rules above)
|
||||
- ✓ Every `SP-017, SP-018` response format has contract tests (listed in trace table)
|
||||
- ✓ Every `SP-011..SP-016, SP-023, SP-024` converter/validation rule has unit tests (listed in trace table)
|
||||
|
||||
**Status**: All rows have **TODO** status. Tests must be written before integration into production CI pipeline.
|
||||
|
||||
---
|
||||
|
||||
## CI Pipeline
|
||||
|
||||
```yaml
|
||||
# ci/julia-pipeline.yaml (placeholder)
|
||||
name: Julia YiemAgent CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: julia-actions/julia-buildpkg@latest
|
||||
- run: julia --project=. -e 'using Pkg; Pkg.instantiate()'
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Unit tests" # include tests/unit/run_tests.jl"'
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit
|
||||
steps:
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Integration" # include tests/integration/run_tests.jl"'
|
||||
|
||||
contract:
|
||||
runs-on: ubuntu-fail
|
||||
needs: unit
|
||||
steps:
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Contract" # include tests/contract/run_tests.jl"'
|
||||
|
||||
perf:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit, integration]
|
||||
steps:
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Performance" # include tests/perf/run_tests.jl"'
|
||||
# Fail if NFR-101 or NFR-102 thresholds breached
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: julia --eval 'using Aqua; Aqua.test_all(YiemAgent)'
|
||||
```
|
||||
|
||||
**Gap Check**: Per ASG §5, every spec item maps to a CI gate. If a `SP-XXX` or `WT-XXX` row has no test file/function, the trace table row must be filled before merging.
|
||||
@@ -0,0 +1,217 @@
|
||||
# Implementation Plan: YiemAgent Agent Framework
|
||||
|
||||
## 1. Implementation Phases
|
||||
|
||||
### Phase 1: Type System & Core Structures (COMPLETED)
|
||||
|
||||
**Files**: `src/type.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-001`: Define Julia struct types for messages (`userMessage`, `assistantMessage`, `assistantMessageToolCall`, `toolResultMessage`) with keyword constructors — **COMPLETED** (see `src/type.jl:95-249`)
|
||||
- `IMP-002`: Define message content types (`textContent`, `imageContent`, `reasoningContent`) with abstract `messageContent` base type — **COMPLETED** (see `src/type.jl:74-87`)
|
||||
- `IMP-003`: Define tool types (`agentTool`, `agentToolCall`) with lifecycle hooks (`prepareArguments`, `validateRequiredArgs`) — **COMPLETED** (see `src/type.jl:319-328`)
|
||||
- `IMP-004`: Define agent context/state types (`agentContext`, `agentState`) — **COMPLETED** (see `src/type.jl:357-415`)
|
||||
- `IMP-005`: Define LLM model/usage/cost types (`llmModel`, `modelCost`, `llmUsage`) — **COMPLETED** (see `src/type.jl:45-67`)
|
||||
- `IMP-006`: Define tool call lifecycle result types (`preparedToolCall`, `immediateOutcome`, `executedOutcome`, `finalizedOutcome`) — **COMPLETED** (see `src/type.jl:600-710`)
|
||||
- `IMP-007`: Define agent loop config and event types (`agentLoopConfig`, `abortSignal`, `agentToolResult`, `toolExecStartEvent`, etc.) — **COMPLETED** (see `src/type.jl:450-567`)
|
||||
- `IMP-008`: Export all types from `type` module — **COMPLETED** (see `src/type.jl:2-26`)
|
||||
|
||||
**Validation Tests**: Unit tests for struct construction with keyword args (SP-001..SP-008, SP-019..SP-022)
|
||||
|
||||
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Utility & Formatting Functions (COMPLETED)
|
||||
|
||||
**Files**: `src/utils.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-010`: Implement `prepareContext(state, eventSink) -> agentContext` — **COMPLETED** (see `src/utils.jl:112-126`)
|
||||
- TODO: Filter tools based on user intent and tool description (`src/utils.jl:114`)
|
||||
- TODO: Inject system prompt context (e.g., time, session state) (`src/utils.jl:118`)
|
||||
- TODO: Prune/reorder messages (`src/utils.jl:121`)
|
||||
- `IMP-011`: Implement `formatMsgForLLM(ctx, eventSink) -> Dict{String, Any}` — **COMPLETED** (see `src/utils.jl:159-244`)
|
||||
- Converts system prompt to system message
|
||||
- Converts conversation messages via `_userMessageToOpenAI`, `_assistantMessageToOpenAI`, `_assistantMessageToolCallToOpenAI`, `_toolResultMessageToOpenAI`
|
||||
- Converts `ctx.tools` to OpenAI function tool format via `_toolsToOpenAI`
|
||||
- `IMP-012`: Implement message converters (`_userMessageToOpenAI`, `_assistantMessageToOpenAI`, `_assistantMessageToolCallToOpenAI`, `_toolResultMessageToOpenAI`) — **COMPLETED** (see `src/utils.jl:321-387`)
|
||||
- `IMP-013`: Implement `_messageContentToBlocks` — converts `textContent`/`imageContent` to OpenAI content blocks — **COMPLETED** (see `src/utils.jl:396-413`)
|
||||
- `IMP-014`: Implement `_toolsToOpenAI` — converts `OrderedDict{String, agentTool}` to OpenAI tools array — **COMPLETED** (see `src/utils.jl:429-447`)
|
||||
- `IMP-015`: Implement `beforeToolCall` hook default (returns `beforeToolCallResult(false, "N/A")`) — **COMPLETED** (see `src/utils.jl:278-284`)
|
||||
- `IMP-016`: Implement `afterToolCall` hook default (returns `nothing`) — **COMPLETED** (see `src/utils.jl:310-316`)
|
||||
- `IMP-017`: Implement `validateRequiredArgs(args, inputSchema)` — validates required fields in tool arguments — **COMPLETED** (see `src/utils.jl:479-497`)
|
||||
- `IMP-018`: Implement `validateToolArguments(tool, prepared)` — wrapper that calls tool-specific or default hook — **COMPLETED** (see `src/utils.jl:533-546`)
|
||||
|
||||
**Validation Tests**: Unit tests for converters, validation hooks, and OpenAI format output structure (SP-011..SP-016, SP-023, SP-024)
|
||||
|
||||
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Tool Registry & MCP Integration (COMPLETED)
|
||||
|
||||
**Files**: `src/toolRegistry.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-020`: Implement `_extract_text_content(result)` — extracts text from MCP tool result content array — **COMPLETED** (see `src/toolRegistry.jl:19-33`)
|
||||
- `IMP-021`: Implement `_wrap_mcp_tool(mcpserver, tool_def)` — wraps MCP tool as `agentTool` with `execute` calling `mcpserver("tools/call", ...)` — **COMPLETED** (see `src/toolRegistry.jl:41-106`)
|
||||
- Handles MCP error responses (`haskey(response, "error")`)
|
||||
- Extracts text and `isError` from result
|
||||
- `IMP-022`: Implement `register_mcp_tools(tools, mcpserver)` — queries `tools/list` with pagination (`nextCursor`) — **COMPLETED** (see `src/toolRegistry.jl:118-186`)
|
||||
- Handles both `result.tools` and `result.error` response shapes
|
||||
- Skips already registered tools (idempotent)
|
||||
- `IMP-023`: Implement `registerTool(tools, tool)` — adds tool to OrderedDict — **COMPLETED** (see `src/toolRegistry.jl:200-205`)
|
||||
- `IMP-024`: Implement `listTool(tools, mcpserver)` — builtin tool that triggers runtime discovery and lists all tools — **COMPLETED** (see `src/toolRegistry.jl:235-271`)
|
||||
- `IMP-025`: Implement `registerAllTools(tools, mcpserver)` — discovers existing tools + registers `listTools` — **COMPLETED** (see `src/toolRegistry.jl:286-289`)
|
||||
|
||||
**Validation Tests**: Unit tests for MCP tools/list parsing, pagination, tool registration idempotency; contract tests for JSON-RPC 2.0 format (SP-009, SP-010)
|
||||
|
||||
**Traceability**: FR-002, FR-004
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Agent Core & Loop (COMPLETED)
|
||||
|
||||
**Files**: `src/agentCore.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-030`: Define `yiemAgent` struct with channels, callbacks, MCP server — **COMPLETED** (see `src/agentCore.jl:17-95`)
|
||||
- `IMP-031`: Implement `yiemAgent` constructor — creates channels, discovers MCP tools, spawns `_agentLoop` task — **COMPLETED** (see `src/agentCore.jl:126-199`)
|
||||
- `IMP-032`: Implement `_agentLoop(agent)` — background `@spawn` task managing `inputChannel`/`followUpChannel` priority dispatch — **COMPLETED** (see `src/agentCore.jl:225-364`)
|
||||
- Handles shutdown signal: drains all channels, breaks loop
|
||||
- On error: catches, calls `eventSink(errMsg)`, exits loop
|
||||
- `IMP-033`: Implement `_processMessage(...)` — drains input, calls `llmCall`, extracts tool calls, executes, loops until no tool calls — **COMPLETED** (see `src/agentCore.jl:394-544`)
|
||||
- `while true` loop: `llmCall()` → `_extractToolCalls()` → if `hasToolCalls && length > 0` execute → append results → repeat
|
||||
- On `toolResultBatch.terminate == true`: build final response and break
|
||||
- Catches all errors and calls `eventSink(err_msg)`
|
||||
- `IMP-034`: Implement `_extractToolCalls(response)` — handles **both** formats:
|
||||
- Format 1: `response["message"]["tool_calls"]` (LMStudio/vLLM style) — **COMPLETED** (see `src/agentCore.jl:671-683`)
|
||||
- Format 2: `response.content` blocks with `type => "tool_calls"` or `type => "tool_call"` (OpenAI API style) — **COMPLETED** (see `src/agentCore.jl:685-719`)
|
||||
- Extracts `reasoning_content`, `content` text blocks, `api`, `provider`, `model`, `usage`, `finish_reason` — **COMPLETED** (see `src/agentCore.jl:723-797`)
|
||||
- Returns `(hasToolCalls, toolCallList, assistant_message)` — **COMPLETED** (see `src/agentCore.jl:799-827`)
|
||||
- `IMP-035`: Implement `prepareToolCall(context, assistantMsg, toolCall, config, signal, eventSink)` — resolves tool, validates args, runs `beforeToolCall` hook, never throws — **COMPLETED** (see `src/agentCore.jl:974-1035`)
|
||||
- `IMP-036`: Implement `executePreparedToolCall(prep, signal, eventSink)` — calls `tool.execute(toolCallId, args, ...)` — **COMPLETED** (see `src/agentCore.jl:1078-1100`)
|
||||
- `IMP-037`: Implement `finalizeExecutedToolCall(context, msg, prep, executed, config, signal, eventSink)` — runs `afterToolCall` hook, catches errors — **COMPLETED** (see `src/agentCore.jl:1159-1198`)
|
||||
- `IMP-038`: Implement `prepareToolCallArguments(tool, toolCall)` — calls `tool.prepareArguments` if present — **COMPLETED** (see `src/agentCore.jl:908-917`)
|
||||
- `IMP-039`: Implement `createErrorToolResult(msg)` — builds `agentToolResult` with error content — **COMPLETED** (see `src/agentCore.jl:572-574`)
|
||||
- `IMP-040`: Implement `createToolResultMessage(f)` — builds `toolResultMessage` from `finalizedOutcome` — **COMPLETED** (see `src/agentCore.jl:606-612`)
|
||||
|
||||
**Note**: Sequential and parallel execution implementations continue beyond line 1207 of `src/agentCore.jl` — implementation incomplete, requires review to ensure execution methods properly call hooks and emit lifecycle events. See IMP-041.
|
||||
|
||||
**Validation Tests**: Integration tests for full agent loop with mocked LLM and MCP; contract tests for response format extraction (WT-001..WT-032, SP-017, SP-018)
|
||||
|
||||
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Public API (COMPLETED)
|
||||
|
||||
**Files**: `src/api.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-050`: Implement `promptAgent(agent, msg::AbstractDict)` — put to `inputChannel`, returns agent for chaining — **COMPLETED** (see `src/api.jl:36-39`)
|
||||
- `IMP-051`: Implement `promptAgent(agent, text::String)` — wraps text as OpenAI `Dict` and put to `inputChannel` — **COMPLETED** (see `src/api.jl:41-44`)
|
||||
- `IMP-052`: Implement `takeResponse(agent)` — blocks on `outputChannel` `take!`, returns OpenAI dict response — **COMPLETED** (see `src/api.jl:66-68`)
|
||||
- `IMP-053`: Implement `followUp(agent, msg::AbstractDict)` — put to `followUpChannel`, returns agent — **COMPLETED** (see `src/api.jl:94-97`)
|
||||
- `IMP-054`: Implement `followUp(agent, text::String)` — wraps text as OpenAI `Dict` and put to `followUpChannel` — **COMPLETED** (see `src/api.jl:99-102`)
|
||||
- `IMP-055`: Implement `stopAgent(agent)` — puts `:shutdown`, `fetch(_agentLoop)`, closes all channels — **COMPLETED** (see `src/api.jl:126-139`)
|
||||
|
||||
**Validation Tests**: Integration tests for send/receive/stop lifecycle (WT-001..WT-005)
|
||||
|
||||
**Traceability**: FR-001, FR-006
|
||||
|
||||
---
|
||||
|
||||
## 2. Module Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── YiemAgent.jl # Parent module, includes submodules in dependency order
|
||||
├── type.jl # All Julia struct types (exports 30+ types)
|
||||
├── utils.jl # Formatting, validation, hooks (default implementations)
|
||||
├── toolRegistry.jl # MCP tool discovery, registration, listTools builtin
|
||||
├── agentCore.jl # yiemAgent struct, _agentLoop, _processMessage, extractToolCalls
|
||||
└── api.jl # Public: promptAgent, takeResponse, followUp, stopAgent
|
||||
```
|
||||
|
||||
**Include order** (per `src/YiemAgent.jl` lines 6-19):
|
||||
1. `type.jl` (no dependencies)
|
||||
2. `utils.jl` (depends on `type`)
|
||||
3. `toolRegistry.jl` (depends on `type`)
|
||||
4. `agentCore.jl` (depends on `type`, `utils`, `toolRegistry`)
|
||||
5. `api.jl` (depends on `type`, `utils`, `agentCore`, `toolRegistry`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Task Dependencies
|
||||
|
||||
| Task ID | Title | Depends On | Status |
|
||||
|---------|-------|------------|--------|
|
||||
| IMP-001..IMP-008 | Type system | — | **COMPLETED** |
|
||||
| IMP-010..IMP-018 | Utils (formatMsgForLLM, converters, hooks) | IMP-001..IMP-008 | **COMPLETED** |
|
||||
| IMP-020..IMP-025 | Tool registry (MCP discovery) | IMP-003, IMP-017 | **COMPLETED** |
|
||||
| IMP-030..IMP-040 | Agent core (agent loop, tool call execution) | IMP-001..IMP-008, IMP-010..IMP-018, IMP-020..IMP-025 | **INCOMPLETE*** (see note) |
|
||||
| IMP-050..IMP-055 | Public API | IMP-030..IMP-040 | **COMPLETED** |
|
||||
|
||||
**NOTE on IMP-030..IMP-040**: The sequential and parallel execution functions (`executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls`) begin after line 1207 of `src/agentCore.jl` but the full implementation is truncated in file reads and requires review. The exported functions (`executePreparedToolCall`, `prepareToolCall`, `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls`) are declared (`src/agentCore.jl:4-5`) but their full bodies may be incomplete. **Requires manual validation.**
|
||||
|
||||
**Traceability**: IMP-001..IMP-008 → SP-001..SP-008, IMP-020..IMP-025 → SP-009, SP-010; IMP-010..IMP-018 → SP-011..SP-016, SP-023, SP-024; IMP-030..IMP-040 → SP-017, SP-018, SP-019..SP-022
|
||||
|
||||
---
|
||||
|
||||
## 4. Remaining Work (TODOs)
|
||||
|
||||
| File | Line | Description | Priority |
|
||||
|------|------|-------------|----------|
|
||||
| `src/utils.jl` | 114 | Filter tools from `state.tools` based on user intent and tool description | High |
|
||||
| `src/utils.jl` | 118 | Add system prompt context: time, session state, user preferences | Medium |
|
||||
| `src/utils.jl` | 121 | Prune/reorder/conversation messages before sending to LLM | Medium |
|
||||
| `src/agentCore.jl` | post-1207 | Complete and validate `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls` implementations (currently exported but body may be incomplete) | **Critical** |
|
||||
| `tests/unit/...` | — | Write unit tests for all struct types (IMP-001..IMP-008) and converters (SP-011..SP-016) | High |
|
||||
| `tests/unit/...` | — | Write unit tests for tool validation hooks (SP-023, SP-024) | High |
|
||||
| `tests/integration/...` | — | Write integration tests for agent loop with mocked LLM/MCP (WT-001..WT-032) | High |
|
||||
| `tests/contract/...` | — | Write contract tests for response formats (SP-017, SP-018) | Medium |
|
||||
| `tests/perf/...` | — | Write performance tests (NFR-101, NFR-102) | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 5. Build & Deployment
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd /home/ton/docker-apps/sommpanion/YiemAgent
|
||||
julia --project=. -e 'using Pkg; Pkg.instantiate(); using Pkg; Pkg.precompile()'
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
julia --project=./tests -e 'using Pkg; Pkg.instantiate()'
|
||||
julia --project=./tests -e 'using Test; Test.@testset "Unit" begin include("tests/unit/run_tests.jl") end'
|
||||
julia --project=./tests -e 'using Test; Test.@testset "Integration" begin include("tests/integration/run_tests.jl") end'
|
||||
```
|
||||
|
||||
### CI/CD Preparation
|
||||
|
||||
- Add `.github/workflows/julia.yaml` referencing `ci/julia-pipeline.yaml` steps
|
||||
- Add `Project.toml` dependencies:
|
||||
```toml
|
||||
[deps]
|
||||
JSON = "682c8566-3430-5439-8f4e-b64832e8aaba"
|
||||
DataStructures = "892a3cad-33e6-51e5-808c-f3d90072be70"
|
||||
HTTP = "cd6eee30-4dd5-5329-8bad-3259ec48d0f4"
|
||||
# plus: UUIDs, Dates, PrettyPrinting, Serialization, DataFrames, NATS, LibPQ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Risk Mitigation
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Line 1207+ of `agentCore.jl` (sequential/parallel execution) is incomplete or broken | Manually review and validate `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls` against spec SP-021 (agentLoopConfig) and trace to test `test_tool_call_execution_sequence` |
|
||||
| `GeneralUtils` internal package unavailable in CI | Ensure `GeneralUtils` is installed as dependency (via `Pkg.develop` or registry) |
|
||||
| NATS `mcpServer` struct is abstract — no concrete implementation | Provide at least one mock `mcpServer` for tests that returns canned JSON responses |
|
||||
| Message history grows unbounded | Implement `prepareContext` pruning (TODO in code); add NFR-104 for max history tokens |
|
||||
@@ -0,0 +1,445 @@
|
||||
# Runbook: YiemAgent Agent Framework
|
||||
|
||||
**Severity**: N/A (library — not a deployed service)
|
||||
**Estimated Time**: N/A
|
||||
|
||||
This runbook documents operational guidance for developers working with the YiemAgent agent framework. It is not an operations runbook for a production service, but a developer handbook for debugging, troubleshooting, and extending the framework.
|
||||
|
||||
---
|
||||
|
||||
## 1. Agent Lifecycle
|
||||
|
||||
### 1.1: Create an Agent
|
||||
|
||||
**Steps**:
|
||||
```julia
|
||||
using YiemAgent
|
||||
|
||||
# 1. Implement llmCall function
|
||||
function my_llmCall(messages; eventSink=nothing)
|
||||
# Call LLM API (e.g., OpenAI, Anthropic, LMStudio)
|
||||
# Return dict with keys: "message", "content", "finish_reason", "api", "provider", "model", "usage"
|
||||
eventSink("my_llmCall: sending $(length(messages["messages"])) messages"; publish=true)
|
||||
# ... API call ...
|
||||
return response_dict
|
||||
end
|
||||
|
||||
# 2. Implement mcpServer callable struct
|
||||
struct MyMCPClient
|
||||
conn::NATS.Connection # or HTTP.Client
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
function (mcp::MyMCPClient)(method::String, args...; eventSink=nothing)
|
||||
# JSON-RPC 2.0 calls:
|
||||
# mcp("tools/list", sessionId; eventSink=eventSink)
|
||||
# mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)
|
||||
# ... NATS/HTTP call ...
|
||||
return jsonrpc_response
|
||||
end
|
||||
|
||||
# 3. Create agent
|
||||
mcp = MyMCPClient(nats_conn, "yiemagent:tools", "agent_1")
|
||||
agent = yiemAgent(
|
||||
my_llmCall;
|
||||
systemPrompt="You are a wine assistant.\nYou have tools for searching wine inventory.",
|
||||
tools=mcp_tools, # optional: pass pre-discovered tools
|
||||
prepareContext=my_prepareContext, # optional: custom context preparation
|
||||
formatMsgForLLM=my_formatMsgForLLM, # optional: custom formatting for provider
|
||||
beforeToolCall=my_beforeToolCall, # optional: approval gate
|
||||
afterToolCall=my_afterToolCall, # optional: result sanitization
|
||||
mcpServer=mcp,
|
||||
parallelToolExecute=false,
|
||||
eventSink=my_eventSink,
|
||||
)
|
||||
```
|
||||
|
||||
> **Reference**: `2-solution_design.md:SD-001` (channel-based async), `SD-002` (background task), `SD-004` (MCP struct)
|
||||
|
||||
### 1.2: Send a Message
|
||||
|
||||
```julia
|
||||
# Option 1: Send a string message (automatically wrapped as OpenAI dict)
|
||||
promptAgent(agent, "What wines have notes of oak and vanilla?")
|
||||
|
||||
# Option 2: Send a full OpenAI-format dict with images
|
||||
msg = Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "What wines match these notes?"),
|
||||
Dict("type" => "image_url", "image_url" => Dict("url" => "data:image/png;base64,..."))
|
||||
]
|
||||
)
|
||||
promptAgent(agent, msg)
|
||||
```
|
||||
|
||||
### 1.3: Receive a Response
|
||||
|
||||
```julia
|
||||
# Blocks until agent finishes processing (no tool calls or tool calls complete)
|
||||
response = takeResponse(agent)
|
||||
# response is a Dict in OpenAI format:
|
||||
# Dict("role" => "assistant", "content" => [Dict("type" => "text", "text" => "...")])
|
||||
|
||||
# For text-only response:
|
||||
if response["content"][1]["type"] == "text"
|
||||
println(response["content"][1]["text"])
|
||||
end
|
||||
```
|
||||
|
||||
### 1.4: Send Follow-up While Agent is Busy
|
||||
|
||||
```julia
|
||||
# While the agent is processing tool calls (between promptAgent(...) and takeResponse(...))
|
||||
followUp(agent, "Also check European wines")
|
||||
```
|
||||
|
||||
### 1.5: Stop the Agent
|
||||
|
||||
```julia
|
||||
stopAgent(agent)
|
||||
# Sends :shutdown, closes all channels. Agent object is destroyed.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Troubleshooting
|
||||
|
||||
### 2.1: Agent Loop Exits Silently
|
||||
|
||||
**Symptoms**:
|
||||
- `takeResponse(agent)` hangs indefinitely
|
||||
- No output in eventSink
|
||||
|
||||
**Steps**:
|
||||
1. Check if `eventSink` is receiving error messages:
|
||||
```julia
|
||||
global error_count = 0
|
||||
my_eventSink(msg; publish=false) = begin
|
||||
if occursin("error", lowercase(msg)) || occursin("failed", lowercase(msg))
|
||||
println("ERROR EVENT: $msg")
|
||||
global error_count += 1
|
||||
end
|
||||
end
|
||||
```
|
||||
2. Check network connectivity to LLM API endpoint
|
||||
3. Verify `formatMsgForLLM` produces valid OpenAI-format message structure (`4-specification.md:SP-011..SP-016`)
|
||||
|
||||
> **Reference**: `4-specification.md` (OpenAI format specs), `3-walkthrough.md:WT-030` (LLM timeout)
|
||||
|
||||
### 2.2: Tool Call Not Executing
|
||||
|
||||
**Symptoms**:
|
||||
- LLM returns `tool_calls` but agent returns text response immediately
|
||||
- `beforeToolCall` hook is missing or incorrectly implemented
|
||||
|
||||
**Steps**:
|
||||
1. Verify `prepareContext` returns `agentContext` with `tools` field (not `nothing`) (`src/utils.jl:123`)
|
||||
2. Check `beforeToolCall` hook is not blocking execution:
|
||||
```julia
|
||||
function my_beforeToolCall(ctx, signal, eventSink)
|
||||
# Returning nothing → allows execution
|
||||
# Returning beforeToolCallResult(true, "blocked") → blocks
|
||||
eventSink("beforeToolCall for $(ctx.toolCall.name)"; publish=true)
|
||||
nothing # ← must return nothing (not beforeToolCallResult(false, "N/A"))
|
||||
end
|
||||
```
|
||||
3. Verify tool exists in `context.tools[toolCall.name]` (`src/agentCore.jl:985`)
|
||||
4. Check `validateToolArguments` is not throwing (catches and returns error tool result) (`src/utils.jl:533`)
|
||||
|
||||
> **Reference**: `4-specification.md:SP-021` (agentLoopConfig with beforeToolCall hook), `3-walkthrough.md:WT-032` (beforeToolCall blocks)
|
||||
|
||||
### 2.3: MCP Server Returns No Tools
|
||||
|
||||
**Symptoms**:
|
||||
- `println("[toolRegistry] Discovered 0 MCP tools."` on startup
|
||||
- Agent has no tools available at all times
|
||||
|
||||
**Steps**:
|
||||
1. Verify `mcpServer` callable struct correctly handles `mcp("tools/list", sessionId; eventSink)`
|
||||
2. Check JSON-RPC 2.0 response shape matches `4-specification.md:SP-009`:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"tools": [{"name": "...", ...}],
|
||||
"nextCursor": "" # empty string = no more pages
|
||||
}
|
||||
}
|
||||
```
|
||||
3. Verify `tools/list` error response uses `haskey(response, "error")` check (`src/toolRegistry.jl:128`)
|
||||
|
||||
> **Reference**: `4-specification.md:SP-009` (tools/list format), `3-walkthrough.md:WT-020` (MCP discovery)
|
||||
|
||||
### 2.4: LLM Response Format Not Parsed
|
||||
|
||||
**Symptoms**:
|
||||
- Agent always treats LLM response as text, never extracts tool calls
|
||||
- LLM correctly returns `tool_calls` but agent ignores them
|
||||
|
||||
**Steps**:
|
||||
1. Verify `llmCall` returns a dict containing `message` (LMStudio/vLLM format, `4-specification.md:SP-017`) or `content` (OpenAI format, `4-specification.md:SP-018`)
|
||||
2. Check for both format variations:
|
||||
- Format 1: `response["message"]["tool_calls"]` (LMStudio style, see `src/agentCore.jl:671-683`)
|
||||
- Format 2: `response.content[{type => "tool_calls"}]` (OpenAI style, see `src/agentCore.jl:700-705`)
|
||||
3. Verify `finish_reason` field matches: `"tool_calls"` triggers tool execution, `"stop"`/`"end_turn"` breaks loop (`src/agentCore.jl:723-724`)
|
||||
|
||||
> **Reference**: `4-specification.md:SP-017, SP-018`, `3-walkthrough.md:WT-011, WT-012` (LLM response handling)
|
||||
|
||||
### 2.5: Tool Execution Hangs
|
||||
|
||||
**Symptoms**:
|
||||
- `tool.execute(...)` starts but never returns
|
||||
- EventSink receives `toolExecutionStart` but never `toolExecutionEnd`
|
||||
|
||||
**Steps**:
|
||||
1. Implement `abortSignal` checking in tool execute function:
|
||||
```julia
|
||||
function my_tool_execute(toolCallId, args, signal, eventSink)
|
||||
while !signal.aborted && !all_conditions_met
|
||||
# Long-running operation with periodic abort checks
|
||||
sleep(0.1)
|
||||
end
|
||||
if signal.aborted
|
||||
return agentToolResult([textContent("Cancelled")], Dict(), nothing, false)
|
||||
end
|
||||
return agentToolResult([textContent("Done")], Dict(), nothing, false)
|
||||
end
|
||||
```
|
||||
2. External caller sets `signal.aborted = true` (if `abortSignal` is shared via closure)
|
||||
3. Add timeout logic to tool execute function:
|
||||
```julia
|
||||
start_time = time()
|
||||
timeout_seconds = 30.0
|
||||
while time() - start_time < timeout_seconds && !signal.aborted
|
||||
# operation...
|
||||
sleep(0.5)
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `4-specification.md:SP-022` (abortSignal), `3-walkthrough.md:WT-030` (LLM timeout)
|
||||
|
||||
### 2.6: Message History Grows Unbounded
|
||||
|
||||
**Symptoms**:
|
||||
- Memory usage grows linearly with conversation length
|
||||
- LLM context window exceeded for long sessions
|
||||
|
||||
**Steps**:
|
||||
1. Implement pruning in `prepareContext` (`src/utils.jl:112-126`):
|
||||
```julia
|
||||
function my_prepareContext(state::agentState, eventSink)
|
||||
# Prune old messages, keep last N turns
|
||||
max_turns = 20
|
||||
recent_msgs = isempty(state.messages) ? state.messages :
|
||||
state.messages[max(1, end-2*max_turns):end]
|
||||
|
||||
# Optionally inject system context (time, user prefs)
|
||||
preparedMessages = recent_msgs
|
||||
preparedSystemPrompt = state.systemPrompt * "\nCurrent time: $(now())"
|
||||
|
||||
# Filter tools (optional: keep only relevant subset)
|
||||
filteredTools = state.tools # or filter by intent...
|
||||
|
||||
return agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
|
||||
end
|
||||
```
|
||||
2. Add hard limit in `_agentLoop`:
|
||||
```julia
|
||||
if length(agent._state.messages) > 200
|
||||
# Truncate oldest messages
|
||||
agent._state.messages = agent._state.messages[end-100:end]
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `3-walkthrough.md` (agent message flow), `1-requirements.md:NFR-103` (channel buffer limits)
|
||||
|
||||
---
|
||||
|
||||
## 3. Extending the Framework
|
||||
|
||||
### 3.1: Add a Custom LLM Provider
|
||||
|
||||
**Steps**:
|
||||
1. Implement `llmCall` function that accepts `FormattedMessages::Dict{String, Any}` and returns response dict
|
||||
2. Ensure response dict contains at least one of:
|
||||
- `message.tool_calls` format (LMStudio/vLLM) — `4-specification.md:SP-017`
|
||||
- `content[{type=>"tool_calls"}]` format (OpenAI) — `4-specification.md:SP-018`
|
||||
3. Response dict may optionally contain: `api`, `provider`, `model`, `usage` (`llmUsage` or dict), `finish_reason`, `error_message` (`4-specification.md:SP-017`, `SP-018.4`)
|
||||
|
||||
```julia
|
||||
function my_openai_llmCall(messages::Dict{String, Any}; eventSink=nothing)
|
||||
# messages["messages"] contains OpenAI-format messages
|
||||
# messages["temperature"] (default 0.7)
|
||||
# messages["tools"] (if tools are set)
|
||||
|
||||
body = Dict(
|
||||
"model" => "gpt-4",
|
||||
"messages" => messages["messages"],
|
||||
"tools" => get(messages, "tools", Any[])
|
||||
)
|
||||
|
||||
resp = HTTP.post("https://api.openai.com/v1/chat/completions",
|
||||
headers, JSON.json(body))
|
||||
json_resp = JSON.parse(String(resp.body))
|
||||
|
||||
# Extract first completion
|
||||
choice = json_resp["choices"][1]["message"]
|
||||
|
||||
# Build uniform response
|
||||
return Dict(
|
||||
"api" => "openai",
|
||||
"provider" => "openai",
|
||||
"model" => "gpt-4",
|
||||
"message" => choice,
|
||||
"usage" => json_resp["usage"],
|
||||
"finish_reason" => json_resp["choices"][1]["finish_reason"]
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `1-requirements.md:FR-002`, `4-specification.md:SP-017, SP-018`
|
||||
|
||||
### 3.2: Add a Custom Tool
|
||||
|
||||
**Steps**:
|
||||
1. Define tool schema in `inputSchema` (JSON Schema format):
|
||||
```julia
|
||||
schema = Dict{String, Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"query" => Dict("type" => "string", "description" => "Search query"),
|
||||
"max_results" => Dict("type" => "integer", "default" => 5)
|
||||
),
|
||||
"required" => ["query"]
|
||||
)
|
||||
```
|
||||
2. Implement `execute` function:
|
||||
```julia
|
||||
tool = agentTool(
|
||||
name="searchWine",
|
||||
label="Search Wine Inventory",
|
||||
description="Search for wines matching a query",
|
||||
inputSchema=schema,
|
||||
execute=(toolCallId, args, signal, eventSink) -> begin
|
||||
query = args["query"]
|
||||
max_results = get(args, "max_results", Dict{Any,Any}()["max_results"] => 5...) # handle default
|
||||
|
||||
wines = run_search(query, max_results)
|
||||
|
||||
result_text = join(["- $(w.name) (\$$(w.price))" for w in wines], "\n")
|
||||
return agentToolResult(
|
||||
[textContent("Found $(length(wines)) wines:\n$result_text")],
|
||||
Dict("count" => length(wines)),
|
||||
nothing, false # content, details, usage, terminate
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing, # uses default validator
|
||||
parallelToolExecute = false
|
||||
)
|
||||
|
||||
# Register: push!(tools, tool.name => tool)
|
||||
```
|
||||
3. Optionally override validation:
|
||||
```julia
|
||||
function my_validate(args)
|
||||
if haskey(args, "query") && length(args["query"]) < 3
|
||||
return "Query must be at least 3 characters"
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
agentTool(
|
||||
...,
|
||||
validateRequiredArgs = my_validate, # replaces default validator
|
||||
)
|
||||
```
|
||||
|
||||
> **Reference**: `1-requirements.md:FR-004`, `4-specification.md:SP-009, SP-010`
|
||||
|
||||
### 3.3: Implement User Approval Gate (beforeToolCall Hook)
|
||||
|
||||
**Steps**:
|
||||
1. Implement `beforeToolCall` that blocks expensive/destructive operations:
|
||||
```julia
|
||||
function my_approval_toolCall(ctx::beforeToolCallContext, signal::abortSignal, eventSink)
|
||||
if ctx.toolCall.name in ("delete_wine", "update_price_bulk")
|
||||
eventSink("Approval required for $(ctx.toolCall.name) with args $(ctx.args)")
|
||||
# In production, send to UI and wait for user input
|
||||
approved = ask_user_for_approval(ctx.toolCall.name, ctx.args) # blocking call
|
||||
if !approved
|
||||
return beforeToolCallResult(true, "User denied approval for $(ctx.toolCall.name)")
|
||||
end
|
||||
end
|
||||
return nothing # allow execution
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `2-solution_design.md:SD-005` (lifecycle hooks), `3-walkthrough.md:WT-032` (beforeToolCall blocks)
|
||||
|
||||
### 3.4: Implement Result Sanitization (afterToolCall Hook)
|
||||
|
||||
**Steps**:
|
||||
1. Implement `afterToolCall` to mask sensitive data:
|
||||
```julia
|
||||
function my_sanitize_toolCall(ctx::afterToolCallContext, signal::abortSignal, eventSink)
|
||||
if ctx.toolCall.name == "getCustomerInfo"
|
||||
# Mask sensitive fields before LLM sees result
|
||||
result = ctx.result
|
||||
for (i, block) in enumerate(result.content)
|
||||
if block isa textContent
|
||||
masked = replace(block.text, r"\d{12}" => "***-***-****") # mask SSN
|
||||
result = agentToolResult(
|
||||
[replace(block.text, r"\d{12}" => "***-***-****") for block in result.content],
|
||||
result.details,
|
||||
result.usage,
|
||||
result.terminate
|
||||
)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
return nothing # pass through for other tools
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `2-solution_design.md:SD-005` (lifecycle hooks)
|
||||
|
||||
---
|
||||
|
||||
## 4. Common Error Codes
|
||||
|
||||
| Error Condition | Response Code | Description |
|
||||
|-----------------|---------------|-------------|
|
||||
| Tool not found | `immediateOutcome("Tool <name> not found", true)` | `context.tools[toolCall.name]` returns `nothing` |
|
||||
| Missing required args | `immediateOutcome("Missing required arguments: <fields>", true)` | `validateRequiredArgs` returns error string |
|
||||
| Custom validation failed | `immediateOutcome("<tool.validateRequiredArgs result>", true)` | Tool-specific validation hook returns error |
|
||||
| beforeToolCall blocked | `immediateOutcome("Tool execution was blocked: <reason>", true)` | `beforeToolCall` returns `beforeToolCallResult(true, reason)` |
|
||||
| Operation aborted | `immediateOutcome("Operation aborted", true)` | `signal.aborted == true` after `beforeToolCall` |
|
||||
| Tool execution crashed | `immediateOutcome("<exception message>", true)` | `preptool.execute()` throws exception |
|
||||
| MCP error | `agentToolResult(["MCP error: <err_msg>"], {..., "error"=>err_msg}, ...)` | Server returns `response["error"]` JSON-RPC envelope |
|
||||
| MCP network error | `agentToolResult(["MCP call error: <err_msg>"], {..., "error"=>err_msg}, ...)` | Exception during `mcp("tools/call", ...)` |
|
||||
|
||||
> **Reference**: `4-specification.md:SP-009, SP-010`, `src/agentCore.jl:989, 1013, 1019, 1033, 1097`
|
||||
|
||||
---
|
||||
|
||||
## 5. Debugging Checklist
|
||||
|
||||
Per ASG Framework §Pre-Code Debugging Habit:
|
||||
|
||||
| Stage | Debug Question |
|
||||
|-------|----------------|
|
||||
| **Implementation** | If a tool is not found, does `prepareToolCall` return `immediateOutcome` with "Tool not found"? |
|
||||
| **Testing** | If an LLM response has `tool_calls`, does `_extractToolCalls` return `(true, toolCallList, assistantMessageToolCall)`? |
|
||||
| **Deploying** | If the agent loop exits, does `eventSink` contain the error message? |
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
- **API contract**: [`4-specification.md`](4-specification.md) — message types, MCP JSON-RPC format, response extraction formats
|
||||
- **Solution decisions**: [`2-solution_design.md`](2-solution_design.md) — component architecture, lifecycle hooks, format-agnostic LLM responses
|
||||
- **Walkthroughs**: [`3-walkthrough.md`](3-walkthrough.md) — end-to-end agent flow, error paths, hook sequences
|
||||
- **Validation tests**: [`6-validation.md`](6-validation.md) — test traceability matrix mapping requirements/specs to test files
|
||||
@@ -1,92 +0,0 @@
|
||||
# Dynamic Tool Loading
|
||||
|
||||
Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory without hardcoding filenames in the main module.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. `src/tools/registry.jl` defines a `loadTools(dir::String)` function that scans a directory for `.jl` files
|
||||
2. Each tool file must define a single function: `getTool()::agentTool`
|
||||
3. `loadTools()` sorts files alphabetically, includes each one, calls `getTool()`, and registers the result
|
||||
4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── tools/
|
||||
│ ├── registry.jl # Tool loader (do not edit)
|
||||
│ ├── getWeather.jl # Your tool
|
||||
│ └── query_db.jl # Another tool
|
||||
├── type.jl
|
||||
├── utils.jl
|
||||
├── agentCore.jl
|
||||
├── api.jl
|
||||
└── YiemAgent.jl
|
||||
```
|
||||
|
||||
## Creating a Tool
|
||||
|
||||
Each `.jl` file in `src/tools/` must define `getTool()` returning an `agentTool`:
|
||||
|
||||
```julia
|
||||
# src/tools/getWeather.jl
|
||||
function getTool()::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"),
|
||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Weather in $(city): Sunny, 22C")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
No `module` wrapper needed — the registry includes each file in the current module scope so all types (`agentTool`, `textContent`, `agentToolResult`, etc.) resolve correctly.
|
||||
|
||||
## Loading Tools
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .YiemAgent: toolRegistry
|
||||
|
||||
# Load all tool files from src/tools/
|
||||
tools = YiemAgent.loadTools(joinpath(@__DIR__, "src", "tools"))
|
||||
|
||||
# Create agent with loaded tools
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant.",
|
||||
model = my_model,
|
||||
tools = tools,
|
||||
llmCall = my_llm_call,
|
||||
agentEventSink = my_event_sink
|
||||
)
|
||||
```
|
||||
|
||||
## Available Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `loadTools(dir::String)` | Scan directory and load all `.jl` tool files |
|
||||
| `registerTool(tool::agentTool)` | Register a single tool into the global registry |
|
||||
| `getTools()` | Get deep copy of all registered tools |
|
||||
| `listTools()` | List all registered tools as `(name, label)` pairs |
|
||||
| `clearTools()` | Clear the global registry |
|
||||
|
||||
## File Loading Order
|
||||
|
||||
Files are sorted alphabetically before loading, so `01_database.jl` loads before `02_weather.jl`. This ensures deterministic registration order.
|
||||
@@ -1,2 +0,0 @@
|
||||
# ── executeToolCalls() Julia pseudo code ──────────────────────────
|
||||
# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit
|
||||
@@ -0,0 +1,11 @@
|
||||
check my understanding
|
||||
1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and it will be the latest message in agent._state.messages. then _agentLoop() can pick it as the output to outputChannel
|
||||
2) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is false then _processMessage() loop continue
|
||||
3) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is true then final_response message get pushed into agent._state.messages. _processMessage() loop exit. then _agentLoop() can pick it as the output to outputChannel
|
||||
|
||||
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?
|
||||
@@ -0,0 +1,94 @@
|
||||
can you make sure yiemAgent use correct format when contacting MCP server. you can check MCP server doc at /home/ton/docker-apps/sommpanion/mcp/docs/specification.md?
|
||||
|
||||
smartpack() and smartunpack() doc is at /home/ton/docker-apps/sommpanion/msghandler/README.md
|
||||
|
||||
user code that use yiemAgent is here /home/ton/docker-apps/sommpanion/agent-backend/test/runtest.jl
|
||||
|
||||
this is log folder that logs when I ran yiemAgent. you can check /home/ton/docker-apps/sommpanion/agent-backend/log/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+30
-53
@@ -1,53 +1,30 @@
|
||||
module YiemAgent
|
||||
|
||||
# export agent
|
||||
|
||||
|
||||
""" 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.
|
||||
"""
|
||||
|
||||
include("type.jl")
|
||||
using .type
|
||||
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("tools/registry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
# include("llmfunction.jl")
|
||||
# using .llmfunction
|
||||
|
||||
include("agentCore.jl")
|
||||
using .agentCore
|
||||
|
||||
include("api.jl")
|
||||
using .api
|
||||
|
||||
|
||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
end # module YiemAgent_v1
|
||||
module YiemAgent
|
||||
|
||||
"""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."""
|
||||
|
||||
include("type.jl")
|
||||
using .type
|
||||
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
include("agentCore.jl")
|
||||
using .agentCore
|
||||
|
||||
include("api.jl")
|
||||
using .api
|
||||
|
||||
export promptAgent, takeResponse, followUp, stopAgent
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||
|
||||
|
||||
|
||||
|
||||
end # end module
|
||||
|
||||
+842
-377
File diff suppressed because it is too large
Load Diff
+29
-20
@@ -1,16 +1,15 @@
|
||||
module api
|
||||
|
||||
export prompt
|
||||
export promptAgent, takeResponse, followUp, stopAgent
|
||||
|
||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||
DataFrames, Serde
|
||||
DataFrames
|
||||
using GeneralUtils
|
||||
using ..type, ..utils
|
||||
using ..type, ..utils, ..agentCore, ..toolRegistry
|
||||
|
||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Send a message to the agent's input channel.
|
||||
|
||||
@@ -19,26 +18,31 @@ The agent processes messages from `inputChannel` in the background task.
|
||||
|
||||
# Arguments
|
||||
- `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
|
||||
- The same `agent` instance for chaining
|
||||
|
||||
# Notes
|
||||
- Use `take_response(agent)` to receive the agent's response after sending a message.
|
||||
- Use `follow_up(agent, msg)` to send messages while the agent is still processing.
|
||||
- Use `takeResponse(agent)` to receive the agent's response after sending a message.
|
||||
- Use `followUp(agent, msg)` to send messages while the agent is still processing.
|
||||
|
||||
# Examples
|
||||
```jldoctest
|
||||
julia> run_agent(agent, "Hello!")
|
||||
julia> promptAgent(agent, Dict("role" => "user", "content" => "Hello!"))
|
||||
yiemAgent(...)
|
||||
```
|
||||
"""
|
||||
function run_agent(agent::yiemAgent, msg)
|
||||
function promptAgent(agent::yiemAgent, msg::AbstractDict{String, Any})
|
||||
put!(agent.inputChannel, msg)
|
||||
return agent
|
||||
end
|
||||
|
||||
function promptAgent(agent::yiemAgent, text::String)
|
||||
put!(agent.inputChannel, Dict("role" => "user", "content" => [Dict("type" => "text", "text" => text)]))
|
||||
return agent
|
||||
end
|
||||
|
||||
"""
|
||||
Take a response from the agent's output channel.
|
||||
|
||||
@@ -51,15 +55,15 @@ Blocks until the agent sends a response.
|
||||
- An `assistantMessage` instance representing the agent's response
|
||||
|
||||
# Notes
|
||||
- Use `run_agent(agent, msg)` to send a message before calling this function.
|
||||
- Use `promptAgent(agent, msg)` to send a message before calling this function.
|
||||
|
||||
# Examples
|
||||
```jldoctest
|
||||
julia> response = take_response(agent)
|
||||
julia> response = takeResponse(agent)
|
||||
assistantMessage(...)
|
||||
```
|
||||
"""
|
||||
function take_response(agent::yiemAgent)
|
||||
function takeResponse(agent::yiemAgent)
|
||||
return take!(agent.outputChannel)
|
||||
end
|
||||
|
||||
@@ -71,27 +75,32 @@ and before any tool call results are sent.
|
||||
|
||||
# Arguments
|
||||
- `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
|
||||
- The same `agent` instance for chaining
|
||||
|
||||
# Notes
|
||||
- Use `run_agent(agent, msg)` for the primary message and `follow_up(agent, msg)` for additional
|
||||
- Use `promptAgent(agent, msg)` for the primary message and `followUp(agent, msg)` for additional
|
||||
messages while the agent is processing.
|
||||
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
||||
|
||||
# Examples
|
||||
```jldoctest
|
||||
julia> follow_up(agent, "Also consider red wines")
|
||||
julia> followUp(agent, Dict("role" => "user", "content" => "Also consider red wines"))
|
||||
yiemAgent(...)
|
||||
```
|
||||
"""
|
||||
function follow_up(agent::yiemAgent, msg)
|
||||
function followUp(agent::yiemAgent, msg::AbstractDict{String, Any})
|
||||
put!(agent.followUpChannel, msg)
|
||||
return agent
|
||||
end
|
||||
|
||||
function followUp(agent::yiemAgent, text::String)
|
||||
put!(agent.followUpChannel, Dict("role" => "user", "content" => [Dict("type" => "text", "text" => text)]))
|
||||
return agent
|
||||
end
|
||||
|
||||
"""
|
||||
Gracefully stop the agent.
|
||||
|
||||
@@ -105,19 +114,19 @@ then closes all channels (`inputChannel`, `outputChannel`, `followUpChannel`).
|
||||
- `nothing`
|
||||
|
||||
# Notes
|
||||
- After calling `stop_agent`, the agent is no longer usable. A new agent must be created
|
||||
- After calling `stopAgent`, the agent is no longer usable. A new agent must be created
|
||||
for further interaction.
|
||||
- If the background task throws a `TaskFailedException`, it is rethrown.
|
||||
|
||||
# Examples
|
||||
```jldoctest
|
||||
julia> stop_agent(agent)
|
||||
julia> stopAgent(agent)
|
||||
```
|
||||
"""
|
||||
function stop_agent(agent::yiemAgent)
|
||||
function stopAgent(agent::yiemAgent)
|
||||
put!(agent.inputChannel, :shutdown)
|
||||
try
|
||||
fetch(agent._agent_loop)
|
||||
fetch(agent._agentLoop)
|
||||
catch e
|
||||
if e isa TaskFailedException
|
||||
rethrow(e)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
module toolRegistry
|
||||
|
||||
export registerTool, registerAllTools, clearTools, listTool
|
||||
|
||||
using Dates
|
||||
using JSON, DataStructures
|
||||
using ..type
|
||||
|
||||
|
||||
|
||||
# ── MCP helper functions ────────────────────────────────────────────
|
||||
|
||||
"""
|
||||
Extract text from MCP tool result content array.
|
||||
|
||||
Handles JSON-RPC 2.0 result content format:
|
||||
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
||||
"""
|
||||
function _extract_text_content(result::AbstractDict)::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
|
||||
|
||||
"""
|
||||
Wrap an MCP tool definition as an `agentTool`.
|
||||
|
||||
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::AbstractDict{String, Any}; eventSink=nothing, sessionId=nothing)::agentTool
|
||||
name = tool_def["name"]
|
||||
title = get(tool_def, "title", get(tool_def, "label", 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::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
eventSink) -> begin
|
||||
try
|
||||
response = mcpserver("tools/call", name, args, sessionId; eventSink=eventSink)
|
||||
|
||||
# Parse JSON-RPC 2.0 response envelope
|
||||
if haskey(response, "error")
|
||||
rpc_error = response["error"]
|
||||
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
||||
return agentToolResult(
|
||||
[textContent("MCP error: $err_msg")],
|
||||
Dict{Any,Any}("error" => err_msg),
|
||||
nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
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")` (JSON-RPC 2.0 format),
|
||||
parses the response envelope, and registers each discovered tool.
|
||||
Handles pagination via `nextCursor`. Skips tools already registered.
|
||||
|
||||
# Returns
|
||||
- `Int`: number of new tools registered
|
||||
"""
|
||||
function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::Int
|
||||
if mcpserver === nothing
|
||||
return 0
|
||||
end
|
||||
new_count = 0
|
||||
try
|
||||
eventSink("register_mcp_tools 1")
|
||||
response = mcpserver("tools/list", sessionId; eventSink=eventSink)
|
||||
eventSink("register_mcp_tools 2")
|
||||
# Parse JSON-RPC 2.0 response envelope
|
||||
if haskey(response, "error")
|
||||
rpc_error = response["error"]
|
||||
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
||||
println("[toolRegistry] MCP tools/list failed: $err_msg")
|
||||
return 0
|
||||
end
|
||||
if haskey(response, "result")
|
||||
response = response["result"]
|
||||
end
|
||||
|
||||
tools_array = response["tools"]
|
||||
cursor = get(response, "nextCursor", nothing)
|
||||
|
||||
for tool_def in tools_array
|
||||
name = tool_def["name"]
|
||||
if haskey(tools, name)
|
||||
continue
|
||||
end
|
||||
|
||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
|
||||
tools[name] = wrapped
|
||||
new_count += 1
|
||||
end
|
||||
|
||||
# Paginate: fetch remaining tools if nextCursor is present
|
||||
while cursor !== nothing && cursor !== ""
|
||||
response = sessionId !== nothing ? mcpserver("tools/list", sessionId; eventSink=eventSink) : mcpserver("tools/list"; eventSink=eventSink)
|
||||
if haskey(response, "result")
|
||||
response = response["result"]
|
||||
end
|
||||
tools_array = get(response, "tools", Any[])
|
||||
cursor = get(response, "nextCursor", nothing)
|
||||
for tool_def in tools_array
|
||||
name = tool_def["name"]
|
||||
if haskey(tools, name)
|
||||
continue
|
||||
end
|
||||
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
|
||||
tools[name] = wrapped
|
||||
new_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(tools))")
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
err_msg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
|
||||
eventSink(err_msg)
|
||||
|
||||
errMsg = sprint(showerror, e)
|
||||
println("[toolRegistry] MCP tools/list failed: $errMsg")
|
||||
end
|
||||
|
||||
return new_count
|
||||
end
|
||||
|
||||
"""
|
||||
registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||
|
||||
Add `tool` to `tools`, overwriting any existing tool with the same name.
|
||||
|
||||
# Arguments
|
||||
- `tools`: Tool dict to modify
|
||||
- `tool`: The `agentTool` to register
|
||||
|
||||
# Returns
|
||||
- The same `tools` dict (modified in place)
|
||||
"""
|
||||
function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
|
||||
)::OrderedDict{String, agentTool}
|
||||
tools[tool.name] = tool
|
||||
println("[toolRegistry] Registered tool: $(tool.name)")
|
||||
return tools
|
||||
end
|
||||
|
||||
"""
|
||||
Remove all tools from the dict.
|
||||
|
||||
# Arguments
|
||||
- `tools`: Tool dict to clear
|
||||
|
||||
# Returns
|
||||
- `nothing`
|
||||
"""
|
||||
function clearTools(tools::OrderedDict{String, agentTool})::Nothing
|
||||
empty!(tools)
|
||||
println("[toolRegistry] Registry cleared")
|
||||
return nothing
|
||||
end
|
||||
|
||||
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
|
||||
|
||||
"""
|
||||
listTool(tools::OrderedDict{String, agentTool}, mcpserver) -> agentTool
|
||||
|
||||
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
|
||||
|
||||
First call: queries the MCP server via `mcpserver("tools/list")`, registers
|
||||
all discovered tools into `tools` (in-place mutation, with
|
||||
pagination via nextCursor), then returns the full tool list.
|
||||
|
||||
Subsequent calls: returns the current list (tools remain registered).
|
||||
"""
|
||||
function listTool(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::agentTool
|
||||
return agentTool(
|
||||
name="listTools",
|
||||
label="List Tools",
|
||||
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}(
|
||||
"type" => "object",
|
||||
"properties" => Dict{String,Any}(),
|
||||
"required" => Any[]
|
||||
),
|
||||
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
eventSink) -> begin
|
||||
# Discover and register MCP tools (idempotent — skips already registered)
|
||||
new_count = register_mcp_tools(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
|
||||
eventSink("tools dump: " * sprint(show, tools))
|
||||
|
||||
# Always include listTools itself in the count
|
||||
total = length(tools)
|
||||
|
||||
lines = String[
|
||||
"- $(t.name): $(t.label) — $(t.description)"
|
||||
for (k, t) in tools
|
||||
]
|
||||
result_text = "Available tools ($total):\n" * join(lines, "\n")
|
||||
|
||||
return agentToolResult(
|
||||
[textContent(result_text)],
|
||||
Dict{Any,Any}("count" => total),
|
||||
nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments=nothing,
|
||||
validateRequiredArgs=nothing,
|
||||
parallelToolExecute=false,
|
||||
)
|
||||
end
|
||||
|
||||
# ── High-level API ──────────────────────────────────────────────────
|
||||
|
||||
"""
|
||||
registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver)
|
||||
|
||||
Register all tools for an agent:
|
||||
1. Auto-discover existing tools from the MCP server
|
||||
2. Register the `listTools` tool so the agent can discover new tools at runtime
|
||||
|
||||
# Arguments
|
||||
- `tools`: The tool dict to populate
|
||||
- `mcpserver`: A callable struct that communicates with the MCP server
|
||||
"""
|
||||
function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing)
|
||||
list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
|
||||
registerTool(tools, list_t; eventSink=eventSink)
|
||||
end
|
||||
|
||||
end # module
|
||||
@@ -1,568 +0,0 @@
|
||||
# Tools
|
||||
|
||||
Tools allow the agent to perform actions and fetch data. Each tool defines a **schema** (what arguments it accepts) and an **execution function** (what it does).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Add a new tool by creating a `.jl` file in `src/tools/`. The file must define a `getTool()` function that returns an `agentTool`:
|
||||
|
||||
```julia
|
||||
# src/tools/my_tool.jl
|
||||
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Hello from $(city)!")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "my_tool",
|
||||
label = "My Tool",
|
||||
description = "Says hello to a city.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string", "description" => "City name")
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
When `loadTools()` or `registerTool()` is called, the tool becomes available to the agent.
|
||||
|
||||
## Tool Anatomy
|
||||
|
||||
Each tool has 3 main parts:
|
||||
|
||||
### 1. Schema (`inputSchema`)
|
||||
|
||||
JSON Schema (MCP format) describing the tool's arguments. The `"required"` array lists mandatory fields:
|
||||
|
||||
```julia
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string", "description" => "City name"),
|
||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
|
||||
),
|
||||
"required" => ["city"]
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Execution Function (`execute`)
|
||||
|
||||
A function with the signature:
|
||||
|
||||
```julia
|
||||
execute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
```
|
||||
|
||||
- **`toolCallId`** — unique ID for this invocation (from the LLM's tool call)
|
||||
- **`args`** — validated arguments provided by the LLM
|
||||
- **`signal`** — abort signal for cancellable operations
|
||||
- **`onPartialResult`** — callback for streaming progress updates
|
||||
- **Returns** — `agentToolResult` with content, details, usage, and termination flag
|
||||
|
||||
```julia
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
# Optional: stream progress updates
|
||||
onPartialResult(Dict("status" => "Fetching data..."))
|
||||
|
||||
# Do work
|
||||
result = "Weather in $(args["city"]): Sunny, 22°C"
|
||||
|
||||
# Return result
|
||||
return agentToolResult(
|
||||
[textContent(result)],
|
||||
Dict{Any,Any}(), # details
|
||||
nothing, # usage
|
||||
false # terminate (true to stop agent loop)
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Tool Definition (`getTool()`)
|
||||
|
||||
Returns an `agentTool` struct:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | `String` | Unique identifier (e.g. `"getWeather"`) |
|
||||
| `label` | `String` | Human-readable name (e.g. `"Weather Lookup"`) |
|
||||
| `description` | `String` | What the tool does (shown to the LLM) |
|
||||
| `inputSchema` | `Any` | JSON Schema (MCP format) |
|
||||
| `execute` | `Function` | The execution function |
|
||||
| `prepareArguments` | `Union{Function,Nothing}` | Optional argument transform before validation |
|
||||
| `validateRequiredArgs` | `Union{Function,Nothing}` | Optional custom validation |
|
||||
| `parallelToolExecute` | `Bool` | Run this tool in parallel with others |
|
||||
|
||||
## Argument Validation
|
||||
|
||||
Validation happens **before** tool execution, in the `prepareToolCall` phase. Invalid calls return an error immediately without invoking `execute`, `beforeToolCall`, or logging `toolExecutionStart`.
|
||||
|
||||
### Default: JSON Schema Required Fields
|
||||
|
||||
Set `validateRequiredArgs = nothing` to use the default validator, which checks that all fields in `inputSchema["required"]` are present:
|
||||
|
||||
```julia
|
||||
# src/tools/getWeather.jl — uses default validation
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
# ...
|
||||
validateRequiredArgs = nothing, # uses default
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Custom Validation Hook
|
||||
|
||||
Override `validateRequiredArgs` when you need:
|
||||
- **Cross-field constraints** (e.g. "at least one of X or Y")
|
||||
- **Format validation** (e.g. regex patterns, date parsing)
|
||||
- **Domain rules** (e.g. value ranges, business logic)
|
||||
|
||||
The hook signature takes only `args`:
|
||||
|
||||
```julia
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
if !haskey(args, "timezone") && isempty(city)
|
||||
return "Missing required argument: provide at least one of 'timezone' or 'city'"
|
||||
end
|
||||
|
||||
if tz !== nothing
|
||||
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'"
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
```
|
||||
|
||||
Return `nothing` to pass, or an error `String` to fail. The error is fed back to the LLM so it can retry with corrected arguments.
|
||||
|
||||
## Tool Lifecycle — Framework Internals
|
||||
|
||||
This section traces the full code path from the moment the LLM returns tool calls to the final result being fed back into the conversation. All code references are to `agentCore.jl`.
|
||||
|
||||
### Phase 1: Detect Tool Calls in LLM Response
|
||||
|
||||
After the LLM returns an `assistantMessage`, the loop at `agentCore.jl:220-244` inspects each `content` block:
|
||||
|
||||
```julia
|
||||
# agentCore.jl:217-244
|
||||
has_tool_calls = false
|
||||
tool_call_list = agentToolCall[]
|
||||
|
||||
for content_block in response.content
|
||||
if content_block isa Dict
|
||||
# OpenAI-style: type == "tool_calls" with array of tool calls
|
||||
if get(content_block, :type, "") == "tool_calls"
|
||||
for tc_data in get(content_block, :tool_calls, [])
|
||||
tc = agentToolCall(
|
||||
type="function",
|
||||
id=get(tc_data, :id, string(uuid4())),
|
||||
name=get(tc_data, :function, Dict{String,Any}())[:name],
|
||||
arguments=get(tc_data, :function, Dict{String,Any}())[:arguments],
|
||||
)
|
||||
push!(tool_call_list, tc)
|
||||
end
|
||||
# Alternative style: type == "tool_call" single dict per block
|
||||
elseif get(content_block, :type, "") == "tool_call"
|
||||
tc = agentToolCall(
|
||||
type="function",
|
||||
id=get(tc_data, :id, string(uuid4())),
|
||||
name=get(tc_data, :name, ""),
|
||||
arguments=get(tc_data, :arguments, Dict{String,Any}()),
|
||||
)
|
||||
push!(tool_call_list, tc)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Each content block with `type == "tool_calls"` or `type == "tool_call"` extracts an `agentToolCall` (id, name, arguments dict) and collects them into a `Vector{agentToolCall}`.
|
||||
|
||||
### Phase 2: Dispatch to Sequential or Parallel Execution
|
||||
|
||||
At `agentCore.jl:247`, the framework checks if any tool calls exist and decides execution mode:
|
||||
|
||||
```julia
|
||||
# agentCore.jl:247-265
|
||||
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
||||
config = agentLoopConfig(
|
||||
agent._state.tools,
|
||||
agent.beforeToolCall,
|
||||
agent.afterToolCall,
|
||||
agent.parallelToolExecute ? "parallel" : "sequential",
|
||||
)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
```
|
||||
|
||||
`executeToolCalls` (`agentCore.jl:988-1015`) checks:
|
||||
- `config.toolExecution == "sequential"` → sequential mode
|
||||
- Any tool has `parallelToolExecute == false` → sequential mode
|
||||
- Otherwise → parallel mode
|
||||
|
||||
### Phase 3: Per-Call Preparation (`prepareToolCall`)
|
||||
|
||||
Each tool call goes through `prepareToolCall` (`agentCore.jl:511-547`):
|
||||
|
||||
```
|
||||
1. Look up tool by name: find(t -> t.name == tc.name, context.tools)
|
||||
2. If not found → immediateOutcome("Tool X not found", true)
|
||||
3. Run tool.prepareArguments (if defined) → transforms raw LLM args
|
||||
4. Run validateToolArguments → validateRequiredArgs (hook or default)
|
||||
→ if fails → throws ArgumentError → caught below
|
||||
5. Run beforeToolCall hook (if defined) → can block execution
|
||||
→ if blocked → immediateOutcome("Tool execution was blocked", true)
|
||||
6. Return preparedToolCall(tool, tc, validatedArgs)
|
||||
```
|
||||
|
||||
If any step throws (validation, prepareArguments, beforeToolCall), the catch block at `agentCore.jl:545` converts it to an `immediateOutcome`:
|
||||
|
||||
```julia
|
||||
catch err
|
||||
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
```
|
||||
|
||||
### Phase 4: Execution (`executePreparedToolCall`)
|
||||
|
||||
For each `preparedToolCall`, `executePreparedToolCall` (`agentCore.jl:589-617`) runs:
|
||||
|
||||
```julia
|
||||
function executePreparedToolCall(prep::preparedToolCall, signal, emit)::executedOutcome
|
||||
updateEvents = promise[]
|
||||
accepting = true
|
||||
|
||||
try
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id, prep.args, signal,
|
||||
partialResult -> begin
|
||||
if accepting
|
||||
push!(updateEvents, emit(toolExecUpdateEvent(..., partialResult)))
|
||||
end
|
||||
end
|
||||
)
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(result, false)
|
||||
catch err
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- Calls `tool.execute(id, args, signal, onPartialResult)` — your tool's `executeTool` function
|
||||
- `signal` can be checked inside `executeTool` for cancellation
|
||||
- `onPartialResult` is called for streaming updates, which are emitted as `toolExecutionUpdate` events
|
||||
- `accepting` guard prevents emitting updates after the result is already captured
|
||||
- `wait.(updateEvents)` ensures all streaming updates are delivered before returning
|
||||
- Execution errors are caught and returned as `executedOutcome(isError=true)` — never thrown
|
||||
|
||||
### Phase 5: Finalization (`finalizeExecutedToolCall`)
|
||||
|
||||
After execution, `finalizeExecutedToolCall` (`agentCore.jl:675-706`) runs the `afterToolCall` hook:
|
||||
|
||||
```julia
|
||||
function finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)::finalizedOutcome
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
|
||||
if config.afterToolCall !== nothing
|
||||
try
|
||||
after = config.afterToolCall(afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal)
|
||||
if after !== nothing
|
||||
# Hook can mutate: content, details, usage, terminate, isError
|
||||
result = merge(result, dict(...))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
catch err
|
||||
result = createErrorToolResult(sprint(showerror, err))
|
||||
isError = true
|
||||
end
|
||||
end
|
||||
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
end
|
||||
```
|
||||
|
||||
The hook can:
|
||||
- Mask sensitive data from result content
|
||||
- Normalize usage tracking
|
||||
- Flip `terminate: true` based on business logic
|
||||
- Wrap errors in friendlier messages for the LLM
|
||||
|
||||
If the hook itself throws, the error is caught and converted to an error outcome.
|
||||
|
||||
### Phase 6: Emit Events and Create Result Message
|
||||
|
||||
Each call emits `toolExecutionEnd`:
|
||||
|
||||
```julia
|
||||
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
|
||||
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, finalized.result, finalized.isError))
|
||||
end
|
||||
```
|
||||
|
||||
Then creates the `toolResultMessage` for conversation history (`agentCore.jl:373-379`):
|
||||
|
||||
```julia
|
||||
function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
||||
return toolResultMessage(
|
||||
"toolResult", f.toolCall.id, f.toolCall.name,
|
||||
f.result.content, f.result.details, f.result.usage,
|
||||
get(f.result, :addedToolNames, string[]), f.isError, nowMillis()
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Phase 7: Batch Assembly and Loop Control
|
||||
|
||||
In `executeToolCallsSequential` (`agentCore.jl:795-829`) or `executeToolCallsParallel` (`agentCore.jl:888-936`), all results are collected:
|
||||
|
||||
```julia
|
||||
messages = toolResultMessage[]
|
||||
for finalized in finalizedCalls
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
end
|
||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||
```
|
||||
|
||||
`shouldTerminate` (`agentCore.jl:409`) returns `true` only if ALL tools in the batch set `result.terminate == true`. If `false`, the agent loop at `agentCore.jl:176-308` feeds the tool results back to the LLM for another turn.
|
||||
|
||||
### Data Flow Summary
|
||||
|
||||
```
|
||||
response.content (Vector{Any})
|
||||
└── phase 1: parse content blocks
|
||||
└── tool_call_list :: Vector{agentToolCall}
|
||||
└── phase 2: dispatch to sequential/parallel
|
||||
└── phase 3: prepareToolCall
|
||||
└── preparedToolCall or immediateOutcome
|
||||
└── phase 4: executePreparedToolCall
|
||||
└── executedOutcome
|
||||
└── phase 5: finalizeExecutedToolCall
|
||||
└── finalizedOutcome
|
||||
└── phase 6: createToolResultMessage
|
||||
└── toolResultMessage
|
||||
└── phase 7: agentToolCallBatch
|
||||
└── pushed to agent._state.messages
|
||||
└── loop back to LLM
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
### Sequential
|
||||
|
||||
Tools execute one at a time in order. Required when:
|
||||
- Tools have implicit dependencies
|
||||
- Tools share state (e.g. writing to the same file)
|
||||
- Tools have `parallelToolExecute = false`
|
||||
|
||||
Set globally via `agentLoopConfig.toolExecution = "sequential"`, or per-tool via `parallelToolExecute = false`.
|
||||
|
||||
### Parallel
|
||||
|
||||
Tools execute concurrently when all are independent. Reduces wall-clock time. Set `parallelToolExecute = true` on individual tools, or set `agentLoopConfig.toolExecution = "parallel"`.
|
||||
|
||||
## Streaming Partial Results
|
||||
|
||||
For long-running tools (API calls, file uploads, training), use `onPartialResult` to stream progress:
|
||||
|
||||
```julia
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
onPartialResult(Dict("status" => "Step 1: Fetching data..."))
|
||||
sleep(1)
|
||||
|
||||
onPartialResult(Dict("status" => "Step 2: Processing..."))
|
||||
sleep(1)
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Done!")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
UI listeners and the TUI consume these events in real time via `toolExecutionUpdate`.
|
||||
|
||||
## Loading Tools
|
||||
|
||||
### Auto-load from Directory
|
||||
|
||||
```julia
|
||||
using .toolRegistry
|
||||
|
||||
tools = loadTools("src/tools") # scans for *.jl files with getTool()
|
||||
```
|
||||
|
||||
Files are loaded alphabetically for deterministic registration order.
|
||||
|
||||
### Manual Registration
|
||||
|
||||
```julia
|
||||
tool = getTool() # from your tool module
|
||||
registerTool(tool)
|
||||
```
|
||||
|
||||
## Using Tools with an Agent
|
||||
|
||||
Loading tools only registers them — you must pass them to the `yiemAgent` and provide an `llmCall` function. Here is the complete flow:
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .toolRegistry
|
||||
|
||||
# 1. Load tools from the tools directory
|
||||
tools = loadTools("src/tools")
|
||||
# [toolRegistry] Loading tool from: src/tools/getTime.jl
|
||||
# [toolRegistry] Loaded tool: getTime — Time Lookup
|
||||
# [toolRegistry] Loading tool from: src/tools/getWeather.jl
|
||||
# [toolRegistry] Loaded tool: getWeather — Weather Lookup
|
||||
|
||||
# 2. Define your LLM call function
|
||||
function my_llm_call(messages::Dict)::assistantMessage
|
||||
# Call your LLM API here (OpenAI, Anthropic, local model, etc.)
|
||||
# Return an assistantMessage with the response content
|
||||
# If the LLM wants to call a tool, include tool_call content blocks
|
||||
...
|
||||
end
|
||||
|
||||
# 3. Define your event sink (optional, for logging/debugging)
|
||||
function my_event_sink(event)
|
||||
if event isa toolExecStartEvent
|
||||
println("[EVENT] Tool start: $(event.toolName)")
|
||||
elseif event isa toolExecEndEvent
|
||||
status = event.isError ? "ERROR" : "OK"
|
||||
println("[EVENT] Tool end: $(event.toolName) — $status")
|
||||
end
|
||||
end
|
||||
|
||||
# 4. Create the agent with tools
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant that can check weather and time.",
|
||||
model = my_model,
|
||||
tools = tools, # pass loaded tools
|
||||
llmCall = my_llm_call, # your LLM function
|
||||
agentEventSink = my_event_sink, # event handler
|
||||
)
|
||||
|
||||
# 5. Send a message and get a response
|
||||
run_agent(agent, "What's the weather in Tokyo?")
|
||||
response = take_response(agent)
|
||||
|
||||
# response.content contains the LLM's reply (with tool results if applicable)
|
||||
println(response.content)
|
||||
|
||||
# 6. When done, stop the agent
|
||||
stop_agent(agent)
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **User sends a message** via `run_agent(agent, "What's the weather in Tokyo?")`. The message goes into `inputChannel`.
|
||||
|
||||
2. **Agent loop** (`_agent_loop`) picks it up, converts it to a `userMessage`, and adds it to `agent._state.messages`.
|
||||
|
||||
3. **LLM is called** via `agent.llmCall(formatted_messages)`. The LLM sees the system prompt, conversation history, and the tool definitions in the prompt (via `formatMsgForLLM`).
|
||||
|
||||
4. **If the LLM uses a tool**, it returns a response with `tool_call` content blocks. The agent:
|
||||
- Extracts each tool call (name, arguments)
|
||||
- Runs validation (`validateRequiredArgs` or default)
|
||||
- Executes the tool (or returns an error if validation fails)
|
||||
- Feeds the result back as a `toolResultMessage` in the conversation
|
||||
|
||||
5. **LLM is called again** with the tool results. This repeats until the LLM returns a text response with no tool calls.
|
||||
|
||||
6. **Final response** is sent to `outputChannel` — retrieve it with `take_response(agent)`.
|
||||
|
||||
### Minimal Working Example
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .toolRegistry
|
||||
|
||||
# Load tools
|
||||
tools = loadTools("src/tools")
|
||||
|
||||
# Mock LLM that echoes back a tool call, then a text response
|
||||
call_count = 0
|
||||
function mock_llm_call(messages::Dict)::assistantMessage
|
||||
global call_count += 1
|
||||
if call_count == 1
|
||||
# First call: LLM decides to use getWeather
|
||||
return assistantMessage(
|
||||
content=[
|
||||
Dict("type" => "tool_calls",
|
||||
"tool_calls" => [Dict("id" => "call_1", "name" => "getWeather",
|
||||
"arguments" => Dict("city" => "Tokyo"))])
|
||||
],
|
||||
model = "mock",
|
||||
usage = llmUsage(0, 0)
|
||||
)
|
||||
else
|
||||
# Second call: LLM returns text (after tool result)
|
||||
return assistantMessage(
|
||||
content = [textContent("The weather in Tokyo is sunny, 22°C.")],
|
||||
model = "mock",
|
||||
usage = llmUsage(0, 0)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Create agent
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant.",
|
||||
tools = tools,
|
||||
llmCall = mock_llm_call,
|
||||
agentEventSink = e -> nothing, # no events
|
||||
)
|
||||
|
||||
# Run
|
||||
run_agent(agent, "What's the weather in Tokyo?")
|
||||
response = take_response(agent)
|
||||
|
||||
stop_agent(agent)
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description | Validation |
|
||||
|---|---|---|
|
||||
| `getWeather` | Fetch weather for a city | Default (JSON Schema required) |
|
||||
| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) |
|
||||
|
||||
## Example: Error Flow
|
||||
|
||||
When the LLM calls a tool with invalid arguments:
|
||||
|
||||
```
|
||||
User: "What's the weather?"
|
||||
└── LLM: call getWeather() with no arguments
|
||||
└── prepareToolCall → validateRequiredArgs → "Missing required arguments: city"
|
||||
└── immediateOutcome → error tool result
|
||||
└── LLM sees: "Missing required arguments: city"
|
||||
└── LLM retries: call getWeather(city="Tokyo")
|
||||
└── executeTool → "Weather in Tokyo: Sunny, 22°C"
|
||||
```
|
||||
|
||||
The agent feeds the error back to the LLM as a tool result message, allowing it to self-correct.
|
||||
@@ -1,89 +0,0 @@
|
||||
"""
|
||||
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 validateRequiredArgs(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.
|
||||
|
||||
# Arguments
|
||||
- `toolCallId::String`: Unique identifier for this tool call
|
||||
- `args::Dict{String,Any}`: Parsed arguments from the LLM
|
||||
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
|
||||
- `onPartialResult::Function`: Callback for streaming partial results
|
||||
|
||||
# Returns
|
||||
- `agentToolResult`: Result content with current time data
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
# Simulate time lookup — replace with actual timezone API call
|
||||
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 getTool()::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 = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
"""
|
||||
Execute the getWeather tool.
|
||||
|
||||
# Arguments
|
||||
- `toolCallId::String`: Unique identifier for this tool call
|
||||
- `args::Dict{String,Any}`: Parsed arguments from the LLM
|
||||
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
|
||||
- `onPartialResult::Function`: Callback for streaming partial results
|
||||
|
||||
# Returns
|
||||
- `agentToolResult`: Result content with weather data
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
city = get(args, "city", "")
|
||||
units = get(args, "units", "celsius")
|
||||
|
||||
# Simulate weather fetch — replace with actual API call
|
||||
# You can call onPartialResult() here for streaming progress updates:
|
||||
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
||||
# onPartialResult(Dict("status" => "Processing..."))
|
||||
|
||||
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 getTool()::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 = executeTool, # reference the function defined above
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
@@ -1,142 +0,0 @@
|
||||
module toolRegistry
|
||||
|
||||
export loadTools, registerTool, getTools, listTools, clearTools
|
||||
|
||||
using ..type
|
||||
|
||||
# Global registry — populated at runtime by loadTools() or registerTool()
|
||||
const _registry = Vector{agentTool}()
|
||||
|
||||
"""
|
||||
Load all tool modules from a directory.
|
||||
|
||||
Scans `dir` for `.jl` files. Each file must define a function named
|
||||
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
||||
registration order is deterministic.
|
||||
|
||||
# Tool file format
|
||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`:
|
||||
|
||||
```julia
|
||||
# src/tools/getWeather.jl
|
||||
function getTool()::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"),
|
||||
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Sunny, 22C in $(city)")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
# Arguments
|
||||
- `dir::String`: Directory path to scan for `.jl` tool files
|
||||
|
||||
# Returns
|
||||
- `Vector{agentTool}`: All loaded tools
|
||||
|
||||
# Errors
|
||||
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
||||
"""
|
||||
function loadTools(dir::String)::Vector{agentTool}
|
||||
if !isdir(dir)
|
||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||
end
|
||||
|
||||
tools = agentTool[]
|
||||
jl_files = filter(f -> endswith(f, ".jl"), readdir(dir))
|
||||
sort!(jl_files)
|
||||
|
||||
for filename in jl_files
|
||||
filepath = joinpath(dir, filename)
|
||||
println("[toolRegistry] Loading tool from: $filepath")
|
||||
|
||||
# Include the file in the current module scope so all types resolve
|
||||
# (agentTool, textContent, agentToolResult, etc. are all available)
|
||||
include(filepath)
|
||||
|
||||
# Validate that getTool was defined (include() places it in current module scope)
|
||||
if !isdefined(@__MODULE__, :getTool)
|
||||
throw(ArgumentError(
|
||||
"Tool file $(filepath) does not define a `getTool()` function. " *
|
||||
"Each tool file must define: function getTool()::agentTool ... end"
|
||||
))
|
||||
end
|
||||
|
||||
# Call getTool() — it runs in current scope where types are visible
|
||||
tool = getTool()
|
||||
if !(tool isa agentTool)
|
||||
throw(ArgumentError(
|
||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
||||
))
|
||||
end
|
||||
|
||||
push!(_registry, tool)
|
||||
push!(tools, tool)
|
||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
||||
end
|
||||
|
||||
return tools
|
||||
end
|
||||
|
||||
"""
|
||||
Register a single agentTool into the global registry.
|
||||
|
||||
# Arguments
|
||||
- `tool::agentTool`: The tool to register
|
||||
|
||||
# Returns
|
||||
- `Vector{agentTool}`: Updated registry
|
||||
"""
|
||||
function registerTool(tool::agentTool)::Vector{agentTool}
|
||||
push!(_registry, tool)
|
||||
println("[toolRegistry] Registered tool: $(tool.name)")
|
||||
return _registry
|
||||
end
|
||||
|
||||
"""
|
||||
Get all registered tools.
|
||||
|
||||
# Returns
|
||||
- `Vector{agentTool}`: Copy of the registry
|
||||
"""
|
||||
function getTools()::Vector{agentTool}
|
||||
return deepcopy(_registry)
|
||||
end
|
||||
|
||||
"""
|
||||
List all registered tool names and labels.
|
||||
|
||||
# Returns
|
||||
- `Vector{Tuple{String,String}}`: Pairs of (name, label)
|
||||
"""
|
||||
function listTools()::Vector{Tuple{String,String}}
|
||||
return [(t.name, t.label) for t in _registry]
|
||||
end
|
||||
|
||||
"""
|
||||
Clear all registered tools from the global registry.
|
||||
"""
|
||||
function clearTools()::Nothing
|
||||
empty!(_registry)
|
||||
println("[toolRegistry] Registry cleared")
|
||||
return nothing
|
||||
end
|
||||
|
||||
end # module
|
||||
+121
-191
@@ -4,17 +4,17 @@
|
||||
messageContent, agentMessage, agent,
|
||||
# Model types
|
||||
modelCost, llmModel, llmUsage,
|
||||
# Message content types
|
||||
textContent, imageContent,
|
||||
# Message types
|
||||
userMessage, assistantMessage, toolResultMessage,
|
||||
# Message content types
|
||||
textContent, imageContent, reasoningContent,
|
||||
# Message types
|
||||
userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage,
|
||||
# Tool types
|
||||
agentTool, validateRequiredArgs
|
||||
agentTool, validateRequiredArgs,
|
||||
# Context types
|
||||
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
||||
# Loop & execution types
|
||||
agentLoopConfig, abortSignal, agentToolResult,
|
||||
assistantMsgCtx, afterCtx,
|
||||
agentLoopConfig, abortSignal, agentToolResult,beforeToolCallContext,
|
||||
beforeToolCallResult, afterToolCallContext,
|
||||
# Event types
|
||||
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
|
||||
# Agent
|
||||
@@ -23,7 +23,7 @@
|
||||
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
||||
agentToolCallBatch,
|
||||
# Functions (defined elsewhere)
|
||||
run_agent, take_response, follow_up, stop_agent
|
||||
promptAgent, takeResponse, followUp, stopAgent
|
||||
|
||||
|
||||
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
||||
@@ -31,6 +31,13 @@ using GeneralUtils
|
||||
|
||||
const Timestamp = DateTime
|
||||
|
||||
struct agentToolCall # A tool invocation from the LLM
|
||||
type::String # Always "function"
|
||||
id::String # Unique tool call identifier
|
||||
name::String # Tool name
|
||||
arguments::Dict{String, Any} # Parsed tool arguments
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
# LLM model info #
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
@@ -75,6 +82,10 @@ struct imageContent <: messageContent # Image message content
|
||||
mimeType::String # MIME type (e.g., "image/png")
|
||||
end
|
||||
|
||||
struct reasoningContent <: messageContent # LLM reasoning/thinking content
|
||||
text::String # The reasoning text
|
||||
end
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
# Message types #
|
||||
@@ -108,6 +119,52 @@ function userMessage(; role="user", content=Vector{messageContent}(), timestamp=
|
||||
return userMessage(role, content, timestamp)
|
||||
end
|
||||
|
||||
struct assistantMessageToolCall <: agentMessage # Assistant message containing tool calls
|
||||
role::String # Always "assistant"
|
||||
toolCalls::Vector{agentToolCall} # Tool calls to execute
|
||||
content::Vector{messageContent} # Reasoning/thinking content blocks
|
||||
api::String # API name used (e.g., "openai")
|
||||
provider::String # Provider name (e.g., "anthropic")
|
||||
model::String # Model identifier
|
||||
usage::llmUsage # Token usage for this message
|
||||
stopReason::String # Why generation stopped (e.g., "tool_calls")
|
||||
errorMessage::Union{String, Nothing} # Error if generation failed
|
||||
timestamp::Timestamp # When the message was received
|
||||
end
|
||||
|
||||
"""
|
||||
Create a new assistant message containing tool calls.
|
||||
|
||||
# Arguments
|
||||
- `role::String`: Always "assistant"
|
||||
- `toolCalls::Vector{agentToolCall}`: Tool calls to execute
|
||||
- `content::Vector{messageContent}`: Reasoning/thinking content blocks
|
||||
- `api::String`: API name used
|
||||
- `provider::String`: Provider name
|
||||
- `model::String`: Model identifier
|
||||
- `usage::llmUsage`: Token usage
|
||||
- `stopReason::String`: Why generation stopped
|
||||
- `errorMessage::Union{String, Nothing}`: Error if generation failed
|
||||
- `timestamp::Timestamp`: When the message was received
|
||||
|
||||
# Returns
|
||||
- A new `assistantMessageToolCall` instance
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> tc = agentToolCall("function", "call_1", "getWeather", Dict("city" => "Tokyo"))
|
||||
julia> msg = assistantMessageToolCall(toolCalls=[tc], stopReason="tool_calls")
|
||||
assistantMessageToolCall("assistant", [agentToolCall(...)], messageContent[], "", "", "", llmUsage(0, 0), "tool_calls", nothing, DateTime(...))
|
||||
```
|
||||
"""
|
||||
function assistantMessageToolCall(; role="assistant", toolCalls=agentToolCall[],
|
||||
content=Vector{messageContent}(), api="", provider="", model=nothing, usage=llmUsage(0, 0),
|
||||
stopReason="tool_calls", errorMessage=nothing, timestamp=now())
|
||||
model_str = model isa AbstractString ? String(model) : ""
|
||||
return assistantMessageToolCall(role, toolCalls, content, api, provider, model_str,
|
||||
usage, stopReason, errorMessage, timestamp)
|
||||
end
|
||||
|
||||
struct assistantMessage <: agentMessage # Message from the AI assistant
|
||||
role::String # Always "assistant"
|
||||
content::Vector{messageContent} # Text and/or image content
|
||||
@@ -144,9 +201,10 @@ assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "en
|
||||
```
|
||||
"""
|
||||
function assistantMessage(; role="assistant", content=Vector{messageContent}(),
|
||||
api="", provider="", model="", usage=llmUsage(0, 0), stopReason="end_turn",
|
||||
api="", provider="", model=nothing, usage=llmUsage(0, 0), stopReason="end_turn",
|
||||
errorMessage=nothing, timestamp=now())
|
||||
return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp)
|
||||
model_str = model isa AbstractString ? String(model) : ""
|
||||
return assistantMessage(role, content, api, provider, model_str, usage, stopReason, errorMessage, timestamp)
|
||||
end
|
||||
|
||||
struct toolResultMessage <: agentMessage # Result returned from a tool execution
|
||||
@@ -248,7 +306,7 @@ tool = agentTool(
|
||||
execute=(toolCallId, args, signal, onPartialResult) -> begin
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Sunny, 22C in $(city)")],
|
||||
[textContent("Sunny, 22C in Bangkok")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end,
|
||||
@@ -263,12 +321,23 @@ struct agentTool # A tool available to the agent
|
||||
label::String # Human-readable tool name
|
||||
description::String # What the tool does
|
||||
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||
execute::Function # Tool execution function
|
||||
execute # Tool execution function
|
||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||
end
|
||||
|
||||
"""
|
||||
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
|
||||
"""
|
||||
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
||||
execute, prepareArguments::Union{Function, Nothing}=nothing,
|
||||
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false)
|
||||
return agentTool(name, label, description, inputSchema, execute,
|
||||
prepareArguments, validateRequiredArgs, parallelToolExecute)
|
||||
end
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
# Agent context #
|
||||
@@ -280,7 +349,7 @@ Snapshot of the agent's conversation context.
|
||||
# Arguments
|
||||
- `systemPrompt::String`: System prompt for the agent
|
||||
- `messages::Vector{agentMessage}`: Conversation messages
|
||||
- `tools::Union{Vector{agentTool}, Nothing}`: Available tools
|
||||
- `tools::Union{OrderedDict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
|
||||
|
||||
# Returns
|
||||
- A new `agentContext` instance
|
||||
@@ -288,7 +357,7 @@ Snapshot of the agent's conversation context.
|
||||
struct agentContext # Snapshot of the agent's conversation context
|
||||
systemPrompt::String # System prompt for the agent
|
||||
messages::Vector{agentMessage} # Conversation messages
|
||||
tools::Union{Vector{agentTool}, Nothing} # Available tools
|
||||
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||
end
|
||||
|
||||
|
||||
@@ -297,15 +366,14 @@ end
|
||||
# ------------------------------------------------------------------------------------------------ #
|
||||
|
||||
mutable struct agentState # Mutable runtime state of an agent
|
||||
systemPrompt::String # System prompt text
|
||||
model::llmModel # LLM model to use
|
||||
tools::Vector{agentTool} # Available tools
|
||||
systemPrompt::String # System prompt for the agent
|
||||
model::Union{llmModel, Nothing} # LLM model to use
|
||||
tools::OrderedDict{String, agentTool} # Available tools keyed by name, insertion-ordered
|
||||
|
||||
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
|
||||
messages::Vector{agentMessage}
|
||||
|
||||
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
|
||||
activeRun::Bool # is agent processing user message?
|
||||
errorMessage::Union{String, Nothing} # Last error message
|
||||
end
|
||||
|
||||
@@ -318,7 +386,7 @@ new state from external references.
|
||||
# Arguments
|
||||
- `systemPrompt::String`: System prompt text
|
||||
- `model::llmModel`: LLM model to use (defaults to an unknown model)
|
||||
- `tools::Vector{agentTool}`: Available tools (deep copied)
|
||||
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (deep copied)
|
||||
- `messages::Vector{agentMessage}`: Conversation messages (deep copied)
|
||||
|
||||
# Returns
|
||||
@@ -327,32 +395,23 @@ new state from external references.
|
||||
# Examples
|
||||
```julia
|
||||
julia> state = agentState(systemPrompt="You are a helpful assistant")
|
||||
agentState("You are a helpful assistant", ..., agentTool[], agentMessage[], String[], nothing)
|
||||
```
|
||||
agentState("You are a helpful assistant", OrderedDict{String, agentTool}(), agentMessage[], String[], nothing)
|
||||
"""
|
||||
function agentState(
|
||||
systemPrompt::String="",
|
||||
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||
tools::Vector{agentTool}=agentTool[],
|
||||
messages::Vector{agentMessage}=agentMessage[],
|
||||
systemPrompt::String="",
|
||||
model=llmModel("model_1", "unknown", "unknown", "", false, String[],
|
||||
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||
messages::Vector{agentMessage}=agentMessage[],
|
||||
)
|
||||
agentState(
|
||||
systemPrompt,
|
||||
model,
|
||||
deepcopy(tools),
|
||||
deepcopy(messages),
|
||||
Vector{String}(),
|
||||
false,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
struct agentToolCall # A tool invocation from the LLM
|
||||
type::String # Always "function"
|
||||
id::String # Unique tool call identifier
|
||||
name::String # Tool name
|
||||
arguments::Dict{String, Any} # Parsed tool arguments
|
||||
agentState(
|
||||
systemPrompt,
|
||||
model,
|
||||
tools,
|
||||
deepcopy(messages),
|
||||
Vector{String}(),
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -383,14 +442,12 @@ end
|
||||
"""
|
||||
Configuration for the agent tool execution loop.
|
||||
|
||||
# Arguments
|
||||
- `tools::Vector{agentTool}`: Available tools
|
||||
# Fields
|
||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||
"""
|
||||
struct agentLoopConfig
|
||||
tools::Vector{agentTool}
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
toolExecution::String
|
||||
@@ -426,33 +483,38 @@ end
|
||||
Context passed to the `beforeToolCall` hook.
|
||||
|
||||
# Arguments
|
||||
- `message::assistantMessage`: The assistant message containing the tool call
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call being prepared
|
||||
- `args::Dict{String,Any}`: Validated tool arguments
|
||||
- `args::AbstractDict{String, Any}`: Validated tool arguments
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct assistantMsgCtx
|
||||
message::assistantMessage
|
||||
struct beforeToolCallContext
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
args::AbstractDict{String, Any}
|
||||
context::agentContext
|
||||
end
|
||||
|
||||
struct beforeToolCallResult
|
||||
block::Bool
|
||||
reason::String
|
||||
end
|
||||
|
||||
"""
|
||||
Context passed to the `afterToolCall` hook.
|
||||
|
||||
# Arguments
|
||||
- `message::assistantMessage`: The assistant message containing the tool call
|
||||
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||
- `toolCall::agentToolCall`: The tool call that was executed
|
||||
- `args::Dict{String,Any}`: Tool arguments
|
||||
- `args::AbstractDict{String, Any}`: Tool arguments
|
||||
- `result::agentToolResult`: The raw tool result
|
||||
- `isError::Bool`: Whether execution resulted in an error
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct afterCtx
|
||||
message::assistantMessage
|
||||
struct afterToolCallContext
|
||||
message::assistantMessageToolCall
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
args::AbstractDict{String, Any}
|
||||
result::agentToolResult
|
||||
isError::Bool
|
||||
context::agentContext
|
||||
@@ -464,12 +526,12 @@ Event emitted when a tool call execution starts.
|
||||
# Arguments
|
||||
- `toolCallId::String`: ID of the tool call
|
||||
- `toolName::String`: Name of the tool
|
||||
- `arguments::Dict{String,Any}`: Tool arguments
|
||||
- `arguments::AbstractDict{String, Any}`: Tool arguments
|
||||
"""
|
||||
struct toolExecStartEvent
|
||||
toolCallId::String
|
||||
toolName::String
|
||||
arguments::Dict{String,Any}
|
||||
arguments::AbstractDict{String, Any}
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -478,13 +540,13 @@ Event emitted with partial results during tool execution.
|
||||
# Arguments
|
||||
- `toolCallId::String`: ID of the tool call
|
||||
- `toolName::String`: Name of the tool
|
||||
- `arguments::Dict{String,Any}`: Tool arguments
|
||||
- `arguments::AbstractDict{String, Any}`: Tool arguments
|
||||
- `partialResult::Any`: The partial result data
|
||||
"""
|
||||
struct toolExecUpdateEvent
|
||||
toolCallId::String
|
||||
toolName::String
|
||||
arguments::Dict{String,Any}
|
||||
arguments::AbstractDict{String, Any}
|
||||
partialResult::Any
|
||||
end
|
||||
|
||||
@@ -510,138 +572,6 @@ end
|
||||
|
||||
abstract type agent end
|
||||
|
||||
"""
|
||||
docstring
|
||||
"""
|
||||
mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
_state::agentState # Current state (prompt, model, messages, tools, etc.)
|
||||
|
||||
# user sends prompt message to agent. if agent is idle, it process user message right away.
|
||||
# if agent is running, it process user message after the current tool call finished.
|
||||
inputChannel::Channel
|
||||
|
||||
# Buffers messages the user sends while the agent is busy. Processed after all inputChannel
|
||||
# messages are handled and the agent is idle (not using a tool call).
|
||||
followUpChannel::Channel
|
||||
|
||||
# agent sends response message to user after processing all user messages in inputChannel
|
||||
# and all followUp messages.
|
||||
outputChannel::Channel
|
||||
|
||||
_agent_loop::Union{Task, Nothing} # agent loop running in the background
|
||||
|
||||
# Preprocess/transform messages and context (modify, filter, prune, inject context from memory,
|
||||
# reorder, ...) for a single LLM call in _process_message()'s loop.
|
||||
# returns new Vector{agentMessage}
|
||||
prepareContext ::Union{Function, Nothing}
|
||||
|
||||
# Convert prepareContext()'s new Vector{agentMessage} to LLM message format
|
||||
formatMsgForLLM::Function
|
||||
|
||||
# Actually invoke the LLM to get a completion response. The LLM response comes back as an
|
||||
# assistantMessage whose content is an array of content blocks.
|
||||
# Each block has a type — "text", "thinking", or "toolCall".
|
||||
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
|
||||
llmCall::Function
|
||||
|
||||
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
|
||||
executeToolCalls::Function # execute tool calls ()
|
||||
|
||||
# Callback invoked after executing a tool call to sanitize tools output so the output is ready
|
||||
# to be converted into toolResults message
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
# prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn
|
||||
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
|
||||
sessionId::Union{String, Nothing} # Optional session identifier
|
||||
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
|
||||
parallelToolExecute::Bool # Default: false
|
||||
agentEventSink::Function # agent emits its status via this function
|
||||
end
|
||||
|
||||
"""
|
||||
Create a new yiemAgent instance with a background loop task.
|
||||
|
||||
Spawns a background `@spawn` task that runs the agent loop, listening
|
||||
on `inputChannel` and `followUpChannel` channels concurrently.
|
||||
|
||||
# Keyword Arguments
|
||||
- `systemPrompt::String`: System prompt for the agent
|
||||
- `model`: LLM model to use
|
||||
- `tools::Vector{agentTool}`: Available tools (default: empty)
|
||||
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
|
||||
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
|
||||
- `llmCall::Function`: Function to invoke the LLM (required)
|
||||
- `prepareContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`)
|
||||
- `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`)
|
||||
- `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`)
|
||||
- `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`)
|
||||
- `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`)
|
||||
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
|
||||
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
|
||||
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
|
||||
- `agentEventSink::Function`: Callback to receive agent events
|
||||
|
||||
# Returns
|
||||
- A new `yiemAgent` instance with an active background task
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model)
|
||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
|
||||
```
|
||||
"""
|
||||
function yiemAgent(
|
||||
; systemPrompt::String="You are helpful assistant.",
|
||||
model=nothing,
|
||||
tools::Vector{agentTool}=agentTool[],
|
||||
messages::Vector{agentMessage}=agentMessage[],
|
||||
prepareContext::Union{Function, Nothing}=nothing,
|
||||
formatMsgForLLM::Function=defaultformatMsgForLLM,
|
||||
llmCall::Function,
|
||||
beforeToolCall::Union{Function, Nothing}=nothing,
|
||||
afterToolCall::Union{Function, Nothing}=nothing,
|
||||
# prepareNextTurn::Union{Function, Nothing}=nothing,
|
||||
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
|
||||
sessionId::Union{String, Nothing}=nothing,
|
||||
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
|
||||
parallelToolExecute::Bool=false,
|
||||
agentEventSink::Function,
|
||||
)
|
||||
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
|
||||
inputChannel = Channel(16)
|
||||
followUp = Channel(32)
|
||||
outputChannel = Channel(16)
|
||||
|
||||
# Create struct with a placeholder task, then spawn and replace it
|
||||
agent = yiemAgent(
|
||||
agentState(systemPrompt, model, tools, messages),
|
||||
inputChannel,
|
||||
followUp,
|
||||
outputChannel,
|
||||
nothing, # placeholder — replaced below
|
||||
prepareContext,
|
||||
formatMsgForLLM,
|
||||
llmCall,
|
||||
beforeToolCall,
|
||||
afterToolCall,
|
||||
# prepareNextTurn,
|
||||
# prepareNextTurnWithContext,
|
||||
sessionId,
|
||||
maxRetryDelayMs,
|
||||
parallelToolExecute,
|
||||
agentEventSink,
|
||||
)
|
||||
|
||||
# Spawn the background loop and attach it
|
||||
agent._agent_loop = @spawn _agent_loop(agent)
|
||||
|
||||
return agent
|
||||
end
|
||||
|
||||
|
||||
|
||||
"""
|
||||
preparedToolCall(tool, toolCall, args)
|
||||
|
||||
|
||||
+195
-27
@@ -1,9 +1,11 @@
|
||||
module utils
|
||||
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||
validateToolArguments, _userMessageToOpenAI,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks,
|
||||
_toolsToOpenAI, beforeToolCall, afterToolCall
|
||||
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
|
||||
using GeneralUtils
|
||||
using ..type
|
||||
|
||||
@@ -73,7 +75,6 @@ function availableWineToText(vecd::Vector)::String
|
||||
end
|
||||
|
||||
|
||||
|
||||
"""
|
||||
prepareContext(state::agentState) -> agentContext
|
||||
|
||||
@@ -95,7 +96,7 @@ or pruning and reordering messages before formatting and calling the LLM.
|
||||
messages, and tools to be sent to the LLM
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
```jldoctest
|
||||
# Default: returns an agentContext with deep copies of system prompt, messages, and tools
|
||||
prepareContext(state).messages == deepcopy(state.messages)
|
||||
|
||||
@@ -108,14 +109,14 @@ prepareContext(state).messages == deepcopy(state.messages)
|
||||
# end
|
||||
```
|
||||
"""
|
||||
function prepareContext(state::agentState)::agentContext
|
||||
|
||||
function prepareContext(state::agentState, eventSink)::agentContext
|
||||
|
||||
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||
filteredTools = state.tools
|
||||
|
||||
#TODO add tools to current system prompt
|
||||
#TODO add filtered tools to the current system prompt / modify systemPrompt here
|
||||
preparedSystemPrompt = state.systemPrompt
|
||||
|
||||
|
||||
#TODO add system prompt, adjust/modify and inject additional context into messages
|
||||
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
|
||||
|
||||
@@ -145,7 +146,7 @@ LLM message formats for different APIs/providers.
|
||||
an array of OpenAI-format message dicts
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
```jldoctest
|
||||
# Default output:
|
||||
formatMsgForLLm(ctx) == Dict("messages" => [
|
||||
Dict("role" => "system", "content" => [...]),
|
||||
@@ -155,7 +156,7 @@ formatMsgForLLm(ctx) == Dict("messages" => [
|
||||
])
|
||||
```
|
||||
"""
|
||||
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
|
||||
|
||||
""" openai message format example
|
||||
msg = Dict(
|
||||
@@ -183,19 +184,31 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
Dict("type" => "text", "text" => "let me check."),
|
||||
]
|
||||
),
|
||||
],
|
||||
"tools"=> [
|
||||
Dict(
|
||||
"role" => "toolResult",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "name: Chateau Montelena ..."),
|
||||
]
|
||||
),
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => "getWeather",
|
||||
"description" => "Get current weather",
|
||||
"parameters" => Dict(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string")
|
||||
),
|
||||
"required" => ["city"]
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
"temperature" => 0.7
|
||||
)
|
||||
"""
|
||||
|
||||
openaiReadyMsg = Dict{String, Any}()
|
||||
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
|
||||
messages = Vector{Dict{String, Any}}()
|
||||
|
||||
eventSink("formatMsgForLLM 1")
|
||||
# System prompt as system message
|
||||
if !isempty(ctx.systemPrompt)
|
||||
push!(messages, Dict(
|
||||
@@ -203,21 +216,104 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||
))
|
||||
end
|
||||
|
||||
eventSink("formatMsgForLLM 2")
|
||||
# Conversation messages
|
||||
for msg in ctx.messages
|
||||
if msg isa userMessage
|
||||
push!(messages, _userMessageToOpenAI(msg))
|
||||
elseif msg isa assistantMessageToolCall
|
||||
push!(messages, _assistantMessageToolCallToOpenAI(msg))
|
||||
elseif msg isa assistantMessage
|
||||
push!(messages, _assistantMessageToOpenAI(msg))
|
||||
elseif msg isa toolResultMessage
|
||||
push!(messages, _toolResultMessageToOpenAI(msg))
|
||||
end
|
||||
end
|
||||
eventSink("formatMsgForLLM 3")
|
||||
# Convert ctx.tools into OpenAI tools format
|
||||
tools_array = _toolsToOpenAI(ctx.tools, eventSink)
|
||||
eventSink("formatMsgForLLM 4")
|
||||
openaiReadyMsg["messages"] = messages
|
||||
openaiReadyMsg["temperature"] = 0.7
|
||||
|
||||
return Dict("messages" => messages)
|
||||
if !isempty(tools_array)
|
||||
openaiReadyMsg["tools"] = tools_array
|
||||
end
|
||||
|
||||
return openaiReadyMsg
|
||||
end
|
||||
|
||||
"""
|
||||
beforeToolCall(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult
|
||||
|
||||
Callback invoked before executing a tool call. Use this hook to inspect
|
||||
the tool call and decide whether to allow, block, or modify it.
|
||||
|
||||
Common use cases:
|
||||
- Request user approval via UI before running destructive tools.
|
||||
- Validate business rules that cannot be expressed in the JSON schema.
|
||||
- Check final context (e.g. session state, rate limits, permissions).
|
||||
|
||||
# Arguments
|
||||
- `context::beforeToolCallContext`: Contains the assistant message, tool call,
|
||||
validated arguments, and current conversation context.
|
||||
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||
|
||||
# Returns
|
||||
- `beforeToolCallResult(false, "N/A")` to allow the call to proceed.
|
||||
- `beforeToolCallResult(true, "Reason")` to block the call with a reason.
|
||||
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
|
||||
|
||||
# Example
|
||||
```jldoctest
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
|
||||
if context.toolCall.name == "deleteFile"
|
||||
# Block file deletion unless explicitly approved
|
||||
return beforeToolCallResult(true, "User must approve file deletion")
|
||||
end
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
```
|
||||
"""
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal, eventSink
|
||||
)::beforeToolCallResult
|
||||
|
||||
eventSink("beforeToolCall: $(context.toolCall.name) args=$(context.args)")
|
||||
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
|
||||
"""
|
||||
afterToolCall(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}
|
||||
|
||||
Callback invoked after a tool call finishes executing (before and after errors).
|
||||
Use this hook to post-process the tool result before it is fed back to the LLM.
|
||||
|
||||
Common use cases:
|
||||
- Mask sensitive data (API keys, tokens) from result content.
|
||||
- Normalize usage tracking data into a consistent format.
|
||||
- Inspect the result and set `terminate: true` based on business logic
|
||||
(e.g. "if deployment failed, stop the agent rather than retrying").
|
||||
- Wrap error results in friendlier messages for the LLM to understand.
|
||||
|
||||
# Arguments
|
||||
- `context::afterToolCallContext`: Contains the assistant message, tool call,
|
||||
arguments, raw result, error status, and current conversation context.
|
||||
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||
|
||||
# Returns
|
||||
- `nothing` to pass the result through unchanged.
|
||||
- `agentToolResult(...)` to return a modified result (content, details, usage,
|
||||
terminate flag can all be overridden).
|
||||
|
||||
""" #WORKING arrange result from searchWine tool
|
||||
function afterToolCall(context::afterToolCallContext, signal::abortSignal, eventSink
|
||||
)::Union{agentToolResult, Nothing}
|
||||
|
||||
eventSink("afterToolCall: $(context.toolCall.name) result=$(context.result) error=$(context.isError)")
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
Convert a userMessage to OpenAI message format.
|
||||
@@ -230,6 +326,44 @@ function _userMessageToOpenAI(msg::userMessage)::Dict{String, Any}
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
Convert an assistantMessageToolCall to OpenAI message format.
|
||||
|
||||
Produces a message with role="assistant", content=null, and a tool_calls array:
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"location\": \"San Francisco, CA\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{String, Any}
|
||||
tool_calls = Dict{String, Any}[]
|
||||
for tc in msg.toolCalls
|
||||
push!(tool_calls, Dict(
|
||||
"id" => tc.id,
|
||||
"type" => tc.type,
|
||||
"function" => Dict(
|
||||
"name" => tc.name,
|
||||
"arguments" => JSON.json(tc.arguments)
|
||||
)
|
||||
))
|
||||
end
|
||||
return Dict(
|
||||
"role" => "assistant",
|
||||
"content" => nothing,
|
||||
"tool_calls" => tool_calls
|
||||
)
|
||||
end
|
||||
|
||||
"""
|
||||
Convert an assistantMessage to OpenAI message format.
|
||||
"""
|
||||
@@ -280,7 +414,41 @@ end
|
||||
|
||||
|
||||
"""
|
||||
validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String}
|
||||
_toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}) -> Vector{Dict{String, Any}}
|
||||
|
||||
Convert an OrderedDict of agentTool definitions into OpenAI function tool format.
|
||||
|
||||
Returns an empty vector when `tools` is `nothing` or empty.
|
||||
|
||||
# Examples
|
||||
```jldoctest
|
||||
_toolsToOpenAI(nothing) # => Dict{String, Any}[]
|
||||
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
|
||||
```
|
||||
"""
|
||||
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, eventSink)::Vector{Dict{String, Any}}
|
||||
tools_array = Vector{Dict{String, Any}}()
|
||||
eventSink("_toolsToOpenAI 1")
|
||||
eventSink(string(typeof(tools)))
|
||||
if tools !== nothing
|
||||
for (_, tool) in tools
|
||||
push!(tools_array, Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => tool.name,
|
||||
"description" => tool.description,
|
||||
"parameters" => tool.inputSchema
|
||||
)
|
||||
))
|
||||
end
|
||||
end
|
||||
eventSink("_toolsToOpenAI 2")
|
||||
return tools_array
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any}) -> Union{Nothing,String}
|
||||
|
||||
Validates that all required fields listed in the tool's JSON Schema are present
|
||||
in `args`. Returns `nothing` if validation passes, or a descriptive error string
|
||||
@@ -291,15 +459,15 @@ with a custom validation function that performs additional checks (e.g. type
|
||||
coercion, format validation, cross-field constraints).
|
||||
|
||||
# Arguments
|
||||
- `args::Dict{String,Any}`: The arguments provided by the LLM
|
||||
- `inputSchema::Dict{String,Any}`: The tool's `inputSchema` (JSON Schema format)
|
||||
- `args::AbstractDict{String, Any}`: The arguments provided by the LLM
|
||||
- `inputSchema::AbstractDict{String, Any}`: The tool's `inputSchema` (JSON Schema format)
|
||||
|
||||
# Returns
|
||||
- `nothing` if all required args are present
|
||||
- `String` error message listing missing fields otherwise
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
```jldoctest
|
||||
schema = Dict("required" => ["city"])
|
||||
args = Dict{String,Any}()
|
||||
validateRequiredArgs(args, schema) # => "Missing required arguments: city"
|
||||
@@ -308,7 +476,7 @@ args2 = Dict("city" => "Tokyo")
|
||||
validateRequiredArgs(args2, schema) # => nothing
|
||||
```
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any})::Union{Nothing,String}
|
||||
function validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing,String}
|
||||
required = get(inputSchema, "required", Any[])
|
||||
if isempty(required)
|
||||
return nothing
|
||||
@@ -354,7 +522,7 @@ false `toolExecutionStart` events.
|
||||
and converted to an `immediateOutcome`
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
```jldoctest
|
||||
# With validateRequiredArgs hook set on the tool
|
||||
validateToolArguments(toolWithHook, tc) # => validated args or throws
|
||||
|
||||
@@ -362,7 +530,7 @@ validateToolArguments(toolWithHook, tc) # => validated args or throws
|
||||
validateToolArguments(toolDefault, tc) # => args or throws
|
||||
```
|
||||
"""
|
||||
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{String,Any}
|
||||
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}
|
||||
# Use default (2-arg: args + schema) or tool-specific hook (1-arg: args only)
|
||||
if isnothing(tool.validateRequiredArgs)
|
||||
result = validateRequiredArgs(prepared.arguments, tool.inputSchema)
|
||||
@@ -375,7 +543,7 @@ function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{S
|
||||
end
|
||||
|
||||
return prepared.arguments
|
||||
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
@@ -1,41 +0,0 @@
|
||||
# This file is machine-generated - editing it directly is not advised
|
||||
|
||||
julia_version = "1.11.4"
|
||||
manifest_format = "2.0"
|
||||
project_hash = "71d91126b5a1fb1020e1098d9d492de2a4438fd2"
|
||||
|
||||
[[deps.Base64]]
|
||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.InteractiveUtils]]
|
||||
deps = ["Markdown"]
|
||||
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.Logging]]
|
||||
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.Markdown]]
|
||||
deps = ["Base64"]
|
||||
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.Random]]
|
||||
deps = ["SHA"]
|
||||
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.SHA]]
|
||||
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
|
||||
version = "0.7.0"
|
||||
|
||||
[[deps.Serialization]]
|
||||
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.Test]]
|
||||
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
|
||||
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
version = "1.11.0"
|
||||
@@ -1,2 +0,0 @@
|
||||
[deps]
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
@@ -0,0 +1,642 @@
|
||||
using Test
|
||||
using YiemAgent
|
||||
using YiemAgent.agentCore
|
||||
using YiemAgent.type
|
||||
using JSON
|
||||
|
||||
# Import the function from the private module scope
|
||||
import YiemAgent.agentCore: _extractToolCalls
|
||||
|
||||
@testset "_extractToolCalls" begin
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Format 1: response["message"]["tool_calls"] (LMStudio.jl style) #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "single tool call via message format" begin
|
||||
response = Dict{String,Any}(
|
||||
"finish_reason" => "tool_calls",
|
||||
"index" => 0,
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"content" => "",
|
||||
"reasoning_content" => "Let me check the weather.",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Bangkok, Thailand\"}",
|
||||
),
|
||||
"id" => "tc_001",
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].id == "tc_001"
|
||||
@test tc_list[1].type == "function"
|
||||
@test tc_list[1].arguments["city"] == "Bangkok, Thailand"
|
||||
@test assistant_msg isa assistantMessage
|
||||
@test assistant_msg.role == "assistant"
|
||||
@test assistant_msg.stopReason == "tool_calls"
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1] isa reasoningContent
|
||||
@test assistant_msg.content[1].text == "Let me check the weather."
|
||||
end
|
||||
|
||||
@testset "multiple tool calls via message format" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"content" => "",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Tokyo, Japan\"}",
|
||||
),
|
||||
"id" => "tc_001",
|
||||
),
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{\"timezone\":\"Asia/Tokyo\"}",
|
||||
),
|
||||
"id" => "tc_002",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 2
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].arguments["city"] == "Tokyo, Japan"
|
||||
@test tc_list[2].name == "getTime"
|
||||
@test tc_list[2].arguments["timezone"] == "Asia/Tokyo"
|
||||
@test assistant_msg.role == "assistant"
|
||||
end
|
||||
|
||||
@testset "tool call with empty arguments string" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "listTools",
|
||||
"arguments" => "{}",
|
||||
),
|
||||
"id" => "tc_empty",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "listTools"
|
||||
@test tc_list[1].arguments == Dict{String,Any}()
|
||||
@test assistant_msg.stopReason == "end_turn"
|
||||
end
|
||||
|
||||
@testset "tool call with missing id falls back to uuid" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{\"city\":\"NYC\"}",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test !isempty(tc_list[1].id)
|
||||
@test tc_list[1].name == "getTime"
|
||||
end
|
||||
|
||||
@testset "tool call with non-string arguments (pre-parsed dict)" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => Dict{String,Any}("city" => "London", "units" => "fahrenheit"),
|
||||
),
|
||||
"id" => "tc_parsed",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].arguments["city"] == "London"
|
||||
@test tc_list[1].arguments["units"] == "fahrenheit"
|
||||
end
|
||||
|
||||
@testset "tool call with api/provider/model/usage metadata" begin
|
||||
response = Dict{String,Any}(
|
||||
"api" => "openai",
|
||||
"provider" => "anthropic",
|
||||
"model" => "claude-3-opus",
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{}",
|
||||
),
|
||||
"id" => "tc_meta",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test assistant_msg.api == "openai"
|
||||
@test assistant_msg.provider == "anthropic"
|
||||
@test assistant_msg.model == "claude-3-opus"
|
||||
end
|
||||
|
||||
# --------------------------------------------------------------- #
|
||||
# Format 2: response.content blocks (OpenAI API style) #
|
||||
# --------------------------------------------------------------- #
|
||||
|
||||
@testset "content blocks with tool_calls" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "Let me check."),
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Paris\"}",
|
||||
),
|
||||
"id" => "tc_block_1",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].arguments["city"] == "Paris"
|
||||
# text block before tool_calls should be included in content
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1].text == "Let me check."
|
||||
end
|
||||
|
||||
@testset "content blocks with tool_call (single-call format)" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_single",
|
||||
"name" => "getTime",
|
||||
"arguments" => Dict{String,Any}("timezone" => "Europe/London"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getTime"
|
||||
@test tc_list[1].id == "tc_single"
|
||||
@test tc_list[1].arguments["timezone"] == "Europe/London"
|
||||
end
|
||||
|
||||
@testset "content blocks with reasoning and text" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "reasoning", "text" => "Thinking..."),
|
||||
Dict{String,Any}("type" => "text", "text" => "Here's the answer."),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test length(assistant_msg.content) == 2
|
||||
@test assistant_msg.content[1] isa reasoningContent
|
||||
@test assistant_msg.content[1].text == "Thinking..."
|
||||
@test assistant_msg.content[2] isa textContent
|
||||
@test assistant_msg.content[2].text == "Here's the answer."
|
||||
end
|
||||
|
||||
@testset "content blocks with text and tool_call (tool_call not in content)" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "Sure, I'll check."),
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_mix",
|
||||
"name" => "getWeather",
|
||||
"arguments" => Dict{String,Any}("city" => "London"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
# text block included, tool_call block excluded from content
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1].text == "Sure, I'll check."
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# assistantMessage construction #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "assistantMessage with error_message and errorMessage fallback" begin
|
||||
response = Dict{String,Any}(
|
||||
"error_message" => "rate limit",
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fail")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test assistant_msg.errorMessage == "rate limit"
|
||||
end
|
||||
|
||||
@testset "assistantMessage with usage tracking" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
|
||||
"usage" => llmUsage(100, 50),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.usage.inputTokens == 100
|
||||
@test assistant_msg.usage.outputTokens == 50
|
||||
end
|
||||
|
||||
@testset "assistantMessage with invalid usage defaults to zero" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
|
||||
"usage" => "invalid",
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.usage.inputTokens == 0
|
||||
@test assistant_msg.usage.outputTokens == 0
|
||||
end
|
||||
|
||||
@testset "reasoning_content as textContent" begin
|
||||
response = Dict{String,Any}(
|
||||
"reasoning_content" => textContent("internal thought"),
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "output")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test length(assistant_msg.content) == 2
|
||||
@test assistant_msg.content[1] isa reasoningContent
|
||||
@test assistant_msg.content[1].text == "internal thought"
|
||||
@test assistant_msg.content[2] isa textContent
|
||||
@test assistant_msg.content[2].text == "output"
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# No tool call cases #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "no tool calls found" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "Hello world."),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test assistant_msg.stopReason == "end_turn"
|
||||
end
|
||||
|
||||
@testset "empty message" begin
|
||||
response = Dict{String,Any}()
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test assistant_msg.role == "assistant"
|
||||
@test assistant_msg.stopReason == "end_turn"
|
||||
@test length(assistant_msg.content) == 0
|
||||
end
|
||||
|
||||
@testset "message with empty tool_calls array" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test assistant_msg.role == "assistant"
|
||||
end
|
||||
|
||||
@testset "message field is not a Dict" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => "not a dict",
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fallback")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test length(assistant_msg.content) == 1
|
||||
end
|
||||
|
||||
@testset "Format 1 takes priority over Format 2" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"Format1\"}",
|
||||
),
|
||||
"id" => "tc_fmt1",
|
||||
),
|
||||
],
|
||||
),
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_fmt2",
|
||||
"name" => "getTime",
|
||||
"arguments" => Dict{String,Any}("city" => "Format2"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Edge cases #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@testset "tool call with null arguments" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => nothing,
|
||||
),
|
||||
"id" => "tc_null",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getTime"
|
||||
end
|
||||
|
||||
@testset "tool call with missing function key" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"id" => "tc_nofunc",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == ""
|
||||
end
|
||||
|
||||
@testset "tool call with missing name in function block" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}("arguments" => "{}"),
|
||||
"id" => "tc_noname",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == ""
|
||||
end
|
||||
|
||||
@testset "message format with JSON.Object (JSON.parse result)" begin
|
||||
json_str = JSON.json(Dict(
|
||||
"message" => Dict(
|
||||
"role" => "assistant",
|
||||
"tool_calls" => [
|
||||
Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict("name" => "getWeather", "arguments" => "{\"city\":\"Test\"}"),
|
||||
"id" => "tc_jsonobj",
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
parsed = JSON.parse(json_str)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(parsed)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[1].arguments["city"] == "Test"
|
||||
end
|
||||
|
||||
@testset "tool_calls block with mixed content types (text + tool_calls)" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}("type" => "text", "text" => "I'll check both."),
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{\"city\":\"London\"}",
|
||||
),
|
||||
"id" => "tc_mix1",
|
||||
),
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getTime",
|
||||
"arguments" => "{\"timezone\":\"UTC\"}",
|
||||
),
|
||||
"id" => "tc_mix2",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 2
|
||||
@test tc_list[1].name == "getWeather"
|
||||
@test tc_list[2].name == "getTime"
|
||||
@test length(assistant_msg.content) == 1
|
||||
@test assistant_msg.content[1].text == "I'll check both."
|
||||
end
|
||||
|
||||
@testset "tool_call block without arguments field" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_noargs",
|
||||
"name" => "getTime",
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].arguments == Dict{String,Any}()
|
||||
end
|
||||
|
||||
@testset "tool_calls block with empty tool_calls array" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any[],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
end
|
||||
|
||||
@testset "tool_calls block with non-AbstractDict elements" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_calls",
|
||||
"tool_calls" => Any["not a dict", 42, nothing],
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
end
|
||||
|
||||
@testset "content field is not a Vector" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => "not a vector",
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(tc_list) == 0
|
||||
@test length(assistant_msg.content) == 0
|
||||
end
|
||||
|
||||
@testset "Dict-based response with all metadata fields" begin
|
||||
response = Dict{String,Any}(
|
||||
"api" => "openai",
|
||||
"provider" => "anthropic",
|
||||
"model" => "claude-3-sonnet",
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "tool_call",
|
||||
"id" => "tc_meta",
|
||||
"name" => "getTime",
|
||||
"arguments" => Dict{String,Any}("city" => "Seoul"),
|
||||
),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == true
|
||||
@test length(tc_list) == 1
|
||||
@test tc_list[1].name == "getTime"
|
||||
@test tc_list[1].arguments["city"] == "Seoul"
|
||||
@test assistant_msg.api == "openai"
|
||||
@test assistant_msg.provider == "anthropic"
|
||||
@test assistant_msg.model == "claude-3-sonnet"
|
||||
end
|
||||
|
||||
@testset "default role is assistant" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[Dict{String,Any}("type" => "text", "text" => "no role specified")],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.role == "assistant"
|
||||
end
|
||||
|
||||
@testset "tool call with custom role in message format" begin
|
||||
response = Dict{String,Any}(
|
||||
"message" => Dict{String,Any}(
|
||||
"role" => "custom_role",
|
||||
"tool_calls" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "function",
|
||||
"function" => Dict{String,Any}(
|
||||
"name" => "getWeather",
|
||||
"arguments" => "{}",
|
||||
),
|
||||
"id" => "tc_role",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test assistant_msg.role == "custom_role"
|
||||
end
|
||||
|
||||
@testset "image content block handling" begin
|
||||
response = Dict{String,Any}(
|
||||
"content" => Any[
|
||||
Dict{String,Any}(
|
||||
"type" => "image_url",
|
||||
"image_url" => Dict("url" => "data:image/png;base64,abc123"),
|
||||
),
|
||||
Dict{String,Any}("type" => "text", "text" => "What is this?"),
|
||||
],
|
||||
)
|
||||
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||
@test has_toolcalls == false
|
||||
@test length(assistant_msg.content) == 2
|
||||
@test assistant_msg.content[1] isa textContent
|
||||
@test assistant_msg.content[1].text == ""
|
||||
@test assistant_msg.content[2].text == "What is this?"
|
||||
end
|
||||
|
||||
end
|
||||
+4
-400
@@ -1,401 +1,5 @@
|
||||
using JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64,
|
||||
NATS, Base.Threads
|
||||
using YiemAgent, GeneralUtils, msghandler
|
||||
|
||||
function text2text_instruct_llm(sender_id::String, openai_msg::Dict{String, Any})
|
||||
payloads = [("msg", openai_msg, "dictionary")] # List of tuples
|
||||
_, msg_envelope_json_str = msghandler.smartpack(
|
||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||
payloads;
|
||||
sender_id=sender_id,
|
||||
msg_purpose="text2text",
|
||||
broker_url=config["nats_server_info"]["url"],
|
||||
fileserver_url=config["externalservice"]["fileserver"]["url"])
|
||||
|
||||
reply = NATS.request(agent_conn,
|
||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||
msg_envelope_json_str, timeout=120)
|
||||
|
||||
incoming_env_json_str = String(reply.payload)
|
||||
incoming_env = msghandler.smartunpack(incoming_env_json_str)
|
||||
_llm_response = incoming_env["payloads"][1][2]
|
||||
llm_response = _llm_response["choices"][1]["message"]["content"]
|
||||
return llm_response
|
||||
end
|
||||
|
||||
""" get a single text embedding from a LLM service
|
||||
Example
|
||||
text = ["hello"]
|
||||
embedding = get_embedding(text)
|
||||
"""
|
||||
function get_embedding(text::AbstractArray{String})
|
||||
documents_dict = Dict("documents" => text)
|
||||
payloads = [("documents", documents_dict, "dictionary")]
|
||||
_, msg_envelope_json_str = msghandler.smartpack(
|
||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||
payloads;
|
||||
msg_purpose="embedding",
|
||||
broker_url=config["nats_server_info"]["url"],
|
||||
fileserver_url=config["externalservice"]["fileserver"]["url"])
|
||||
|
||||
reply = NATS.request(agent_conn,
|
||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||
msg_envelope_json_str, timeout=120)
|
||||
incoming_env_json_str = String(reply.payload)
|
||||
incoming_env = msghandler.smartunpack(incoming_env_json_str)
|
||||
embedding_response = incoming_env["payloads"][1][2]
|
||||
|
||||
return embedding_response
|
||||
end
|
||||
|
||||
""" sql = "SELECT * FROM wine;"
|
||||
result = execute_sql_winedb(sql)
|
||||
"""
|
||||
function execute_sql_winedb(sql::T) where {T<:AbstractString}
|
||||
host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':')
|
||||
port = parse(Int, _port)
|
||||
dbname = "winedb"
|
||||
user = config["externalservice"]["sommpanion_db"]["user"]
|
||||
password = config["externalservice"]["sommpanion_db"]["password"]
|
||||
db_connection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password")
|
||||
result = nothing
|
||||
try
|
||||
result = LibPQ.execute(db_connection, sql)
|
||||
catch e
|
||||
LibPQ.close(db_connection)
|
||||
end
|
||||
|
||||
LibPQ.close(db_connection)
|
||||
return result
|
||||
end
|
||||
|
||||
""" find similar sql from vector database
|
||||
sql = "SELECT * FROM wine;"
|
||||
result, distance = similar_sql_vectordb(sql)
|
||||
"""
|
||||
function similar_sql_vectordb(sql::T; maxdistance::Number=0.2) where {T<:AbstractString}
|
||||
tablename = "sqlllm_decision_repository"
|
||||
# get embedding of the query
|
||||
df = find_similar_text_from_vectordb(sql, tablename,
|
||||
"function_input_embedding", execute_sql_vectordb)
|
||||
# println(df[1, [:id, :function_output]])
|
||||
row, col = size(df)
|
||||
distance = row == 0 ? Inf : df[1, :distance]
|
||||
if row != 0 && distance < maxdistance
|
||||
# if there is usable SQL, return it.
|
||||
output_b64 = df[1, :function_output_base64] # pick the closest match
|
||||
output_str = String(base64decode(output_b64))
|
||||
rowid = df[1, :id]
|
||||
println("\n--| similar sql found. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||
pprintln(output_str)
|
||||
return (result=output_str, distance=distance)
|
||||
else
|
||||
println("\n--| similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||
return (result=nothing, distance=nothing)
|
||||
end
|
||||
end
|
||||
|
||||
""" insert query and sql into vector database
|
||||
query = "get all wines from wine table"
|
||||
sql = "SELECT * FROM wine;"
|
||||
insert_sql_vectordb(query, sql)
|
||||
"""
|
||||
function insert_sql_vectordb(query::T1, SQL::T2; maxdistance::Number=3
|
||||
) where {T1<:AbstractString, T2<:AbstractString}
|
||||
|
||||
tablename = "sqlllm_decision_repository"
|
||||
# get embedding of the query
|
||||
# query = state[:thoughtHistory][:question]
|
||||
df = find_similar_text_from_vectordb(query, tablename,
|
||||
"function_input_embedding", execute_sql_vectordb)
|
||||
row, col = size(df)
|
||||
distance = row == 0 ? Inf : df[1, :distance]
|
||||
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
|
||||
_query_embedding = get_embedding([query])
|
||||
_query_embedding = GeneralUtils.dictify(_query_embedding)
|
||||
# println("\n--- _query_embedding() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||
# println(_query_embedding)
|
||||
# println("---\n")
|
||||
query_embedding = _query_embedding["data"][1]["embedding"]
|
||||
query = replace(query, "'" => "")
|
||||
sql_base64 = base64encode(SQL)
|
||||
sql_ = replace(SQL, "'" => "")
|
||||
|
||||
sql =
|
||||
"""
|
||||
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
|
||||
"""
|
||||
# println("\n--| added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
|
||||
# println(sql)
|
||||
_ = execute_sql_vectordb(sql)
|
||||
end
|
||||
end
|
||||
|
||||
""" execute sql against vectordb
|
||||
sql = "SELECT * FROM wine;"
|
||||
result = execute_sql_vectordb(sql)
|
||||
"""
|
||||
function execute_sql_vectordb(sql::T) where {T<:AbstractString}
|
||||
host_url, _port = split(config["externalservice"]["sommpanion_vectordb"]["url"], ':')
|
||||
port = parse(Int, _port)
|
||||
dbname = config["externalservice"]["sommpanion_vectordb"]["dbname"]
|
||||
user = config["externalservice"]["sommpanion_vectordb"]["user"]
|
||||
password = config["externalservice"]["sommpanion_vectordb"]["password"]
|
||||
DBconnection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password")
|
||||
result = LibPQ.execute(DBconnection, sql)
|
||||
close(DBconnection)
|
||||
return result
|
||||
end
|
||||
|
||||
""" search similar decision llm made from vectordb
|
||||
"""
|
||||
function similar_sommelier_decision(recentevents::T1; maxdistance::Integer=3
|
||||
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
|
||||
|
||||
tablename = "sommelier_decision_repository"
|
||||
# find similar
|
||||
df = find_similar_text_from_vectordb(recentevents, tablename,
|
||||
"function_input_embedding", execute_sql_vectordb)
|
||||
row, col = size(df)
|
||||
distance = row == 0 ? Inf : df[1, :distance]
|
||||
if row != 0 && distance < maxdistance
|
||||
# if there is usable decision, return it.
|
||||
rowid = df[1, :id]
|
||||
println("\n--| found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
|
||||
output_b64 = df[1, :function_output_base64] # pick the closest match
|
||||
_output_str = String(base64decode(output_b64))
|
||||
output = copy(JSON.read(_output_str))
|
||||
return output
|
||||
else
|
||||
println("\n--| similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
|
||||
return nothing
|
||||
end
|
||||
end
|
||||
|
||||
""" search similar text from vectordb
|
||||
"""
|
||||
function find_similar_text_from_vectordb(text::T1, tablename::T2, embeddingColumnName::T3,
|
||||
vectorDB::Function; limit::Integer=1
|
||||
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
|
||||
# get embedding from LLM service
|
||||
_embedding = get_embedding([text])
|
||||
_embedding = _embedding["data"][1]["embedding"]
|
||||
_embedding = "$_embedding"
|
||||
|
||||
embedding = _embedding[4:end]
|
||||
|
||||
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
|
||||
sql = """
|
||||
SELECT *, $embeddingColumnName <-> '$embedding' as distance
|
||||
FROM $tablename
|
||||
ORDER BY distance LIMIT $limit;
|
||||
"""
|
||||
response = vectorDB(sql)
|
||||
df = DataFrame(response)
|
||||
|
||||
return df
|
||||
end
|
||||
|
||||
""" insert decision llm made to vectordb
|
||||
"""
|
||||
function insert_sommelier_decision(recentevents::T1, decision::T2; maxdistance::Integer=5
|
||||
) where {T1<:AbstractString, T2<:AbstractDict}
|
||||
tablename = "sommelier_decision_repository"
|
||||
# find similar
|
||||
df = find_similar_text_from_vectordb(recentevents, tablename,
|
||||
"function_input_embedding", execute_sql_vectordb)
|
||||
row, col = size(df)
|
||||
distance = row == 0 ? Inf : df[1, :distance]
|
||||
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
|
||||
_embedding = get_embedding([recentevents])[1]
|
||||
recentevents_embedding = _embedding["data"][1]["embedding"]
|
||||
recentevents = replace(recentevents, "'" => "")
|
||||
decision_json = JSON.json(decision)
|
||||
decision_base64 = base64encode(decision_json)
|
||||
decision = replace(decision_json, "'" => "")
|
||||
|
||||
sql =
|
||||
"""
|
||||
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
|
||||
"""
|
||||
println("\n--| added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
|
||||
println(sql)
|
||||
_ = execute_sql_vectordb(sql)
|
||||
else
|
||||
println("--| similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
|
||||
end
|
||||
end
|
||||
|
||||
config = JSON.parsefile("./appconfig.json")
|
||||
sessionId = "0"
|
||||
backend_session_topic = "sommpanion.testsubject"
|
||||
agent_ch = Channel(8)
|
||||
agent_conn = NATS.connect(config["nats_server_info"]["url"])
|
||||
|
||||
sub2 = NATS.subscribe(agent_conn, backend_session_topic) do msg
|
||||
put!(agent_ch, msg)
|
||||
end
|
||||
|
||||
agent_context = YiemAgent.agentcontext(
|
||||
text2text_instruct_llm,
|
||||
get_embedding,
|
||||
execute_sql_winedb,
|
||||
similar_sql_vectordb,
|
||||
insert_sql_vectordb,
|
||||
similar_sommelier_decision,
|
||||
insert_sommelier_decision
|
||||
)
|
||||
|
||||
# can't instantiate
|
||||
agent = YiemAgent.sommelier(
|
||||
agent_context;
|
||||
name="Janie",
|
||||
id=sessionId, # agent instance id
|
||||
retailername="Yiem Wine Ltd.",
|
||||
llmFormatName=""
|
||||
)
|
||||
|
||||
|
||||
image1_path = "test/large_image.png"
|
||||
image1_bytes = read(image1_path)
|
||||
image1_base64_string = base64encode(image1_bytes)
|
||||
mime_type = "image/png"
|
||||
data1_uri = "data:$(mime_type);base64,$(image1_base64_string)"
|
||||
|
||||
# 1. Read local file and encode to base64 string
|
||||
image2_path = "test/small_image.png"
|
||||
image2_bytes = read(image2_path)
|
||||
image2_base64_string = base64encode(image2_bytes)
|
||||
mime_type = "image/png"
|
||||
data2_uri = "data:$(mime_type);base64,$(image2_base64_string)"
|
||||
|
||||
# 3. Construct payload with the Data URI
|
||||
message = Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "Do you know type of wine in the image?"),
|
||||
Dict(
|
||||
"type" => "image_url",
|
||||
"image_url" => Dict("url" => data1_uri)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
result = YiemAgent.conversation(agent; userinput=message)
|
||||
println("\n$result")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# message = Dict(
|
||||
# "role" => "user",
|
||||
# "content" => [
|
||||
# Dict("type" => "text", "text" =>
|
||||
# "
|
||||
# เป็นงานเลี้ยงทั่วไป
|
||||
# "),
|
||||
# ]
|
||||
# )
|
||||
|
||||
# result = YiemAgent.conversation(agent; userinput=message)
|
||||
# println("\n$result")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# message = Dict(
|
||||
# "role" => "user",
|
||||
# "content" => [
|
||||
# Dict("type" => "text", "text" => "no thanks. that's all"),
|
||||
# ]
|
||||
# )
|
||||
|
||||
# result = YiemAgent.conversation(agent; userinput=message)
|
||||
# println("\n$result")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# message = Dict(
|
||||
# "role" => "user",
|
||||
# "content" => [
|
||||
# Dict("type" => "text", "text" => "What about this wine?"),
|
||||
# Dict(
|
||||
# "type" => "image_url",
|
||||
# "image_url" => Dict("url" => data2_uri)
|
||||
# )
|
||||
# ]
|
||||
# )
|
||||
|
||||
# result = YiemAgent.conversation(agent; userinput=message)
|
||||
# println("\n$result")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
using Test
|
||||
using YiemAgent
|
||||
|
||||
include("toolTest.jl")
|
||||
include("_extractToolCalls.jl")
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
using Revise, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64,
|
||||
NATS, Base.Threads
|
||||
using YiemAgent, GeneralUtils, msghandler
|
||||
|
||||
|
||||
""" Debug
|
||||
|
||||
using JSON, NATS, msghandler
|
||||
using NATS
|
||||
|
||||
conn = NATS.connect("nats.yiem.cc")
|
||||
|
||||
sub = NATS.subscribe(conn, "sommanion.debug") do msg
|
||||
payload = NATS.payload(msg)
|
||||
@info "debug" payload
|
||||
|
||||
open("./log/error.log", "a") do io
|
||||
println(io, payload)
|
||||
end
|
||||
end
|
||||
|
||||
NATS.publish(conn, "sommanion.debug", "order-123")
|
||||
|
||||
|
||||
# ---------------------------- inject this code into codebase to debug --------------------------- #
|
||||
try
|
||||
batch = someFunction(x, y, z)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
err_msg = sprint() do io
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
|
||||
eventSink(err_msg)
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
struct text2textInstructLLM
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
fileserver_url::String
|
||||
end
|
||||
|
||||
function (t::text2textInstructLLM)(openai_msg::Dict{String, Any})
|
||||
|
||||
payloads = [("msg", openai_msg, "dictionary")] # List of tuples
|
||||
_, msg_envelope_json_str = msghandler.smartpack(
|
||||
t.topic,
|
||||
payloads;
|
||||
sender_id=t.senderID,
|
||||
msg_purpose="text2text",
|
||||
fileserver_url=t.fileserver_url)
|
||||
|
||||
reply = NATS.request(t.natsConn, t.topic, msg_envelope_json_str, timeout=180)
|
||||
|
||||
incoming_env_json_str = String(reply.payload)
|
||||
incoming_env = msghandler.smartunpack(incoming_env_json_str)
|
||||
_llm_response = incoming_env["payloads"][1][2]
|
||||
llm_response = _llm_response["choices"][1]
|
||||
|
||||
return llm_response
|
||||
end
|
||||
|
||||
|
||||
|
||||
struct eventSink
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
function (aes::eventSink)(msg::String)
|
||||
NATS.publish(aes.natsConn, aes.topic, msg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
config = JSON.parsefile("./appconfig.json")
|
||||
agent_conn = NATS.connect(config["nats_server_info"]["url"])
|
||||
|
||||
|
||||
#WORKING load tools
|
||||
text2text_llm = text2textInstructLLM(agent_conn,
|
||||
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||
"sender",
|
||||
config["externalservice"]["fileserver"]["url"])
|
||||
|
||||
debugNats = eventSink(agent_conn, "sommanion.debug", "sender")
|
||||
|
||||
agent = YiemAgent.yiemAgent(
|
||||
text2text_llm;
|
||||
eventSink=debugNats
|
||||
)
|
||||
|
||||
msg = Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "What's the weather in Bangkok?"),
|
||||
# Dict(
|
||||
# "type" => "image_url",
|
||||
# "image_url" => Dict("url" => "data:mime_type;base64,image2_base64_string")
|
||||
# ),
|
||||
]
|
||||
)
|
||||
|
||||
push!(agent.inputChannel, msg)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user