Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 189bc2efcf | |||
| 92b3e4081f | |||
| 750eff483b | |||
| 5cc35c78f4 | |||
| ed5415d92a | |||
| 6b3d575ea0 | |||
| da16c80a0a | |||
| 069240912b | |||
| 2aa0d1e9a4 | |||
| 03e1dd7628 | |||
| 70296a3bf2 | |||
| 75b2ce5978 | |||
| d8172a7fbe |
+58
-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 = "0db36d4fb31037ba05065476e6aebaf4cd0e1e8c"
|
||||||
|
|
||||||
[[deps.Accessors]]
|
[[deps.Accessors]]
|
||||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||||
@@ -97,9 +97,9 @@ uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
|||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
|
|
||||||
[[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"]
|
||||||
@@ -146,9 +146,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 +168,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 +208,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 +240,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 +271,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 +294,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 +315,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 +326,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 +406,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 +426,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 +500,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 +527,9 @@ 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.MacroTools]]
|
[[deps.MacroTools]]
|
||||||
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
||||||
@@ -599,9 +587,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 +628,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 +644,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 +676,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 +745,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 +767,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 +828,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 +861,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 +914,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 +932,11 @@ 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.StructTypes]]
|
[[deps.StructTypes]]
|
||||||
deps = ["Dates", "UUIDs"]
|
deps = ["Dates", "UUIDs"]
|
||||||
@@ -970,9 +946,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 +1022,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 +1052,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"]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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"
|
||||||
@@ -34,4 +33,3 @@ 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"
|
|
||||||
|
|||||||
@@ -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 `run_agent(agent, "message")` then `take_response(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 (run_agent, take_response, 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`)
|
||||||
|
|||||||
@@ -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.
|
|
||||||
+5
-9
@@ -3,7 +3,7 @@ module agentCore
|
|||||||
export _agent_loop, OpenAiToUserMessage
|
export _agent_loop, OpenAiToUserMessage
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Serde, Base.Threads
|
DataFrames, Base.Threads
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils
|
using ..type, ..utils
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ function prepareToolCall(
|
|||||||
signal::Union{Nothing, abortSignal},
|
signal::Union{Nothing, abortSignal},
|
||||||
)::Union{preparedToolCall,immediateOutcome}
|
)::Union{preparedToolCall,immediateOutcome}
|
||||||
|
|
||||||
tool = find(t -> t.name == toolCall.name, context.tools)
|
tool = get(context.tools, toolCall.name, nothing)
|
||||||
if tool === nothing
|
if tool === nothing
|
||||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||||
end
|
end
|
||||||
@@ -996,13 +996,9 @@ function executeToolCalls(
|
|||||||
|
|
||||||
hasSequential = false
|
hasSequential = false
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
for t in context.tools
|
t = get(context.tools, tc.name, nothing)
|
||||||
if t.name == tc.name && !t.parallelToolExecute
|
if t !== nothing && !t.parallelToolExecute
|
||||||
hasSequential = true
|
hasSequential = true
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if hasSequential
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
+300
-343
@@ -2,43 +2,6 @@
|
|||||||
|
|
||||||
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).
|
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
|
## Tool Anatomy
|
||||||
|
|
||||||
Each tool has 3 main parts:
|
Each tool has 3 main parts:
|
||||||
@@ -155,220 +118,177 @@ 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.
|
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
|
## Tool Discovery and Lifecycle
|
||||||
|
|
||||||
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`.
|
The agent iterates through tools via a **discover → execute → loop** cycle. Here is the complete flow from the framework author's perspective:
|
||||||
|
|
||||||
### Phase 1: Detect Tool Calls in LLM Response
|
### The Agent Loop
|
||||||
|
|
||||||
After the LLM returns an `assistantMessage`, the loop at `agentCore.jl:220-244` inspects each `content` block:
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# agentCore.jl:217-244
|
# agentCore.jl:175 - _process_message()
|
||||||
has_tool_calls = false
|
while true
|
||||||
|
# 1. Drain messages from inputChannel
|
||||||
|
while isready(agent.inputChannel)
|
||||||
|
raw_msg = take!(agent.inputChannel)
|
||||||
|
user_msg = OpenAiToUserMessage(raw_msg)
|
||||||
|
push!(agent._state.messages, user_msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. Format messages for LLM
|
||||||
|
ctx = agent.prepareContext(agent._state)
|
||||||
|
formatted = agent.formatMsgForLLM(ctx)
|
||||||
|
|
||||||
|
# 3. Call LLM
|
||||||
|
response = agent.llmCall(formatted)
|
||||||
|
|
||||||
|
# 4. Check if LLM used tool calls
|
||||||
|
if has_tool_calls(response.content)
|
||||||
|
# 5. Execute tools, feed results back to LLM, loop
|
||||||
|
else
|
||||||
|
# 6. No tool calls — return final response
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1: Tool Discovery
|
||||||
|
|
||||||
|
Tools are discovered from `agent._state.tools`, which is a `Vector{agentTool}` populated during agent creation:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# Loading tools
|
||||||
|
tools = loadTools("src/tools") # returns Vector{agentTool}
|
||||||
|
|
||||||
|
# Passing to agent
|
||||||
|
agent = yiemAgent(
|
||||||
|
systemPrompt = "...",
|
||||||
|
tools = tools, # ← tools stored in agent._state.tools
|
||||||
|
llmCall = my_llm_call,
|
||||||
|
agentEventSink = my_event_sink,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
When the LLM response contains tool calls, the agent builds an `agentContext` with those tools:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
context = agentContext(
|
||||||
|
agent._state.systemPrompt,
|
||||||
|
agent._state.messages,
|
||||||
|
agent._state.tools, # ← tools available for discovery
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Extract Tool Calls from LLM Response
|
||||||
|
|
||||||
|
The agent inspects the `response.content` blocks for `tool_calls`:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# agentCore.jl:217-245
|
||||||
tool_call_list = agentToolCall[]
|
tool_call_list = agentToolCall[]
|
||||||
|
|
||||||
for content_block in response.content
|
for content_block in response.content
|
||||||
if content_block isa Dict
|
if content_block isa Dict
|
||||||
# OpenAI-style: type == "tool_calls" with array of tool calls
|
if get(content_block, :type, "") == "tool_calls"
|
||||||
if get(content_block, :type, "") == "tool_calls"
|
# OpenAI format: {"type": "tool_calls", "tool_calls": [...]}
|
||||||
for tc_data in get(content_block, :tool_calls, [])
|
for tc_data in get(content_block, :tool_calls, [])
|
||||||
tc = agentToolCall(
|
tc = agentToolCall(
|
||||||
type="function",
|
type = "function",
|
||||||
id=get(tc_data, :id, string(uuid4())),
|
id = get(tc_data, :id, string(uuid4())),
|
||||||
name=get(tc_data, :function, Dict{String,Any}())[:name],
|
name = get(tc_data, :function, Dict())[:name],
|
||||||
arguments=get(tc_data, :function, Dict{String,Any}())[:arguments],
|
arguments = get(tc_data, :function, Dict())[:arguments],
|
||||||
)
|
)
|
||||||
push!(tool_call_list, tc)
|
push!(tool_call_list, tc)
|
||||||
end
|
end
|
||||||
# Alternative style: type == "tool_call" single dict per block
|
elseif get(content_block, :type, "") == "tool_call"
|
||||||
elseif get(content_block, :type, "") == "tool_call"
|
# Alternative format: single tool_call block
|
||||||
tc = agentToolCall(
|
tc = agentToolCall(
|
||||||
type="function",
|
type = "function",
|
||||||
id=get(tc_data, :id, string(uuid4())),
|
id = get(content_block, :id, string(uuid4())),
|
||||||
name=get(tc_data, :name, ""),
|
name = get(content_block, :name, ""),
|
||||||
arguments=get(tc_data, :arguments, Dict{String,Any}()),
|
arguments = get(content_block, :arguments, Dict()),
|
||||||
)
|
)
|
||||||
push!(tool_call_list, tc)
|
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
|
||||||
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
|
||||||
end
|
|
||||||
|
|
||||||
return finalizedOutcome(prep.toolCall, result, isError)
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The hook can:
|
### Step 3: Execute Each Tool Call
|
||||||
- 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.
|
For each tool call, the agent runs through the **prepare → execute → finalize** pipeline:
|
||||||
|
|
||||||
### Phase 6: Emit Events and Create Result Message
|
|
||||||
|
|
||||||
Each call emits `toolExecutionEnd`:
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
|
# agentCore.jl:247-302
|
||||||
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, finalized.result, finalized.isError))
|
if has_tool_calls && length(tool_call_list) > 0
|
||||||
|
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
||||||
|
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, execution_mode)
|
||||||
|
signal = nothing
|
||||||
|
emit = agent.agentEventSink
|
||||||
|
|
||||||
|
# Execute all tool calls (sequential or parallel)
|
||||||
|
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||||
|
|
||||||
|
# Save results to conversation history
|
||||||
|
for tool_result in batch.messages
|
||||||
|
push!(agent._state.messages, tool_result)
|
||||||
|
end
|
||||||
|
|
||||||
|
# If any tool requested termination, break the loop
|
||||||
|
if batch.terminate
|
||||||
|
final_response = build_final_response(batch)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
# Otherwise, loop back to step 2 (format + call LLM again)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
Then creates the `toolResultMessage` for conversation history (`agentCore.jl:373-379`):
|
### Step 4: The Per-Call Pipeline
|
||||||
|
|
||||||
```julia
|
Each tool call goes through three phases:
|
||||||
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
|
│ PREPARE → prepareToolCall() │
|
||||||
└── tool_call_list :: Vector{agentToolCall}
|
│ │
|
||||||
└── phase 2: dispatch to sequential/parallel
|
│ 1. Find tool by name in context.tools │
|
||||||
└── phase 3: prepareToolCall
|
│ 2. Transform args via tool.prepareArguments (if defined) │
|
||||||
└── preparedToolCall or immediateOutcome
|
│ 3. Validate via tool.validateRequiredArgs (or default) │
|
||||||
└── phase 4: executePreparedToolCall
|
│ 4. Run beforeToolCall hook (if defined) │
|
||||||
└── executedOutcome
|
│ └── on any failure → return immediateOutcome (skip execution) │
|
||||||
└── phase 5: finalizeExecutedToolCall
|
│ └── success → return preparedToolCall │
|
||||||
└── finalizedOutcome
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
└── phase 6: createToolResultMessage
|
│ EXECUTE → executePreparedToolCall() │
|
||||||
└── toolResultMessage
|
│ │
|
||||||
└── phase 7: agentToolCallBatch
|
│ 1. emit toolExecutionStart event │
|
||||||
└── pushed to agent._state.messages
|
│ 2. call tool.execute(toolCallId, args, signal, onPartialResult)│
|
||||||
└── loop back to LLM
|
│ 3. wait for all pending update events │
|
||||||
|
│ └── on error → return executedOutcome(isError=true) │
|
||||||
|
│ └── success → return executedOutcome(isError=false) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ FINALIZE → finalizeExecutedToolCall() │
|
||||||
|
│ │
|
||||||
|
│ 1. Run afterToolCall hook (if defined) │
|
||||||
|
│ - can mutate content, details, usage, terminate, isError │
|
||||||
|
│ 2. emit toolExecutionEnd event │
|
||||||
|
│ 3. createToolResultMessage → adds to conversation history │
|
||||||
|
│ └── return finalizedOutcome │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Step 5: Feed Results Back to LLM
|
||||||
|
|
||||||
|
Tool results are added to `agent._state.messages` as `toolResultMessage` objects. On the next loop iteration, `formatMsgForLLM()` converts them to OpenAI format and the LLM receives the results:
|
||||||
|
|
||||||
|
```
|
||||||
|
Conversation history after tool execution:
|
||||||
|
[system] "You are a helpful assistant."
|
||||||
|
[user] "What's the weather in Tokyo?"
|
||||||
|
[assistant] (tool_calls: getWeather(city="Tokyo"))
|
||||||
|
[tool] tool_call_id="call_1", tool_name="getWeather", content="Weather in Tokyo: Sunny, 22°C"
|
||||||
|
```
|
||||||
|
|
||||||
|
The LLM then decides: call another tool, or return a final text answer.
|
||||||
|
|
||||||
## Execution Modes
|
## Execution Modes
|
||||||
|
|
||||||
### Sequential
|
### Sequential
|
||||||
@@ -424,124 +344,175 @@ tool = getTool() # from your tool module
|
|||||||
registerTool(tool)
|
registerTool(tool)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Tools with an Agent
|
## Complete Lifecycle Example
|
||||||
|
|
||||||
Loading tools only registers them — you must pass them to the `yiemAgent` and provide an `llmCall` function. Here is the complete flow:
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
using .YiemAgent
|
# ─── USER SENDS MESSAGE ───────────────────────────────────────────
|
||||||
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?")
|
run_agent(agent, "What's the weather in Tokyo?")
|
||||||
|
|
||||||
|
# ─── LOOP ITERATION 1 ─────────────────────────────────────────────
|
||||||
|
# Agent formats messages and calls LLM
|
||||||
|
formatted = agent.formatMsgForLLM(agent.prepareContext(agent._state))
|
||||||
|
response = agent.llmCall(formatted)
|
||||||
|
# LLM returns: {"content": [{"type": "tool_calls", "tool_calls": [{"name": "getWeather", "arguments": {"city": "Tokyo"}}]}]}
|
||||||
|
|
||||||
|
# Agent extracts tool call, builds context
|
||||||
|
context = agentContext(systemPrompt, messages, agent._state.tools)
|
||||||
|
tool_call_list = [agentToolCall("call_1", "getWeather", Dict("city" => "Tokyo"))]
|
||||||
|
|
||||||
|
# PREPARE: find tool, validate args
|
||||||
|
tool = find(t -> t.name == "getWeather", context.tools) # found!
|
||||||
|
validateRequiredArgs(Dict("city" => "Tokyo"), tool.inputSchema) # passes
|
||||||
|
beforeToolCall_hook(agentMsgCtx, nothing) # nil, skipped
|
||||||
|
|
||||||
|
# EXECUTE: call tool.execute()
|
||||||
|
result = tool.execute("call_1", Dict("city" => "Tokyo"), nothing, onPartialResult)
|
||||||
|
# Returns: agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], Dict(), nothing, false)
|
||||||
|
|
||||||
|
# FINALIZE: afterToolCall hook, emit events
|
||||||
|
finalized = finalizedOutcome(tc, result, false)
|
||||||
|
emit(toolExecEndEvent("call_1", "getWeather", result, false))
|
||||||
|
msg = createToolResultMessage(finalized) # toolResultMessage for conversation history
|
||||||
|
|
||||||
|
# Add result to conversation
|
||||||
|
push!(agent._state.messages, msg)
|
||||||
|
# Messages now: [user: "What's the weather?", assistant: {tool_calls: getWeather}, tool: "Sunny, 22°C"]
|
||||||
|
|
||||||
|
# ─── LOOP ITERATION 2 ─────────────────────────────────────────────
|
||||||
|
# LLM called again with tool result included
|
||||||
|
formatted = agent.formatMsgForLLM(agent.prepareContext(agent._state))
|
||||||
|
response = agent.llmCall(formatted)
|
||||||
|
# LLM returns: {"content": [{"type": "text", "text": "The weather in Tokyo is sunny, 22°C."}]}
|
||||||
|
|
||||||
|
# No tool calls detected → break loop, return final response
|
||||||
|
return assistantMessage(content=[textContent("The weather in Tokyo is sunny, 22°C.")], ...)
|
||||||
|
|
||||||
|
# ─── USER RECEIVES RESPONSE ───────────────────────────────────────
|
||||||
response = take_response(agent)
|
response = take_response(agent)
|
||||||
|
|
||||||
# response.content contains the LLM's reply (with tool results if applicable)
|
|
||||||
println(response.content)
|
println(response.content)
|
||||||
|
# => "[textContent(\"The weather in Tokyo is sunny, 22°C.\")]"
|
||||||
# 6. When done, stop the agent
|
|
||||||
stop_agent(agent)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### How It Works
|
## Self-Modifying Tools
|
||||||
|
|
||||||
1. **User sends a message** via `run_agent(agent, "What's the weather in Tokyo?")`. The message goes into `inputChannel`.
|
The framework includes tools that allow the agent to create new tools at runtime.
|
||||||
|
|
||||||
2. **Agent loop** (`_agent_loop`) picks it up, converts it to a `userMessage`, and adds it to `agent._state.messages`.
|
### `writeTool` — Create New Tool Files
|
||||||
|
|
||||||
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`).
|
`writeTool` is a **file writer**, not a code generator. The LLM provides the tool logic as `executeCode` (the actual Julia code), and `writeTool` wraps it in the required boilerplate.
|
||||||
|
|
||||||
4. **If the LLM uses a tool**, it returns a response with `tool_call` content blocks. The agent:
|
**How it works:**
|
||||||
- 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.
|
The LLM constructs `writeTool` with:
|
||||||
|
- **`executeCode`** — the actual tool logic (Julia code body, NOT wrapped in a function)
|
||||||
|
- **`name`, `label`, `description`** — tool metadata
|
||||||
|
- **`inputSchema`** — parameter schema in MCP format
|
||||||
|
- **`validateCode`, `prepareCode`** (optional) — custom validation/preparation logic
|
||||||
|
|
||||||
6. **Final response** is sent to `outputChannel` — retrieve it with `take_response(agent)`.
|
`writeTool` produces `src/tools/<name>.jl` by:
|
||||||
|
1. Converting the `inputSchema` Dict into a Julia `Dict{String,Any}(...)` string literal
|
||||||
|
2. Indenting `executeCode` with 4 spaces
|
||||||
|
3. Wrapping it inside a `function executeTool(...)::agentToolResult ... end` template
|
||||||
|
4. Appending the `getTool()` definition that returns an `agentTool` struct
|
||||||
|
5. Writing the combined string to disk
|
||||||
|
|
||||||
### Minimal Working Example
|
**Workflow:**
|
||||||
|
|
||||||
|
```
|
||||||
|
LLM decides: "Need a searchWine tool. I'll provide the logic."
|
||||||
|
|
||||||
|
LLM calls writeTool:
|
||||||
|
name: "searchWine"
|
||||||
|
executeCode: "query = args[\"query\"]\nresult = search(query)\nreturn ..."
|
||||||
|
|
||||||
|
writeTool wraps it → src/tools/searchWine.jl:
|
||||||
|
function executeTool(...)::agentToolResult
|
||||||
|
query = args["query"] ← LLM code (indented 4 spaces)
|
||||||
|
result = search(query)
|
||||||
|
return agentToolResult(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function getTool()::agentTool
|
||||||
|
return agentTool(name="searchWine", ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
Restart → loadTools("src/tools") loads searchWine.jl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example specification:**
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
using .YiemAgent
|
Dict(
|
||||||
using .toolRegistry
|
"name" => "searchWine",
|
||||||
|
"label" => "Wine Search",
|
||||||
# Load tools
|
"description" => "Search a wine database by name, region, or variety",
|
||||||
tools = loadTools("src/tools")
|
"inputSchema" => Dict(
|
||||||
|
"type" => "object",
|
||||||
# Mock LLM that echoes back a tool call, then a text response
|
"properties" => Dict(
|
||||||
call_count = 0
|
"query" => Dict("type" => "string", "description" => "Search query"),
|
||||||
function mock_llm_call(messages::Dict)::assistantMessage
|
"maxResults" => Dict("type" => "integer", "default" => 10)
|
||||||
global call_count += 1
|
),
|
||||||
if call_count == 1
|
"required" => ["query"]
|
||||||
# First call: LLM decides to use getWeather
|
),
|
||||||
return assistantMessage(
|
"executeCode" => """
|
||||||
content=[
|
query = args["query"]
|
||||||
Dict("type" => "tool_calls",
|
max_results = get(args, "maxResults", 10)
|
||||||
"tool_calls" => [Dict("id" => "call_1", "name" => "getWeather",
|
# Perform search logic here
|
||||||
"arguments" => Dict("city" => "Tokyo"))])
|
result = "Found 3 wines matching: $query"
|
||||||
],
|
return agentToolResult([textContent(result)], Dict{Any,Any}(), nothing, false)
|
||||||
model = "mock",
|
""",
|
||||||
usage = llmUsage(0, 0)
|
"parallel" => false
|
||||||
)
|
|
||||||
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
|
**Optional hooks:**
|
||||||
run_agent(agent, "What's the weather in Tokyo?")
|
|
||||||
response = take_response(agent)
|
|
||||||
|
|
||||||
stop_agent(agent)
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `validateCode` | Custom validation Julia code (runs before execute). Return `nothing` to pass, or an error `String` to fail. |
|
||||||
|
| `prepareCode` | Argument preparation code (runs before validation). Return modified args dict. |
|
||||||
|
|
||||||
|
### `listTools` — Discover Available Tools
|
||||||
|
|
||||||
|
Returns all registered tools. Primarily useful for **collision detection** before creating a new tool via `writeTool` — the LLM checks existing names before picking a unique one.
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# Result from listTools:
|
||||||
|
# Available tools:
|
||||||
|
# - getWeather: Weather Lookup — Fetch current weather and forecast for a given city.
|
||||||
|
# - getTime: Time Lookup — Get current local time for a timezone or city.
|
||||||
|
# - writeTool: Create Tool — Generate new tool files...
|
||||||
|
# - listTools: List Tools — List all available tools with their names and labels...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Self-Tooling Example
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "I need to search for wines. Do you have a tool for that?"
|
||||||
|
|
||||||
|
# ─── LOOP: Agent realizes no wine search tool exists ─────────────────
|
||||||
|
|
||||||
|
# LLM generates the tool logic and calls writeTool to write it to disk
|
||||||
|
|
||||||
|
[Tool Call] writeTool(name="searchWine", label="Wine Search",
|
||||||
|
description="Search a wine database by name, region, or variety",
|
||||||
|
inputSchema={...},
|
||||||
|
executeCode="query = args[\"query\"]\nresult = \"Found wines...\"\nreturn agentToolResult([textContent(result)], ...)")
|
||||||
|
|
||||||
|
# writeTool generates src/tools/searchWine.jl
|
||||||
|
|
||||||
|
# ─── SYSTEM RESTARTS ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# loadTools("src/tools") loads searchWine.jl alongside all other tools
|
||||||
|
|
||||||
|
# ─── Agent calls the new tool ─────────────────────────────────────────
|
||||||
|
|
||||||
|
[Tool Call] searchWine(query="cabernet", maxResults=5)
|
||||||
|
# Result: "Found 5 cabernet wines..."
|
||||||
|
|
||||||
|
# ─── Final response ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
"The search found 5 cabernet wines: ..."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Available Tools
|
## Available Tools
|
||||||
@@ -550,19 +521,5 @@ stop_agent(agent)
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `getWeather` | Fetch weather for a city | Default (JSON Schema required) |
|
| `getWeather` | Fetch weather for a city | Default (JSON Schema required) |
|
||||||
| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) |
|
| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) |
|
||||||
|
| `writeTool` | Create a new Julia tool module at runtime | Built-in (name + schema validation) |
|
||||||
## Example: Error Flow
|
| `listTools` | List all available tools with descriptions | None (no arguments) |
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
+3
-13
@@ -39,26 +39,16 @@ 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 executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||||
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
|
||||||
@@ -76,8 +66,8 @@ 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" => []
|
||||||
),
|
),
|
||||||
|
|||||||
+3
-17
@@ -1,31 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
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 executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
units = get(args, "units", "celsius")
|
units = get(args, "units", "celsius")
|
||||||
|
|
||||||
# Simulate weather fetch — replace with actual API call
|
|
||||||
# You can call onPartialResult() here for streaming progress updates:
|
|
||||||
# onPartialResult(Dict("status" => "Fetching weather data..."))
|
|
||||||
# onPartialResult(Dict("status" => "Processing..."))
|
|
||||||
|
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
temp = units == "fahrenheit" ? "72" : "22"
|
||||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -44,7 +30,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => ["city"]
|
"required" => ["city"]
|
||||||
),
|
),
|
||||||
execute = executeTool, # reference the function defined above
|
execute = executeTool,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
|
|||||||
+108
-54
@@ -1,12 +1,56 @@
|
|||||||
module toolRegistry
|
module toolRegistry
|
||||||
|
|
||||||
export loadTools, registerTool, getTools, listTools, clearTools
|
export loadTools, registerTool, getTools, clearTools
|
||||||
|
|
||||||
|
using Dates
|
||||||
|
using JSON, DataStructures
|
||||||
using ..type
|
using ..type
|
||||||
|
|
||||||
# Global registry — populated at runtime by loadTools() or registerTool()
|
# Global registry — populated at runtime by loadTools() or registerTool()
|
||||||
const _registry = Vector{agentTool}()
|
const _registry = Vector{agentTool}()
|
||||||
|
|
||||||
|
# Module references — kept alive to prevent GC of tool code that closures depend on
|
||||||
|
const _tool_modules = Vector{Module}()
|
||||||
|
|
||||||
|
# Auto-register the built-in listTools tool
|
||||||
|
function __init__()
|
||||||
|
registerTool(_listTool())
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
List tool definition — lets the agent query available tools for collision detection
|
||||||
|
when creating new tools via writeTool.
|
||||||
|
"""
|
||||||
|
function _listTool()::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()
|
||||||
|
if isempty(tools)
|
||||||
|
result_text = "No tools registered."
|
||||||
|
else
|
||||||
|
lines = String["- $(t.name): $(t.label) — $(t.description)" for 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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Load all tool modules from a directory.
|
Load all tool modules from a directory.
|
||||||
|
|
||||||
@@ -14,33 +58,27 @@ Scans `dir` for `.jl` files. Each file must define a function named
|
|||||||
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
`getTool()::agentTool`. Files are sorted alphabetically so tool
|
||||||
registration order is deterministic.
|
registration order is deterministic.
|
||||||
|
|
||||||
|
Each `.jl` file is loaded into its own **submodule** so that all functions
|
||||||
|
defined in the file (`validateRequiredArgs`, `prepareArguments`, `executeTool`,
|
||||||
|
and any helper functions) are namespaced and never collide with other tools.
|
||||||
|
|
||||||
# Tool file format
|
# Tool file format
|
||||||
Each `.jl` file defines one function `getTool()` that returns an `agentTool`:
|
Each `.jl` file defines one function `getTool()` that returns an `agentTool`.
|
||||||
|
Inside the file you can freely define as many helper functions as you need —
|
||||||
|
they will all be scoped under the tool's submodule.
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# src/tools/getWeather.jl
|
# src/tools/getWeather.jl
|
||||||
|
|
||||||
|
# These are namespaced — no collision with getTime.validateRequiredArgs, etc.
|
||||||
|
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||||
|
...
|
||||||
|
end
|
||||||
|
|
||||||
function getTool()::agentTool
|
function getTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "getWeather",
|
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
|
end
|
||||||
```
|
```
|
||||||
@@ -54,42 +92,68 @@ end
|
|||||||
# Errors
|
# Errors
|
||||||
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
- Throws `ArgumentError` if a tool file does not define a `getTool` function
|
||||||
"""
|
"""
|
||||||
function loadTools(dir::String)::Vector{agentTool}
|
function loadTools(dir::String)::OrderedDict{String, agentTool}
|
||||||
if !isdir(dir)
|
if !isdir(dir)
|
||||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
throw(ArgumentError("Tool directory does not exist: $dir"))
|
||||||
end
|
end
|
||||||
|
|
||||||
tools = agentTool[]
|
tools = OrderedDict{String, agentTool}()
|
||||||
jl_files = filter(f -> endswith(f, ".jl"), readdir(dir))
|
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
|
||||||
sort!(jl_files)
|
sort!(jl_files)
|
||||||
|
|
||||||
for filename in jl_files
|
for filename in jl_files
|
||||||
filepath = joinpath(dir, filename)
|
filepath = joinpath(dir, filename)
|
||||||
println("[toolRegistry] Loading tool from: $filepath")
|
|
||||||
|
|
||||||
# Include the file in the current module scope so all types resolve
|
# Derive a unique module name from the filename only (not full path).
|
||||||
# (agentTool, textContent, agentToolResult, etc. are all available)
|
# e.g. "getWeather.jl" -> "_tool_getWeather"
|
||||||
include(filepath)
|
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
|
||||||
|
|
||||||
# Validate that getTool was defined (include() places it in current module scope)
|
# Build the complete module as a string and eval the parsed code.
|
||||||
if !isdefined(@__MODULE__, :getTool)
|
# Julia does not allow `module ... end` inside eval(quote ...),
|
||||||
throw(ArgumentError(
|
# and constructing the module AST by hand is fragile.
|
||||||
"Tool file $(filepath) does not define a `getTool()` function. " *
|
# Instead, we generate the full module source as a string,
|
||||||
"Each tool file must define: function getTool()::agentTool ... end"
|
# parse it, and eval the resulting expression.
|
||||||
))
|
# Also import Dates, UUIDs, DataStructures, JSON — common dependencies
|
||||||
|
# that tool files use (and that the ..type module transitively uses).
|
||||||
|
file_content = read(filepath, String)
|
||||||
|
module_code = """
|
||||||
|
module $(mod_name)
|
||||||
|
using ..type
|
||||||
|
using Dates, UUIDs, DataStructures, JSON
|
||||||
|
$(file_content)
|
||||||
end
|
end
|
||||||
|
"""
|
||||||
|
mod = eval(Meta.parse(module_code))
|
||||||
|
|
||||||
# Call getTool() — it runs in current scope where types are visible
|
# Call getTool() via Core.eval in the submodule's scope.
|
||||||
tool = getTool()
|
# This evaluates getTool() entirely within the new module's world,
|
||||||
if !(tool isa agentTool)
|
# completely avoiding world-age issues — no invokelatest needed.
|
||||||
throw(ArgumentError(
|
# Note: all uses of `tool` must be inside the `try` block because
|
||||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
# Julia 1.12's SSA form doesn't track `tool` as definitely assigned
|
||||||
))
|
# after a `try-catch` where it's only assigned inside `try`.
|
||||||
|
try
|
||||||
|
tool = Core.eval(mod, :(getTool()))
|
||||||
|
if !(tool isa agentTool)
|
||||||
|
throw(ArgumentError(
|
||||||
|
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
||||||
|
))
|
||||||
|
end
|
||||||
|
# Keep module reference alive — closures in the agentTool (execute,
|
||||||
|
# validateRequiredArgs, prepareArguments) may reference module-scoped
|
||||||
|
# functions. Without this, GC could collect the module.
|
||||||
|
push!(_tool_modules, mod)
|
||||||
|
push!(_registry, tool)
|
||||||
|
tools[tool.name] = tool
|
||||||
|
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
||||||
|
catch e
|
||||||
|
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
|
||||||
|
throw(ArgumentError(
|
||||||
|
"Tool file $(filepath) does not define a `getTool()` function in module $(mod_name). " *
|
||||||
|
"Each tool file must define: function getTool()::agentTool ... end"
|
||||||
|
))
|
||||||
|
end
|
||||||
|
rethrow(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
push!(_registry, tool)
|
|
||||||
push!(tools, tool)
|
|
||||||
println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
@@ -120,16 +184,6 @@ function getTools()::Vector{agentTool}
|
|||||||
return deepcopy(_registry)
|
return deepcopy(_registry)
|
||||||
end
|
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.
|
Clear all registered tools from the global registry.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""
|
||||||
|
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, restart the agent so `loadTools("src/tools")` picks
|
||||||
|
up the new file. The new tool is immediately available.
|
||||||
|
|
||||||
|
# Example
|
||||||
|
|
||||||
|
1. Agent calls writeTool with a spec for a "searchWine" tool
|
||||||
|
2. writeTool generates src/tools/searchWine.jl
|
||||||
|
3. Restart agent — loadTools() picks up the new file
|
||||||
|
4. Agent calls searchWine with args
|
||||||
|
|
||||||
|
# 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 `getTool()` 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 getTool()::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::Function) -> 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 is loaded on next agent restart via loadTools()
|
||||||
|
write(filepath, tool_code)
|
||||||
|
|
||||||
|
onPartialResult(Dict("status" => "Done"))
|
||||||
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")],
|
||||||
|
Dict{Any,Any}(
|
||||||
|
"file" => filepath,
|
||||||
|
"name" => tool_name,
|
||||||
|
"label" => tool_label,
|
||||||
|
"description" => tool_description,
|
||||||
|
),
|
||||||
|
nothing, false
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
prepareArguments = nothing,
|
||||||
|
validateRequiredArgs = nothing,
|
||||||
|
parallelToolExecute = false
|
||||||
|
)
|
||||||
|
end
|
||||||
+28
-17
@@ -9,7 +9,7 @@
|
|||||||
# Message types
|
# Message types
|
||||||
userMessage, assistantMessage, toolResultMessage,
|
userMessage, 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
|
||||||
@@ -248,7 +248,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,
|
||||||
@@ -269,6 +269,17 @@ struct agentTool # A tool available to the agent
|
|||||||
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::Function, 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 +291,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{Dict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
|
||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A new `agentContext` instance
|
- A new `agentContext` instance
|
||||||
@@ -288,7 +299,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{Dict{String, agentTool}, Nothing} # Available tools keyed by name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -297,9 +308,9 @@ 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::llmModel # 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}
|
||||||
@@ -318,7 +329,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,13 +338,13 @@ 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=llmModel{String}("", "", "unknown", "unknown", "", false, String[],
|
||||||
tools::Vector{agentTool}=agentTool[],
|
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||||
|
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||||
messages::Vector{agentMessage}=agentMessage[],
|
messages::Vector{agentMessage}=agentMessage[],
|
||||||
)
|
)
|
||||||
agentState(
|
agentState(
|
||||||
@@ -384,13 +395,13 @@ end
|
|||||||
Configuration for the agent tool execution loop.
|
Configuration for the agent tool execution loop.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `tools::Vector{agentTool}`: Available tools
|
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name
|
||||||
- `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}
|
tools::OrderedDict{String, agentTool}
|
||||||
beforeToolCall::Union{Function, Nothing}
|
beforeToolCall::Union{Function, Nothing}
|
||||||
afterToolCall::Union{Function, Nothing}
|
afterToolCall::Union{Function, Nothing}
|
||||||
toolExecution::String
|
toolExecution::String
|
||||||
@@ -569,7 +580,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
|||||||
# Keyword Arguments
|
# Keyword Arguments
|
||||||
- `systemPrompt::String`: System prompt for the agent
|
- `systemPrompt::String`: System prompt for the agent
|
||||||
- `model`: LLM model to use
|
- `model`: LLM model to use
|
||||||
- `tools::Vector{agentTool}`: Available tools (default: empty)
|
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
|
||||||
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
|
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
|
||||||
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
|
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
|
||||||
- `llmCall::Function`: Function to invoke the LLM (required)
|
- `llmCall::Function`: Function to invoke the LLM (required)
|
||||||
@@ -588,14 +599,14 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
|||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
```julia
|
```julia
|
||||||
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model)
|
julia> tools = loadTools("src/tools")
|
||||||
|
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=...)
|
||||||
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
|
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
function yiemAgent(
|
function yiemAgent(
|
||||||
; systemPrompt::String="You are helpful assistant.",
|
; systemPrompt::String="You are helpful assistant.",
|
||||||
model=nothing,
|
model=nothing,
|
||||||
tools::Vector{agentTool}=agentTool[],
|
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
|
||||||
messages::Vector{agentMessage}=agentMessage[],
|
messages::Vector{agentMessage}=agentMessage[],
|
||||||
prepareContext::Union{Function, Nothing}=nothing,
|
prepareContext::Union{Function, Nothing}=nothing,
|
||||||
formatMsgForLLM::Function=defaultformatMsgForLLM,
|
formatMsgForLLM::Function=defaultformatMsgForLLM,
|
||||||
|
|||||||
+2
-2
@@ -113,7 +113,7 @@ function prepareContext(state::agentState)::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
|
||||||
@@ -375,7 +375,7 @@ function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{S
|
|||||||
end
|
end
|
||||||
|
|
||||||
return prepared.arguments
|
return prepared.arguments
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,136 @@
|
|||||||
|
using Test
|
||||||
|
using YiemAgent
|
||||||
|
using YiemAgent.toolRegistry
|
||||||
|
using YiemAgent.type
|
||||||
|
|
||||||
|
# Path to the real tools directory
|
||||||
|
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
||||||
|
|
||||||
|
@testset "loadTools" begin
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 1. loadTools throws on non-existent directory #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@test_throws ArgumentError loadTools("/nonexistent/dir/that/does/not/exist")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 2. loadTools throws if a .jl file does not define getTool() #
|
||||||
|
# Must run BEFORE any other loadTools call (getTool binding #
|
||||||
|
# persists in module scope after include()). #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
bad_dir = mktempdir()
|
||||||
|
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
||||||
|
@test_throws ArgumentError loadTools(bad_dir)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 3. loadTools loads actual tool files from src/tools/ #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
loaded = loadTools(TOOLS_DIR)
|
||||||
|
@test !isempty(loaded)
|
||||||
|
@test length(loaded) == 3
|
||||||
|
|
||||||
|
names = [k for k in keys(loaded)]
|
||||||
|
@test "getTime" in names
|
||||||
|
@test "getWeather" in names
|
||||||
|
@test "writeTool" in names
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 4. loadTools returns tools sorted alphabetically by filename #
|
||||||
|
# (getTime.jl < getWeather.jl < writeTool.jl) #
|
||||||
|
# because 'T' < 'W' in ASCII #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@test collect(keys(loaded))[1] == "getTime"
|
||||||
|
@test collect(keys(loaded))[2] == "getWeather"
|
||||||
|
@test collect(keys(loaded))[3] == "writeTool"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 5. 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"]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 6. 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°F", result_w2.content[1].text)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 7. getTools / registerTool / clearTools #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
registry_tools = getTools()
|
||||||
|
@test !isempty(registry_tools)
|
||||||
|
@test any(t -> t.name == "getTime", registry_tools)
|
||||||
|
@test any(t -> t.name == "getWeather", registry_tools)
|
||||||
|
|
||||||
|
clearTools()
|
||||||
|
@test isempty(getTools())
|
||||||
|
|
||||||
|
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(test_tool)
|
||||||
|
reg = getTools()
|
||||||
|
@test any(t -> t.name == "manualTool", reg)
|
||||||
|
@test count(t -> t.name == "manualTool", reg) == 1
|
||||||
|
@test reg[1].parallelToolExecute == true
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 8. getTools returns deep copy (mutations don't affect registry) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
copy1 = getTools()
|
||||||
|
copy2 = getTools()
|
||||||
|
@test copy1 !== copy2
|
||||||
|
empty!(copy1)
|
||||||
|
@test !isempty(getTools())
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user