Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+107
-94
@@ -2,7 +2,7 @@
|
||||
|
||||
julia_version = "1.12.6"
|
||||
manifest_format = "2.0"
|
||||
project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6"
|
||||
project_hash = "aa163e2bf572632825162936e107be18384fd40f"
|
||||
|
||||
[[deps.Accessors]]
|
||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||
@@ -44,6 +44,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 +64,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"
|
||||
@@ -90,16 +102,28 @@ 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"
|
||||
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||
version = "0.7.8"
|
||||
|
||||
[[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"
|
||||
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
|
||||
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
||||
version = "0.2.11"
|
||||
version = "0.2.13"
|
||||
|
||||
[[deps.Compat]]
|
||||
deps = ["TOML", "UUIDs"]
|
||||
@@ -130,6 +154,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,9 +176,9 @@ 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"
|
||||
@@ -168,9 +198,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 +238,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 = "d2facc77c08c1c2bfb1a77c148edd05b3db5410b"
|
||||
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||
version = "0.25.129"
|
||||
version = "0.25.130"
|
||||
|
||||
[deps.Distributions.extensions]
|
||||
DistributionsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -240,15 +270,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"
|
||||
|
||||
[[deps.EzXML]]
|
||||
deps = ["Printf", "XML2_jll"]
|
||||
git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3"
|
||||
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
|
||||
version = "1.2.3"
|
||||
version = "0.1.11"
|
||||
|
||||
[[deps.FileIO]]
|
||||
deps = ["Pkg", "Requires", "UUIDs"]
|
||||
@@ -277,9 +301,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]
|
||||
@@ -300,11 +324,11 @@ version = "1.1.0"
|
||||
|
||||
[[deps.GeneralUtils]]
|
||||
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"]
|
||||
git-tree-sha1 = "93293126d24d3929ef6a5067f347bc28c6582c71"
|
||||
git-tree-sha1 = "129b8fa1bf3bf6d8c0a080f39db09b9b986ef8de"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
|
||||
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||
version = "0.5.10"
|
||||
version = "0.5.11"
|
||||
|
||||
[[deps.Graphs]]
|
||||
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
|
||||
@@ -321,9 +345,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 = "0a58fbbdee93d132a2fb1159b7f5e1b5c2465e71"
|
||||
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||
version = "2.5.5"
|
||||
version = "2.6.4"
|
||||
|
||||
[[deps.HashArrayMappedTries]]
|
||||
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
|
||||
@@ -332,9 +356,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"]
|
||||
@@ -412,9 +436,9 @@ version = "1.8.0"
|
||||
|
||||
[[deps.JSON]]
|
||||
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
|
||||
git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211"
|
||||
git-tree-sha1 = "65979512c25a0727f050e6e4be40f0fd9ec893f7"
|
||||
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
weakdeps = ["ArrowTypes"]
|
||||
|
||||
[deps.JSON.extensions]
|
||||
@@ -432,9 +456,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"]
|
||||
@@ -506,12 +530,6 @@ version = "1.11.3+1"
|
||||
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
||||
version = "1.11.0"
|
||||
|
||||
[[deps.Libiconv_jll]]
|
||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||
git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809"
|
||||
uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531"
|
||||
version = "1.18.0+0"
|
||||
|
||||
[[deps.LinearAlgebra]]
|
||||
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
|
||||
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
@@ -539,9 +557,15 @@ 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.MacroTools]]
|
||||
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
||||
@@ -599,9 +623,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"
|
||||
@@ -640,15 +664,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,9 +680,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"
|
||||
version = "2.8.7"
|
||||
|
||||
[[deps.Pkg]]
|
||||
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
|
||||
@@ -688,15 +712,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 = "4ac881f5432bd93463a41767a814a45245be22b6"
|
||||
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
|
||||
version = "3.4.0"
|
||||
version = "3.4.6"
|
||||
|
||||
[deps.PrettyTables.extensions]
|
||||
PrettyTablesExcelExt = "XLSX"
|
||||
@@ -757,15 +781,15 @@ version = "1.3.1"
|
||||
|
||||
[[deps.Reseau]]
|
||||
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
|
||||
git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5"
|
||||
git-tree-sha1 = "701ef63506992668c0069a4d1ac1540b77258e6c"
|
||||
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
|
||||
version = "1.3.1"
|
||||
version = "1.3.6"
|
||||
|
||||
[[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 = "ec46aed6a3a8cc6b67839ca361e7b4aa32eaeee1"
|
||||
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
version = "3.15.1"
|
||||
version = "3.16.3"
|
||||
weakdeps = ["Distributed"]
|
||||
|
||||
[deps.Revise.extensions]
|
||||
@@ -779,15 +803,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 = "7fb25a964849d90a0446366cdefca822e0e84900"
|
||||
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
|
||||
version = "3.0.5"
|
||||
version = "3.0.6"
|
||||
|
||||
[deps.Roots.extensions]
|
||||
RootsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -840,12 +864,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"
|
||||
@@ -879,9 +897,9 @@ version = "1.12.0"
|
||||
|
||||
[[deps.SpecialFunctions]]
|
||||
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
|
||||
git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f"
|
||||
git-tree-sha1 = "c3ac026e735264e9bdc6a9bcbd1b1e781b36e3bc"
|
||||
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
|
||||
version = "2.8.0"
|
||||
version = "2.8.3"
|
||||
|
||||
[deps.SpecialFunctions.extensions]
|
||||
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
|
||||
@@ -932,9 +950,9 @@ version = "0.34.12"
|
||||
|
||||
[[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"
|
||||
@@ -950,17 +968,16 @@ git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a"
|
||||
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"
|
||||
|
||||
[[deps.StringManipulation]]
|
||||
deps = ["PrecompileTools"]
|
||||
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
|
||||
git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484"
|
||||
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
|
||||
version = "0.4.4"
|
||||
version = "0.4.7"
|
||||
|
||||
[[deps.StringViews]]
|
||||
git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8"
|
||||
uuid = "354b36f9-a18e-4713-926e-db85100087ba"
|
||||
version = "1.3.7"
|
||||
|
||||
[[deps.StructTypes]]
|
||||
deps = ["Dates", "UUIDs"]
|
||||
@@ -970,9 +987,9 @@ version = "1.11.0"
|
||||
|
||||
[[deps.StructUtils]]
|
||||
deps = ["Dates", "UUIDs"]
|
||||
git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
|
||||
git-tree-sha1 = "c65ae4aa47e543c278aea0a3468786d33021a3ff"
|
||||
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
|
||||
version = "2.8.2"
|
||||
version = "2.8.4"
|
||||
|
||||
[deps.StructUtils.extensions]
|
||||
StructUtilsMeasurementsExt = ["Measurements"]
|
||||
@@ -1046,9 +1063,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
|
||||
version = "0.11.3"
|
||||
|
||||
[[deps.URIs]]
|
||||
git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a"
|
||||
git-tree-sha1 = "3b0738bd7c5645641845da25cbd99800b8718689"
|
||||
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||
version = "1.6.1"
|
||||
version = "1.6.2"
|
||||
|
||||
[[deps.UTCDateTimes]]
|
||||
deps = ["Dates", "TimeZones"]
|
||||
@@ -1076,23 +1093,11 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
|
||||
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
|
||||
version = "1.6.1"
|
||||
|
||||
[[deps.XML2_jll]]
|
||||
deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"]
|
||||
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.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", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"]
|
||||
path = "."
|
||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
|
||||
[[deps.Zlib_jll]]
|
||||
deps = ["Libdl"]
|
||||
@@ -1116,6 +1121,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", "Revise", "UUIDs"]
|
||||
git-tree-sha1 = "e82a79cf6602541ea25409aded57b2ace4a7c29f"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/msghandler"
|
||||
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||
version = "1.2.1"
|
||||
|
||||
[[deps.nghttp2_jll]]
|
||||
deps = ["Artifacts", "Libdl"]
|
||||
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
||||
|
||||
+2
-2
@@ -19,10 +19,10 @@ 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"
|
||||
@@ -34,4 +34,4 @@ JSON = "1.6.1"
|
||||
LLMMCTS = "0.1.5"
|
||||
NATS = "0.1.0"
|
||||
SQLLLM = "0.2.8"
|
||||
Serde = "3.7.2"
|
||||
msghandler = "1.2.1"
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
# 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.
|
||||
|
||||
## Changelog
|
||||
## Getting Started
|
||||
|
||||
### 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`
|
||||
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
|
||||
2. Create a `yiemAgent` with `loadTools("src/tools")`
|
||||
3. Call `runAgent(agent, "message")` then `takeResponse(agent)`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── YiemAgent.jl # Module entry point
|
||||
├── type.jl # Core types (messages, tools, agent state)
|
||||
├── utils.jl # Message formatting, validation
|
||||
├── agentCore.jl # Agent loop, tool execution pipeline
|
||||
├── api.jl # Public API (runAgent, takeResponse, etc.)
|
||||
└── tools/
|
||||
├── registry.jl # Tool registry (loadTools, registerTool, listTools)
|
||||
├── getWeather.jl # Weather lookup tool
|
||||
├── getTime.jl # Time lookup tool
|
||||
├── writeTool.jl # Create new tool files (self-modifying)
|
||||
└── README.md # Tool development guide
|
||||
```
|
||||
|
||||
## Tool Development
|
||||
|
||||
See `src/tools/README.md` for:
|
||||
- Tool anatomy (schema, execute, getTool)
|
||||
- Validation hooks
|
||||
- Agent loop lifecycle
|
||||
- Self-modifying tools (`writeTool`)
|
||||
|
||||
+1599
File diff suppressed because it is too large
Load Diff
@@ -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 +1,11 @@
|
||||
# ── executeToolCalls() Julia pseudo code ──────────────────────────
|
||||
# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit
|
||||
_processMessage 10
|
||||
JSON.Object{String, Any}("finish_reason" => "stop", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "The weather in Bangkok is currently Sunny with a temperature of 22°C.", "reasoning_content" => "The user is asking for the weather in Bangkok.\nI have already retrieved the weather information in the previous turn and provided it to the user.\nThe user's current input is \"What's the weather in Bangkok?\", which is the same question as before.\nI should provide the same answer again.\nNo new tool calls are needed.\nI will simply state the weather information retrieved previously.\nWeather in Bangkok: Sunny, 22°C.\nI will output the answer directly.\n"))
|
||||
_processMessage 11
|
||||
hasToolCalls: false
|
||||
toolCallList: YiemAgent.type.agentToolCall[]
|
||||
_processMessage 17
|
||||
_processMessage 18
|
||||
|
||||
---
|
||||
here is my log. from the log, it seems like _processMessage() is finished but somehow the log didn't show _agentLoop 5 message. _processMessage() may not exit properly but why?
|
||||
|
||||
|
||||
+14
-3
@@ -1,7 +1,6 @@
|
||||
module YiemAgent
|
||||
|
||||
# export agent
|
||||
|
||||
export register_all_tools
|
||||
|
||||
""" 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.
|
||||
@@ -13,9 +12,21 @@ module YiemAgent
|
||||
include("utils.jl")
|
||||
using .utils
|
||||
|
||||
include("tools/registry.jl")
|
||||
include("tools/getWeather.jl")
|
||||
include("tools/getTime.jl")
|
||||
include("tools/writeTool.jl")
|
||||
|
||||
include("toolRegistry.jl")
|
||||
using .toolRegistry
|
||||
|
||||
function register_all_tools(store::toolRegistry.toolStore)
|
||||
registerTool(store, getWeatherTool())
|
||||
registerTool(store, getTimeTool())
|
||||
registerTool(store, writeToolTool())
|
||||
registerTool(store, listTool(store))
|
||||
return store.tools
|
||||
end
|
||||
|
||||
# include("llmfunction.jl")
|
||||
# using .llmfunction
|
||||
|
||||
|
||||
+686
-337
File diff suppressed because it is too large
Load Diff
+16
-17
@@ -3,14 +3,13 @@ module api
|
||||
export prompt
|
||||
|
||||
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.
|
||||
|
||||
@@ -25,16 +24,16 @@ The agent processes messages from `inputChannel` in the background task.
|
||||
- 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> runAgent(agent, "Hello!")
|
||||
yiemAgent(...)
|
||||
```
|
||||
"""
|
||||
function run_agent(agent::yiemAgent, msg)
|
||||
function runAgent(agent::yiemAgent, msg)
|
||||
put!(agent.inputChannel, msg)
|
||||
return agent
|
||||
end
|
||||
@@ -51,15 +50,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 `runAgent(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
|
||||
|
||||
@@ -77,17 +76,17 @@ and before any tool call results are sent.
|
||||
- The same `agent` instance for chaining
|
||||
|
||||
# Notes
|
||||
- Use `run_agent(agent, msg)` for the primary message and `follow_up(agent, msg)` for additional
|
||||
- Use `runAgent(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, "Also consider red wines")
|
||||
yiemAgent(...)
|
||||
```
|
||||
"""
|
||||
function follow_up(agent::yiemAgent, msg)
|
||||
function followUp(agent::yiemAgent, msg)
|
||||
put!(agent.followUpChannel, msg)
|
||||
return agent
|
||||
end
|
||||
@@ -105,19 +104,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,179 @@
|
||||
module toolRegistry
|
||||
|
||||
export toolStore, registerTool, getTools, clearTools, listTool
|
||||
|
||||
using Dates
|
||||
using JSON, DataStructures
|
||||
using ..type
|
||||
|
||||
"""
|
||||
Per-agent isolated tool storage.
|
||||
|
||||
Each agent gets its own `toolStore` so tool registration is independent —
|
||||
`registerTool(store, tool)` only affects that agent's tool set.
|
||||
|
||||
# Fields
|
||||
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
|
||||
- `name::String` — identifier for debugging/logs
|
||||
"""
|
||||
struct toolStore
|
||||
tools::OrderedDict{String, agentTool}
|
||||
name::String
|
||||
end
|
||||
|
||||
"""
|
||||
toolStore(; name="default") -> toolStore
|
||||
|
||||
Create a new empty tool store.
|
||||
|
||||
# Keyword Arguments
|
||||
- `name::String`: Display name for logging (default: `"default"`)
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1")
|
||||
toolStore(OrderedDict{String, agentTool}(), "agent1")
|
||||
```
|
||||
"""
|
||||
function toolStore(; name::String="default")::toolStore
|
||||
toolStore(OrderedDict{String, agentTool}(), name)
|
||||
end
|
||||
|
||||
"""
|
||||
listTool(store::toolStore) -> agentTool
|
||||
|
||||
Return an `agentTool` definition for listing registered tools.
|
||||
|
||||
Each call produces a **new** tool object that captures (closes over)
|
||||
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
||||
at runtime.
|
||||
|
||||
# Arguments
|
||||
- `store`: The tool store whose tools will be listed when the tool runs
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1");
|
||||
|
||||
julia> register_all_tools(store) # auto-registers listTools
|
||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||
[toolRegistry:agent1] Registered tool: listTools
|
||||
|
||||
julia> tools = getTools(store)
|
||||
OrderedDict{String, agentTool} with 4 entries:
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
"writeTool" => agentTool(...)
|
||||
"listTools" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function listTool(store::toolStore)::agentTool
|
||||
return agentTool(
|
||||
name = "listTools",
|
||||
label = "List Tools",
|
||||
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict{String,Any}(),
|
||||
"required" => Any[]
|
||||
),
|
||||
execute = (toolCallId, args, signal, onPartialResult) -> begin
|
||||
tools = getTools(store)
|
||||
if isempty(tools)
|
||||
result_text = "No tools registered."
|
||||
else
|
||||
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, t) in tools]
|
||||
result_text = "Available tools:\n" * join(lines, "\n")
|
||||
end
|
||||
return agentToolResult(
|
||||
[textContent(result_text)],
|
||||
Dict{Any,Any}("count" => length(tools)),
|
||||
nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
|
||||
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
|
||||
|
||||
"""
|
||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||
|
||||
Add `tool` to `store`, overwriting any existing tool with the same name.
|
||||
|
||||
# Arguments
|
||||
- `store`: Tool store to modify
|
||||
- `tool`: The `agentTool` to register
|
||||
|
||||
# Returns
|
||||
- The same `store.tools` dict (modified in place)
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> store = toolStore(name="agent1");
|
||||
|
||||
julia> registerTool(store, listTool(store))
|
||||
[toolRegistry:agent1] Registered tool: listTools
|
||||
OrderedDict{String, agentTool} with 1 entry:
|
||||
"listTools" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
|
||||
store.tools[tool.name] = tool
|
||||
println("[$(store.name)] Registered tool: $(tool.name)")
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
Return the tools registered in `store`.
|
||||
|
||||
The returned dict is the **same object** stored inside `store` — mutations
|
||||
to it (e.g. via `registerTool`) are visible through subsequent calls.
|
||||
|
||||
# Arguments
|
||||
- `store`: Tool store to query
|
||||
|
||||
# Returns
|
||||
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> tools = getTools(store)
|
||||
OrderedDict{String, agentTool} with 2 entries:
|
||||
"getWeather" => agentTool(...)
|
||||
"getTime" => agentTool(...)
|
||||
```
|
||||
"""
|
||||
function getTools(store::toolStore)::OrderedDict{String, agentTool}
|
||||
return store.tools
|
||||
end
|
||||
|
||||
"""
|
||||
Remove all tools from `store`.
|
||||
|
||||
# Arguments
|
||||
- `store`: Tool store to clear
|
||||
|
||||
# Returns
|
||||
- `nothing`
|
||||
|
||||
# Example
|
||||
```julia
|
||||
julia> clearTools(store)
|
||||
[toolRegistry:agent1] Registry cleared
|
||||
nothing
|
||||
|
||||
julia> getTools(store)
|
||||
OrderedDict{String, agentTool} with 0 entries
|
||||
```
|
||||
"""
|
||||
function clearTools(store::toolStore)::Nothing
|
||||
empty!(store.tools)
|
||||
println("[$(store.name)] Registry cleared")
|
||||
return nothing
|
||||
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.
|
||||
+12
-18
@@ -1,3 +1,6 @@
|
||||
using .type
|
||||
using Dates
|
||||
|
||||
"""
|
||||
Validate required arguments for the getTime tool.
|
||||
|
||||
@@ -13,7 +16,7 @@ Demonstrates custom validation beyond simple required-field checking:
|
||||
- `nothing` if validation passes
|
||||
- `String` error message if validation fails
|
||||
"""
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function getTimeValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
tz = get(args, "timezone", nothing)
|
||||
city = get(args, "city", "")
|
||||
|
||||
@@ -39,26 +42,17 @@ 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
|
||||
Returns mock time data for the given timezone or city.
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
onPartialResult)
|
||||
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
|
||||
@@ -68,7 +62,7 @@ end
|
||||
"""
|
||||
Define and return the getTime agentTool.
|
||||
"""
|
||||
function getTool()::agentTool
|
||||
function getTimeTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getTime",
|
||||
label = "Time Lookup",
|
||||
@@ -76,14 +70,14 @@ function getTool()::agentTool
|
||||
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")
|
||||
"timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'"),
|
||||
"city" => Dict("type" => "string", "description" => "City name as fallback")
|
||||
),
|
||||
"required" => []
|
||||
),
|
||||
execute = executeTool,
|
||||
execute = getTimeExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
validateRequiredArgs = getTimeValidateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
|
||||
+18
-22
@@ -1,37 +1,33 @@
|
||||
using msghandler
|
||||
using .type
|
||||
|
||||
"""
|
||||
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
|
||||
Returns mock weather data for the given city and temperature units.
|
||||
"""
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
city = get(args, "city", "")
|
||||
units = get(args, "units", "celsius")
|
||||
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||
agentEventSink)
|
||||
|
||||
# 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..."))
|
||||
agentEventSink("Getting weather...")
|
||||
|
||||
temp = units == "fahrenheit" ? "72" : "22"
|
||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
||||
city = get(args, "city", "")
|
||||
units = get(args, "units", "celsius")
|
||||
temp = units == "fahrenheit" ? "72" : "22"
|
||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
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
|
||||
function getWeatherTool()::agentTool
|
||||
return agentTool(
|
||||
name = "getWeather",
|
||||
label = "Weather Lookup",
|
||||
@@ -44,7 +40,7 @@ function getTool()::agentTool
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = executeTool, # reference the function defined above
|
||||
execute = getWeatherExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,276 @@
|
||||
using .type
|
||||
using JSON
|
||||
|
||||
"""
|
||||
Tool that writes new Julia tool module files to disk.
|
||||
|
||||
The agent can use this tool when it encounters a task that no existing tool
|
||||
can handle. Provide the tool's name, label, description, inputSchema, and
|
||||
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
|
||||
|
||||
After calling this tool, add the new file to `YiemAgent.jl` with an `include()`
|
||||
statement (after `include("toolRegistry.jl")`), then restart the agent.
|
||||
The new tool must be registered in `register_all_tools()` in `toolRegistry.jl`.
|
||||
|
||||
# Example
|
||||
|
||||
1. Agent calls writeTool with a spec for a "searchWine" tool
|
||||
2. writeTool generates src/tools/searchWine.jl
|
||||
3. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
||||
4. Developer adds `registerTool(store, searchWineTool())` to register_all_tools()
|
||||
5. Restart agent — new tool is available
|
||||
|
||||
# How It Works
|
||||
|
||||
writeTool is a **file writer**, not a code generator. The LLM provides the
|
||||
tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
|
||||
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
|
||||
- Indents `executeCode` with 4 spaces
|
||||
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
||||
- Appends `writeToolTool()` returning an `agentTool` struct
|
||||
- Writes the combined string to `src/tools/<name>.jl`
|
||||
|
||||
# Important Notes
|
||||
|
||||
- The `executeCode` string is embedded literally into the generated tool.
|
||||
Use `args["param_name"]` to access input parameters.
|
||||
- The code string should be the function body (NOT wrapped in a function).
|
||||
Lines will be indented with 4 spaces inside the execute function.
|
||||
- Tool names must be valid Julia identifiers (lowercase letters, digits, underscores,
|
||||
no leading digits or special characters).
|
||||
"""
|
||||
|
||||
"""
|
||||
Validate that a tool name is a valid Julia identifier.
|
||||
"""
|
||||
function validateToolName(name::String)::Union{Nothing,String}
|
||||
if !occursin(r"^[a-zA-Z_][a-zA-Z0-9_!]*$", name)
|
||||
return "Invalid tool name: '$name'. Tool names must be valid Julia identifiers (letters, digits, underscores, starting with a letter or underscore)."
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
Indent a multi-line code string by the specified number of spaces.
|
||||
"""
|
||||
function indent_code(code::String, n::Int)::String
|
||||
prefix = " "^n
|
||||
lines = split(code, '\n')
|
||||
result_lines = String[prefix * line for line in lines]
|
||||
return join(result_lines, "\n")
|
||||
end
|
||||
|
||||
"""
|
||||
Convert a Julia Dict to a valid Julia Dict{String,Any}(...) literal string.
|
||||
"""
|
||||
function dict_to_julia_literal(d)::String
|
||||
if d isa Dict
|
||||
items = String[]
|
||||
for (k, v) in d
|
||||
key_str = json_string(k)
|
||||
val_str = value_to_julia(v)
|
||||
push!(items, "$key_str => $val_str")
|
||||
end
|
||||
return "Dict{String,Any}(" * join(items, ", ") * ")"
|
||||
else
|
||||
return value_to_julia(d)
|
||||
end
|
||||
end
|
||||
|
||||
function value_to_julia(v)::String
|
||||
if v isa Dict
|
||||
return dict_to_julia_literal(v)
|
||||
elseif v isa Vector
|
||||
items = [value_to_julia(x) for x in v]
|
||||
return "[" * join(items, ", ") * "]"
|
||||
elseif v isa String
|
||||
escaped = replace(v, "\\" => "\\\\")
|
||||
escaped = replace(escaped, "\"" => "\\\"")
|
||||
return "\"$escaped\""
|
||||
elseif v isa Number
|
||||
return string(v)
|
||||
elseif v isa Bool
|
||||
return string(v)
|
||||
elseif v === nothing
|
||||
return "nothing"
|
||||
else
|
||||
return "\"$(v)\""
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
Convert any Julia value to a JSON string.
|
||||
"""
|
||||
function json_string(v)::String
|
||||
return JSON.json(v)
|
||||
end
|
||||
|
||||
"""
|
||||
Define and return the writeTool agentTool.
|
||||
"""
|
||||
function writeToolTool()::agentTool
|
||||
return agentTool(
|
||||
name = "writeTool",
|
||||
label = "Create Tool",
|
||||
description = "Write a new Julia tool module file to src/tools/<name>.jl. The LLM provides the tool logic as executeCode; writeTool wraps it in Julia boilerplate and writes the file. Restart the agent to load the new tool.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"name" => Dict("type" => "string", "description" => "Unique tool name (valid Julia identifier, no spaces or special chars)"),
|
||||
"label" => Dict("type" => "string", "description" => "Human-readable tool name shown in tool descriptions"),
|
||||
"description" => Dict("type" => "string", "description" => "What the tool does (shown to LLM for tool selection decisions)"),
|
||||
"inputSchema" => Dict(
|
||||
"type" => "object",
|
||||
"description" => "JSON Schema describing tool parameters in MCP format"
|
||||
),
|
||||
"executeCode" => Dict("type" => "string", "description" => "Julia code for the execute function body. Use args[\"key\"] to access parameters. Do NOT wrap in a function definition."),
|
||||
"validateCode" => Dict("type" => "string", "optional" => true, "description" => "Optional custom validation Julia code (runs before execute). Use args[\"key\"] to access parameters. Return nothing to pass, or a string error message to fail."),
|
||||
"prepareCode" => Dict("type" => "string", "optional" => true, "description" => "Optional argument preparation code (runs before validation). Return modified args dict."),
|
||||
"parallel" => Dict("type" => "boolean", "default" => false, "description" => "Whether this tool can run in parallel with other tools")
|
||||
),
|
||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
||||
),
|
||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
|
||||
tool_name = get(args, "name", "")::String
|
||||
tool_label = get(args, "label", tool_name)::String
|
||||
tool_description = get(args, "description", "")::String
|
||||
tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any}
|
||||
execute_code = get(args, "executeCode", "")::String
|
||||
validate_code = get(args, "validateCode", nothing)::Union{String,Nothing}
|
||||
prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing}
|
||||
parallel = get(args, "parallel", false)::Bool
|
||||
|
||||
# Validate tool name
|
||||
name_err = validateToolName(tool_name)
|
||||
if name_err !== nothing
|
||||
return agentToolResult(
|
||||
[textContent(name_err)],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
# Validate required fields
|
||||
if isempty(tool_name)
|
||||
return agentToolResult(
|
||||
[textContent("Missing required field: 'name'")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
if isempty(tool_description)
|
||||
return agentToolResult(
|
||||
[textContent("Missing required field: 'description'")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
if isempty(execute_code)
|
||||
return agentToolResult(
|
||||
[textContent("Missing required field: 'executeCode'")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
onPartialResult(Dict("status" => "Generating tool: $tool_name"))
|
||||
|
||||
# Build the tool file path
|
||||
script_dir = dirname(@__FILE__)
|
||||
tools_dir = dirname(script_dir)
|
||||
filepath = joinpath(tools_dir, "$(tool_name).jl")
|
||||
|
||||
# Check for naming conflicts
|
||||
if isfile(filepath)
|
||||
return agentToolResult(
|
||||
[textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
onPartialResult(Dict("status" => "Writing file: $(basename(filepath))"))
|
||||
|
||||
# Convert schema Dict to a Julia Dict literal string
|
||||
schema_literal = dict_to_julia_literal(tool_schema)
|
||||
|
||||
# Build optional validation function
|
||||
validate_section = if validate_code !== nothing && !isempty(validate_code)
|
||||
indented = indent_code(validate_code, 4)
|
||||
"function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
# Build optional prepare function
|
||||
prepare_section = if prepare_code !== nothing && !isempty(prepare_code)
|
||||
indented = indent_code(prepare_code, 4)
|
||||
"function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
# Indent user's execute code for embedding inside execute function body
|
||||
indented_exec = indent_code(execute_code, 4)
|
||||
|
||||
# Escape description for Julia string literal
|
||||
escaped_desc = replace(tool_description, "\\" => "\\\\")
|
||||
escaped_desc = replace(escaped_desc, "\"" => "\\\"")
|
||||
|
||||
# Build the complete tool file content
|
||||
parts = String[]
|
||||
push!(parts, "# Auto-generated tool: $tool_name\n")
|
||||
push!(parts, "# Generated by writeTool at $(now())\n\n")
|
||||
if !isempty(validate_section)
|
||||
push!(parts, validate_section)
|
||||
push!(parts, "\n")
|
||||
end
|
||||
if !isempty(prepare_section)
|
||||
push!(parts, prepare_section)
|
||||
push!(parts, "\n")
|
||||
end
|
||||
push!(parts, "\n")
|
||||
push!(parts, "# Execute function\n")
|
||||
push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n")
|
||||
push!(parts, "$indented_exec\n")
|
||||
push!(parts, "end\n\n")
|
||||
push!(parts, "# Tool definition\n")
|
||||
push!(parts, "function getTool()::agentTool\n")
|
||||
push!(parts, " return agentTool(\n")
|
||||
push!(parts, " name = \"$(tool_name)\",\n")
|
||||
push!(parts, " label = \"$(tool_label)\",\n")
|
||||
push!(parts, " description = \"$(escaped_desc)\",\n")
|
||||
push!(parts, " inputSchema = $schema_literal,\n")
|
||||
push!(parts, " execute = executeTool,\n")
|
||||
if validate_code !== nothing && !isempty(validate_code)
|
||||
push!(parts, " validateRequiredArgs = validateRequiredArgs,\n")
|
||||
else
|
||||
push!(parts, " validateRequiredArgs = nothing,\n")
|
||||
end
|
||||
if prepare_code !== nothing && !isempty(prepare_code)
|
||||
push!(parts, " prepareArguments = prepareArguments,\n")
|
||||
else
|
||||
push!(parts, " prepareArguments = nothing,\n")
|
||||
end
|
||||
push!(parts, " parallelToolExecute = $parallel\n")
|
||||
push!(parts, " )\n")
|
||||
push!(parts, "end\n")
|
||||
|
||||
tool_code = join(parts)
|
||||
|
||||
# Write the file — tool must be included in YiemAgent.jl and registered in register_all_tools()
|
||||
write(filepath, tool_code)
|
||||
|
||||
onPartialResult(Dict("status" => "Done"))
|
||||
|
||||
return agentToolResult(
|
||||
[textContent("Tool '$(tool_name)' written to $filepath. Add include(\"tools/$(tool_name).jl\") to YiemAgent.jl and registerTool(store, $(tool_name)Tool()) to register_all_tools(), then restart the agent.")],
|
||||
Dict{Any,Any}(
|
||||
"file" => filepath,
|
||||
"name" => tool_name,
|
||||
"label" => tool_label,
|
||||
"description" => tool_description,
|
||||
),
|
||||
nothing, false
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
+47
-166
@@ -9,12 +9,12 @@
|
||||
# Message types
|
||||
userMessage, 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
|
||||
runAgent, takeResponse, followUp, stopAgent
|
||||
|
||||
|
||||
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
||||
@@ -144,9 +144,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 +249,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 +264,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 +292,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 +300,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 +309,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 +329,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,24 +338,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,
|
||||
)
|
||||
agentState(
|
||||
systemPrompt,
|
||||
model,
|
||||
deepcopy(tools),
|
||||
deepcopy(messages),
|
||||
Vector{String}(),
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -384,13 +394,11 @@ end
|
||||
Configuration for the agent tool execution loop.
|
||||
|
||||
# Arguments
|
||||
- `tools::Vector{agentTool}`: Available tools
|
||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||
"""
|
||||
struct agentLoopConfig
|
||||
tools::Vector{agentTool}
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
toolExecution::String
|
||||
@@ -431,13 +439,18 @@ Context passed to the `beforeToolCall` hook.
|
||||
- `args::Dict{String,Any}`: Validated tool arguments
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct assistantMsgCtx
|
||||
struct beforeToolCallContext
|
||||
message::assistantMessage
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
context::agentContext
|
||||
end
|
||||
|
||||
struct beforeToolCallResult
|
||||
block::Bool
|
||||
reason::String
|
||||
end
|
||||
|
||||
"""
|
||||
Context passed to the `afterToolCall` hook.
|
||||
|
||||
@@ -449,7 +462,7 @@ Context passed to the `afterToolCall` hook.
|
||||
- `isError::Bool`: Whether execution resulted in an error
|
||||
- `context::agentContext`: Current conversation context
|
||||
"""
|
||||
struct afterCtx
|
||||
struct afterToolCallContext
|
||||
message::assistantMessage
|
||||
toolCall::agentToolCall
|
||||
args::Dict{String,Any}
|
||||
@@ -510,138 +523,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)
|
||||
|
||||
|
||||
+154
-15
@@ -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, agentEventSink
|
||||
|
||||
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
|
||||
|
||||
@@ -108,12 +109,12 @@ prepareContext(state).messages == deepcopy(state.messages)
|
||||
# end
|
||||
```
|
||||
"""
|
||||
function prepareContext(state::agentState)::agentContext
|
||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
||||
|
||||
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||
filteredTools = state.tools
|
||||
|
||||
#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
|
||||
@@ -155,7 +156,7 @@ formatMsgForLLm(ctx) == Dict("messages" => [
|
||||
])
|
||||
```
|
||||
"""
|
||||
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
|
||||
""" openai message format example
|
||||
msg = Dict(
|
||||
@@ -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}}()
|
||||
|
||||
agentEventSink("formatMsgForLLM 1")
|
||||
# System prompt as system message
|
||||
if !isempty(ctx.systemPrompt)
|
||||
push!(messages, Dict(
|
||||
@@ -203,7 +216,7 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||
))
|
||||
end
|
||||
|
||||
agentEventSink("formatMsgForLLM 2")
|
||||
# Conversation messages
|
||||
for msg in ctx.messages
|
||||
if msg isa userMessage
|
||||
@@ -214,8 +227,100 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
push!(messages, _toolResultMessageToOpenAI(msg))
|
||||
end
|
||||
end
|
||||
agentEventSink("formatMsgForLLM 3")
|
||||
# Convert ctx.tools into OpenAI tools format
|
||||
tools_array = _toolsToOpenAI(ctx.tools, agentEventSink)
|
||||
agentEventSink("formatMsgForLLM 4")
|
||||
openaiReadyMsg["messages"] = messages
|
||||
openaiReadyMsg["temperature"] = 0.7
|
||||
|
||||
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
|
||||
```julia
|
||||
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
|
||||
)::beforeToolCallResult
|
||||
|
||||
# final context check
|
||||
|
||||
# seek user approval via UI
|
||||
|
||||
# other check
|
||||
|
||||
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).
|
||||
|
||||
"""
|
||||
function afterToolCall(context::afterToolCallContext, signal::abortSignal
|
||||
)::Union{agentToolResult, Nothing}
|
||||
|
||||
# modify context.result if needed and return agentToolResult
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
#TODO
|
||||
function agentEventSink(x)
|
||||
|
||||
return Dict("messages" => messages)
|
||||
end
|
||||
|
||||
|
||||
@@ -279,6 +384,40 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
_toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}) -> Vector{Dict{String, Any}}
|
||||
|
||||
Convert an OrderedDict of agentTool definitions into OpenAI function tool format.
|
||||
|
||||
Returns an empty vector when `tools` is `nothing` or empty.
|
||||
|
||||
# Examples
|
||||
```julia
|
||||
_toolsToOpenAI(nothing) # => Dict{String, Any}[]
|
||||
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
|
||||
```
|
||||
"""
|
||||
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, agentEventSink)::Vector{Dict{String, Any}}
|
||||
tools_array = Vector{Dict{String, Any}}()
|
||||
agentEventSink("_toolsToOpenAI 1")
|
||||
agentEventSink(string(typeof(tools)))
|
||||
if tools !== nothing
|
||||
for (_, tool) in tools
|
||||
push!(tools_array, Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => tool.name,
|
||||
"description" => tool.description,
|
||||
"parameters" => tool.inputSchema
|
||||
)
|
||||
))
|
||||
end
|
||||
end
|
||||
agentEventSink("_toolsToOpenAI 2")
|
||||
return tools_array
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
validateRequiredArgs(args::Dict{String,Any}, inputSchema::Dict{String,Any}) -> Union{Nothing,String}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export addNewMessage, conversation, decisionMaker, reflector, generatechat,
|
||||
generalconversation, detectWineryName, generateSituationReport
|
||||
|
||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||
DataFrames, Serde
|
||||
DataFrames
|
||||
using GeneralUtils
|
||||
using ..type, ..util, ..llmfunction
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recomme
|
||||
extractWineAttributes_2, paraphrase, SQLexecution
|
||||
|
||||
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures,
|
||||
Base64, Serde, LibPQ, NATS
|
||||
Base64, LibPQ, NATS
|
||||
using GeneralUtils, SQLLLM
|
||||
using ..type, ..util
|
||||
|
||||
|
||||
@@ -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,638 @@
|
||||
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 textContent
|
||||
@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].text == "Thinking..."
|
||||
@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].text == "internal thought"
|
||||
@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")
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
using Test
|
||||
using Dates
|
||||
using YiemAgent
|
||||
using YiemAgent.toolRegistry
|
||||
using YiemAgent.type
|
||||
using YiemAgent.agentCore
|
||||
|
||||
@testset "register_all_tools with toolStore" begin
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. register_all_tools registers all static tools + listTools #
|
||||
# ------------------------------------------------------------------ #
|
||||
store = toolStore(name="test1")
|
||||
loaded = register_all_tools(store)
|
||||
@test !isempty(loaded)
|
||||
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
|
||||
|
||||
names = [k for k in keys(loaded)]
|
||||
@test "getTime" in names
|
||||
@test "getWeather" in names
|
||||
@test "writeTool" in names
|
||||
@test "listTools" in names
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 2. register_all_tools returns tools in registration order #
|
||||
# ------------------------------------------------------------------ #
|
||||
@test collect(keys(loaded))[1] == "getWeather"
|
||||
@test collect(keys(loaded))[2] == "getTime"
|
||||
@test collect(keys(loaded))[3] == "writeTool"
|
||||
@test collect(keys(loaded))[4] == "listTools"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. Verify loaded tool fields are correct #
|
||||
# ------------------------------------------------------------------ #
|
||||
# getTime
|
||||
time_tool = loaded["getTime"]
|
||||
@test time_tool.name == "getTime"
|
||||
@test time_tool.label == "Time Lookup"
|
||||
@test time_tool.validateRequiredArgs !== nothing
|
||||
@test time_tool.parallelToolExecute == false
|
||||
@test time_tool.inputSchema["required"] == Any[]
|
||||
|
||||
# getWeather
|
||||
weather = loaded["getWeather"]
|
||||
@test weather.name == "getWeather"
|
||||
@test weather.label == "Weather Lookup"
|
||||
@test weather.execute !== nothing
|
||||
@test weather.parallelToolExecute == false
|
||||
@test weather.inputSchema["required"] == ["city"]
|
||||
|
||||
# writeTool
|
||||
wt = loaded["writeTool"]
|
||||
@test wt.name == "writeTool"
|
||||
@test wt.label == "Create Tool"
|
||||
@test wt.execute !== nothing
|
||||
@test "name" in wt.inputSchema["required"]
|
||||
@test "executeCode" in wt.inputSchema["required"]
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. Tool execution returns valid results #
|
||||
# ------------------------------------------------------------------ #
|
||||
sig = nothing
|
||||
op = x -> x # no-op partial result callback
|
||||
|
||||
# execute getTime
|
||||
result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op)
|
||||
@test result_t isa agentToolResult
|
||||
@test result_t.content[1] isa textContent
|
||||
@test occursin("Tokyo", result_t.content[1].text)
|
||||
|
||||
# execute getTime with timezone
|
||||
result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op)
|
||||
@test result_tz isa agentToolResult
|
||||
@test occursin("America/New_York", result_tz.content[1].text)
|
||||
|
||||
# execute getWeather
|
||||
result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op)
|
||||
@test result_w isa agentToolResult
|
||||
@test result_w.content[1] isa textContent
|
||||
@test occursin("Bangkok", result_w.content[1].text)
|
||||
|
||||
# execute getWeather with units
|
||||
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
||||
@test occursin("72\u00b0F", result_w2.content[1].text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. getTools / registerTool / clearTools (per-store isolation) #
|
||||
# ------------------------------------------------------------------ #
|
||||
store3 = toolStore(name="test3")
|
||||
registry_tools = getTools(store3)
|
||||
@test isempty(registry_tools)
|
||||
|
||||
# Register tools manually
|
||||
registerTool(store3, loaded["getTime"])
|
||||
registerTool(store3, loaded["getWeather"])
|
||||
registerTool(store3, loaded["writeTool"])
|
||||
|
||||
reg = getTools(store3)
|
||||
@test !isempty(reg)
|
||||
@test "getTime" in keys(reg)
|
||||
@test "getWeather" in keys(reg)
|
||||
@test "writeTool" in keys(reg)
|
||||
@test collect(keys(reg))[1] == "getTime"
|
||||
@test collect(keys(reg))[2] == "getWeather"
|
||||
@test collect(keys(reg))[3] == "writeTool"
|
||||
|
||||
clearTools(store3)
|
||||
@test isempty(getTools(store3))
|
||||
|
||||
test_tool = agentTool(
|
||||
name = "manualTool",
|
||||
label = "Manual Tool",
|
||||
description = "Registered manually",
|
||||
inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]),
|
||||
execute = (toolCallId, args, signal, onPartialResult) ->
|
||||
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = true
|
||||
)
|
||||
registerTool(store3, test_tool)
|
||||
reg = getTools(store3)
|
||||
@test haskey(reg, "manualTool")
|
||||
@test length(reg) == 1
|
||||
@test reg["manualTool"].parallelToolExecute == true
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 6. getTools returns direct reference (mutations affect registry) #
|
||||
# ------------------------------------------------------------------ #
|
||||
copy1 = getTools(store3)
|
||||
copy2 = getTools(store3)
|
||||
@test copy1 === copy2 # same reference, not a deep copy
|
||||
empty!(copy1)
|
||||
@test isempty(getTools(store3)) # mutation propagates
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. Per-store isolation — two stores don't share tools #
|
||||
# ------------------------------------------------------------------ #
|
||||
storeA = toolStore(name="isolationA")
|
||||
storeB = toolStore(name="isolationB")
|
||||
|
||||
registerTool(storeA, loaded["getTime"])
|
||||
registerTool(storeB, loaded["getWeather"])
|
||||
|
||||
regA = getTools(storeA)
|
||||
regB = getTools(storeB)
|
||||
|
||||
@test "getTime" in keys(regA)
|
||||
@test "getWeather" ∉ keys(regA)
|
||||
@test "getWeather" in keys(regB)
|
||||
@test "getTime" ∉ keys(regB)
|
||||
|
||||
clearTools(storeA)
|
||||
@test isempty(getTools(storeA))
|
||||
@test !isempty(getTools(storeB)) # storeB unaffected
|
||||
end
|
||||
|
||||
@testset "listTool" begin
|
||||
store = toolStore(name="test_list")
|
||||
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
|
||||
|
||||
# register_all_tools auto-registers listTool
|
||||
@test "listTools" in keys(store.tools)
|
||||
|
||||
# listTool returns an agentTool, not a string or array
|
||||
list_t = listTool(store)
|
||||
@test list_t isa agentTool
|
||||
@test list_t.name == "listTools"
|
||||
@test list_t.label == "List Tools"
|
||||
@test isempty(list_t.inputSchema["required"])
|
||||
|
||||
# Verify all tools appear (3 loaded + listTools = 4)
|
||||
result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x)
|
||||
@test result isa agentToolResult
|
||||
@test result.content[1] isa textContent
|
||||
@test occursin("listTools", result.content[1].text)
|
||||
@test occursin("getWeather", result.content[1].text)
|
||||
@test occursin("getTime", result.content[1].text)
|
||||
@test occursin("writeTool", result.content[1].text)
|
||||
@test result.details["count"] == 4
|
||||
|
||||
# Each listTool call creates an independent closure
|
||||
storeB = toolStore(name="test_listB")
|
||||
registerTool(storeB, store.tools["getWeather"])
|
||||
list_tB = listTool(storeB)
|
||||
|
||||
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
||||
resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x)
|
||||
|
||||
@test occursin("getWeather", resultA.content[1].text)
|
||||
@test occursin("getWeather", resultB.content[1].text)
|
||||
@test occursin("getTime", resultA.content[1].text)
|
||||
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
|
||||
end
|
||||
|
||||
@testset "executePreparedToolCall with static tools" begin
|
||||
# Tests executePreparedToolCall with statically loaded tools.
|
||||
# The world-age issue is resolved because tool.execute comes from
|
||||
# a statically included module, not a dynamically created one.
|
||||
|
||||
store = toolStore(name="test_static")
|
||||
register_all_tools(store)
|
||||
|
||||
weather_tool = store.tools["getWeather"]
|
||||
|
||||
# Create a preparedToolCall that mimics what prepareToolCall() returns
|
||||
tool_call = agentToolCall(
|
||||
"function", "call-static-1", "getWeather",
|
||||
Dict{String,Any}("city" => "San Francisco")
|
||||
)
|
||||
prep = preparedToolCall(
|
||||
weather_tool, tool_call, Dict{String,Any}("city" => "San Francisco")
|
||||
)
|
||||
sig = abortSignal(false)
|
||||
|
||||
# This call goes through: executePreparedToolCall -> prep.tool.execute(...)
|
||||
result = executePreparedToolCall(
|
||||
prep, sig, x -> nothing
|
||||
)
|
||||
|
||||
@test result isa executedOutcome
|
||||
@test result.isError == false
|
||||
@test result.result.content[1] isa textContent
|
||||
@test occursin("San Francisco", result.result.content[1].text)
|
||||
end
|
||||
|
||||
@testset "executePreparedToolCall with validation (static tools)" begin
|
||||
# Tests executePreparedToolCall with a tool that has custom validation hooks.
|
||||
# This exercises the full tool execution path including validation.
|
||||
|
||||
store = toolStore(name="test_static_validate")
|
||||
register_all_tools(store)
|
||||
|
||||
time_tool = store.tools["getTime"]
|
||||
|
||||
tool_call = agentToolCall(
|
||||
"function", "call-static-2", "getTime",
|
||||
Dict{String,Any}("timezone" => "America/New_York")
|
||||
)
|
||||
prep = preparedToolCall(
|
||||
time_tool, tool_call, Dict{String,Any}("timezone" => "America/New_York")
|
||||
)
|
||||
sig = abortSignal(false)
|
||||
|
||||
result = executePreparedToolCall(
|
||||
prep, sig, x -> nothing
|
||||
)
|
||||
|
||||
@test result isa executedOutcome
|
||||
@test result.isError == false
|
||||
@test result.result.content[1] isa textContent
|
||||
@test occursin("America/New_York", result.result.content[1].text)
|
||||
end
|
||||
|
||||
@testset "executeToolCallsSequential with static tools (full pipeline)" begin
|
||||
# Tests the full tool execution pipeline: executeToolCallsSequential
|
||||
# which calls prepareToolCall -> executePreparedToolCall -> finalizeExecutedToolCall
|
||||
# with statically loaded tools.
|
||||
|
||||
store = toolStore(name="test_full_pipeline")
|
||||
register_all_tools(store)
|
||||
|
||||
# Build agentContext from the store's tools
|
||||
tools = getTools(store)
|
||||
ctx = agentContext(
|
||||
"test system prompt",
|
||||
agentMessage[],
|
||||
tools
|
||||
)
|
||||
|
||||
# Create an assistant message containing tool calls
|
||||
assistant_msg = assistantMessage(
|
||||
role="assistant",
|
||||
content=Vector{messageContent}(),
|
||||
api="openai",
|
||||
provider="test",
|
||||
model="test-model",
|
||||
usage=llmUsage(0, 0),
|
||||
stopReason="tool_calls",
|
||||
errorMessage=nothing,
|
||||
timestamp=now()
|
||||
)
|
||||
|
||||
# Create tool calls for multiple statically loaded tools
|
||||
tool_calls = [
|
||||
agentToolCall(
|
||||
"function", "call-seq-1", "getWeather",
|
||||
Dict{String,Any}("city" => "Tokyo")
|
||||
),
|
||||
agentToolCall(
|
||||
"function", "call-seq-2", "getTime",
|
||||
Dict{String,Any}("timezone" => "Europe/London")
|
||||
),
|
||||
]
|
||||
|
||||
config = agentLoopConfig(
|
||||
nothing, nothing, "sequential"
|
||||
)
|
||||
sig = abortSignal(false)
|
||||
|
||||
# Execute the full pipeline
|
||||
batch = executeToolCallsSequential(
|
||||
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
|
||||
)
|
||||
|
||||
@test batch.messages isa Vector{toolResultMessage}
|
||||
@test length(batch.messages) == 2
|
||||
@test batch.messages[1].toolName == "getWeather"
|
||||
@test batch.messages[1].isError == false
|
||||
@test occursin("Tokyo", batch.messages[1].content[1].text)
|
||||
@test batch.messages[2].toolName == "getTime"
|
||||
@test batch.messages[2].isError == false
|
||||
@test occursin("Europe/London", batch.messages[2].content[1].text)
|
||||
end
|
||||
|
||||
@testset "executeToolCallsParallel with static tools (full pipeline)" begin
|
||||
# Same as above but tests parallel execution path.
|
||||
|
||||
store = toolStore(name="test_parallel")
|
||||
register_all_tools(store)
|
||||
|
||||
tools = getTools(store)
|
||||
ctx = agentContext(
|
||||
"test system prompt",
|
||||
agentMessage[],
|
||||
tools
|
||||
)
|
||||
|
||||
assistant_msg = assistantMessage(
|
||||
role="assistant",
|
||||
content=Vector{messageContent}(),
|
||||
api="openai",
|
||||
provider="test",
|
||||
model="test-model",
|
||||
usage=llmUsage(0, 0),
|
||||
stopReason="tool_calls",
|
||||
errorMessage=nothing,
|
||||
timestamp=now()
|
||||
)
|
||||
|
||||
tool_calls = [
|
||||
agentToolCall(
|
||||
"function", "call-par-1", "getWeather",
|
||||
Dict{String,Any}("city" => "Paris")
|
||||
),
|
||||
agentToolCall(
|
||||
"function", "call-par-2", "getTime",
|
||||
Dict{String,Any}("city" => "Sydney")
|
||||
),
|
||||
]
|
||||
|
||||
config = agentLoopConfig(
|
||||
nothing, nothing, "parallel"
|
||||
)
|
||||
sig = abortSignal(false)
|
||||
|
||||
batch = executeToolCallsParallel(
|
||||
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
|
||||
)
|
||||
|
||||
@test batch.messages isa Vector{toolResultMessage}
|
||||
@test length(batch.messages) == 2
|
||||
@test batch.messages[1].toolName == "getWeather"
|
||||
@test batch.messages[1].isError == false
|
||||
@test batch.messages[2].toolName == "getTime"
|
||||
@test batch.messages[2].isError == false
|
||||
end
|
||||
+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
|
||||
|
||||
agentEventSink(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 agentEventSink
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
function (aes::agentEventSink)(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 = agentEventSink(agent_conn, "sommanion.debug", "sender")
|
||||
|
||||
agent = YiemAgent.yiemAgent(
|
||||
text2text_llm;
|
||||
agentEventSink=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