Compare commits

...

20 Commits

Author SHA1 Message Date
ton c13aeb3a74 Merge pull request 'V0.8.0 verify tool use' (#43) from v0.8.0-verify_tool_use into v0.8.0
Reviewed-on: #43
2026-08-10 13:10:57 +00:00
ton 287704778f update 2026-08-10 20:07:15 +07:00
ton a9fa23f01b update 2026-08-10 19:19:58 +07:00
ton c5cb18f0f1 update 2026-08-10 16:10:04 +07:00
ton c78f4b023d update 2026-08-10 14:55:09 +07:00
ton 1b69f69c7d update 2026-08-10 13:33:45 +07:00
ton 3891099eaa update readme 2026-08-10 10:39:30 +07:00
ton 268d340e2f Merge pull request 'V0.8.0 use tool module' (#42) from v0.8.0-use_tool_module into v0.8.0
Reviewed-on: #42
2026-08-10 02:48:25 +00:00
ton 189bc2efcf update 2026-08-10 09:43:35 +07:00
ton 92b3e4081f update 2026-08-09 22:50:02 +07:00
ton 750eff483b update 2026-08-09 22:08:04 +07:00
ton 5cc35c78f4 Merge pull request 'V0.8.0 add tools' (#41) from v0.8.0-add_tools into v0.8.0
Reviewed-on: #41
2026-08-09 11:47:02 +00:00
ton ed5415d92a update 2026-08-09 08:45:35 +07:00
ton 6b3d575ea0 update 2026-08-09 06:50:37 +07:00
ton da16c80a0a update 2026-08-09 06:27:48 +07:00
ton 069240912b update 2026-08-09 04:45:31 +07:00
ton 2aa0d1e9a4 update 2026-08-08 19:44:06 +07:00
ton 03e1dd7628 update 2026-08-08 18:49:51 +07:00
ton 70296a3bf2 update 2026-08-08 12:21:52 +07:00
ton 75b2ce5978 update 2026-08-08 12:11:53 +07:00
20 changed files with 2408 additions and 979 deletions
+58 -94
View File
@@ -2,7 +2,7 @@
julia_version = "1.12.6"
manifest_format = "2.0"
project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6"
project_hash = "0db36d4fb31037ba05065476e6aebaf4cd0e1e8c"
[[deps.Accessors]]
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
@@ -97,9 +97,9 @@ uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.8"
[[deps.CommonSolve]]
git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662"
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
version = "0.2.11"
version = "0.2.13"
[[deps.Compat]]
deps = ["TOML", "UUIDs"]
@@ -146,9 +146,9 @@ version = "1.6.0"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
[[deps.Crayons]]
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
git-tree-sha1 = "54b76cbb40d9a0f5368c880725b2f141da77c94f"
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
version = "4.1.1"
version = "4.2.0"
[[deps.DBInterface]]
git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b"
@@ -168,9 +168,9 @@ version = "1.8.2"
[[deps.DataStructures]]
deps = ["OrderedCollections"]
git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e"
git-tree-sha1 = "b0bc6d2cad1fed8b7fd59a1551a991cb3d2809e6"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.19.5"
version = "0.19.6"
[[deps.DataValueInterfaces]]
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
@@ -208,9 +208,9 @@ version = "1.11.0"
[[deps.Distributions]]
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
git-tree-sha1 = "cd3c5ac74cd3923c8945c6a81518c46abd0e73a3"
git-tree-sha1 = "d2facc77c08c1c2bfb1a77c148edd05b3db5410b"
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
version = "0.25.129"
version = "0.25.130"
[deps.Distributions.extensions]
DistributionsChainRulesCoreExt = "ChainRulesCore"
@@ -240,15 +240,9 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
version = "1.0.7"
[[deps.ExprTools]]
git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec"
git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4"
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
version = "0.1.10"
[[deps.EzXML]]
deps = ["Printf", "XML2_jll"]
git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3"
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
version = "1.2.3"
version = "0.1.11"
[[deps.FileIO]]
deps = ["Pkg", "Requires", "UUIDs"]
@@ -277,9 +271,9 @@ version = "1.11.0"
[[deps.FillArrays]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3"
git-tree-sha1 = "5bad39456d9f0166184fce2248783dd9862645c1"
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
version = "1.16.0"
version = "1.17.0"
weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"]
[deps.FillArrays.extensions]
@@ -300,11 +294,11 @@ version = "1.1.0"
[[deps.GeneralUtils]]
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"]
git-tree-sha1 = "93293126d24d3929ef6a5067f347bc28c6582c71"
git-tree-sha1 = "129b8fa1bf3bf6d8c0a080f39db09b9b986ef8de"
repo-rev = "main"
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
version = "0.5.10"
version = "0.5.11"
[[deps.Graphs]]
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
@@ -321,9 +315,9 @@ version = "1.14.0"
[[deps.HTTP]]
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
git-tree-sha1 = "c2c808326222b6dc4bec295a83b55f79aeec98e0"
git-tree-sha1 = "0a58fbbdee93d132a2fb1159b7f5e1b5c2465e71"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "2.5.5"
version = "2.6.4"
[[deps.HashArrayMappedTries]]
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
@@ -332,9 +326,9 @@ version = "0.2.0"
[[deps.HypergeometricFunctions]]
deps = ["Gamma", "LinearAlgebra"]
git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10"
git-tree-sha1 = "31bb6c92405c084617facc1d7ed9eb6c402d061e"
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
version = "0.3.29"
version = "0.3.30"
[[deps.ICU_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
@@ -412,9 +406,9 @@ version = "1.8.0"
[[deps.JSON]]
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211"
git-tree-sha1 = "65979512c25a0727f050e6e4be40f0fd9ec893f7"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "1.6.1"
version = "1.7.0"
weakdeps = ["ArrowTypes"]
[deps.JSON.extensions]
@@ -432,9 +426,9 @@ weakdeps = ["ArrowTypes"]
[[deps.JuliaInterpreter]]
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006"
git-tree-sha1 = "c3d401f110454b4ea24a76be33f6ee0d7d385103"
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
version = "0.10.12"
version = "0.11.4"
[[deps.JuliaSyntaxHighlighting]]
deps = ["StyledStrings"]
@@ -506,12 +500,6 @@ version = "1.11.3+1"
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
version = "1.11.0"
[[deps.Libiconv_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809"
uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531"
version = "1.18.0+0"
[[deps.LinearAlgebra]]
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -539,9 +527,9 @@ version = "1.11.0"
[[deps.LoweredCodeUtils]]
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f"
git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
version = "3.6.2"
version = "3.8.0"
[[deps.MacroTools]]
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
@@ -599,9 +587,9 @@ version = "0.1.1"
[[deps.NanoDates]]
deps = ["Dates", "Parsers"]
git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f"
git-tree-sha1 = "77c7e98ca39aefb481f9b97a2f4f5c5471c08a1d"
uuid = "46f1a544-deae-4307-8689-c12aa3c955c6"
version = "1.0.3"
version = "1.1.0"
[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
@@ -640,15 +628,15 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.6+0"
[[deps.OrderedCollections]]
git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4"
git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.8.2"
version = "2.0.1"
[[deps.PDMats]]
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65"
git-tree-sha1 = "123266c25174ef6c8d4718920abc206452cf8de6"
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
version = "0.11.40"
version = "0.11.41"
weakdeps = ["StatsBase"]
[deps.PDMats.extensions]
@@ -656,9 +644,9 @@ weakdeps = ["StatsBase"]
[[deps.Parsers]]
deps = ["Dates", "PrecompileTools", "UUIDs"]
git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9"
git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.8.6"
version = "2.8.7"
[[deps.Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
@@ -688,15 +676,15 @@ uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.5.2"
[[deps.PrettyPrinting]]
git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e"
git-tree-sha1 = "0b7f4ad437e31c51cf5b91fb103579b04025170a"
uuid = "54e16d92-306c-5ea0-a30b-337be88ac337"
version = "0.4.2"
version = "0.4.3"
[[deps.PrettyTables]]
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325"
git-tree-sha1 = "4ac881f5432bd93463a41767a814a45245be22b6"
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
version = "3.4.0"
version = "3.4.6"
[deps.PrettyTables.extensions]
PrettyTablesExcelExt = "XLSX"
@@ -757,15 +745,15 @@ version = "1.3.1"
[[deps.Reseau]]
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5"
git-tree-sha1 = "701ef63506992668c0069a4d1ac1540b77258e6c"
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
version = "1.3.1"
version = "1.3.6"
[[deps.Revise]]
deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7"
deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "ec46aed6a3a8cc6b67839ca361e7b4aa32eaeee1"
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
version = "3.15.1"
version = "3.16.3"
weakdeps = ["Distributed"]
[deps.Revise.extensions]
@@ -779,15 +767,15 @@ version = "0.9.0"
[[deps.Rmath_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8"
git-tree-sha1 = "6d40b2fe70437b01397d2a4d5b020008da4e7019"
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
version = "0.5.1+0"
version = "0.5.2+0"
[[deps.Roots]]
deps = ["Accessors", "CommonSolve", "Printf"]
git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455"
git-tree-sha1 = "7fb25a964849d90a0446366cdefca822e0e84900"
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
version = "3.0.5"
version = "3.0.6"
[deps.Roots.extensions]
RootsChainRulesCoreExt = "ChainRulesCore"
@@ -840,12 +828,6 @@ git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80"
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
version = "1.4.10"
[[deps.Serde]]
deps = ["CSV", "Dates", "EzXML", "JSON", "TOML", "UUIDs", "YAML"]
git-tree-sha1 = "f397fc8779cc53e4677c2708f3802c6996f28d00"
uuid = "db9b398d-9517-45f8-9a95-92af99003e0e"
version = "3.7.2"
[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
version = "1.11.0"
@@ -879,9 +861,9 @@ version = "1.12.0"
[[deps.SpecialFunctions]]
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f"
git-tree-sha1 = "c3ac026e735264e9bdc6a9bcbd1b1e781b36e3bc"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "2.8.0"
version = "2.8.3"
[deps.SpecialFunctions.extensions]
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
@@ -932,9 +914,9 @@ version = "0.34.12"
[[deps.StatsFuns]]
deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5"
git-tree-sha1 = "91a5737baed20ee31f3faea0e51f57461f6a689e"
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
version = "2.2.0"
version = "2.2.1"
[deps.StatsFuns.extensions]
StatsFunsChainRulesCoreExt = "ChainRulesCore"
@@ -950,17 +932,11 @@ git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a"
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
version = "1.0.0"
[[deps.StringEncodings]]
deps = ["Libiconv_jll"]
git-tree-sha1 = "b765e46ba27ecf6b44faf70df40c57aa3a547dcb"
uuid = "69024149-9ee7-55f6-a4c4-859efe599b68"
version = "0.3.7"
[[deps.StringManipulation]]
deps = ["PrecompileTools"]
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484"
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
version = "0.4.4"
version = "0.4.7"
[[deps.StructTypes]]
deps = ["Dates", "UUIDs"]
@@ -970,9 +946,9 @@ version = "1.11.0"
[[deps.StructUtils]]
deps = ["Dates", "UUIDs"]
git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
git-tree-sha1 = "c65ae4aa47e543c278aea0a3468786d33021a3ff"
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
version = "2.8.2"
version = "2.8.4"
[deps.StructUtils.extensions]
StructUtilsMeasurementsExt = ["Measurements"]
@@ -1046,9 +1022,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.11.3"
[[deps.URIs]]
git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a"
git-tree-sha1 = "3b0738bd7c5645641845da25cbd99800b8718689"
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
version = "1.6.1"
version = "1.6.2"
[[deps.UTCDateTimes]]
deps = ["Dates", "TimeZones"]
@@ -1076,23 +1052,11 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
version = "1.6.1"
[[deps.XML2_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"]
git-tree-sha1 = "3f3315d89fc954a28f5b471bce698ed6e27481be"
uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a"
version = "2.15.3+0"
[[deps.YAML]]
deps = ["Base64", "Dates", "Printf", "StringEncodings"]
git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403"
uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
version = "0.4.16"
[[deps.YiemAgent]]
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serde", "Serialization", "URIs", "UUIDs"]
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"]
path = "."
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
version = "0.7.4"
version = "0.8.0"
[[deps.Zlib_jll]]
deps = ["Libdl"]
-2
View File
@@ -19,7 +19,6 @@ PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
Serde = "db9b398d-9517-45f8-9a95-92af99003e0e"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
@@ -34,4 +33,3 @@ JSON = "1.6.1"
LLMMCTS = "0.1.5"
NATS = "0.1.0"
SQLLLM = "0.2.8"
Serde = "3.7.2"
+30 -19
View File
@@ -1,23 +1,34 @@
# YiemAgent
## TODO
- [WORKING] build prompt()
- [ ] build agent runLoop()
- [ ] build MCP server connector
- [ ] executeplan() to execute the plan
- [ ] add comprehensive tests
Julia framework for building agents with tool use.
## Changelog
## Getting Started
### Version 0.8.0
- Converted snake_case fields to camelCase:
- `llmModel`: `base_url``baseUrl`, `context_window``contextWindow`, `max_tokens``maxTokens`
- Converted PascalCase type references to camelCase:
- `AgentState``agentState`
- `AgentTool``agentTool`
- `AgentMessage``agentMessage`
- `PendingMessageQueue``pendingMessageQueue`
- `ActiveRun``activeRun`
- `StreamFn``streamFn`
- `ThinkingLevel``thinkingLevel`
- `ToolExecutionMode``toolExecutionMode`
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
2. Create a `yiemAgent` with `loadTools("src/tools")`
3. Call `run_agent(agent, "message")` then `take_response(agent)`
## Architecture
```
src/
├── YiemAgent.jl # Module entry point
├── type.jl # Core types (messages, tools, agent state)
├── utils.jl # Message formatting, validation
├── agentCore.jl # Agent loop, tool execution pipeline
├── api.jl # Public API (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`)
-92
View File
@@ -1,92 +0,0 @@
# Dynamic Tool Loading
Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory without hardcoding filenames in the main module.
## How It Works
1. `src/tools/registry.jl` defines a `loadTools(dir::String)` function that scans a directory for `.jl` files
2. Each tool file must define a single function: `getTool()::agentTool`
3. `loadTools()` sorts files alphabetically, includes each one, calls `getTool()`, and registers the result
4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent`
## Directory Structure
```
src/
├── tools/
│ ├── registry.jl # Tool loader (do not edit)
│ ├── getWeather.jl # Your tool
│ └── query_db.jl # Another tool
├── type.jl
├── utils.jl
├── agentCore.jl
├── api.jl
└── YiemAgent.jl
```
## Creating a Tool
Each `.jl` file in `src/tools/` must define `getTool()` returning an `agentTool`:
```julia
# src/tools/getWeather.jl
function getTool()::agentTool
return agentTool(
name = "getWeather",
label = "Weather Lookup",
description = "Fetch current weather and forecast for a given city.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"city" => Dict("type" => "string", "description" => "City and country"),
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
),
"required" => ["city"]
),
execute = (toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Weather in $(city): Sunny, 22C")],
Dict{Any,Any}(), nothing, false
)
end,
prepareArguments = nothing,
parallelToolExecute = false
)
end
```
No `module` wrapper needed — the registry includes each file in the current module scope so all types (`agentTool`, `textContent`, `agentToolResult`, etc.) resolve correctly.
## Loading Tools
```julia
using .YiemAgent
using .YiemAgent: toolRegistry
# Load all tool files from src/tools/
tools = YiemAgent.loadTools(joinpath(@__DIR__, "src", "tools"))
# Create agent with loaded tools
agent = yiemAgent(
systemPrompt = "You are a helpful assistant.",
model = my_model,
tools = tools,
llmCall = my_llm_call,
agentEventSink = my_event_sink
)
```
## Available Functions
| Function | Description |
|----------|-------------|
| `loadTools(dir::String)` | Scan directory and load all `.jl` tool files |
| `registerTool(tool::agentTool)` | Register a single tool into the global registry |
| `getTools()` | Get deep copy of all registered tools |
| `listTools()` | List all registered tools as `(name, label)` pairs |
| `clearTools()` | Clear the global registry |
## File Loading Order
Files are sorted alphabetically before loading, so `01_database.jl` loads before `02_weather.jl`. This ensures deterministic registration order.
+1 -1
View File
@@ -13,7 +13,7 @@ module YiemAgent
include("utils.jl")
using .utils
include("tools/registry.jl")
include("toolRegistry.jl")
using .toolRegistry
# include("llmfunction.jl")
+5 -9
View File
@@ -3,7 +3,7 @@ module agentCore
export _agent_loop, OpenAiToUserMessage
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde, Base.Threads
DataFrames, Base.Threads
using GeneralUtils
using ..type, ..utils
@@ -516,7 +516,7 @@ function prepareToolCall(
signal::Union{Nothing, abortSignal},
)::Union{preparedToolCall,immediateOutcome}
tool = find(t -> t.name == toolCall.name, context.tools)
tool = get(context.tools, toolCall.name, nothing)
if tool === nothing
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
end
@@ -996,13 +996,9 @@ function executeToolCalls(
hasSequential = false
for tc in toolCalls
for t in context.tools
if t.name == tc.name && !t.parallelToolExecute
hasSequential = true
break
end
end
if hasSequential
t = get(context.tools, tc.name, nothing)
if t !== nothing && !t.parallelToolExecute
hasSequential = true
break
end
end
+1 -1
View File
@@ -3,7 +3,7 @@ module api
export prompt
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde
DataFrames
using GeneralUtils
using ..type, ..utils
+243
View File
@@ -0,0 +1,243 @@
module toolRegistry
export ToolStore, loadTools, registerTool, getTools, clearTools, listTool
using Dates
using JSON, DataStructures
using ..type
"""
Per-agent isolated tool storage.
Each agent gets its own `ToolStore` so tool registration is independent —
`registerTool(store, tool)` only affects that agent's tool set.
# Fields
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
- `name::String` — identifier for debugging/logs
"""
struct ToolStore
tools::OrderedDict{String, agentTool}
name::String
end
"""
Create a new isolated tool store.
# Keyword Arguments
- `name::String`: Identifier for this store (default: "default")
# Examples
```julia
store = ToolStore(name="agent1")
tools = loadTools(store, "src/tools")
registerTool(store, my_tool)
agent = yiemAgent(tools=getTools(store), llmCall=..., _tool_store=store)
```
"""
function ToolStore(; name::String="default")::ToolStore
ToolStore(OrderedDict{String, agentTool}(), name)
end
"""
List tool definition — lets the agent query available tools for collision detection
when creating new tools via writeTool.
# Arguments
- `store::ToolStore`: The tool store to list from
Each `ToolStore` gets its own `listTool` instance bound to that store,
so each agent sees only its own tools.
"""
function listTool(store::ToolStore)::agentTool
return agentTool(
name = "listTools",
label = "List Tools",
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute = (toolCallId, args, signal, onPartialResult) -> begin
tools = getTools(store)
if isempty(tools)
result_text = "No tools registered."
else
lines = String["- $(t.name): $(t.label)$(t.description)" for (k, t) in tools]
result_text = "Available tools:\n" * join(lines, "\n")
end
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => length(tools)),
nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
)
end
"""
Load all tool modules from a directory into a specific ToolStore.
Scans `dir` for `.jl` files. Each file must define a function named
`getTool()::agentTool`. Files are sorted alphabetically so tool
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.
# Arguments
- `store::ToolStore`: The tool store to register tools into
- `dir::String`: Directory path to scan for `.jl` tool files
# Returns
- `OrderedDict{String, agentTool}`: All loaded tools keyed by name
# Errors
- Throws `ArgumentError` if a tool file does not define a `getTool` function
# Examples
```julia
julia> store = ToolStore(name="agent1")
julia> tools = loadTools(store, "src/tools")
OrderedDict{String, agentTool} with 3 entries:
"getWeather" => agentTool(...)
"getTime" => agentTool(...)
"listTools" => agentTool(...)
```
"""
function loadTools(store::ToolStore, dir::String)::OrderedDict{String, agentTool}
if !isdir(dir)
throw(ArgumentError("Tool directory does not exist: $dir"))
end
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
sort!(jl_files)
for filename in jl_files
filepath = joinpath(dir, filename)
# Derive a unique module name from the filename only (not full path).
# e.g. "getWeather.jl" -> "_tool_getWeather"
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
# Build the complete module as a string and eval the parsed code.
# Julia does not allow `module ... end` inside eval(quote ...),
# and constructing the module AST by hand is fragile.
# Instead, we generate the full module source as a string,
# parse it, and eval the resulting expression.
# Each tool file declares its own dependencies via `using` statements
# at the top of the file — the registry only injects `using ..type`
# to make core types (agentTool, textContent, etc.) available.
file_content = read(filepath, String)
module_code = """
module $(mod_name)
using ..type
$(file_content)
end
"""
mod = eval(Meta.parse(module_code))
# Call getTool() via Core.eval in the submodule's scope.
# This evaluates getTool() entirely within the new module's world,
# completely avoiding world-age issues — no invokelatest needed.
# Note: all uses of `tool` must be inside the `try` block because
# 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
store.tools[tool.name] = tool
println("[$(store.name)] 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
return store.tools
end
"""
Register a single agentTool into a specific ToolStore.
# Arguments
- `store::ToolStore`: The tool store to register into
- `tool::agentTool`: The tool to register
# Returns
- `OrderedDict{String, agentTool}`: Updated tool dict for this store
# Examples
```julia
julia> store = ToolStore(name="agent1")
julia> registerTool(store, my_tool)
[toolRegistry:agent1] Registered tool: my_tool
```
"""
function registerTool(store::ToolStore, tool::agentTool)::OrderedDict{String, agentTool}
store.tools[tool.name] = tool
println("[$(store.name)] Registered tool: $(tool.name)")
return store.tools
end
"""
Get the registered tools from a specific ToolStore.
Returns the internal `OrderedDict` directly — O(1) lookup by name,
ordered iteration preserving registration order.
# Arguments
- `store::ToolStore`: The tool store to query
# Returns
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
# Examples
```julia
julia> getTools(store)
OrderedDict{String, agentTool} with 3 entries:
"listTools" => agentTool(...)
"getWeather" => agentTool(...)
"getTime" => agentTool(...)
```
"""
function getTools(store::ToolStore)::OrderedDict{String, agentTool}
return store.tools
end
"""
Clear all registered tools from a specific ToolStore.
# Arguments
- `store::ToolStore`: The tool store to clear
# Returns
- `nothing`
# Examples
```julia
julia> clearTools(store)
[toolRegistry:agent1] Registry cleared
```
"""
function clearTools(store::ToolStore)::Nothing
empty!(store.tools)
println("[$(store.name)] Registry cleared")
return nothing
end
end # module
+1575 -521
View File
File diff suppressed because it is too large Load Diff
+5 -13
View File
@@ -1,3 +1,5 @@
using Dates
"""
Validate required arguments for the getTime tool.
@@ -39,26 +41,16 @@ end
"""
Execute the getTime tool.
# Arguments
- `toolCallId::String`: Unique identifier for this tool call
- `args::Dict{String,Any}`: Parsed arguments from the LLM
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
- `onPartialResult::Function`: Callback for streaming partial results
# Returns
- `agentToolResult`: Result content with current time data
Returns mock time data for the given timezone or city.
"""
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
tz = get(args, "timezone", nothing)
city = get(args, "city", "")
# Simulate time lookup — replace with actual timezone API call
if tz !== nothing
result = "Current time in $(tz): $(now())"
else
result = "Current time in $(city): $(now())"
end
return agentToolResult(
[textContent(result)],
Dict{Any,Any}(), nothing, false
@@ -76,8 +68,8 @@ function getTool()::agentTool
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"timezone" => Dict("type" => "string", "description", "IANA timezone, e.g. 'America/New_York'"),
"city" => Dict("type" => "string", "description", "City name as fallback")
"timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'"),
"city" => Dict("type" => "string", "description" => "City name as fallback")
),
"required" => []
),
+3 -17
View File
@@ -1,31 +1,17 @@
"""
Execute the getWeather tool.
# Arguments
- `toolCallId::String`: Unique identifier for this tool call
- `args::Dict{String,Any}`: Parsed arguments from the LLM
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
- `onPartialResult::Function`: Callback for streaming partial results
# Returns
- `agentToolResult`: Result content with weather data
Returns mock weather data for the given city and temperature units.
"""
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
city = get(args, "city", "")
units = get(args, "units", "celsius")
# Simulate weather fetch — replace with actual API call
# You can call onPartialResult() here for streaming progress updates:
# onPartialResult(Dict("status" => "Fetching weather data..."))
# onPartialResult(Dict("status" => "Processing..."))
temp = units == "fahrenheit" ? "72" : "22"
unit_symbol = units == "celsius" ? "°C" : "°F"
return agentToolResult(
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
Dict{Any,Any}(), nothing, false
)
)
end
"""
@@ -44,7 +30,7 @@ function getTool()::agentTool
),
"required" => ["city"]
),
execute = executeTool, # reference the function defined above
execute = executeTool,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
-142
View File
@@ -1,142 +0,0 @@
module toolRegistry
export loadTools, registerTool, getTools, listTools, clearTools
using ..type
# Global registry — populated at runtime by loadTools() or registerTool()
const _registry = Vector{agentTool}()
"""
Load all tool modules from a directory.
Scans `dir` for `.jl` files. Each file must define a function named
`getTool()::agentTool`. Files are sorted alphabetically so tool
registration order is deterministic.
# Tool file format
Each `.jl` file defines one function `getTool()` that returns an `agentTool`:
```julia
# src/tools/getWeather.jl
function getTool()::agentTool
return agentTool(
name = "getWeather",
label = "Weather Lookup",
description = "Fetch current weather and forecast for a given city.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"city" => Dict("type" => "string", "description" => "City and country"),
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
),
"required" => ["city"]
),
execute = (toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Sunny, 22C in $(city)")],
Dict{Any,Any}(), nothing, false
)
end,
prepareArguments = nothing,
parallelToolExecute = false
)
end
```
# Arguments
- `dir::String`: Directory path to scan for `.jl` tool files
# Returns
- `Vector{agentTool}`: All loaded tools
# Errors
- Throws `ArgumentError` if a tool file does not define a `getTool` function
"""
function loadTools(dir::String)::Vector{agentTool}
if !isdir(dir)
throw(ArgumentError("Tool directory does not exist: $dir"))
end
tools = agentTool[]
jl_files = filter(f -> endswith(f, ".jl"), readdir(dir))
sort!(jl_files)
for filename in jl_files
filepath = joinpath(dir, filename)
println("[toolRegistry] Loading tool from: $filepath")
# Include the file in the current module scope so all types resolve
# (agentTool, textContent, agentToolResult, etc. are all available)
include(filepath)
# Validate that getTool was defined (include() places it in current module scope)
if !isdefined(@__MODULE__, :getTool)
throw(ArgumentError(
"Tool file $(filepath) does not define a `getTool()` function. " *
"Each tool file must define: function getTool()::agentTool ... end"
))
end
# Call getTool() — it runs in current scope where types are visible
tool = getTool()
if !(tool isa agentTool)
throw(ArgumentError(
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
))
end
push!(_registry, tool)
push!(tools, tool)
println("[toolRegistry] Loaded tool: $(tool.name)$(tool.label)")
end
return tools
end
"""
Register a single agentTool into the global registry.
# Arguments
- `tool::agentTool`: The tool to register
# Returns
- `Vector{agentTool}`: Updated registry
"""
function registerTool(tool::agentTool)::Vector{agentTool}
push!(_registry, tool)
println("[toolRegistry] Registered tool: $(tool.name)")
return _registry
end
"""
Get all registered tools.
# Returns
- `Vector{agentTool}`: Copy of the registry
"""
function getTools()::Vector{agentTool}
return deepcopy(_registry)
end
"""
List all registered tool names and labels.
# Returns
- `Vector{Tuple{String,String}}`: Pairs of (name, label)
"""
function listTools()::Vector{Tuple{String,String}}
return [(t.name, t.label) for t in _registry]
end
"""
Clear all registered tools from the global registry.
"""
function clearTools()::Nothing
empty!(_registry)
println("[toolRegistry] Registry cleared")
return nothing
end
end # module
+273
View File
@@ -0,0 +1,273 @@
using JSON
"""
Tool that writes new Julia tool module files to disk.
The agent can use this tool when it encounters a task that no existing tool
can handle. Provide the tool's name, label, description, inputSchema, and
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
After calling this tool, restart the agent so `loadTools(agent._tool_store, "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(store, "src/tools")
write(filepath, tool_code)
onPartialResult(Dict("status" => "Done"))
return agentToolResult(
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools(agent._tool_store, \"src/tools\") 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
+37 -21
View File
@@ -9,7 +9,7 @@
# Message types
userMessage, assistantMessage, toolResultMessage,
# Tool types
agentTool, validateRequiredArgs
agentTool, validateRequiredArgs,
# Context types
agentContext, agentState, agentToolCall, prepareNextTurnContext,
# Loop & execution types
@@ -248,7 +248,7 @@ tool = agentTool(
execute=(toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Sunny, 22C in $(city)")],
[textContent("Sunny, 22C in Bangkok")],
Dict{Any,Any}(), nothing, false
)
end,
@@ -269,6 +269,17 @@ struct agentTool # A tool available to the agent
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
end
"""
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
"""
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
execute::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 #
@@ -280,7 +291,7 @@ Snapshot of the agent's conversation context.
# Arguments
- `systemPrompt::String`: System prompt for the agent
- `messages::Vector{agentMessage}`: Conversation messages
- `tools::Union{Vector{agentTool}, Nothing}`: Available tools
- `tools::Union{Dict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
# Returns
- A new `agentContext` instance
@@ -288,7 +299,7 @@ Snapshot of the agent's conversation context.
struct agentContext # Snapshot of the agent's conversation context
systemPrompt::String # System prompt for the agent
messages::Vector{agentMessage} # Conversation messages
tools::Union{Vector{agentTool}, Nothing} # Available tools
tools::Union{Dict{String, agentTool}, Nothing} # Available tools keyed by name
end
@@ -297,9 +308,9 @@ end
# ------------------------------------------------------------------------------------------------ #
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
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::Vector{agentMessage}
@@ -318,7 +329,7 @@ new state from external references.
# Arguments
- `systemPrompt::String`: System prompt text
- `model::llmModel`: LLM model to use (defaults to an unknown model)
- `tools::Vector{agentTool}`: Available tools (deep copied)
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (deep copied)
- `messages::Vector{agentMessage}`: Conversation messages (deep copied)
# Returns
@@ -327,13 +338,13 @@ new state from external references.
# Examples
```julia
julia> state = agentState(systemPrompt="You are a helpful assistant")
agentState("You are a helpful assistant", ..., agentTool[], agentMessage[], String[], nothing)
```
agentState("You are a helpful assistant", OrderedDict{String, agentTool}(), agentMessage[], String[], nothing)
"""
function agentState(
systemPrompt::String="",
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
tools::Vector{agentTool}=agentTool[],
model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[],
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
messages::Vector{agentMessage}=agentMessage[],
)
agentState(
@@ -384,13 +395,13 @@ end
Configuration for the agent tool execution loop.
# Arguments
- `tools::Vector{agentTool}`: Available tools
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
"""
struct agentLoopConfig
tools::Vector{agentTool}
tools::OrderedDict{String, agentTool}
beforeToolCall::Union{Function, Nothing}
afterToolCall::Union{Function, Nothing}
toolExecution::String
@@ -556,9 +567,10 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
sessionId::Union{String, Nothing} # Optional session identifier
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
parallelToolExecute::Bool # Default: false
agentEventSink::Function # agent emits its status via this function
end
parallelToolExecute::Bool # Default: false
agentEventSink::Function # agent emits its status via this function
_tool_store::Any # Reference to the ToolStore for runtime registration
end
"""
Create a new yiemAgent instance with a background loop task.
@@ -569,7 +581,7 @@ on `inputChannel` and `followUpChannel` channels concurrently.
# Keyword Arguments
- `systemPrompt::String`: System prompt for the agent
- `model`: LLM model to use
- `tools::Vector{agentTool}`: Available tools (default: empty)
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
- `llmCall::Function`: Function to invoke the LLM (required)
@@ -582,20 +594,22 @@ on `inputChannel` and `followUpChannel` channels concurrently.
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
- `agentEventSink::Function`: Callback to receive agent events
- `tool_store::Union{Any, Nothing}`: ToolStore for runtime tool registration (default: `nothing`)
# Returns
- A new `yiemAgent` instance with an active background task
# Examples
```julia
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model)
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...)
```
julia> store = ToolStore(name="agent1")
julia> tools = loadTools(store, "src/tools")
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
"""
function yiemAgent(
; systemPrompt::String="You are helpful assistant.",
model=nothing,
tools::Vector{agentTool}=agentTool[],
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
messages::Vector{agentMessage}=agentMessage[],
prepareContext::Union{Function, Nothing}=nothing,
formatMsgForLLM::Function=defaultformatMsgForLLM,
@@ -608,6 +622,7 @@ function yiemAgent(
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false,
agentEventSink::Function,
tool_store::Union{Any, Nothing}=nothing,
)
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
inputChannel = Channel(16)
@@ -632,6 +647,7 @@ function yiemAgent(
maxRetryDelayMs,
parallelToolExecute,
agentEventSink,
tool_store,
)
# Spawn the background loop and attach it
+2 -2
View File
@@ -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
filteredTools = state.tools
#TODO add tools to current system prompt
#TODO add filtered tools to the current system prompt / modify systemPrompt here
preparedSystemPrompt = state.systemPrompt
#TODO add system prompt, adjust/modify and inject additional context into messages
@@ -375,7 +375,7 @@ function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{S
end
return prepared.arguments
end
end
+1 -1
View File
@@ -4,7 +4,7 @@ export addNewMessage, conversation, decisionMaker, reflector, generatechat,
generalconversation, detectWineryName, generateSituationReport
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde
DataFrames
using GeneralUtils
using ..type, ..util, ..llmfunction
+1 -1
View File
@@ -5,7 +5,7 @@ export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recomme
extractWineAttributes_2, paraphrase, SQLexecution
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures,
Base64, Serde, LibPQ, NATS
Base64, LibPQ, NATS
using GeneralUtils, SQLLLM
using ..type, ..util
-41
View File
@@ -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"
-2
View File
@@ -1,2 +0,0 @@
[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+173
View File
@@ -0,0 +1,173 @@
using Test
using YiemAgent
using YiemAgent.toolRegistry
using YiemAgent.type
# Path to the real tools directory
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@testset "loadTools with ToolStore" begin
# ------------------------------------------------------------------ #
# 1. loadTools throws on non-existent directory #
# ------------------------------------------------------------------ #
store = ToolStore(name="test1")
@test_throws ArgumentError loadTools(store, "/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(store, bad_dir)
# ------------------------------------------------------------------ #
# 3. loadTools loads actual tool files from src/tools/ #
# ------------------------------------------------------------------ #
store2 = ToolStore(name="test2")
loaded = loadTools(store2, 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 (per-store isolation) #
# ------------------------------------------------------------------ #
store3 = ToolStore(name="test3")
registry_tools = getTools(store3)
@test isempty(registry_tools)
# listTool is not auto-registered anymore — each store starts empty
# Register tools manually
registerTool(store3, loaded["getTime"])
registerTool(store3, loaded["getWeather"])
registerTool(store3, loaded["writeTool"])
reg = getTools(store3)
@test !isempty(reg)
@test "getTime" in keys(reg)
@test "getWeather" in keys(reg)
@test "writeTool" in keys(reg)
@test collect(keys(reg))[1] == "getTime"
@test collect(keys(reg))[2] == "getWeather"
@test collect(keys(reg))[3] == "writeTool"
clearTools(store3)
@test isempty(getTools(store3))
test_tool = agentTool(
name = "manualTool",
label = "Manual Tool",
description = "Registered manually",
inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]),
execute = (toolCallId, args, signal, onPartialResult) ->
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = true
)
registerTool(store3, test_tool)
reg = getTools(store3)
@test haskey(reg, "manualTool")
@test length(reg) == 1
@test reg["manualTool"].parallelToolExecute == true
# ------------------------------------------------------------------ #
# 8. getTools returns direct reference (mutations affect registry) #
# ------------------------------------------------------------------ #
copy1 = getTools(store3)
copy2 = getTools(store3)
@test copy1 === copy2 # same reference, not a deep copy
empty!(copy1)
@test isempty(getTools(store3)) # mutation propagates
# ------------------------------------------------------------------ #
# 9. Per-store isolation — two stores don't share tools #
# ------------------------------------------------------------------ #
storeA = ToolStore(name="isolationA")
storeB = ToolStore(name="isolationB")
registerTool(storeA, loaded["getTime"])
registerTool(storeB, loaded["getWeather"])
regA = getTools(storeA)
regB = getTools(storeB)
@test "getTime" in keys(regA)
@test "getWeather" keys(regA)
@test "getWeather" in keys(regB)
@test "getTime" keys(regB)
clearTools(storeA)
@test isempty(getTools(storeA))
@test !isempty(getTools(storeB)) # storeB unaffected
end