Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7abf844ea | |||
| 00447e4dde | |||
| 25f8468696 | |||
| a29a82b74c | |||
| fd616409dd | |||
| 2543e6cbf1 | |||
| b8067c2d33 | |||
| 510cf6126c | |||
| 90fb97a4e7 | |||
| 77adeb3a6b | |||
| 4e592173a6 | |||
| 0cacb5c94a | |||
| 6c96409969 | |||
| 06d51c1ee9 | |||
| 2ad3d1df38 | |||
| 83c7770877 | |||
| bad14fbe7f | |||
| 578e8f55bd | |||
| ae3e432b02 | |||
| 7c14390400 | |||
| 89885c1583 | |||
| 5a27630ccf | |||
| ed91260468 | |||
| c13aeb3a74 | |||
| 287704778f | |||
| a9fa23f01b | |||
| c5cb18f0f1 | |||
| c78f4b023d | |||
| 1b69f69c7d | |||
| 3891099eaa | |||
| 268d340e2f | |||
| 189bc2efcf | |||
| 92b3e4081f | |||
| 750eff483b | |||
| 5cc35c78f4 | |||
| ed5415d92a | |||
| 6b3d575ea0 | |||
| da16c80a0a | |||
| 069240912b | |||
| 2aa0d1e9a4 | |||
| 03e1dd7628 | |||
| 70296a3bf2 | |||
| 75b2ce5978 | |||
| d8172a7fbe |
+107
-94
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
julia_version = "1.12.6"
|
julia_version = "1.12.6"
|
||||||
manifest_format = "2.0"
|
manifest_format = "2.0"
|
||||||
project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6"
|
project_hash = "aa163e2bf572632825162936e107be18384fd40f"
|
||||||
|
|
||||||
[[deps.Accessors]]
|
[[deps.Accessors]]
|
||||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||||
@@ -44,6 +44,12 @@ git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6"
|
|||||||
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
|
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
|
||||||
version = "0.4.0"
|
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.ArrowTypes]]
|
||||||
deps = ["Sockets", "UUIDs"]
|
deps = ["Sockets", "UUIDs"]
|
||||||
git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101"
|
git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101"
|
||||||
@@ -58,6 +64,12 @@ version = "1.11.0"
|
|||||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
||||||
|
[[deps.BitIntegers]]
|
||||||
|
deps = ["Random"]
|
||||||
|
git-tree-sha1 = "091d591a060e43df1dd35faab3ca284925c48e46"
|
||||||
|
uuid = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1"
|
||||||
|
version = "0.3.7"
|
||||||
|
|
||||||
[[deps.BufferedStreams]]
|
[[deps.BufferedStreams]]
|
||||||
git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
|
git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
|
||||||
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
|
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
|
||||||
@@ -90,16 +102,28 @@ git-tree-sha1 = "40956acdbef3d8c7cc38cba42b56034af8f8581a"
|
|||||||
uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
||||||
version = "0.3.4"
|
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.CodecZlib]]
|
||||||
deps = ["TranscodingStreams", "Zlib_jll"]
|
deps = ["TranscodingStreams", "Zlib_jll"]
|
||||||
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
|
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
|
||||||
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||||
version = "0.7.8"
|
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]]
|
[[deps.CommonSolve]]
|
||||||
git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662"
|
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
|
||||||
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
||||||
version = "0.2.11"
|
version = "0.2.13"
|
||||||
|
|
||||||
[[deps.Compat]]
|
[[deps.Compat]]
|
||||||
deps = ["TOML", "UUIDs"]
|
deps = ["TOML", "UUIDs"]
|
||||||
@@ -130,6 +154,12 @@ weakdeps = ["InverseFunctions"]
|
|||||||
[deps.CompositionsBase.extensions]
|
[deps.CompositionsBase.extensions]
|
||||||
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
|
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
|
||||||
|
|
||||||
|
[[deps.ConcurrentUtilities]]
|
||||||
|
deps = ["Serialization", "Sockets"]
|
||||||
|
git-tree-sha1 = "3c9be947934c38475bafe822c6d61aaed17f0738"
|
||||||
|
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
|
||||||
|
version = "2.6.0"
|
||||||
|
|
||||||
[[deps.ConstructionBase]]
|
[[deps.ConstructionBase]]
|
||||||
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
|
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
|
||||||
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
|
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
|
||||||
@@ -146,9 +176,9 @@ version = "1.6.0"
|
|||||||
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
|
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
|
||||||
|
|
||||||
[[deps.Crayons]]
|
[[deps.Crayons]]
|
||||||
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
|
git-tree-sha1 = "54b76cbb40d9a0f5368c880725b2f141da77c94f"
|
||||||
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
|
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
|
||||||
version = "4.1.1"
|
version = "4.2.0"
|
||||||
|
|
||||||
[[deps.DBInterface]]
|
[[deps.DBInterface]]
|
||||||
git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b"
|
git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b"
|
||||||
@@ -168,9 +198,9 @@ version = "1.8.2"
|
|||||||
|
|
||||||
[[deps.DataStructures]]
|
[[deps.DataStructures]]
|
||||||
deps = ["OrderedCollections"]
|
deps = ["OrderedCollections"]
|
||||||
git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e"
|
git-tree-sha1 = "b0bc6d2cad1fed8b7fd59a1551a991cb3d2809e6"
|
||||||
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||||
version = "0.19.5"
|
version = "0.19.6"
|
||||||
|
|
||||||
[[deps.DataValueInterfaces]]
|
[[deps.DataValueInterfaces]]
|
||||||
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
|
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
|
||||||
@@ -208,9 +238,9 @@ version = "1.11.0"
|
|||||||
|
|
||||||
[[deps.Distributions]]
|
[[deps.Distributions]]
|
||||||
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
|
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"
|
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||||
version = "0.25.129"
|
version = "0.25.130"
|
||||||
|
|
||||||
[deps.Distributions.extensions]
|
[deps.Distributions.extensions]
|
||||||
DistributionsChainRulesCoreExt = "ChainRulesCore"
|
DistributionsChainRulesCoreExt = "ChainRulesCore"
|
||||||
@@ -240,15 +270,9 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
|
|||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
||||||
[[deps.ExprTools]]
|
[[deps.ExprTools]]
|
||||||
git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec"
|
git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4"
|
||||||
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
|
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
|
|
||||||
[[deps.EzXML]]
|
|
||||||
deps = ["Printf", "XML2_jll"]
|
|
||||||
git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3"
|
|
||||||
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
|
|
||||||
version = "1.2.3"
|
|
||||||
|
|
||||||
[[deps.FileIO]]
|
[[deps.FileIO]]
|
||||||
deps = ["Pkg", "Requires", "UUIDs"]
|
deps = ["Pkg", "Requires", "UUIDs"]
|
||||||
@@ -277,9 +301,9 @@ version = "1.11.0"
|
|||||||
|
|
||||||
[[deps.FillArrays]]
|
[[deps.FillArrays]]
|
||||||
deps = ["LinearAlgebra"]
|
deps = ["LinearAlgebra"]
|
||||||
git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3"
|
git-tree-sha1 = "5bad39456d9f0166184fce2248783dd9862645c1"
|
||||||
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
|
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"]
|
weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"]
|
||||||
|
|
||||||
[deps.FillArrays.extensions]
|
[deps.FillArrays.extensions]
|
||||||
@@ -300,11 +324,11 @@ version = "1.1.0"
|
|||||||
|
|
||||||
[[deps.GeneralUtils]]
|
[[deps.GeneralUtils]]
|
||||||
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"]
|
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-rev = "main"
|
||||||
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
|
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
|
||||||
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||||
version = "0.5.10"
|
version = "0.5.11"
|
||||||
|
|
||||||
[[deps.Graphs]]
|
[[deps.Graphs]]
|
||||||
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
|
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
|
||||||
@@ -321,9 +345,9 @@ version = "1.14.0"
|
|||||||
|
|
||||||
[[deps.HTTP]]
|
[[deps.HTTP]]
|
||||||
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
|
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"
|
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||||
version = "2.5.5"
|
version = "2.6.4"
|
||||||
|
|
||||||
[[deps.HashArrayMappedTries]]
|
[[deps.HashArrayMappedTries]]
|
||||||
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
|
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
|
||||||
@@ -332,9 +356,9 @@ version = "0.2.0"
|
|||||||
|
|
||||||
[[deps.HypergeometricFunctions]]
|
[[deps.HypergeometricFunctions]]
|
||||||
deps = ["Gamma", "LinearAlgebra"]
|
deps = ["Gamma", "LinearAlgebra"]
|
||||||
git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10"
|
git-tree-sha1 = "31bb6c92405c084617facc1d7ed9eb6c402d061e"
|
||||||
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
|
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
|
||||||
version = "0.3.29"
|
version = "0.3.30"
|
||||||
|
|
||||||
[[deps.ICU_jll]]
|
[[deps.ICU_jll]]
|
||||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||||
@@ -412,9 +436,9 @@ version = "1.8.0"
|
|||||||
|
|
||||||
[[deps.JSON]]
|
[[deps.JSON]]
|
||||||
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
|
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
|
||||||
git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211"
|
git-tree-sha1 = "65979512c25a0727f050e6e4be40f0fd9ec893f7"
|
||||||
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
version = "1.6.1"
|
version = "1.7.0"
|
||||||
weakdeps = ["ArrowTypes"]
|
weakdeps = ["ArrowTypes"]
|
||||||
|
|
||||||
[deps.JSON.extensions]
|
[deps.JSON.extensions]
|
||||||
@@ -432,9 +456,9 @@ weakdeps = ["ArrowTypes"]
|
|||||||
|
|
||||||
[[deps.JuliaInterpreter]]
|
[[deps.JuliaInterpreter]]
|
||||||
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
|
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
|
||||||
git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006"
|
git-tree-sha1 = "c3d401f110454b4ea24a76be33f6ee0d7d385103"
|
||||||
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
|
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
|
||||||
version = "0.10.12"
|
version = "0.11.4"
|
||||||
|
|
||||||
[[deps.JuliaSyntaxHighlighting]]
|
[[deps.JuliaSyntaxHighlighting]]
|
||||||
deps = ["StyledStrings"]
|
deps = ["StyledStrings"]
|
||||||
@@ -506,12 +530,6 @@ version = "1.11.3+1"
|
|||||||
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
||||||
version = "1.11.0"
|
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.LinearAlgebra]]
|
||||||
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
|
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
|
||||||
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||||
@@ -539,9 +557,15 @@ version = "1.11.0"
|
|||||||
|
|
||||||
[[deps.LoweredCodeUtils]]
|
[[deps.LoweredCodeUtils]]
|
||||||
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
|
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
|
||||||
git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f"
|
git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
|
||||||
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
|
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]]
|
[[deps.MacroTools]]
|
||||||
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
||||||
@@ -599,9 +623,9 @@ version = "0.1.1"
|
|||||||
|
|
||||||
[[deps.NanoDates]]
|
[[deps.NanoDates]]
|
||||||
deps = ["Dates", "Parsers"]
|
deps = ["Dates", "Parsers"]
|
||||||
git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f"
|
git-tree-sha1 = "77c7e98ca39aefb481f9b97a2f4f5c5471c08a1d"
|
||||||
uuid = "46f1a544-deae-4307-8689-c12aa3c955c6"
|
uuid = "46f1a544-deae-4307-8689-c12aa3c955c6"
|
||||||
version = "1.0.3"
|
version = "1.1.0"
|
||||||
|
|
||||||
[[deps.NetworkOptions]]
|
[[deps.NetworkOptions]]
|
||||||
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
|
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
|
||||||
@@ -640,15 +664,15 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
|
|||||||
version = "0.5.6+0"
|
version = "0.5.6+0"
|
||||||
|
|
||||||
[[deps.OrderedCollections]]
|
[[deps.OrderedCollections]]
|
||||||
git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4"
|
git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c"
|
||||||
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
|
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
|
||||||
version = "1.8.2"
|
version = "2.0.1"
|
||||||
|
|
||||||
[[deps.PDMats]]
|
[[deps.PDMats]]
|
||||||
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
|
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
|
||||||
git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65"
|
git-tree-sha1 = "123266c25174ef6c8d4718920abc206452cf8de6"
|
||||||
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
|
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
|
||||||
version = "0.11.40"
|
version = "0.11.41"
|
||||||
weakdeps = ["StatsBase"]
|
weakdeps = ["StatsBase"]
|
||||||
|
|
||||||
[deps.PDMats.extensions]
|
[deps.PDMats.extensions]
|
||||||
@@ -656,9 +680,9 @@ weakdeps = ["StatsBase"]
|
|||||||
|
|
||||||
[[deps.Parsers]]
|
[[deps.Parsers]]
|
||||||
deps = ["Dates", "PrecompileTools", "UUIDs"]
|
deps = ["Dates", "PrecompileTools", "UUIDs"]
|
||||||
git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9"
|
git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3"
|
||||||
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
|
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
|
||||||
version = "2.8.6"
|
version = "2.8.7"
|
||||||
|
|
||||||
[[deps.Pkg]]
|
[[deps.Pkg]]
|
||||||
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
|
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"
|
version = "1.5.2"
|
||||||
|
|
||||||
[[deps.PrettyPrinting]]
|
[[deps.PrettyPrinting]]
|
||||||
git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e"
|
git-tree-sha1 = "0b7f4ad437e31c51cf5b91fb103579b04025170a"
|
||||||
uuid = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
uuid = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
|
|
||||||
[[deps.PrettyTables]]
|
[[deps.PrettyTables]]
|
||||||
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
|
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
|
||||||
git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325"
|
git-tree-sha1 = "4ac881f5432bd93463a41767a814a45245be22b6"
|
||||||
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
|
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
|
||||||
version = "3.4.0"
|
version = "3.4.6"
|
||||||
|
|
||||||
[deps.PrettyTables.extensions]
|
[deps.PrettyTables.extensions]
|
||||||
PrettyTablesExcelExt = "XLSX"
|
PrettyTablesExcelExt = "XLSX"
|
||||||
@@ -757,15 +781,15 @@ version = "1.3.1"
|
|||||||
|
|
||||||
[[deps.Reseau]]
|
[[deps.Reseau]]
|
||||||
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
|
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
|
||||||
git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5"
|
git-tree-sha1 = "701ef63506992668c0069a4d1ac1540b77258e6c"
|
||||||
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
|
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
|
||||||
version = "1.3.1"
|
version = "1.3.6"
|
||||||
|
|
||||||
[[deps.Revise]]
|
[[deps.Revise]]
|
||||||
deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
|
deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
|
||||||
git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7"
|
git-tree-sha1 = "ec46aed6a3a8cc6b67839ca361e7b4aa32eaeee1"
|
||||||
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
version = "3.15.1"
|
version = "3.16.3"
|
||||||
weakdeps = ["Distributed"]
|
weakdeps = ["Distributed"]
|
||||||
|
|
||||||
[deps.Revise.extensions]
|
[deps.Revise.extensions]
|
||||||
@@ -779,15 +803,15 @@ version = "0.9.0"
|
|||||||
|
|
||||||
[[deps.Rmath_jll]]
|
[[deps.Rmath_jll]]
|
||||||
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||||
git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8"
|
git-tree-sha1 = "6d40b2fe70437b01397d2a4d5b020008da4e7019"
|
||||||
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
|
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
|
||||||
version = "0.5.1+0"
|
version = "0.5.2+0"
|
||||||
|
|
||||||
[[deps.Roots]]
|
[[deps.Roots]]
|
||||||
deps = ["Accessors", "CommonSolve", "Printf"]
|
deps = ["Accessors", "CommonSolve", "Printf"]
|
||||||
git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455"
|
git-tree-sha1 = "7fb25a964849d90a0446366cdefca822e0e84900"
|
||||||
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
|
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
|
||||||
version = "3.0.5"
|
version = "3.0.6"
|
||||||
|
|
||||||
[deps.Roots.extensions]
|
[deps.Roots.extensions]
|
||||||
RootsChainRulesCoreExt = "ChainRulesCore"
|
RootsChainRulesCoreExt = "ChainRulesCore"
|
||||||
@@ -840,12 +864,6 @@ git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80"
|
|||||||
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
|
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
|
||||||
version = "1.4.10"
|
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]]
|
[[deps.Serialization]]
|
||||||
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -879,9 +897,9 @@ version = "1.12.0"
|
|||||||
|
|
||||||
[[deps.SpecialFunctions]]
|
[[deps.SpecialFunctions]]
|
||||||
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
|
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
|
||||||
git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f"
|
git-tree-sha1 = "c3ac026e735264e9bdc6a9bcbd1b1e781b36e3bc"
|
||||||
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
|
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
|
||||||
version = "2.8.0"
|
version = "2.8.3"
|
||||||
|
|
||||||
[deps.SpecialFunctions.extensions]
|
[deps.SpecialFunctions.extensions]
|
||||||
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
|
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
|
||||||
@@ -932,9 +950,9 @@ version = "0.34.12"
|
|||||||
|
|
||||||
[[deps.StatsFuns]]
|
[[deps.StatsFuns]]
|
||||||
deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
|
deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
|
||||||
git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5"
|
git-tree-sha1 = "91a5737baed20ee31f3faea0e51f57461f6a689e"
|
||||||
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
|
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
|
||||||
version = "2.2.0"
|
version = "2.2.1"
|
||||||
|
|
||||||
[deps.StatsFuns.extensions]
|
[deps.StatsFuns.extensions]
|
||||||
StatsFunsChainRulesCoreExt = "ChainRulesCore"
|
StatsFunsChainRulesCoreExt = "ChainRulesCore"
|
||||||
@@ -950,17 +968,16 @@ git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a"
|
|||||||
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
|
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
|
||||||
version = "1.0.0"
|
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.StringManipulation]]
|
||||||
deps = ["PrecompileTools"]
|
deps = ["PrecompileTools"]
|
||||||
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
|
git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484"
|
||||||
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
|
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.StructTypes]]
|
||||||
deps = ["Dates", "UUIDs"]
|
deps = ["Dates", "UUIDs"]
|
||||||
@@ -970,9 +987,9 @@ version = "1.11.0"
|
|||||||
|
|
||||||
[[deps.StructUtils]]
|
[[deps.StructUtils]]
|
||||||
deps = ["Dates", "UUIDs"]
|
deps = ["Dates", "UUIDs"]
|
||||||
git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
|
git-tree-sha1 = "c65ae4aa47e543c278aea0a3468786d33021a3ff"
|
||||||
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
|
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
|
||||||
version = "2.8.2"
|
version = "2.8.4"
|
||||||
|
|
||||||
[deps.StructUtils.extensions]
|
[deps.StructUtils.extensions]
|
||||||
StructUtilsMeasurementsExt = ["Measurements"]
|
StructUtilsMeasurementsExt = ["Measurements"]
|
||||||
@@ -1046,9 +1063,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
|
|||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
|
||||||
[[deps.URIs]]
|
[[deps.URIs]]
|
||||||
git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a"
|
git-tree-sha1 = "3b0738bd7c5645641845da25cbd99800b8718689"
|
||||||
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||||
version = "1.6.1"
|
version = "1.6.2"
|
||||||
|
|
||||||
[[deps.UTCDateTimes]]
|
[[deps.UTCDateTimes]]
|
||||||
deps = ["Dates", "TimeZones"]
|
deps = ["Dates", "TimeZones"]
|
||||||
@@ -1076,23 +1093,11 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
|
|||||||
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
|
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
|
||||||
version = "1.6.1"
|
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.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 = "."
|
path = "."
|
||||||
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
|
||||||
version = "0.7.4"
|
version = "0.8.0"
|
||||||
|
|
||||||
[[deps.Zlib_jll]]
|
[[deps.Zlib_jll]]
|
||||||
deps = ["Libdl"]
|
deps = ["Libdl"]
|
||||||
@@ -1116,6 +1121,14 @@ git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026"
|
|||||||
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
|
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
|
||||||
version = "1.0.21+0"
|
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.nghttp2_jll]]
|
||||||
deps = ["Artifacts", "Libdl"]
|
deps = ["Artifacts", "Libdl"]
|
||||||
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
||||||
|
|||||||
+2
-2
@@ -19,10 +19,10 @@ PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
|
|||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
||||||
Serde = "db9b398d-9517-45f8-9a95-92af99003e0e"
|
|
||||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||||
|
msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||||
|
|
||||||
[compat]
|
[compat]
|
||||||
Base64 = "1.11.0"
|
Base64 = "1.11.0"
|
||||||
@@ -34,4 +34,4 @@ JSON = "1.6.1"
|
|||||||
LLMMCTS = "0.1.5"
|
LLMMCTS = "0.1.5"
|
||||||
NATS = "0.1.0"
|
NATS = "0.1.0"
|
||||||
SQLLLM = "0.2.8"
|
SQLLLM = "0.2.8"
|
||||||
Serde = "3.7.2"
|
msghandler = "1.2.1"
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
# YiemAgent
|
# YiemAgent
|
||||||
|
|
||||||
## TODO
|
Julia framework for building agents with tool use.
|
||||||
- [WORKING] build prompt()
|
|
||||||
- [ ] build agent runLoop()
|
|
||||||
- [ ] build MCP server connector
|
|
||||||
- [ ] executeplan() to execute the plan
|
|
||||||
- [ ] add comprehensive tests
|
|
||||||
|
|
||||||
## Changelog
|
## Getting Started
|
||||||
|
|
||||||
### Version 0.8.0
|
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
|
||||||
- Converted snake_case fields to camelCase:
|
2. Create a `yiemAgent` with `loadTools("src/tools")`
|
||||||
- `llmModel`: `base_url` → `baseUrl`, `context_window` → `contextWindow`, `max_tokens` → `maxTokens`
|
3. Call `runAgent(agent, "message")` then `takeResponse(agent)`
|
||||||
- Converted PascalCase type references to camelCase:
|
|
||||||
- `AgentState` → `agentState`
|
## Architecture
|
||||||
- `AgentTool` → `agentTool`
|
|
||||||
- `AgentMessage` → `agentMessage`
|
```
|
||||||
- `PendingMessageQueue` → `pendingMessageQueue`
|
src/
|
||||||
- `ActiveRun` → `activeRun`
|
├── YiemAgent.jl # Module entry point
|
||||||
- `StreamFn` → `streamFn`
|
├── type.jl # Core types (messages, tools, agent state)
|
||||||
- `ThinkingLevel` → `thinkingLevel`
|
├── utils.jl # Message formatting, validation
|
||||||
- `ToolExecutionMode` → `toolExecutionMode`
|
├── 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,5 @@
|
|||||||
# ── executeToolCalls() Julia pseudo code ──────────────────────────
|
check my understand:
|
||||||
# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit
|
1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and
|
||||||
|
it will be the latest message in agent._state.messages. then _agentLoop() can pick it as
|
||||||
|
the output to outputChannel
|
||||||
|
2) if LLM use tool calls but toolResultBatch.terminate is false, assistantMessageToolCall
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
check my understanding
|
||||||
|
1) if LLM didn't use tool calls, assistantMessage get pushed into agent._state.messages and it will be the latest message in agent._state.messages. then _agentLoop() can pick it as the output to outputChannel
|
||||||
|
2) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is false then _processMessage() loop continue
|
||||||
|
3) if LLM use tool calls, assistantMessageToolCall get pushed into agent._state.messages. then toolResult get pushed into agent._state.messages. if toolResultBatch.terminate is true then final_response message get pushed into agent._state.messages. _processMessage() loop exit. then _agentLoop() can pick it as the output to outputChannel
|
||||||
|
|
||||||
|
Is my understanding correct?
|
||||||
|
|
||||||
|
|
||||||
+14
-3
@@ -1,7 +1,6 @@
|
|||||||
module YiemAgent
|
module YiemAgent
|
||||||
|
|
||||||
# export agent
|
export register_all_tools
|
||||||
|
|
||||||
|
|
||||||
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
||||||
files and each file can only depend on the file included before it.
|
files and each file can only depend on the file included before it.
|
||||||
@@ -13,9 +12,21 @@ module YiemAgent
|
|||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
using .utils
|
using .utils
|
||||||
|
|
||||||
include("tools/registry.jl")
|
include("tools/getWeather.jl")
|
||||||
|
include("tools/getTime.jl")
|
||||||
|
include("tools/writeTool.jl")
|
||||||
|
|
||||||
|
include("toolRegistry.jl")
|
||||||
using .toolRegistry
|
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")
|
# include("llmfunction.jl")
|
||||||
# using .llmfunction
|
# using .llmfunction
|
||||||
|
|
||||||
|
|||||||
+734
-339
File diff suppressed because it is too large
Load Diff
+16
-17
@@ -3,14 +3,13 @@ module api
|
|||||||
export prompt
|
export prompt
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Serde
|
DataFrames
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils
|
using ..type, ..utils, ..agentCore, ..toolRegistry
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Send a message to the agent's input channel.
|
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
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
- Use `take_response(agent)` to receive the agent's response after sending a message.
|
- Use `takeResponse(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 `followUp(agent, msg)` to send messages while the agent is still processing.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> run_agent(agent, "Hello!")
|
julia> runAgent(agent, "Hello!")
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function run_agent(agent::yiemAgent, msg)
|
function runAgent(agent::yiemAgent, msg)
|
||||||
put!(agent.inputChannel, msg)
|
put!(agent.inputChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
@@ -51,15 +50,15 @@ Blocks until the agent sends a response.
|
|||||||
- An `assistantMessage` instance representing the agent's response
|
- An `assistantMessage` instance representing the agent's response
|
||||||
|
|
||||||
# Notes
|
# 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
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> response = take_response(agent)
|
julia> response = takeResponse(agent)
|
||||||
assistantMessage(...)
|
assistantMessage(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function take_response(agent::yiemAgent)
|
function takeResponse(agent::yiemAgent)
|
||||||
return take!(agent.outputChannel)
|
return take!(agent.outputChannel)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -77,17 +76,17 @@ and before any tool call results are sent.
|
|||||||
- The same `agent` instance for chaining
|
- The same `agent` instance for chaining
|
||||||
|
|
||||||
# Notes
|
# 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.
|
messages while the agent is processing.
|
||||||
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> follow_up(agent, "Also consider red wines")
|
julia> followUp(agent, "Also consider red wines")
|
||||||
yiemAgent(...)
|
yiemAgent(...)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function follow_up(agent::yiemAgent, msg)
|
function followUp(agent::yiemAgent, msg)
|
||||||
put!(agent.followUpChannel, msg)
|
put!(agent.followUpChannel, msg)
|
||||||
return agent
|
return agent
|
||||||
end
|
end
|
||||||
@@ -105,19 +104,19 @@ then closes all channels (`inputChannel`, `outputChannel`, `followUpChannel`).
|
|||||||
- `nothing`
|
- `nothing`
|
||||||
|
|
||||||
# Notes
|
# 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.
|
for further interaction.
|
||||||
- If the background task throws a `TaskFailedException`, it is rethrown.
|
- If the background task throws a `TaskFailedException`, it is rethrown.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```jldoctest
|
```jldoctest
|
||||||
julia> stop_agent(agent)
|
julia> stopAgent(agent)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function stop_agent(agent::yiemAgent)
|
function stopAgent(agent::yiemAgent)
|
||||||
put!(agent.inputChannel, :shutdown)
|
put!(agent.inputChannel, :shutdown)
|
||||||
try
|
try
|
||||||
fetch(agent._agent_loop)
|
fetch(agent._agentLoop)
|
||||||
catch e
|
catch e
|
||||||
if e isa TaskFailedException
|
if e isa TaskFailedException
|
||||||
rethrow(e)
|
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.
|
Validate required arguments for the getTime tool.
|
||||||
|
|
||||||
@@ -13,7 +16,7 @@ Demonstrates custom validation beyond simple required-field checking:
|
|||||||
- `nothing` if validation passes
|
- `nothing` if validation passes
|
||||||
- `String` error message if validation fails
|
- `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)
|
tz = get(args, "timezone", nothing)
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
|
|
||||||
@@ -39,26 +42,17 @@ end
|
|||||||
"""
|
"""
|
||||||
Execute the getTime tool.
|
Execute the getTime tool.
|
||||||
|
|
||||||
# Arguments
|
Returns mock time data for the given timezone or city.
|
||||||
- `toolCallId::String`: Unique identifier for this tool call
|
|
||||||
- `args::Dict{String,Any}`: Parsed arguments from the LLM
|
|
||||||
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
|
|
||||||
- `onPartialResult::Function`: Callback for streaming partial results
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- `agentToolResult`: Result content with current time data
|
|
||||||
"""
|
"""
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
|
onPartialResult)
|
||||||
tz = get(args, "timezone", nothing)
|
tz = get(args, "timezone", nothing)
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
|
|
||||||
# Simulate time lookup — replace with actual timezone API call
|
|
||||||
if tz !== nothing
|
if tz !== nothing
|
||||||
result = "Current time in $(tz): $(now())"
|
result = "Current time in $(tz): $(now())"
|
||||||
else
|
else
|
||||||
result = "Current time in $(city): $(now())"
|
result = "Current time in $(city): $(now())"
|
||||||
end
|
end
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent(result)],
|
[textContent(result)],
|
||||||
Dict{Any,Any}(), nothing, false
|
Dict{Any,Any}(), nothing, false
|
||||||
@@ -68,7 +62,7 @@ end
|
|||||||
"""
|
"""
|
||||||
Define and return the getTime agentTool.
|
Define and return the getTime agentTool.
|
||||||
"""
|
"""
|
||||||
function getTool()::agentTool
|
function getTimeTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "getTime",
|
name = "getTime",
|
||||||
label = "Time Lookup",
|
label = "Time Lookup",
|
||||||
@@ -76,14 +70,14 @@ function getTool()::agentTool
|
|||||||
inputSchema = Dict{String,Any}(
|
inputSchema = Dict{String,Any}(
|
||||||
"type" => "object",
|
"type" => "object",
|
||||||
"properties" => Dict(
|
"properties" => Dict(
|
||||||
"timezone" => Dict("type" => "string", "description", "IANA timezone, e.g. 'America/New_York'"),
|
"timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'"),
|
||||||
"city" => Dict("type" => "string", "description", "City name as fallback")
|
"city" => Dict("type" => "string", "description" => "City name as fallback")
|
||||||
),
|
),
|
||||||
"required" => []
|
"required" => []
|
||||||
),
|
),
|
||||||
execute = executeTool,
|
execute = getTimeExecute,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = validateRequiredArgs,
|
validateRequiredArgs = getTimeValidateRequiredArgs,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
+18
-22
@@ -1,37 +1,33 @@
|
|||||||
|
using msghandler
|
||||||
|
using .type
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Execute the getWeather tool.
|
Execute the getWeather tool.
|
||||||
|
|
||||||
# Arguments
|
Returns mock weather data for the given city and temperature units.
|
||||||
- `toolCallId::String`: Unique identifier for this tool call
|
|
||||||
- `args::Dict{String,Any}`: Parsed arguments from the LLM
|
|
||||||
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
|
|
||||||
- `onPartialResult::Function`: Callback for streaming partial results
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- `agentToolResult`: Result content with weather data
|
|
||||||
"""
|
"""
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
city = get(args, "city", "")
|
agentEventSink)
|
||||||
units = get(args, "units", "celsius")
|
|
||||||
|
|
||||||
# Simulate weather fetch — replace with actual API call
|
agentEventSink("Getting weather...")
|
||||||
# You can call onPartialResult() here for streaming progress updates:
|
|
||||||
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
|
||||||
# onPartialResult(Dict("status" => "Processing..."))
|
|
||||||
|
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
city = get(args, "city", "")
|
||||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
units = get(args, "units", "celsius")
|
||||||
|
temp = units == "fahrenheit" ? "72" : "22"
|
||||||
|
unit_symbol = units == "celsius" ? "°C" : "°F"
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
||||||
Dict{Any,Any}(), nothing, false
|
Dict{Any,Any}(),
|
||||||
|
nothing,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Define and return the getWeather agentTool.
|
Define and return the getWeather agentTool.
|
||||||
"""
|
"""
|
||||||
function getTool()::agentTool
|
function getWeatherTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "getWeather",
|
name = "getWeather",
|
||||||
label = "Weather Lookup",
|
label = "Weather Lookup",
|
||||||
@@ -44,7 +40,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => ["city"]
|
"required" => ["city"]
|
||||||
),
|
),
|
||||||
execute = executeTool, # reference the function defined above
|
execute = getWeatherExecute,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
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
|
||||||
+112
-182
@@ -4,17 +4,17 @@
|
|||||||
messageContent, agentMessage, agent,
|
messageContent, agentMessage, agent,
|
||||||
# Model types
|
# Model types
|
||||||
modelCost, llmModel, llmUsage,
|
modelCost, llmModel, llmUsage,
|
||||||
# Message content types
|
# Message content types
|
||||||
textContent, imageContent,
|
textContent, imageContent, reasoningContent,
|
||||||
# Message types
|
# Message types
|
||||||
userMessage, assistantMessage, toolResultMessage,
|
userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage,
|
||||||
# Tool types
|
# Tool types
|
||||||
agentTool, validateRequiredArgs
|
agentTool, validateRequiredArgs,
|
||||||
# Context types
|
# Context types
|
||||||
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
agentContext, agentState, agentToolCall, prepareNextTurnContext,
|
||||||
# Loop & execution types
|
# Loop & execution types
|
||||||
agentLoopConfig, abortSignal, agentToolResult,
|
agentLoopConfig, abortSignal, agentToolResult,beforeToolCallContext,
|
||||||
assistantMsgCtx, afterCtx,
|
beforeToolCallResult, afterToolCallContext,
|
||||||
# Event types
|
# Event types
|
||||||
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
|
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
|
||||||
# Agent
|
# Agent
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
|
||||||
agentToolCallBatch,
|
agentToolCallBatch,
|
||||||
# Functions (defined elsewhere)
|
# Functions (defined elsewhere)
|
||||||
run_agent, take_response, follow_up, stop_agent
|
runAgent, takeResponse, followUp, stopAgent
|
||||||
|
|
||||||
|
|
||||||
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
|
||||||
@@ -31,6 +31,13 @@ using GeneralUtils
|
|||||||
|
|
||||||
const Timestamp = DateTime
|
const Timestamp = DateTime
|
||||||
|
|
||||||
|
struct agentToolCall # A tool invocation from the LLM
|
||||||
|
type::String # Always "function"
|
||||||
|
id::String # Unique tool call identifier
|
||||||
|
name::String # Tool name
|
||||||
|
arguments::Dict{String, Any} # Parsed tool arguments
|
||||||
|
end
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
# LLM model info #
|
# LLM model info #
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
@@ -75,6 +82,10 @@ struct imageContent <: messageContent # Image message content
|
|||||||
mimeType::String # MIME type (e.g., "image/png")
|
mimeType::String # MIME type (e.g., "image/png")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct reasoningContent <: messageContent # LLM reasoning/thinking content
|
||||||
|
text::String # The reasoning text
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
# Message types #
|
# Message types #
|
||||||
@@ -108,6 +119,52 @@ function userMessage(; role="user", content=Vector{messageContent}(), timestamp=
|
|||||||
return userMessage(role, content, timestamp)
|
return userMessage(role, content, timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct assistantMessageToolCall <: agentMessage # Assistant message containing tool calls
|
||||||
|
role::String # Always "assistant"
|
||||||
|
toolCalls::Vector{agentToolCall} # Tool calls to execute
|
||||||
|
content::Vector{messageContent} # Reasoning/thinking content blocks
|
||||||
|
api::String # API name used (e.g., "openai")
|
||||||
|
provider::String # Provider name (e.g., "anthropic")
|
||||||
|
model::String # Model identifier
|
||||||
|
usage::llmUsage # Token usage for this message
|
||||||
|
stopReason::String # Why generation stopped (e.g., "tool_calls")
|
||||||
|
errorMessage::Union{String, Nothing} # Error if generation failed
|
||||||
|
timestamp::Timestamp # When the message was received
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create a new assistant message containing tool calls.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `role::String`: Always "assistant"
|
||||||
|
- `toolCalls::Vector{agentToolCall}`: Tool calls to execute
|
||||||
|
- `content::Vector{messageContent}`: Reasoning/thinking content blocks
|
||||||
|
- `api::String`: API name used
|
||||||
|
- `provider::String`: Provider name
|
||||||
|
- `model::String`: Model identifier
|
||||||
|
- `usage::llmUsage`: Token usage
|
||||||
|
- `stopReason::String`: Why generation stopped
|
||||||
|
- `errorMessage::Union{String, Nothing}`: Error if generation failed
|
||||||
|
- `timestamp::Timestamp`: When the message was received
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A new `assistantMessageToolCall` instance
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
```julia
|
||||||
|
julia> tc = agentToolCall("function", "call_1", "getWeather", Dict("city" => "Tokyo"))
|
||||||
|
julia> msg = assistantMessageToolCall(toolCalls=[tc], stopReason="tool_calls")
|
||||||
|
assistantMessageToolCall("assistant", [agentToolCall(...)], messageContent[], "", "", "", llmUsage(0, 0), "tool_calls", nothing, DateTime(...))
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function assistantMessageToolCall(; role="assistant", toolCalls=agentToolCall[],
|
||||||
|
content=Vector{messageContent}(), api="", provider="", model=nothing, usage=llmUsage(0, 0),
|
||||||
|
stopReason="tool_calls", errorMessage=nothing, timestamp=now())
|
||||||
|
model_str = model isa AbstractString ? String(model) : ""
|
||||||
|
return assistantMessageToolCall(role, toolCalls, content, api, provider, model_str,
|
||||||
|
usage, stopReason, errorMessage, timestamp)
|
||||||
|
end
|
||||||
|
|
||||||
struct assistantMessage <: agentMessage # Message from the AI assistant
|
struct assistantMessage <: agentMessage # Message from the AI assistant
|
||||||
role::String # Always "assistant"
|
role::String # Always "assistant"
|
||||||
content::Vector{messageContent} # Text and/or image content
|
content::Vector{messageContent} # Text and/or image content
|
||||||
@@ -144,9 +201,10 @@ assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "en
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function assistantMessage(; role="assistant", content=Vector{messageContent}(),
|
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())
|
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
|
end
|
||||||
|
|
||||||
struct toolResultMessage <: agentMessage # Result returned from a tool execution
|
struct toolResultMessage <: agentMessage # Result returned from a tool execution
|
||||||
@@ -248,7 +306,7 @@ tool = agentTool(
|
|||||||
execute=(toolCallId, args, signal, onPartialResult) -> begin
|
execute=(toolCallId, args, signal, onPartialResult) -> begin
|
||||||
city = args["city"]
|
city = args["city"]
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent("Sunny, 22C in $(city)")],
|
[textContent("Sunny, 22C in Bangkok")],
|
||||||
Dict{Any,Any}(), nothing, false
|
Dict{Any,Any}(), nothing, false
|
||||||
)
|
)
|
||||||
end,
|
end,
|
||||||
@@ -263,12 +321,23 @@ struct agentTool # A tool available to the agent
|
|||||||
label::String # Human-readable tool name
|
label::String # Human-readable tool name
|
||||||
description::String # What the tool does
|
description::String # What the tool does
|
||||||
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
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
|
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||||
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||||
end
|
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 #
|
# Agent context #
|
||||||
@@ -280,7 +349,7 @@ Snapshot of the agent's conversation context.
|
|||||||
# Arguments
|
# Arguments
|
||||||
- `systemPrompt::String`: System prompt for the agent
|
- `systemPrompt::String`: System prompt for the agent
|
||||||
- `messages::Vector{agentMessage}`: Conversation messages
|
- `messages::Vector{agentMessage}`: Conversation messages
|
||||||
- `tools::Union{Vector{agentTool}, Nothing}`: Available tools
|
- `tools::Union{OrderedDict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A new `agentContext` instance
|
- A new `agentContext` instance
|
||||||
@@ -288,7 +357,7 @@ Snapshot of the agent's conversation context.
|
|||||||
struct agentContext # Snapshot of the agent's conversation context
|
struct agentContext # Snapshot of the agent's conversation context
|
||||||
systemPrompt::String # System prompt for the agent
|
systemPrompt::String # System prompt for the agent
|
||||||
messages::Vector{agentMessage} # Conversation messages
|
messages::Vector{agentMessage} # Conversation messages
|
||||||
tools::Union{Vector{agentTool}, Nothing} # Available tools
|
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -297,15 +366,14 @@ end
|
|||||||
# ------------------------------------------------------------------------------------------------ #
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
|
|
||||||
mutable struct agentState # Mutable runtime state of an agent
|
mutable struct agentState # Mutable runtime state of an agent
|
||||||
systemPrompt::String # System prompt text
|
systemPrompt::String # System prompt for the agent
|
||||||
model::llmModel # LLM model to use
|
model::Union{llmModel, Nothing} # LLM model to use
|
||||||
tools::Vector{agentTool} # Available tools
|
tools::OrderedDict{String, agentTool} # Available tools keyed by name, insertion-ordered
|
||||||
|
|
||||||
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
|
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
|
||||||
messages::Vector{agentMessage}
|
messages::Vector{agentMessage}
|
||||||
|
|
||||||
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
|
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
|
||||||
activeRun::Bool # is agent processing user message?
|
|
||||||
errorMessage::Union{String, Nothing} # Last error message
|
errorMessage::Union{String, Nothing} # Last error message
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -318,7 +386,7 @@ new state from external references.
|
|||||||
# Arguments
|
# Arguments
|
||||||
- `systemPrompt::String`: System prompt text
|
- `systemPrompt::String`: System prompt text
|
||||||
- `model::llmModel`: LLM model to use (defaults to an unknown model)
|
- `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)
|
- `messages::Vector{agentMessage}`: Conversation messages (deep copied)
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
@@ -327,32 +395,23 @@ new state from external references.
|
|||||||
# Examples
|
# Examples
|
||||||
```julia
|
```julia
|
||||||
julia> state = agentState(systemPrompt="You are a helpful assistant")
|
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(
|
function agentState(
|
||||||
systemPrompt::String="",
|
systemPrompt::String="",
|
||||||
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
model=llmModel("model_1", "unknown", "unknown", "", false, String[],
|
||||||
tools::Vector{agentTool}=agentTool[],
|
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||||
messages::Vector{agentMessage}=agentMessage[],
|
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||||
|
messages::Vector{agentMessage}=agentMessage[],
|
||||||
)
|
)
|
||||||
agentState(
|
agentState(
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
model,
|
model,
|
||||||
deepcopy(tools),
|
deepcopy(tools),
|
||||||
deepcopy(messages),
|
deepcopy(messages),
|
||||||
Vector{String}(),
|
Vector{String}(),
|
||||||
false,
|
nothing,
|
||||||
nothing,
|
)
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
struct agentToolCall # A tool invocation from the LLM
|
|
||||||
type::String # Always "function"
|
|
||||||
id::String # Unique tool call identifier
|
|
||||||
name::String # Tool name
|
|
||||||
arguments::Dict{String, Any} # Parsed tool arguments
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -384,13 +443,11 @@ end
|
|||||||
Configuration for the agent tool execution loop.
|
Configuration for the agent tool execution loop.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `tools::Vector{agentTool}`: Available tools
|
|
||||||
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
|
||||||
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
|
||||||
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
|
||||||
"""
|
"""
|
||||||
struct agentLoopConfig
|
struct agentLoopConfig
|
||||||
tools::Vector{agentTool}
|
|
||||||
beforeToolCall::Union{Function, Nothing}
|
beforeToolCall::Union{Function, Nothing}
|
||||||
afterToolCall::Union{Function, Nothing}
|
afterToolCall::Union{Function, Nothing}
|
||||||
toolExecution::String
|
toolExecution::String
|
||||||
@@ -426,31 +483,36 @@ end
|
|||||||
Context passed to the `beforeToolCall` hook.
|
Context passed to the `beforeToolCall` hook.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `message::assistantMessage`: The assistant message containing the tool call
|
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||||
- `toolCall::agentToolCall`: The tool call being prepared
|
- `toolCall::agentToolCall`: The tool call being prepared
|
||||||
- `args::Dict{String,Any}`: Validated tool arguments
|
- `args::Dict{String,Any}`: Validated tool arguments
|
||||||
- `context::agentContext`: Current conversation context
|
- `context::agentContext`: Current conversation context
|
||||||
"""
|
"""
|
||||||
struct assistantMsgCtx
|
struct beforeToolCallContext
|
||||||
message::assistantMessage
|
message::assistantMessageToolCall
|
||||||
toolCall::agentToolCall
|
toolCall::agentToolCall
|
||||||
args::Dict{String,Any}
|
args::Dict{String,Any}
|
||||||
context::agentContext
|
context::agentContext
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct beforeToolCallResult
|
||||||
|
block::Bool
|
||||||
|
reason::String
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Context passed to the `afterToolCall` hook.
|
Context passed to the `afterToolCall` hook.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `message::assistantMessage`: The assistant message containing the tool call
|
- `message::assistantMessageToolCall`: The assistant message containing the tool call
|
||||||
- `toolCall::agentToolCall`: The tool call that was executed
|
- `toolCall::agentToolCall`: The tool call that was executed
|
||||||
- `args::Dict{String,Any}`: Tool arguments
|
- `args::Dict{String,Any}`: Tool arguments
|
||||||
- `result::agentToolResult`: The raw tool result
|
- `result::agentToolResult`: The raw tool result
|
||||||
- `isError::Bool`: Whether execution resulted in an error
|
- `isError::Bool`: Whether execution resulted in an error
|
||||||
- `context::agentContext`: Current conversation context
|
- `context::agentContext`: Current conversation context
|
||||||
"""
|
"""
|
||||||
struct afterCtx
|
struct afterToolCallContext
|
||||||
message::assistantMessage
|
message::assistantMessageToolCall
|
||||||
toolCall::agentToolCall
|
toolCall::agentToolCall
|
||||||
args::Dict{String,Any}
|
args::Dict{String,Any}
|
||||||
result::agentToolResult
|
result::agentToolResult
|
||||||
@@ -510,138 +572,6 @@ end
|
|||||||
|
|
||||||
abstract type agent 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)
|
preparedToolCall(tool, toolCall, args)
|
||||||
|
|
||||||
|
|||||||
+194
-15
@@ -1,9 +1,11 @@
|
|||||||
module utils
|
module utils
|
||||||
|
|
||||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI,
|
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
|
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 GeneralUtils
|
||||||
using ..type
|
using ..type
|
||||||
|
|
||||||
@@ -73,7 +75,6 @@ function availableWineToText(vecd::Vector)::String
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
prepareContext(state::agentState) -> agentContext
|
prepareContext(state::agentState) -> agentContext
|
||||||
|
|
||||||
@@ -108,12 +109,12 @@ prepareContext(state).messages == deepcopy(state.messages)
|
|||||||
# end
|
# 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
|
#TODO filter tools from state.tools based on user intend in user message and tool description
|
||||||
filteredTools = state.tools
|
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
|
preparedSystemPrompt = state.systemPrompt
|
||||||
|
|
||||||
#TODO add system prompt, adjust/modify and inject additional context into messages
|
#TODO add system prompt, adjust/modify and inject additional context into messages
|
||||||
@@ -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
|
""" openai message format example
|
||||||
msg = Dict(
|
msg = Dict(
|
||||||
@@ -183,19 +184,31 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
|||||||
Dict("type" => "text", "text" => "let me check."),
|
Dict("type" => "text", "text" => "let me check."),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
"tools"=> [
|
||||||
Dict(
|
Dict(
|
||||||
"role" => "toolResult",
|
"type" => "function",
|
||||||
"content" => [
|
"function" => Dict(
|
||||||
Dict("type" => "text", "text" => "name: Chateau Montelena ..."),
|
"name" => "getWeather",
|
||||||
]
|
"description" => "Get current weather",
|
||||||
),
|
"parameters" => Dict(
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => Dict(
|
||||||
|
"city" => Dict("type" => "string")
|
||||||
|
),
|
||||||
|
"required" => ["city"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
],
|
],
|
||||||
"temperature" => 0.7
|
"temperature" => 0.7
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
openaiReadyMsg = Dict{String, Any}()
|
||||||
|
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
|
||||||
messages = Vector{Dict{String, Any}}()
|
messages = Vector{Dict{String, Any}}()
|
||||||
|
agentEventSink("formatMsgForLLM 1")
|
||||||
# System prompt as system message
|
# System prompt as system message
|
||||||
if !isempty(ctx.systemPrompt)
|
if !isempty(ctx.systemPrompt)
|
||||||
push!(messages, Dict(
|
push!(messages, Dict(
|
||||||
@@ -203,19 +216,113 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
|||||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
|
agentEventSink("formatMsgForLLM 2")
|
||||||
# Conversation messages
|
# Conversation messages
|
||||||
for msg in ctx.messages
|
for msg in ctx.messages
|
||||||
if msg isa userMessage
|
if msg isa userMessage
|
||||||
push!(messages, _userMessageToOpenAI(msg))
|
push!(messages, _userMessageToOpenAI(msg))
|
||||||
|
elseif msg isa assistantMessageToolCall
|
||||||
|
push!(messages, _assistantMessageToolCallToOpenAI(msg))
|
||||||
elseif msg isa assistantMessage
|
elseif msg isa assistantMessage
|
||||||
push!(messages, _assistantMessageToOpenAI(msg))
|
push!(messages, _assistantMessageToOpenAI(msg))
|
||||||
elseif msg isa toolResultMessage
|
elseif msg isa toolResultMessage
|
||||||
push!(messages, _toolResultMessageToOpenAI(msg))
|
push!(messages, _toolResultMessageToOpenAI(msg))
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -230,6 +337,44 @@ function _userMessageToOpenAI(msg::userMessage)::Dict{String, Any}
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert an assistantMessageToolCall to OpenAI message format.
|
||||||
|
|
||||||
|
Produces a message with role="assistant", content=null, and a tool_calls array:
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": null,
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_1",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"arguments": "{\"location\": \"San Francisco, CA\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{String, Any}
|
||||||
|
tool_calls = Dict{String, Any}[]
|
||||||
|
for tc in msg.toolCalls
|
||||||
|
push!(tool_calls, Dict(
|
||||||
|
"id" => tc.id,
|
||||||
|
"type" => tc.type,
|
||||||
|
"function" => Dict(
|
||||||
|
"name" => tc.name,
|
||||||
|
"arguments" => JSON.json(tc.arguments)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
end
|
||||||
|
return Dict(
|
||||||
|
"role" => "assistant",
|
||||||
|
"content" => nothing,
|
||||||
|
"tool_calls" => tool_calls
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Convert an assistantMessage to OpenAI message format.
|
Convert an assistantMessage to OpenAI message format.
|
||||||
"""
|
"""
|
||||||
@@ -279,6 +424,40 @@ function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{
|
|||||||
end
|
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}
|
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
|
generalconversation, detectWineryName, generateSituationReport
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Serde
|
DataFrames
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..util, ..llmfunction
|
using ..type, ..util, ..llmfunction
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recomme
|
|||||||
extractWineAttributes_2, paraphrase, SQLexecution
|
extractWineAttributes_2, paraphrase, SQLexecution
|
||||||
|
|
||||||
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures,
|
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures,
|
||||||
Base64, Serde, LibPQ, NATS
|
Base64, LibPQ, NATS
|
||||||
using GeneralUtils, SQLLLM
|
using GeneralUtils, SQLLLM
|
||||||
using ..type, ..util
|
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,642 @@
|
|||||||
|
using Test
|
||||||
|
using YiemAgent
|
||||||
|
using YiemAgent.agentCore
|
||||||
|
using YiemAgent.type
|
||||||
|
using JSON
|
||||||
|
|
||||||
|
# Import the function from the private module scope
|
||||||
|
import YiemAgent.agentCore: _extractToolCalls
|
||||||
|
|
||||||
|
@testset "_extractToolCalls" begin
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Format 1: response["message"]["tool_calls"] (LMStudio.jl style) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@testset "single tool call via message format" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"finish_reason" => "tool_calls",
|
||||||
|
"index" => 0,
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"content" => "",
|
||||||
|
"reasoning_content" => "Let me check the weather.",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => "{\"city\":\"Bangkok, Thailand\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_001",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
@test tc_list[1].id == "tc_001"
|
||||||
|
@test tc_list[1].type == "function"
|
||||||
|
@test tc_list[1].arguments["city"] == "Bangkok, Thailand"
|
||||||
|
@test assistant_msg isa assistantMessage
|
||||||
|
@test assistant_msg.role == "assistant"
|
||||||
|
@test assistant_msg.stopReason == "tool_calls"
|
||||||
|
@test length(assistant_msg.content) == 1
|
||||||
|
@test assistant_msg.content[1] isa reasoningContent
|
||||||
|
@test assistant_msg.content[1].text == "Let me check the weather."
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "multiple tool calls via message format" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"content" => "",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => "{\"city\":\"Tokyo, Japan\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_001",
|
||||||
|
),
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => "{\"timezone\":\"Asia/Tokyo\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_002",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 2
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
@test tc_list[1].arguments["city"] == "Tokyo, Japan"
|
||||||
|
@test tc_list[2].name == "getTime"
|
||||||
|
@test tc_list[2].arguments["timezone"] == "Asia/Tokyo"
|
||||||
|
@test assistant_msg.role == "assistant"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with empty arguments string" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "listTools",
|
||||||
|
"arguments" => "{}",
|
||||||
|
),
|
||||||
|
"id" => "tc_empty",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "listTools"
|
||||||
|
@test tc_list[1].arguments == Dict{String,Any}()
|
||||||
|
@test assistant_msg.stopReason == "end_turn"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with missing id falls back to uuid" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => "{\"city\":\"NYC\"}",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test !isempty(tc_list[1].id)
|
||||||
|
@test tc_list[1].name == "getTime"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with non-string arguments (pre-parsed dict)" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => Dict{String,Any}("city" => "London", "units" => "fahrenheit"),
|
||||||
|
),
|
||||||
|
"id" => "tc_parsed",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].arguments["city"] == "London"
|
||||||
|
@test tc_list[1].arguments["units"] == "fahrenheit"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with api/provider/model/usage metadata" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"api" => "openai",
|
||||||
|
"provider" => "anthropic",
|
||||||
|
"model" => "claude-3-opus",
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => "{}",
|
||||||
|
),
|
||||||
|
"id" => "tc_meta",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test assistant_msg.api == "openai"
|
||||||
|
@test assistant_msg.provider == "anthropic"
|
||||||
|
@test assistant_msg.model == "claude-3-opus"
|
||||||
|
end
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- #
|
||||||
|
# Format 2: response.content blocks (OpenAI API style) #
|
||||||
|
# --------------------------------------------------------------- #
|
||||||
|
|
||||||
|
@testset "content blocks with tool_calls" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}("type" => "text", "text" => "Let me check."),
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_calls",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => "{\"city\":\"Paris\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_block_1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
@test tc_list[1].arguments["city"] == "Paris"
|
||||||
|
# text block before tool_calls should be included in content
|
||||||
|
@test length(assistant_msg.content) == 1
|
||||||
|
@test assistant_msg.content[1].text == "Let me check."
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "content blocks with tool_call (single-call format)" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_call",
|
||||||
|
"id" => "tc_single",
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => Dict{String,Any}("timezone" => "Europe/London"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getTime"
|
||||||
|
@test tc_list[1].id == "tc_single"
|
||||||
|
@test tc_list[1].arguments["timezone"] == "Europe/London"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "content blocks with reasoning and text" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}("type" => "reasoning", "text" => "Thinking..."),
|
||||||
|
Dict{String,Any}("type" => "text", "text" => "Here's the answer."),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
@test length(assistant_msg.content) == 2
|
||||||
|
@test assistant_msg.content[1] isa reasoningContent
|
||||||
|
@test assistant_msg.content[1].text == "Thinking..."
|
||||||
|
@test assistant_msg.content[2] isa textContent
|
||||||
|
@test assistant_msg.content[2].text == "Here's the answer."
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "content blocks with text and tool_call (tool_call not in content)" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}("type" => "text", "text" => "Sure, I'll check."),
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_call",
|
||||||
|
"id" => "tc_mix",
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => Dict{String,Any}("city" => "London"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
# text block included, tool_call block excluded from content
|
||||||
|
@test length(assistant_msg.content) == 1
|
||||||
|
@test assistant_msg.content[1].text == "Sure, I'll check."
|
||||||
|
end
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# assistantMessage construction #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@testset "assistantMessage with error_message and errorMessage fallback" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"error_message" => "rate limit",
|
||||||
|
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fail")],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test assistant_msg.errorMessage == "rate limit"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "assistantMessage with usage tracking" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
|
||||||
|
"usage" => llmUsage(100, 50),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test assistant_msg.usage.inputTokens == 100
|
||||||
|
@test assistant_msg.usage.outputTokens == 50
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "assistantMessage with invalid usage defaults to zero" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
|
||||||
|
"usage" => "invalid",
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test assistant_msg.usage.inputTokens == 0
|
||||||
|
@test assistant_msg.usage.outputTokens == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "reasoning_content as textContent" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"reasoning_content" => textContent("internal thought"),
|
||||||
|
"content" => Any[Dict{String,Any}("type" => "text", "text" => "output")],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test length(assistant_msg.content) == 2
|
||||||
|
@test assistant_msg.content[1] isa reasoningContent
|
||||||
|
@test assistant_msg.content[1].text == "internal thought"
|
||||||
|
@test assistant_msg.content[2] isa textContent
|
||||||
|
@test assistant_msg.content[2].text == "output"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# No tool call cases #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@testset "no tool calls found" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}("type" => "text", "text" => "Hello world."),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
@test assistant_msg.stopReason == "end_turn"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "empty message" begin
|
||||||
|
response = Dict{String,Any}()
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
@test assistant_msg.role == "assistant"
|
||||||
|
@test assistant_msg.stopReason == "end_turn"
|
||||||
|
@test length(assistant_msg.content) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "message with empty tool_calls array" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
@test assistant_msg.role == "assistant"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "message field is not a Dict" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => "not a dict",
|
||||||
|
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fallback")],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
@test length(assistant_msg.content) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "Format 1 takes priority over Format 2" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => "{\"city\":\"Format1\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_fmt1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_call",
|
||||||
|
"id" => "tc_fmt2",
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => Dict{String,Any}("city" => "Format2"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Edge cases #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@testset "tool call with null arguments" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => nothing,
|
||||||
|
),
|
||||||
|
"id" => "tc_null",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getTime"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with missing function key" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"id" => "tc_nofunc",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with missing name in function block" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}("arguments" => "{}"),
|
||||||
|
"id" => "tc_noname",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "message format with JSON.Object (JSON.parse result)" begin
|
||||||
|
json_str = JSON.json(Dict(
|
||||||
|
"message" => Dict(
|
||||||
|
"role" => "assistant",
|
||||||
|
"tool_calls" => [
|
||||||
|
Dict(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict("name" => "getWeather", "arguments" => "{\"city\":\"Test\"}"),
|
||||||
|
"id" => "tc_jsonobj",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
parsed = JSON.parse(json_str)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(parsed)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
@test tc_list[1].arguments["city"] == "Test"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool_calls block with mixed content types (text + tool_calls)" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}("type" => "text", "text" => "I'll check both."),
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_calls",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => "{\"city\":\"London\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_mix1",
|
||||||
|
),
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => "{\"timezone\":\"UTC\"}",
|
||||||
|
),
|
||||||
|
"id" => "tc_mix2",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 2
|
||||||
|
@test tc_list[1].name == "getWeather"
|
||||||
|
@test tc_list[2].name == "getTime"
|
||||||
|
@test length(assistant_msg.content) == 1
|
||||||
|
@test assistant_msg.content[1].text == "I'll check both."
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool_call block without arguments field" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_call",
|
||||||
|
"id" => "tc_noargs",
|
||||||
|
"name" => "getTime",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].arguments == Dict{String,Any}()
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool_calls block with empty tool_calls array" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_calls",
|
||||||
|
"tool_calls" => Any[],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool_calls block with non-AbstractDict elements" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_calls",
|
||||||
|
"tool_calls" => Any["not a dict", 42, nothing],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "content field is not a Vector" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => "not a vector",
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(tc_list) == 0
|
||||||
|
@test length(assistant_msg.content) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "Dict-based response with all metadata fields" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"api" => "openai",
|
||||||
|
"provider" => "anthropic",
|
||||||
|
"model" => "claude-3-sonnet",
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "tool_call",
|
||||||
|
"id" => "tc_meta",
|
||||||
|
"name" => "getTime",
|
||||||
|
"arguments" => Dict{String,Any}("city" => "Seoul"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == true
|
||||||
|
@test length(tc_list) == 1
|
||||||
|
@test tc_list[1].name == "getTime"
|
||||||
|
@test tc_list[1].arguments["city"] == "Seoul"
|
||||||
|
@test assistant_msg.api == "openai"
|
||||||
|
@test assistant_msg.provider == "anthropic"
|
||||||
|
@test assistant_msg.model == "claude-3-sonnet"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "default role is assistant" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[Dict{String,Any}("type" => "text", "text" => "no role specified")],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test assistant_msg.role == "assistant"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "tool call with custom role in message format" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"message" => Dict{String,Any}(
|
||||||
|
"role" => "custom_role",
|
||||||
|
"tool_calls" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "function",
|
||||||
|
"function" => Dict{String,Any}(
|
||||||
|
"name" => "getWeather",
|
||||||
|
"arguments" => "{}",
|
||||||
|
),
|
||||||
|
"id" => "tc_role",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test assistant_msg.role == "custom_role"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "image content block handling" begin
|
||||||
|
response = Dict{String,Any}(
|
||||||
|
"content" => Any[
|
||||||
|
Dict{String,Any}(
|
||||||
|
"type" => "image_url",
|
||||||
|
"image_url" => Dict("url" => "data:image/png;base64,abc123"),
|
||||||
|
),
|
||||||
|
Dict{String,Any}("type" => "text", "text" => "What is this?"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
|
||||||
|
@test has_toolcalls == false
|
||||||
|
@test length(assistant_msg.content) == 2
|
||||||
|
@test assistant_msg.content[1] isa textContent
|
||||||
|
@test assistant_msg.content[1].text == ""
|
||||||
|
@test assistant_msg.content[2].text == "What is this?"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
+4
-400
@@ -1,401 +1,5 @@
|
|||||||
using JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64,
|
using Test
|
||||||
NATS, Base.Threads
|
using YiemAgent
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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