Compare commits

..

167 Commits

Author SHA1 Message Date
ton af236bcf92 Merge pull request 'V0.9.1' (#58) from v0.9.1 into main
Reviewed-on: #58
2026-10-02 14:44:27 +00:00
ton 1c2e2e5340 Merge pull request 'V0.9.1 fix ghost response' (#57) from v0.9.1-fix_ghost_response into v0.9.1
Reviewed-on: #57
2026-10-02 14:44:04 +00:00
ton 48d89033cc update 2026-10-02 21:43:10 +07:00
ton befbf62e3b update 2026-10-02 20:10:04 +07:00
ton 4e476bd39d up version 2026-09-27 19:29:05 +07:00
ton 66ced320f0 revert to v0.9.0 2026-09-22 22:25:51 +07:00
ton 1817630795 Update Project.toml 2026-09-22 09:38:36 +00:00
ton dbac0db9b9 Merge pull request 'V0.9.1' (#53) from v0.9.1 into main
Reviewed-on: #53
2026-09-22 09:36:46 +00:00
ton ce46888663 Merge pull request 'update' (#52) from v0.9.1-filterout_toolcall_llm into v0.9.1
Reviewed-on: #52
2026-09-22 09:36:32 +00:00
ton 2a2d44ab0c update 2026-09-22 16:35:40 +07:00
ton 0620983b48 Merge pull request 'V0.9.0' (#51) from v0.9.0 into main
Reviewed-on: #51
2026-09-12 13:05:13 +00:00
ton af81503f8b Merge pull request 'V0.9.0 agent status' (#50) from v0.9.0-agent_status into v0.9.0
Reviewed-on: #50
2026-09-12 13:04:59 +00:00
ton 4624ad99ab update 2026-09-12 17:04:00 +07:00
ton 2fd363d9ef bump version 2026-09-12 16:12:56 +07:00
ton ea6012b2fa update 2026-09-12 16:09:00 +07:00
ton 6e53f57707 Merge pull request 'v0.7.4' (#39) from v0.7.4 into main
Reviewed-on: #39
2026-07-28 00:16:48 +00:00
ton 1eea6c66b6 Merge pull request 'update' (#38) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #38
2026-07-27 10:43:41 +00:00
ton 37ff68c963 update 2026-07-27 17:43:11 +07:00
ton e5fc800c83 Merge pull request 'update' (#37) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #37
2026-07-27 09:55:11 +00:00
ton 4397ec5fb3 update 2026-07-27 16:54:36 +07:00
ton 9e3f5a0967 Merge pull request 'v0.7.4' (#36) from v0.7.4 into main
Reviewed-on: #36
2026-07-27 02:48:28 +00:00
ton 30b79a3b34 Merge pull request 'fix tool name' (#35) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #35
2026-07-27 02:47:43 +00:00
ton e5b8518c00 fix tool name 2026-07-27 09:47:07 +07:00
ton d115e60ddf Merge pull request 'update' (#34) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #34
2026-07-26 16:02:35 +00:00
ton e0e6aced33 update 2026-07-26 23:01:51 +07:00
ton c803238f86 Merge pull request 'v0.7.4-add_vector_search' (#33) from v0.7.4-add_vector_search into v0.7.4
Reviewed-on: #33
2026-07-26 15:42:34 +00:00
ton 4d6cdbcf4a update 2026-07-26 22:42:09 +07:00
ton 206c0d2c62 Merge pull request 'v0.7.4-add_vector_search' (#32) from v0.7.4-add_vector_search into main
Reviewed-on: #32
2026-07-26 15:21:53 +00:00
ton 597319a147 update 2026-07-26 22:21:10 +07:00
ton abfe6f45fb update 2026-07-25 10:13:02 +07:00
ton f8b3150c17 update 2026-07-24 16:44:49 +07:00
ton caed2a82d6 update 2026-07-24 15:51:32 +07:00
ton 7ec3edfd77 update 2026-07-24 08:31:36 +07:00
ton d7adfaffa8 update 2026-07-23 21:14:45 +07:00
ton 5bde0ca1f2 Merge pull request 'update' (#31) from v0.7.4-use_yaml_format into v0.7.4
Reviewed-on: #31
2026-07-22 03:20:13 +00:00
ton a3ab288cfe update 2026-07-22 10:18:28 +07:00
ton 17b0974d82 Merge pull request 'v0.7.4-predefine_wine_search' (#30) from v0.7.4-predefine_wine_search into v0.7.4
Reviewed-on: #30
2026-07-21 13:17:25 +00:00
ton 8fed0b5e8f update 2026-07-21 20:11:12 +07:00
ton 567d6b79d7 up version 2026-07-21 18:02:02 +07:00
ton 74be3e5717 Merge pull request 'v0.7.3' (#29) from v0.7.3 into main
Reviewed-on: #29
2026-07-20 05:26:19 +00:00
ton 402b6fcadd Merge pull request 'update' (#28) from v0.7.3-fix_item_info into v0.7.3
Reviewed-on: #28
2026-07-20 05:24:33 +00:00
ton ef523aaa48 update 2026-07-20 12:23:03 +07:00
ton ddbb135b6b update 2026-07-17 12:22:56 +07:00
ton afda364484 update 2026-07-17 12:03:36 +07:00
ton af73d955eb Merge pull request 'v0.7.1' (#27) from v0.7.1 into main
Reviewed-on: #27
2026-07-17 03:28:17 +00:00
ton 39cf9a72a1 Merge pull request 'v0.7.1-fix_single_items_info_frontend' (#26) from v0.7.1-fix_single_items_info_frontend into v0.7.1
Reviewed-on: #26
2026-07-17 03:28:06 +00:00
ton 1c829ad854 update 2026-07-17 10:14:13 +07:00
ton da98baddb6 update 2026-07-17 10:06:32 +07:00
ton c5fbaabf42 Merge pull request 'v0.7.0' (#25) from v0.7.0 into main
Reviewed-on: #25
2026-07-17 00:03:01 +00:00
ton 8080905bad Merge pull request 'v0.7.0-output_openai_msg' (#24) from v0.7.0-output_openai_msg into v0.7.0
Reviewed-on: #24
2026-07-17 00:02:47 +00:00
ton b349c3a8b6 update 2026-07-17 07:01:55 +07:00
ton 0148e03d6a update 2026-07-16 23:31:36 +07:00
ton e718cc4a5c update 2026-07-16 23:13:18 +07:00
ton 87bc6a46a1 update 2026-07-16 22:31:40 +07:00
ton 44bb8baf7c Merge pull request 'update' (#23) from v0.6.0-output_text_image into main
Reviewed-on: #23
2026-07-15 11:51:43 +00:00
ton 18b2d54ba7 update 2026-07-15 18:51:21 +07:00
ton b3c3bb9b75 Merge pull request 'update' (#22) from v0.6.0-output_text_image into main
Reviewed-on: #22
2026-07-15 11:50:09 +00:00
ton d004193b19 update 2026-07-15 18:49:51 +07:00
ton 8898226825 Merge pull request 'v0.6.0-output_text_image' (#21) from v0.6.0-output_text_image into main
Reviewed-on: #21
2026-07-15 11:48:20 +00:00
ton 686b9b2e92 update 2026-07-15 18:47:18 +07:00
ton 3acf46964b update 2026-07-15 14:25:04 +07:00
ton 5c7caf0b49 Merge pull request 'update' (#20) from v0.6.0-output_text_image into main
Reviewed-on: #20
2026-07-15 07:01:48 +00:00
ton ad917ea8d0 update 2026-07-15 14:01:32 +07:00
ton edeef4ed2a Merge pull request 'v0.6.0-output_text_image' (#19) from v0.6.0-output_text_image into main
Reviewed-on: #19
2026-07-15 06:59:56 +00:00
ton 31daa805f3 update 2026-07-15 13:59:28 +07:00
ton c9937ab5d7 update 2026-07-15 13:59:01 +07:00
ton 4610137f04 Merge pull request 'update' (#18) from v0.6.0-output_text_image into main
Reviewed-on: #18
2026-07-15 05:20:58 +00:00
ton 9d7eed7cde update 2026-07-15 12:20:44 +07:00
ton aedc53bf86 Merge pull request 'update' (#17) from v0.6.0-output_text_image into main
Reviewed-on: #17
2026-07-15 05:17:56 +00:00
ton 286da3cf2c update 2026-07-15 12:16:59 +07:00
ton 7fa988313d Merge pull request 'update' (#16) from v0.6.0-output_text_image into main
Reviewed-on: #16
2026-07-15 05:15:31 +00:00
ton e5b19dd268 update 2026-07-15 12:14:32 +07:00
ton 0df4159261 Merge pull request 'update' (#15) from v0.6.0-output_text_image into main
Reviewed-on: #15
2026-07-15 05:11:05 +00:00
ton 45e8ded111 update 2026-07-15 12:10:36 +07:00
ton 9167ece0c0 Merge pull request 'v0.6.0' (#14) from v0.6.0 into main
Reviewed-on: #14
2026-07-15 04:57:47 +00:00
ton a6a9395ecc Merge pull request 'update' (#13) from v0.6.0-output_text_image into v0.6.0
Reviewed-on: #13
2026-07-15 04:57:11 +00:00
ton a503d4d759 update 2026-07-15 11:56:44 +07:00
ton f45a036971 Merge pull request 'v0.6.0' (#12) from v0.6.0 into main
Reviewed-on: #12
2026-07-15 04:38:48 +00:00
ton 24b85be58b Merge pull request 'updatet' (#11) from v0.6.0-output_text_image into v0.6.0
Reviewed-on: #11
2026-07-15 04:38:18 +00:00
ton a798cd119e updatet 2026-07-14 18:08:36 +07:00
ton fa338dd0f8 Merge pull request 'v0.5.0' (#10) from v0.5.0 into main
Reviewed-on: #10
2026-07-12 08:03:41 +00:00
ton 8d4bf5f01f Merge pull request 'v0.5.0-tool_role' (#9) from v0.5.0-tool_role into v0.5.0
Reviewed-on: #9
2026-07-12 08:03:26 +00:00
ton cd6f6ef961 update 2026-07-12 11:14:30 +07:00
ton fdec34832d update 2026-07-12 10:54:15 +07:00
ton 3c72373b85 update 2026-07-12 05:54:47 +07:00
ton 688a8c4df2 update 2026-07-11 21:45:49 +07:00
ton 8bd4986be2 Merge pull request 'v0.4.3' (#8) from v0.4.3 into main
Reviewed-on: #8
2026-07-10 10:47:34 +00:00
ton 6e5809fc9b Merge pull request 'v0.4.3-fix_markdown_response' (#7) from v0.4.3-fix_markdown_response into v0.4.3
Reviewed-on: #7
2026-07-10 10:47:18 +00:00
ton 2b7c0041e5 up version 2026-07-10 17:46:55 +07:00
ton 9ff0b48eec update 2026-07-10 17:45:58 +07:00
ton afeb4c7aef Merge pull request 'v0.4.2' (#6) from v0.4.2 into main
Reviewed-on: #6
2026-07-09 13:20:36 +00:00
ton 2942a89730 Merge pull request 'v0.4.2-limit_consecutive_same_tool_use' (#5) from v0.4.2-limit_consecutive_same_tool_use into v0.4.2
Reviewed-on: #5
2026-07-09 13:20:21 +00:00
ton 6a66f58e63 update 2026-07-09 20:19:41 +07:00
ton 1d0353d793 update 2026-07-09 20:14:04 +07:00
ton 0f6aa7c79f update 2026-07-09 19:45:04 +07:00
ton 0320fd321f Merge pull request 'v0.4.1' (#4) from v0.4.1 into main
Reviewed-on: #4
2026-07-09 01:01:53 +00:00
ton c29dccf597 up version 2026-07-09 08:01:08 +07:00
ton 70cf04b0db Merge pull request 'v0.4.1-fix_agent_not_respond' (#3) from v0.4.1-fix_agent_not_respond into v0.4.1
Reviewed-on: #3
2026-07-09 00:58:33 +00:00
ton 4d57f0146b update 2026-07-09 07:56:33 +07:00
ton d33aa14dc8 use md system prompt 2026-07-07 07:54:24 +07:00
ton 0ed3edd48a update 2026-07-06 06:10:12 +07:00
ton fb91b51573 update 2026-07-05 20:48:24 +07:00
ton 0bbd227920 update 2026-07-04 17:37:43 +07:00
ton 3487770f77 update 2026-07-04 15:42:00 +07:00
ton 7d27f9e567 update 2026-07-04 13:43:44 +07:00
ton 511b4d682d update 2026-07-04 13:42:33 +07:00
ton 5ba91d8acc update compat 2026-07-04 13:41:00 +07:00
ton 709f7e7115 Merge pull request 'v0.4.0' (#2) from v0.4.0 into main
Reviewed-on: #2
2026-07-04 06:34:08 +00:00
ton 3529384cdb Merge pull request 'v0.4.0-fix_appcontext' (#1) from v0.4.0-fix_appcontext into v0.4.0
Reviewed-on: #1
2026-07-04 06:33:31 +00:00
ton d1921fa403 update 2026-07-04 13:23:46 +07:00
ton b4f2a6185b update 2026-06-30 21:18:59 +07:00
ton d076d5f912 update 2026-06-28 22:03:58 +07:00
ton e63dd7d898 update 2026-06-27 08:00:41 +07:00
ton 299a485e4e update docstring 2026-06-25 08:19:25 +07:00
ton 57dd6df942 add context 2026-06-24 21:02:56 +07:00
ton 9c22cf2e31 use Dict string key 2026-06-24 20:46:24 +07:00
ton de20764610 update 2026-06-24 19:50:43 +07:00
ton 906afc6422 update 2026-06-24 08:47:36 +07:00
ton f6eaf4751b update 2026-06-21 09:48:17 +07:00
ton 99b2fda461 use JSON instead 2026-06-21 08:51:39 +07:00
ton b5a00bc694 update 2025-07-30 18:08:57 +07:00
ton 4eb55537f7 update 2025-07-23 18:28:38 +07:00
narawat lamaiin 0a1032c545 update 2025-07-18 07:54:59 +07:00
narawat lamaiin 68a20b5080 update 2025-07-17 11:48:23 +07:00
narawat lamaiin 8a9c9606c7 update 2025-07-14 19:33:12 +07:00
narawat lamaiin bad2ca35ed update 2025-07-14 08:54:51 +07:00
narawat lamaiin f2b56640cc update 2025-06-17 12:53:32 +07:00
narawat lamaiin 5d552d96c4 add example 2025-06-17 12:52:00 +07:00
narawat lamaiin e0dc7d29b2 update 2025-06-15 08:02:59 +07:00
narawat lamaiin 932611a439 update 2025-06-09 06:33:48 +07:00
narawat lamaiin a5c6360b4e update 2025-06-03 10:08:54 +07:00
narawat lamaiin 03f50379c9 mark new version 2025-05-26 07:14:19 +07:00
ton a7da0b8123 Merge pull request 'v0.3.0' (#5) from v0.3.0 into main
Reviewed-on: #5
2025-05-26 00:07:21 +00:00
narawat lamaiin e524813021 update 2025-05-26 07:05:14 +07:00
narawat lamaiin 3444f00062 update 2025-05-19 21:10:04 +07:00
narawat lamaiin 919d8ec85e update 2025-05-18 17:21:51 +07:00
narawat lamaiin 3a88e0e7d4 update 2025-05-17 21:36:29 +07:00
narawat lamaiin 68c2c2f12b update 2025-05-17 12:18:25 +07:00
narawat lamaiin 3e79c0bfed update 2025-05-16 10:26:50 +07:00
narawat lamaiin d0c26e52e8 update 2025-05-14 21:21:35 +07:00
narawat lamaiin a0152a3c29 update 2025-05-04 20:56:17 +07:00
narawat lamaiin 1fc5dfe820 mark new version 2025-05-02 15:27:29 +07:00
ton 4b2575f4a4 Merge pull request 'v0.2.0' (#4) from v0.2.0 into main
Reviewed-on: #4
2025-05-02 08:21:05 +00:00
narawat lamaiin a01a91e7b9 update 2025-05-01 12:05:59 +07:00
narawat lamaiin aa8436c0ed update 2025-05-01 08:04:01 +07:00
narawat lamaiin cccad676db update 2025-05-01 07:59:37 +07:00
narawat lamaiin 03de659c9b update companion 2025-04-30 12:58:32 +07:00
narawat lamaiin affb96f0cf update 2025-04-29 18:45:52 +07:00
narawat lamaiin f19f302bd9 update 2025-04-29 11:01:36 +07:00
narawat lamaiin 7ca4f5276d update 2025-04-26 06:20:09 +07:00
narawat lamaiin 44804041a3 update 2025-04-25 21:12:27 +07:00
narawat lamaiin 48a3704f6d update 2025-04-13 21:46:54 +07:00
tonaerospace 8321a13afc update 2025-04-04 15:23:34 +07:00
tonaerospace b26ae31d4c mark new version 2025-04-04 15:23:11 +07:00
ton b397bf7bdb Merge pull request 'v0.1.4' (#3) from v0.1.4 into main
Reviewed-on: #3
2025-04-04 08:14:57 +00:00
narawat lamaiin c0edf7dadf update 2025-04-04 15:04:02 +07:00
narawat lamaiin c21f943b12 update 2025-04-01 21:17:15 +07:00
narawat lamaiin b8fd772a28 update 2025-03-31 21:30:14 +07:00
narawat lamaiin 883f581b2a update 2025-03-22 15:34:00 +07:00
narawat lamaiin 5a890860a6 update 2025-03-22 09:42:51 +07:00
tonaerospace 7d5bc14a09 mark new version 2025-03-21 10:13:53 +07:00
ton 37ba3a9d31 Merge pull request 'v0.1.3-dev' (#2) from v0.1.3-dev into main
Reviewed-on: #2
2025-03-21 03:09:16 +00:00
tonaerospace bfadd53033 update 2025-03-21 10:03:08 +07:00
tonaerospace 8fc3afe348 update 2025-03-20 16:15:38 +07:00
tonaerospace c60037226a update 2025-03-13 19:11:20 +07:00
narawat lamaiin db6c9c5f2b update 2025-03-07 13:34:15 +07:00
narawat lamaiin 6504099959 update 2025-01-31 09:50:44 +07:00
38 changed files with 8841 additions and 4908 deletions
+573 -250
View File
File diff suppressed because it is too large Load Diff
+13 -7
View File
@@ -1,27 +1,33 @@
name = "YiemAgent" name = "YiemAgent"
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
version = "0.9.1"
authors = ["narawat lamaiin <narawat@outlook.com>"] authors = ["narawat lamaiin <narawat@outlook.com>"]
version = "0.1.2"
[deps] [deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241"
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1" LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337" 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"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10"
[compat] [compat]
Base64 = "1.11.0"
CSV = "0.10.15"
DataFrames = "1.7.0" DataFrames = "1.7.0"
GeneralUtils = "0.1, 0.2" GeneralUtils = "0.6.9"
LLMMCTS = "0.1.2" HTTP = "2.4.0"
SQLLLM = "0.2.0" JSON = "1.6.1"
NATS = "0.1.0"
msghandler = "1.4.0"
+310 -6
View File
@@ -1,7 +1,311 @@
version 0.1.0 # YiemAgent
TODO:
[WORKING] build MCTS() for planning
[] executeplan() to execute the plan
Change from version: 0.0.9 Julia framework for building agents with tool use and MCP (Model Context Protocol) support.
-
## Quick Start
```julia
using YiemAgent
# 1. Create an LLM callable (see "Custom LLM Callable" below)
llmCall = ...
# 2. Create a callable MCP server (see "MCP Server Integration" below)
mcpServer = ...
# 3. Create the agent
agent = yiemAgent(llmCall; mcpServer=mcpServer, eventSink=println)
# 4. Interact
promptAgent(agent, "What tools do you have?")
response = takeResponse(agent)
# 5. Stop when done
stopAgent(agent)
```
## Agent API
| Function | Description |
|----------|-------------|
| `promptAgent(agent, text)` | Send plain text to the agent's input channel |
| `promptAgent(agent, msg)` | Send a structured message (OpenAI format dict) to the input channel |
| `takeResponse(agent)` | Block and take the agent's response from output channel |
| `followUp(agent, text)` | Send plain text follow-up while agent is still processing |
| `followUp(agent, msg)` | Send a structured follow-up (OpenAI format dict) while agent is processing |
| `stopAgent(agent)` | Gracefully stop the agent and close channels |
## Creating a yiemAgent
```julia
yiemAgent(llmCall;
systemPrompt::String = "You are a helpful assistant.",
model = nothing,
messages::Vector{agentMessage} = agentMessage[],
prepareContext::Function = prepareContext,
formatMsgForLLM::Function = formatMsgForLLM,
beforeToolCall::Function = beforeToolCall,
afterToolCall::Function = afterToolCall,
sessionId::Union{String, Nothing} = nothing,
maxRetryDelayMs::Union{Int64, Nothing} = nothing,
parallelToolExecute::Bool = false,
eventSink::Function = eventSink,
mcpServer = nothing
)
```
| Parameter | Description |
|-----------|-------------|
| `llmCall` | **(required)** Function that receives formatted messages and returns an LLM response |
| `systemPrompt` | System prompt for the agent |
| `model` | LLM model configuration (`llmModel` struct) |
| `messages` | Initial conversation messages |
| `prepareContext` | Hook to transform messages/context before each LLM call |
| `formatMsgForLLM` | Hook to convert agent context to LLM API format |
| `beforeToolCall` | Hook to block/allow tool execution |
| `afterToolCall` | Hook to post-process tool results |
| `sessionId` | Optional session identifier for MCP server |
| `maxRetryDelayMs` | Maximum retry delay in milliseconds |
| `parallelToolExecute` | Run multiple tool calls in parallel |
| `eventSink` | Callback for agent events/debug messages |
| `mcpServer` | Callable struct for MCP server communication |
## Usage Examples
### Text Messages
```julia
# Send plain text
promptAgent(agent, "What tools do you have?")
response = takeResponse(agent)
# Follow-up while agent is processing
promptAgent(agent, "Tell me about wine")
followUp(agent, "Also include prices")
response = takeResponse(agent)
```
### Structured Messages (OpenAI Format)
```julia
promptAgent(agent, Dict("role" => "user", "content" => "Hello"))
response = takeResponse(agent)
```
### Output Format
`takeResponse()` returns a `Dict{String, Any}` in OpenAI message format:
```julia
Dict(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => "The weather in Tokyo is Sunny, 22°C."),
# ... more content blocks
]
)
```
Content block types:
| Block Type | Fields | Description |
|------------|--------|-------------|
| `text` | `"type" => "text"`, `"text" => "..."` | Plain text response from the LLM |
| `image_url` | `"type" => "image_url"`, `"image_url" => Dict("url" => "...")` | Image content (rare, used by multimodal models) |
Example — extracting the text response:
```julia
response = takeResponse(agent)
text_response = first(c["text"] for c in response["content"] if c["type"] == "text")
```
### Multimodal Messages (Text + Image)
```julia
# Encode images to data URIs
image_path = "test/image.png"
image_bytes = read(image_path)
base64_string = base64encode(image_bytes)
data_uri = "data:image/png;base64,$(base64_string)"
# Build message with text and image content blocks
message = Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Do you have Brunello di Montalcino?"),
Dict("type" => "image_url", "image_url" => Dict("url" => data_uri))
]
)
promptAgent(agent, message)
response = takeResponse(agent)
```
### Custom Hooks
```julia
# beforeToolCall — request user approval for destructive tools
function my_before_tool_call(context::beforeToolCallContext, signal::abortSignal)::beforeToolCallResult
if context.toolCall.name == "deleteFile"
return beforeToolCallResult(true, "User must approve file deletion")
end
return beforeToolCallResult(false, "N/A")
end
# afterToolCall — mask sensitive data from results
function my_after_tool_call(context::afterToolCallContext, signal::abortSignal)::Union{agentToolResult, Nothing}
if context.isError
return agentToolResult(
[textContent("The tool encountered an error. Please try again.")],
context.result.details, context.result.usage, context.result.terminate
)
end
return nothing
end
agent = yiemAgent(llmCall;
mcpServer=mcpServer,
beforeToolCall=my_before_tool_call,
afterToolCall=my_after_tool_call,
eventSink=println
)
```
### Custom LLM Callable
```julia
function my_llm_call(messages_dict::Dict{String, Any})
# messages_dict contains "messages" key with OpenAI-format message array,
# plus optional "tools", "temperature", etc.
# Call your LLM provider (OpenAI, Anthropic, local model, etc.)
raw_response = call_my_llm_provider(messages_dict)
# Return a dict with these fields:
return Dict(
"message" => Dict("role" => "assistant", "content" => raw_response["content"]),
"finish_reason" => raw_response["finish_reason"],
"api" => "my_provider",
"provider" => "my_provider",
"model" => "my-model-name",
"usage" => llmUsage(100, 50)
)
end
```
### Custom Context Preparation
```julia
function my_prepare_context(state::agentState, eventSink)::agentContext
sysPrompt = state.systemPrompt * "\nCurrent time: $(now())"
msgs = deepcopy(state.messages)
# Prune old messages to fit context window
max_messages = 20
if length(msgs) > max_messages
msgs = msgs[end-max_messages+1:end]
end
return agentContext(sysPrompt, msgs, state.tools)
end
agent = yiemAgent(llmCall; prepareContext=my_prepare_context, ...)
```
## MCP Server Integration
YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement `tools/list` and `tools/call` methods.
### Protocol
```julia
# tools/list — returns tool definitions
mcpServer("tools/list")
# → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...))
# tools/call — executes a tool
mcpServer("tools/call", toolName, arguments)
# → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...))
# Protocol error responses include "error" instead of "result":
Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"..."))
```
### Example: NATS-based MCP Server Client
```julia
struct mcpServer
natsConn::NATS.Connection
topic::String
senderID::String
fileserver_url::String
end
# tools/list implementation
function (c::mcpServer)(method::String)
payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}())
payloads = [("payload", payload, "dictionary")]
_, msg_envelope = msghandler.smartpack(c.topic, payloads;
sender_id=c.senderID, msg_purpose="mcp_tools_list", fileserver_url=c.fileserver_url)
reply = NATS.request(c.natsConn, c.topic, msg_envelope, timeout=180)
incoming = msghandler.smartunpack(String(reply.payload))
return incoming["payloads"][1][2]
end
# tools/call implementation
function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any})
payload = Dict("jsonrpc" => "2.0", "id" => 2, "method" => method,
"params" => Dict("name" => toolName, "arguments" => arguments))
payloads = [("payload", payload, "dictionary")]
_, msg_envelope = msghandler.smartpack(c.topic, payloads;
sender_id=c.senderID, msg_purpose="mcp_tool_call", fileserver_url=c.fileserver_url)
reply = NATS.request(c.natsConn, c.topic, msg_envelope, timeout=180)
incoming = msghandler.smartunpack(String(reply.payload))
return incoming["payloads"][1][2]
end
```
## Tool Discovery
Tools are discovered dynamically via the MCP server:
1. `listTools` is the only pre-registered tool at agent creation
2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime
3. Supports pagination via `nextCursor` for large tool sets
4. New tools become immediately available without restarting the agent
## Hooks
Hooks allow customization of the agent's behavior at key points in the execution pipeline.
| Hook | Signature | Purpose |
|------|-----------|---------|
| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Transform messages/context before LLM call (filter tools, inject context, prune messages) |
| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | Convert agent context to LLM API message format |
| `beforeToolCall` | `(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult` | Block/allow tool execution; request user approval |
| `afterToolCall` | `(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}` | Post-process tool results; mask sensitive data; set `terminate` flag |
| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages throughout the pipeline |
## Agent Lifecycle
1. **Creation** — `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel`
2. **Input** — User messages enter via `promptAgent()` (immediate) or `followUp()` (queued while agent is busy)
3. **Processing** — `_processMessage()` handles LLM calls, tool execution, and conversation history in a loop
4. **Tool Execution Pipeline** — each tool call follows three phases:
- `prepareToolCall` — resolve tool, validate arguments, run `beforeToolCall` hook
- `executePreparedToolCall` — invoke `tool.execute()`
- `finalizeExecutedToolCall` — run `afterToolCall` hook, construct `toolResultMessage`
5. **Termination** — tool `execute` functions can set `terminate=true` to stop the agent loop
6. **Shutdown** — `stopAgent()` sends a `:shutdown` signal, drains channels, and closes the agent
## Architecture
```
src/
├── YiemAgent.jl # Module entry point
├── type.jl # Core types (messages, tools, agent state, events)
├── utils.jl # Message formatting, context preparation, validation
├── toolRegistry.jl # Tool store, MCP discovery, listTools registration
├── agentCore.jl # Agent loop, tool execution pipeline, yiemAgent struct
└── api.jl # Public API (promptAgent, takeResponse, followUp, stopAgent)
```
+82
View File
@@ -0,0 +1,82 @@
# Store Policy
- Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
- If you found wines in the store's database, they are in stock.
- You can only recommend wines that are currently in our inventory
- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
- Ask the user one question at a time.
- Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database.
- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
- Spicy foods should be paired only with light red wines.
- We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such.
- Gift box, gift card, and custom messages are available. Inform the user to contact our sales team.
# Store Guidelines
- Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting.
- Customer may provide images for you to look up.
- Encourage the customer to explore different options and try new things.
- If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
- Your store carries only wine.
- Vintage 0 means non-vintage.
- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.
# Prompt
Search the database as broad as possible under the informantion you have will increase the chance to find wine. Avoid uneccessary parameter such as region, country, tasting notes unless the user specify
# Situation
Your customer is coming into the store
# Role
Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. You are working under your mentor supervision.
# Objective
1. Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences.
2. Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences.
# Responsibility Includes
1. According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the objective
2. Keep the conversation with the customer going smoothly
3. Obey your mentor's suggestions.
# Responsibility Does NOT Include
1. Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
2. Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
3. Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
# Available Actions
- **CHAT_BOX** which you can use to talk with the user.
- **SEARCH_WINE_DATABASE** allows you to check information about wines you want in your inventory's database. The input is text that specify supported search criteria includeing: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity.
- Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD."
- Example query 2: "Red or white wine, medium tannin, price under 700 USD"
- Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France
- **PRESENT_WINE_GUIDELINE** which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
- **END_CONVER_GUIDELINE** which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
# Response Format
You should respond to the user with interleaving plan, action_name, action_input:
1. **plan**: Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
2. **action_name**: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name
3. **action_input**: The input to the action you are about to perform according to your plan.
After the action is executed you gets "action_result". It is the output from the action you selected.
Assistant should only respond in JSON format as described below:
```json
{
"plan": "...",
"action_name": "...",
"action_input": "..."
}
```
+54
View File
@@ -0,0 +1,54 @@
{
"nats_server_info": {
"description": "nats server",
"url": "nats.yiem.cc"
},
"testingOrProduction": "testing",
"agentId": "2b74b87a-5413-4fe2-a4d3-405891051680",
"agentCentralConfigSubject": "/yiem/hq/agent/sommelier/backend/config/api/v1.1",
"this_service_name": "agent_backend",
"this_service_input_channel": {
"mqtt": [
"/yiem/hq/agent/sommpanion/backend/db/api_v1"
],
"nats": [
"sommpanion.backend.agentbackend.v1.inbox"
]
},
"agentRole": "sommelier",
"organization": "yiem_hq",
"externalservice": {
"servicesloadbalancer": {
"nats": "sommpanion.backend.servicesloadbalancer.v1.inbox"
},
"textembedding": {
"url": "textembedding.api.v1"
},
"textimage_to_text_llm": {
"url": "https://llmcoder.yiem.cc/v1/chat/completions",
"modelname": "Qwen3.6-35B-A3B-UD-Q4_K_M"
},
"virtualWineCustomer_1": {
"serviceSubject": "",
"modelName": "qwen3:8b"
},
"sommpanion_db" : {
"description": "A database connection info for LibPQ client",
"url": "192.168.88.106:5432",
"dbname": "winedb",
"user": "admin",
"password": "admin@Sommpanion_0.0"
},
"sommpanion_vectordb" : {
"description": "A wine database connection info for LibPQ client",
"url": "192.168.88.106:5433",
"dbname": "vectordb",
"user": "admin",
"password": "admin@Sommpanion_0.0"
},
"fileserver": {
"description": "temporary file server",
"url": "https://fileserver.yiem.cc"
}
}
}
+155
View File
@@ -0,0 +1,155 @@
# YiemAgent: Agile Agent Framework
## 1. Business Context & Success Metrics
### Business Goal
YiemAgent is a Julia-based AI agent framework that enables building chatbot assistants with tool-calling capabilities, model-agnostic LLM integration, and runtime tool discovery via MCP (Model Context Protocol). It provides an asynchronous agent loop with channel-based message passing, supporting both single-turn and multi-turn conversations with automatic tool call resolution.
### User Stories
- **US-001**: As a developer, I want to create an AI agent with a simple constructor so that I can quickly prototype chatbot functionality in Julia
- **US-002**: As a developer, I want the agent to discover tools automatically from an MCP server so that I don't need to manually register tool APIs
- **US-003**: As a developer, I want the agent to handle tool calls automatically in a loop so that the LLM can use tools without external orchestration
- **US-004**: As a developer, I want to intercept tool calls with before/after hooks so that I can add user approval, logging, or result sanitization
- **US-005**: As a developer, I want the framework to support multiple LLM models and providers through a pluggable `llmCall` interface so that I can swap models without changing agent logic
### KPIs & Targets
- **KPI-001**: Agent loop processes messages with <500ms overhead for sequential tool calls (validates US-003)
- **KPI-002**: MCP tool discovery completes within 2 seconds for 50+ tools (validates US-002)
- **KPI-003**: 100% of message types have OpenAI format conversion (validates US-001)
## 2. Technical Boundaries
### In Scope
- Julia module implementing an AI agent with asynchronous message processing
- Channel-based input (user→agent), follow-up (async queue), and output (agent→user) interfaces
- LLM model abstraction with tool call extraction from multiple response formats (OpenAI content blocks, LMStudio/vLLM format)
- Tool registry with MCP server integration (tools/list, tools/call via JSON-RPC 2.0)
- Agent loop with before/after tool call hooks for lifecycle management
- Message type system with OpenAI format conversion utilities
- Abort signal and termination flag support via tool implementations
### Out of Scope
- GUI/CLI interface (exposed via channels only; UI layer is external)
- Persistent chat history or long-term memory (TODO comments in code)
- Multi-agent orchestration (single agent instance per `yiemAgent` struct)
- Embedding generation or retrieval-augmented generation (RAG)
- Natural language-to-code generation or execution sandboxes
### Dependencies
- **Julia Standard Library**: `Dates`, `UUIDs`, `Base.Threads`
- **External Packages**: `JSON`, `DataStructures`, `HTTP`, `PrettyPrinting`, `Serialization`, `DataFrames`
- **Internal Package**: `GeneralUtils` (project-specific utility library)
- **Optional**: `NATS` (MCP server communication), `LibPQ` (PostgreSQL)
### Deployment Constraints
- **NFR-001**: Library designed for embedded use in Docker containers (MicroK8s cluster, per `NFR-501` from template)
- **NFR-002**: Must support Julia 1.9+ for module include/export semantics compatibility
- **NFR-003**: Thread-safe channel operations; single-agent instance processes messages sequentially on background `@spawn` task
## 3. Functional Requirements (FR)
### FR-001: Agent Message Processing
The agent shall process user messages through an asynchronous loop that:
- Accepts messages via `inputChannel` (capacity 16) and `followUpChannel` (capacity 32)
- Prioritizes `inputChannel` messages over `followUpChannel` messages
- Feeds responses to `outputChannel` (capacity 16) in OpenAI message format
- Exits cleanly on `:shutdown` signal
**Traceability**: US-001, US-003
### FR-002: LLM Model Agnosticism
The agent shall accept a pluggable `llmCall` function that:
- Receives formatted messages and returns an LLM response dict
- The response dict may contain `message.tool_calls` (LMStudio format) or `content` blocks (OpenAI format)
- The framework extracts tool calls regardless of response format
**Traceability**: US-005
### FR-003: Tool Call Execution Loop
The agent shall automatically execute tool calls returned by the LLM:
- Extract tool calls via `_extractToolCalls()` which supports both `message.tool_calls` and `content[{type="tool_calls"}]` formats
- Build `agentContext` via `prepareContext()` (pluggable hook)
- Format messages via `formatMsgForLLM()` to OpenAI format
- Extract tool calls → loop: execute tool calls → append results to history → call LLM again
- Break loop when LLM response has no tool calls
- Support `terminate` flag: if `toolResultBatch.terminate == true`, break loop and return final response
**Traceability**: US-003
### FR-004: Tool Discovery and Registration
The agent shall:
- Query MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0) to discover tools
- Support pagination via `nextCursor` field
- Wrap each MCP tool definition as an `agentTool` with `execute` hook that calls `mcpserver("tools/call", name, args, sessionId)`
- Register a builtin `listTools` tool that triggers runtime discovery and lists all available tools
- Skip tools already registered (idempotent)
**Traceability**: US-002
### FR-005: Tool Call Lifecycle Hooks
The agent shall support two lifecycle hooks:
- `beforeToolCall(beforeToolCallContext, signal)` — can block execution by returning `beforeToolCallResult(true, reason)`
- `afterToolCall(afterToolCallContext)` — can transform result by returning modified `agentToolResult`
- Errors from hooks are caught and converted to `immediateOutcome`/error tool result messages (never throw)
**Traceability**: US-004
### FR-006: Message Type Conversion
The agent shall:
- Convert internal message types (`userMessage`, `assistantMessage`, `assistantMessageToolCall`, `toolResultMessage`) to OpenAI format via `_userMessageToOpenAI()`, `_assistantMessageToOpenAI()`, `_assistantMessageToolCallToOpenAI()`, `_toolResultMessageToOpenAI()`
- Convert internal `messageContent` types (`textContent`, `imageContent`, `reasoningContent`) to OpenAI content blocks via `_messageContentToBlocks()`
- Convert `agentTool` definitions to OpenAI function tool format via `_toolsToOpenAI()`
**Traceability**: US-001
## 4. Non-Functional Requirements (NFRs)
### 4.1 Performance & Scalability
- **NFR-101**: Agent loop message processing overhead <500ms per sequential tool call (measured KPI: KPI-001)
- **NFR-102**: MCP tool discovery for 50+ tools completes within 2 seconds (measured KPI: KPI-002)
- **NFR-103**: Channel buffers prevent unbounded memory growth — `inputChannel` (16), `followUpChannel` (32), `outputChannel` (16)
### 4.2 Availability & Reliability
- **NFR-201**: Agent loop catches all errors via `try/catch` in `_agentLoop()` and calls `eventSink(errMsg)` before exit (measured by event sink logging)
- **NFR-202**: `prepareToolCall()` never throws errors — validation failures return `immediateOutcome` with error tool result
### 4.3 Privacy & Security
- **Data Classification**: Tool arguments and results may contain PII (user messages); MCP tool results treated as untrusted input
- **No built-in auth/authz**: Responsibility of `mcpServer` implementation and `beforeToolCall` hook
- **Input validation**: JSON schema validation via `validateToolArguments()` checks required fields only (tools need custom validation via `validateRequiredArgs` hook)
### 4.4 Observability & Telemetry
- **Required Logs**: `agentLoop` events (via `eventSink`) emit state machine steps (`_agentLoop 1` through `_agentLoop 6`), `_processMessage` steps (1 through 18), `prepareToolCall` steps (1 through 10)
- **Critical Metrics**:
- `agent._state.messages.length` — conversation history size (logged at each step)
- `toolExecutionStart`/`toolExecutionEnd` events (via `toolExecStartEvent`, `toolExecEndEvent` structs)
- **Retention**: No built-in log retention; agent is a library, does not manage external logging
## 5. Acceptance Conditions
- [ ] **FR-001**: Agent processes messages through channel system with priority ordering (inputChannel > followUpChannel)
- [ ] **FR-002**: `llmCall` is pluggable; response formats supported include `message.tool_calls` dict and `content` array blocks
- [ ] **FR-003**: Agent loops on tool calls until LLM returns no tool calls or terminate flag is set
- [ ] **FR-004**: MCP tools are discovered via `tools/list` with pagination; all registered tools are callable via `tools/call`
- [ ] **FR-005**: `beforeToolCall` hook can block; `afterToolCall` hook can transform result; errors return error tool result
- [ ] **FR-006**: All message types (userMessage, assistantMessage, assistantMessageToolCall, toolResultMessage) convert to OpenAI format
- [ ] **NFR-101**: Message processing overhead <500ms per sequential tool call
- [ ] **NFR-102**: MCP discovery for 50+ tools completes <2 seconds
- [ ] **NFR-201**: Agent loop catches errors and exits gracefully without panics
+119
View File
@@ -0,0 +1,119 @@
# Solution Design: YiemAgent Agent Framework
## 1. Problem Decomposition
| Problem | Description | User Impact |
|---------|-------------|-------------|
| P-001 | Developers need a chatbot framework in Julia that handles LLM calls and tool calls without writing boilerplate | Must write separate request/response handling, tool call parsing, and loop management code for each agent |
| P-002 | Tool discovery is manual and repetitive | Must register every tool individually; no automatic discovery from MCP-compatible servers |
| P-003 | Multiple LLM providers have different response formats | Must handle `message.tool_calls` (LMStudio/vLLM) vs `content` blocks (OpenAI API) separately |
| P-004 | Tool execution needs lifecycle hooks for observability and control | No built-in before/after hooks for authorization, logging, or result sanitization |
| P-005 | Agent state management is error-prone | Must handle message history, tool registration, and pending tool call tracking manually |
## 2. Solution Approach
**Approach**: "Build a channel-based agent system where the LLM talks to tools automatically, tools talk to MCP servers automatically, and you compose agents by injecting `llmCall` and `mcpServer` functions — the framework owns the loop, the state, and the format conversion."
**Key Principles**:
- **Channel-based async processing**: Each agent owns three channels (`inputChannel`, `followUpChannel`, `outputChannel`); a background `@spawn` task runs the agent loop, processing messages in priority order
- **Tool call loop**: The agent calls `llmCall()`, extracts tool calls, executes them (sequentially or in parallel), appends results, and calls `llmCall()` again — repeating until no tool calls remain
- **Pluggable hooks**: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, and `llmCall` are all injectable functions — the framework provides sensible defaults, but you override what you need
- **MCP-native tool discovery**: Tools are discovered at startup via MCP `tools/list` and can be discovered at runtime via the `listTools` builtin tool (idempotent registration with pagination support)
- **Format-agnostic LLM responses**: `_extractToolCalls()` handles both OpenAI content blocks and LMStudio/vLLM nested message format, allowing the same agent to work with any model provider
## 3. Decision Rationale
| Decision ID | Decision | Rationale | Alternatives Rejected |
|-------------|----------|-----------|----------------------|
| SD-001 | Use channel-based async processing with `@spawn` task | Provides non-blocking message queues; `inputChannel` blocks if full (backpressure); `followUpChannel` allows async follow-ups while agent is busy | Threading with shared state — race conditions; callback-based async — hard to compose |
| SD-002 | Background `_agentLoop` task runs message dispatch loop | Isolates message processing from construction; agent can be created and used concurrently | Synchronous agent.run(message) — blocks caller; manual task management — boilerplate |
| SD-003 | `_processMessage` runs `llmCall()` in an inner loop (while loop) until no tool calls remain | Handles multi-turn tool use transparently — agent decides when to stop based on LLM response | External loop in caller code — duplicates agent loop logic; single-turn only — no multi-step tool use |
| SD-004 | MCP server integration via callable struct `mcpServer(method, args...)` | MCP is model-agnostic protocol; callable struct allows type-stable single dispatch; supports NATS transport (struct has `natsConn`, `topic`, `senderID` fields) | Ad-hoc HTTP client for MCP — breaks NATS-only transports; tight coupling to specific transport |
| SD-005 | Tool call lifecycle: `prepareToolCall()` → `executePreparedToolCall()` → `finalizeExecutedToolCall()` — three phases, never throw errors | Preparation validates args and runs `beforeToolCall` hook (can block); execution calls `tool.execute()`; finalization runs `afterToolCall` hook. Each phase returns a result type, not exceptions — agent loop catches all and builds error tool result | `try/catch` in loop — loses context; throwing errors breaks tool result message contract |
| SD-006 | Message types are Julia structs with keyword constructors (`userMessage(content=[...])`) | Type stable; keyword constructors provide defaults (`role="user"`, `timestamp=now()`); supports both positional and keyword usage | `Dict{String, Any}` for messages — lose type safety, docgen, IDE support |
| SD-007 | `eventSink` function passed to all deep lifecycle calls | Provides hooks for external monitoring (logging, metrics, NATS pub/sub) without modifying core logic | Built-in `println` — can't disable or redirect; no metrics integration |
| SD-008 | `abortSignal` struct with `.aborted::Bool` field passed to `prepareToolCall()` and hooks | Simple mutable state for interrupting long-running tool calls (no async cancellation — just flag check) | Threading with `Task.cancel()` — Julia thread model doesn't support it; `@async` spawn — complex cleanup |
| SD-009 | `_extractToolCalls()` supports both `message.tool_calls` and `content[{type="tool_calls"}]` formats | LMStudio.jl/vLLM vs OpenAI API have different response shapes; framework must work with both | Separate parser per provider — maintenance burden; require provider wrapper — adds indirection |
## 4. High-Level Component Diagram
```mermaid
flowchart TD
User[External Caller] -->|promptAgent / followUp| InputCh[inputChannel]
subagent AgentLoop [Agent Loop Task]
InputCh -->|take!| Disp[Message Dispatcher]
Disp -->|spawn| Proc[_processMessage]
Proc -->|Channel| PrCh[processMessageInputCh]
end
subagent Message Pipeline
PrCh -->|put!| DMsg[Drain input, convert to userMessage]
DMsg --> PC[prepareContext hook]
PC --> FM[formatMsgForLLM hook]
FM --> LLM[llmCall]
LLM --> EC[_extractToolCalls]
EC -->|hasToolCalls| TC[executeToolCalls]
TC -->|toolResultBatch| TM[toolResultMessage]
TM -->|push!| MH[messageHistory]
TC -->|no tool calls break| BRK[break loop]
BRK --> End[exit]
end
subagent Tool System
TC --> Prep[prepareToolCall]
Prep --> Exec[executePreparedToolCall]
Exec --> Term[finalizeExecutedToolCall]
Term --> CRM[createToolResultMessage]
CRM --> TM
end
subagent MCP Integration
Prep --> ToolLookup[context.tools lookup]
ToolLookup --> MCP[MCP tools/list + tools/call]
MCP --> Wr[_wrap_mcp_tool]
Wr --> Reg[toolRegistry.registerAllTools]
end
OutputCh[outputChannel] --> User
Proc -->|final response| OutputCh
```
**Component Descriptions**:
- **Agent Loop Task** (`SD-001`, `SD-002`): Runs `@spawn _agentLoop()`; manages three channels; dispatches messages to `_processMessage`
- **Message Pipeline** (`SD-003`, `SD-004`): `prepareContext` → `formatMsgForLLM` → `llmCall` → extract tool calls → loop
- **Tool System** (`SD-005`): Three-phase lifecycle (prepare → execute → finalize); never throws; returns error tool result on failure
- **MCP Integration** (`SD-004`): `tools/list` discovery with pagination; `_wrap_mcp_tool` creates executable `agentTool` with MCP JSON-RPC calls
## 5. Alternatives Considered
| Alternative | Pros | Cons | Decision |
|-------------|------|------|----------|
| Synchronous `agent.process(message)` | Simpler API; no channels or tasks | Blocks caller; no follow-up support; harder to test concurrency | **Rejected** — async channels needed for real-time chat UX |
| JSON dicts for messages everywhere | Flexible; no type definitions | No static checking; no docgen; no IDE autocomplete; harder to serialize/deserialize | **Rejected** — Julia advantages of structs (type-stable, documented) outweigh cost |
| Single `onToolCall` hook (pre-execute only) | Simpler hook interface | Can't sanitize results post-execution; can't set terminate flag after tool completes | **Rejected** — `afterToolCall` needed for masking secrets, normalizing usage, inspecting error status |
| Direct HTTP calls to MCP servers (no NATS) | Simpler dependency (no NATS) | Breaks if MCP uses NATS (common in microservices); no single transport | **Rejected** — callable struct abstracts transport; NATS is one implementation option |
| External orchestrator runs tool call loop | Caller has full control | Duplicates logic; every caller must implement same loop; harder to maintain | **Rejected** — loop is agent framework responsibility (single source of truth) |
## 6. Risk Assessment
| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| `llmCall` throws (network timeout, API error) | Agent loop exits with error in `eventSink` | Medium | `try/catch` in `_processMessage` and `_agentLoop`; eventSink receives error string |
| Tool `prepareArguments` or `validateRequiredArgs` hook throws | `prepareToolCall` catches and returns `immediateOutcome` error | Low | Hook errors caught; error tool result sent to LLM for recovery |
| MCP server unreachable | Tool discovery returns 0 tools; `listTools` still available | Medium | `register_mcp_tools` returns 0 on error; tool execution calls catch errors |
| Long-running tool call hangs | Caller blocks on `outputChannel`; no built-in timeout | Medium | `eventSink` passes abort signal; caller sets `signal.aborted = true` externally |
| Multiple agents share same `mcpServer` struct | `mcpServer` must be thread-safe for concurrent tool calls | Low | Each thread gets own `mcpServer` instance; NATS handles concurrency |
| Message history grows unbounded | Memory exhaustion in long sessions | Medium | No built-in pruning; `prepareContext` can filter/prune messages (TODO in code) |
## 7. Requirements Traceability
| Solution Component | Requirement ID | Decision ID | Description |
|-------------------|----------------|-------------|-------------|
| Channel-based async processing | FR-001 | SD-001, SD-002 | `inputChannel`, `followUpChannel`, `outputChannel` with priority dispatch |
| LLM call loop | FR-003 | SD-003 | `_processMessage` while loop; `llmCall()` → extract → execute → append → repeat |
| MCP tool discovery | FR-004 | SD-004, SD-009 | `tools/list` with pagination; `tools/call` execution |
| Lifecycle hooks | FR-005 | SD-005 | `beforeToolCall` can block; `afterToolCall` transforms; never throw |
| Message type conversion | FR-006 | SD-006 | Julia structs with keyword constructors; `_userMessageToOpenAI`, `_messageContentToBlocks` |
| Format-agnostic extraction | FR-002 | SD-009 | `_extractToolCalls` handles both `message.tool_calls` and `content` blocks |
| Event sink observability | NFR-201, NFR-103 | SD-007 | `eventSink` passed to all lifecycle steps; logs state machine transitions |
+318
View File
@@ -0,0 +1,318 @@
# Walkthrough: YiemAgent Agent Framework
## Structural Guidance
- One **overview sequence diagram** per participant (high-level flow)
- **Detailed sub-diagrams** for complex interaction points (tool call loop, MCP discovery)
- Use sequence diagrams for request/response patterns; lifecycle states for multi-step operations
---
## External Caller
_The Developer calling `YiemAgent.jl` via `promptAgent`, `takeResponse`, `followUp`, and `stopAgent`._
### Session Start: Create Agent
**WT-001**: Developer creates an `yiemAgent` instance with `llmCall` and `mcpServer`
> Cross-reference: `2-solution_design.md:SD-001` (channel-based async), `SD-004` (MCP struct)
**Details**:
```julia
mcp = mcpServer(conn, topic, senderID)
agent = yiemAgent(llmCall, mcpServer=mcp, systemPrompt="You are a helpful assistant")
```
**Sequence**:
```mermaid
sequenceDiagram
participant Dev as Developer
part agent YiemAgent Constructor
Dev->>agent: yiemAgent(llmCall, ; mcpServer=mcp)
agent->>agent: create channels (input=16, followUp=32, output=16)
agent->>agent: registerAllTools(tools, mcp)
agent->>mcp: mcp("tools/list", sessionId)
mcp-->>agent: response (tools array + nextCursor)
agent->>agent: _wrap_mcp_tool for each tool
agent->>agent: registerTool(tools, listTools_tool)
agent->>agent: println("[toolRegistry] Discovered N MCP tools")
agent->>agent: yiemAgent struct with placeholder task
agent->>agent: @spawn _agentLoop(agent)
agent-->>Dev: yiemAgent instance
```
> **Traceability**: FR-004 (tool discovery), US-001 (agent creation)
### Send Message: User Prompt
**WT-002**: Developer sends a user message to the agent
> Cross-reference: `2-solution_design.md:SD-002` (background task dispatch)
**Sequence**:
```mermaid
sequenceDiagram
participant Dev as Developer
part agent AgentLoop
Dev->>agent: promptAgent(agent, text="What's the weather?")
agent->>agent: put!(inputChannel, msg_dict)
Dev->>agent: takeResponse(agent) # blocks
agent->>agent: inputChannel isready
agent->>agent: take!(inputChannel)
agent->>Dev: (returns agent for chaining)
```
> **Traceability**: FR-001 (channel processing), PR-001, SD-001, SD-002
### Receive Response
**WT-003**: Agent returns response on `outputChannel`
**Sequence**:
```mermaid
sequenceDiagram
participant Dev as Developer
part agent AgentLoop
AgentLoop->>AgentLoop: _processMessage runs
AgentLoop->>AgentLoop: while loop, llmCall, tool calls...
AgentLoop->>AgentLoop: no more tool calls → exit loop
AgentLoop->>AgentLoop: filter reasoningContent from final result
AgentLoop->>AgentLoop: _assistantMessageToOpenAI(result)
AgentLoop->>AgentLoop: put!(outputChannel, respondToUI)
AgentLoop->>Dev: take!(outputChannel) # unblocks
Dev->>Dev: receive OpenAI dict response
```
> **Traceability**: FR-001, FR-003, FR-006, PR-001
### Send Follow-up While Agent Busy
**WT-004**: Developer sends a follow-up message while agent is processing a tool call
**Sequence**:
```mermaid
sequenceDiagram
participant Dev as Developer
part agent AgentLoop
Dev->>agent: promptAgent(text="What's the weather in Tokyo?") # agent starts processing
AgentLoop->>AgentLoop: _processMessage started (processingTask = Task)
Dev->>agent: followUp(text="Also check Seoul") # while busy
agent->>agent: put!(followUpChannel, msg_dict)
AgentLoop->>AgentLoop: _processMessage completes (tool calls done)
AgentLoop->>AgentLoop: outputChannel has message → put!(outputChannel, response), take!(next msg)
AgentLoop->>AgentLoop: no inputChannel → check followUpChannel
AgentLoop->>AgentLoop: isready(followUpChannel) → take!(followUpMsg), put!(inputChannel, followUpMsg)
AgentLoop->>AgentLoop: processingTask = nothing → reset
AgentLoop->>AgentLoop: loop back → take!(inputChannel) from followUp
Dev->>agent: takeResponse() → blocks
AgentLoop->>Dev: next response via outputChannel
```
> **Traceability**: FR-001 (follow-up priority), PR-001, SD-001, SD-002
### Stop Agent
**WT-005**: Developer stops the agent gracefully
**Sequence**:
```mermaid
sequenceDiagram
participant Dev as Developer
part agent AgentLoop
Dev->>agent: stopAgent(agent)
agent->>agent: put!(inputChannel, :shutdown)
AgentLoop->>AgentLoop: check newUserMsg === :shutdown
AgentLoop->>AgentLoop: drain inputChannel (while isready)
AgentLoop->>AgentLoop: drain followUpChannel (while isready)
AgentLoop->>AgentLoop: break, catch, fetch(_agentLoop)
Dev->>agent: close(inputChannel, outputChannel, followUpChannel)
Dev-->>Dev: nothing (agent destroyed)
```
> **Traceability**: FR-001, NFR-201 (graceful exit), PR-001
---
## LLM Provider
_The external LLM API (e.g. Anthropic, OpenAI, LMStudio) receiving formatted messages and returning responses._
### Receive Formatted Message
**WT-010**: Agent sends LLM request in OpenAI format
> Cross-reference: `2-solution_design.md:SD-003` (formatMsgForLLM)
**Sequence**:
```mermaid
sequenceDiagram
part agent AgentLoop
AgentLoop->>AgentLoop: preparedContext = prepareContext(state)
AgentLoop->>AgentLoop: formattedMessages = formatMsgForLLM(preparedContext)
Note over AgentLoop: messages: [system, user, assistant, tool, ...]<br/>tools: [{"function": {"name": "getWeather", "parameters": {...}}}]
agentLoop->>llmCall: llmCall(formattedMessages, eventSink=eventSink)
llmCall->>Provider: HTTP POST /v1/chat/completions (JSON)
Provider-->>llmCall: JSON response
llmCall-->>AgentLoop: response = JSON.Object{String, Any}(...)
```
> **Traceability**: FR-002, SD-003, SD-009
### Receive Response: No Tool Calls
**WT-11**: Agent receives text-only response
**Sequence**:
```mermaid
sequenceDiagram
llmCall-->>AgentLoop: response = {finish_reason=> "stop", message=> {role=> "assistant", content=> "It's 22°C in Bangkok"}}
AgentLoop->>AgentLoop: _extractToolCalls(response)
AgentLoop->>AgentLoop: hasToolCalls = false
AgentLoop->>AgentLoop: assistant_msg = assistantMessage(content=[textContent("It's 22°C")], ...)
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
AgentLoop->>AgentLoop: break (no tool calls)
AgentLoop->>AgentLoop: filter reasoningContent (if any)
AgentLoop->>AgentLoop: create final assistantMessage
AgentLoop->>AgentLoop: put!(outputChannel, OpenAI format dict)
```
> **Traceability**: FR-003 (loop exit), FR-006 (OpenAI conversion), PR-003
### Receive Response: With Tool Calls
**WT-012**: Agent receives tool call response
**Sequence**:
```mermaid
sequenceDiagram
llmCall-->>AgentLoop: response = {finish_reason=> "tool_calls",<br/>message=> {role=> "assistant",<br/>reasoning_content=> "I need to call getWeather...",<br/>tool_calls=> [{"id"=>"call_1", "function"=>{name=>"getWeather", arguments=>"{\"city\":\"Bangkok\"}"}}, ...]}}
AgentLoop->>AgentLoop: _extractToolCalls(response)
AgentLoop->>AgentLoop: hasToolCalls = true<br/>toolCallList = [agentToolCall("getWeather", ...)]
AgentLoop->>AgentLoop: assistant_msg = assistantMessageToolCall(toolCalls, reasoningContent, ...)
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
AgentLoop->>AgentLoop: config = agentLoopConfig(beforeToolCall, afterToolCall, parallel="sequential")
AgentLoop->>AgentLoop: toolResultBatch = executeToolCalls(...)
...
```
> **Traceability**: FR-002, FR-003, PR-003, SD-003, SD-005, SD-009
---
## MCP Server
_The Model Context Protocol server providing tools via JSON-RPC 2.0 (transport via NATS or HTTP).
### Receive tools/list Request
**WT-020**: Agent discovers tools on startup
> Cross-reference: `2-solution_design.md:SD-004` (MCP integration)
**Sequence**:
```mermaid
sequenceDiagram
part agent AgentLoop
agent->>mcp: mcp("tools/list", sessionId; eventSink=eventSink)
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/list", "id"=>..., "params"=>{...}}
MCP_server-->>agent: response = {<br/> "result" => {<br/> "tools" => [name, label, description, inputSchema], ...<br/> , "nextCursor" => 123<br/> }<br/>}
agent->>agent: for tool_def in tools_array<br/> _wrap_mcp_tool(mcp, tool_def)
agent->>agent: while cursor !== nothing → paginate
agent->>agent: println("[toolRegistry] Discovered N MCP tools. Total: $(length(tools))")
```
> **Traceability**: FR-004 (MCP discovery), PR-004
### Receive tools/call Request
**WT-021**: Agent executes tool via MCP
**Sequence**:
```mermaid
sequenceDiagram
part agent Tool System
agent->>mcp: mcp("tools/call", "getWeather", args={"city" => "Bangkok"}, sessionId; eventSink=eventSink)
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/call", "params"=>{name, arguments, sessionId}}
MCP_server-->>agent: response = {<br/> "result" => {<br/> "content" => [{"type"=>"text", "text"=>"Sunny, 22°C"}],<br/> "isError" => false<br/> }<br/>}
agent->>agent: check haskey(response, "error") → no
agent->>agent: result_data = response["result"]
agent->>agent: content_text = _extract_text_content(result_data)
agent->>agent: return agentToolResult([textContent(content_text)], details={isError=>false}, nothing, false)
```
> **Traceability**: FR-004 (tool execution), PR-004, SD-004, SD-005
### Receive Error from MCP Server
**WT-022**: MCP server returns error
**Sequence**:
```mermaid
sequenceDiagram
MCP_server-->>agent: response = {<br/> "error" => {<br/> "code" => -32601,<br/> "message" => "Method not found"<br/> }<br/>}
agent->>agent: haskey(response, "error") → true
agent->>agent: return agentToolResult(<br/> [textContent("MCP error: Method not found")],<br/> details={"error" => "Method not found"},<br/> nothing, false<br/>)
agent->>AgentLoop: finalOutcome(error=true)
agent->>AgentLoop: push!(agentMsgHistory, toolResultMessage(...))
agent->>AgentLoop: next LLM turn receives error result
```
> **Traceability**: FR-004 (error handling), NFR-202 (never throw), PR-004, SD-005
---
## Error Paths and Edge Cases
**WT-030**: LLM times out (network error)
> Same participant POV — from caller's perspective, they block on `takeResponse()` until task exits
```mermaid
sequenceDiagram
participant Dev as Developer
part agent AgentLoop
Dev->>agent: takeResponse(agent) # blocks
agent->>llmCall: llmCall(messages) # calls external API
lltimout
LLM provider-->>AgentLoop: ERROR (timeout)
AgentLoop->>AgentLoop: @error "Agent loop failed"
AgentLoop->>AgentLoop: eventSink("error: ...")
AgentLoop->>Dev: fetch(_agentLoop) throws TaskFailedException
Dev->>agent: stopAgent(agent) # cleanup channels
```
**Traceability**: PR-001, NFR-201 (catch error and exit), SD-001
**WT-031**: Tool execution throws exception
```mermaid
sequenceDiagram
agent->>agent: executePreparedToolCall
agent->>tool: tool.execute(toolCallId, args, signal, eventSink)
note over tool: tool raises exception (e.g., network error, invalid JSON)
tool-->>agent: throw Exception
agent->>agent: catch e
agent->>agent: executedOutcome(createErrorToolResult("error message"), true)
agent->>AgentLoop: finalizeExecutedToolCall
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
AgentLoop->>AgentLoop: push!(history, toolResultMessage)
AgentLoop->>AgentLoop: next llmCall with error in message history
```
**Traceability**: FR-005 (hook errors caught), NFR-202 (never throw), SD-005
**WT-032**: beforeToolCall hook blocks execution
```mermaid
sequenceDiagram
agent->>agent: beforeToolCall(beforeCtx, signal, eventSink)
agent->>agent: returns beforeToolCallResult(true, "User must approve")
agent->>agent: return immediateOutcome(createErrorToolResult("Tool blocked"), true)
agent->>AgentLoop: executeToolCalls → immediateOutcome
AgentLoop->>AgentLoop: finalizeExecutedToolCall
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
AgentLoop->>AgentLoop: push! to history
```
**Traceability**: FR-005 (beforeToolCall can block), SD-005
+518
View File
@@ -0,0 +1,518 @@
# Specification: YiemAgent Agent Framework
## 1. Message Type System (`src/type.jl`)
### SP-001: userMessage
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role::String` | `String` | Yes | Always `"user"` |
| `content::Vector{messageContent}` | `Vector{messageContent}` | No | Plain text and/or image content |
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was sent |
**Keyword Constructor**: `userMessage(; role="user", content=[...], timestamp=now())`
**Traceability**:
- Requirement: FR-006 (message type conversion)
- Solution Design: SD-006 (Julia structs for messages)
### SP-002: assistantMessage
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role::String` | `String` | Yes | Always `"assistant"` |
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Text and/or image content |
| `api::String` | `String` | Yes | API name used (e.g., `"openai"`) |
| `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`) |
| `model::String` | `String` | Yes | Model identifier |
| `usage::llmUsage` | `llmUsage` | Yes | Token usage (in/out) |
| `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"end_turn"`) |
| `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed |
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was received |
**Keyword Constructor**: `assistantMessage(; role="assistant", content=[...], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="end_turn", errorMessage=nothing, timestamp=now())`
**Traceability**:
- Requirement: FR-006 (message type conversion)
- Solution Design: SD-006
### SP-003: assistantMessageToolCall
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role::String` | `String` | Yes | Always `"assistant"` |
| `toolCalls::Vector{agentToolCall}` | `Vector{agentToolCall}` | Yes | Tool calls to execute |
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Reasoning/thinking content blocks |
| `api::String` | `String` | Yes | API name used |
| `provider::String` | `String` | Yes | Provider name |
| `model::String` | `String` | Yes | Model identifier |
| `usage::llmUsage` | `llmUsage` | Yes | Token usage |
| `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"tool_calls"`) |
| `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed |
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was received |
**Keyword Constructor**: `assistantMessageToolCall(; role="assistant", toolCalls=[...], content=[], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="tool_calls", errorMessage=nothing, timestamp=now())`
**Traceability**:
- Requirement: FR-003 (tool call loop)
- Solution Design: SD-005 (lifecycle phases), SD-009 (format-agnostic LLM responses)
### SP-004: toolResultMessage
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role::String` | `String` | Yes | Always `"toolResult"` (note: `"tool"` in example) |
| `toolCallId::String` | `String` | Yes | ID matching the tool call |
| `toolName::String` | `String` | Yes | Name of the executed tool |
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Tool output content |
| `details::Any` | `Any` | Yes | Additional tool-specific details |
| `usage::Union{llmUsage, Nothing}` | `Union{llmUsage, Nothing}` | Yes | Token usage if applicable |
| `addedToolNames::Union{Vector{String}, Nothing}` | `Union{Vector{String}, Nothing}` | Yes | Tools added during execution |
| `isError::Bool` | `Bool` | Yes | Whether the tool call resulted in an error |
| `timestamp::Timestamp` | `DateTime` | Yes | When the result was recorded |
**Keyword Constructor**: `toolResultMessage(; role="tool", toolCallId="", toolName="", content=[], details=nothing, usage=nothing, addedToolNames=nothing, isError=false, timestamp=now())`
**Traceability**:
- Requirement: FR-005 (tool lifecycle)
- Solution Design: SD-005
### SP-005: agentToolCall
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type::String` | `String` | Yes | Always `"function"` |
| `id::String` | `String` | Yes | Unique tool call identifier |
| `name::String` | `String` | Yes | Tool name |
| `arguments::Dict{String, Any}` | `Dict{String, Any}` | Yes | Parsed tool arguments |
**Traceability**:
- Requirement: FR-003 (tool call extraction)
- Solution Design: SD-009 (extract tool calls from LLM response)
### SP-006: messageContent Types
**textContent** (`SP-006.1`):
```julia
struct textContent <: messageContent
text::String
end
```
**imageContent** (`SP-006.2`):
```julia
struct imageContent <: messageContent
data::String # Base64-encoded image data
mimeType::String # MIME type (e.g., "image/png")
end
```
**reasoningContent** (`SP-006.3`):
```julia
struct reasoningContent <: messageContent
text::String
end
```
**Traceability**:
- Requirement: FR-006 (message content conversion)
- Solution Design: SD-006
## 2. LLM Model Configuration (`src/type.jl`)
### SP-007: llmModel
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id::String` | `String` | Yes | Unique model identifier |
| `name::String` | `String` | Yes | Human-readable model name |
| `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`, `"openai"`) |
| `baseUrl::String` | `String` | Yes | API endpoint base URL |
| `reasoning::Bool` | `Bool` | Yes | Whether the model supports chain-of-thought |
| `input::Vector{String}` | `Vector{String}` | Yes | Supported input modalities (e.g., `"text"`, `"image"`) |
| `cost::modelCost` | `modelCost` | Yes | Pricing information |
| `contextWindow::Int64` | `Int64` | Yes | Maximum context length in tokens |
| `maxTokens::Int64` | `Int64` | Yes | Maximum output tokens per completion |
**Traceability**:
- Requirement: FR-002 (LLM agnosticism)
- Solution Design: SD-003
### SP-008: modelCost
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `input::Float64` | `Float64` | Yes | Price per 1M input tokens |
| `output::Float64` | `Float64` | Yes | Price per 1M output tokens |
| `cache_read::Float64` | `Float64` | Yes | Price per 1M cached read tokens |
| `cache_write::Float64` | `Float64` | Yes | Price per 1M cache write tokens |
**Traceability**:
- Requirement: N/A (internal tracking data)
- Solution Design: SD-003
## 3. MCP Tool Integration (`src/toolRegistry.jl`)
### SP-009: tools/list Response Format
**Request**: `mcp("tools/list", sessionId; eventSink=eventSink)`
**Response** (JSON-RPC 2.0):
```json
{
"result": {
"tools": [
{
"name": "tool_name",
"title"?: "Human-readable title",
"label"?: "Fallback title",
"description": "What the tool does",
"inputSchema": {
"type": "object",
"properties": {
"field": { "type": "string", "description": "..." }
},
"required": ["field"]
}
}
],
"nextCursor"?: "pagination_cursor_or_empty"
}
}
```
**Response error**:
```json
{
"error": {
"code": -32601,
"message": "Method not found"
}
}
```
**Traceability**:
- Requirement: FR-004 (MCP discovery)
- Solution Design: SD-004
### SP-010: tools/call Request/Response Format
**Request**: `mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)`
**Response** (JSON-RPC 2.0):
```json
{
"result": {
"content": [
{"type": "text", "text": "Tool output text"}
],
"isError": false
}
}
```
**Response error**:
```json
{
"result": {
"content": [
{"type": "text", "text": "Error message"}
],
"isError": true
}
}
```
**Traceability**:
- Requirement: FR-004 (tool execution)
- Solution Design: SD-004 (`_wrap_mcp_tool`)
## 4. OpenAI Message Format (`src/utils.jl`)
### SP-011: system message format
```julia
Dict(
"role" => "system",
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
)
```
**Traceability**:
- Requirement: FR-006
- Solution Design: SD-006
### SP-012: userMessage → OpenAI format
```julia
Dict(
"role" => "user",
"content" => _messageContentToBlocks(msg.content)
)
```
Where `_messageContentToBlocks` converts:
- `textContent(t)` → `Dict("type" => "text", "text" => t)`
- `imageContent(data, mime)` → `Dict("type" => "image_url", "image_url" => Dict("url" => "data:$mime;base64,$data"))`
**Traceability**:
- Requirement: FR-006
- Solution Design: SD-006
### SP-013: assistantMessageToolCall → OpenAI format
```julia
Dict(
"role" => "assistant",
"content" => nothing,
"tool_calls" => [
Dict(
"id" => tc.id,
"type" => tc.type,
"function" => Dict(
"name" => tc.name,
"arguments" => JSON.json(tc.arguments) # serialized as JSON string
)
)
]
)
```
**Traceability**:
- Requirement: FR-003
- Solution Design: SD-009
### SP-014: assistantMessage → OpenAI format
```julia
Dict(
"role" => "assistant",
"content" => _messageContentToBlocks(msg.content)
)
```
**Traceability**:
- Requirement: FR-006
- Solution Design: SD-006
### SP-015: toolResultMessage → OpenAI format
```julia
Dict(
"role" => "tool",
"tool_call_id" => msg.toolCallId,
"content" => _messageContentToBlocks(msg.content)
)
```
**Traceability**:
- Requirement: FR-005, FR-006
- Solution Design: SD-005
### SP-016: agentTool → OpenAI tools format
```julia
Dict(
"type" => "function",
"function" => Dict(
"name" => tool.name,
"description" => tool.description,
"parameters" => tool.inputSchema
)
)
```
**Traceability**:
- Requirement: FR-004
- Solution Design: SD-004, SD-006
## 5. LLM Response Format Agnostic Extraction (`src/agentCore.jl`)
### SP-017: format 1 — LMStudio.jl / vLLM style (dict with nested `message.tool_calls`)
**Response dict** (key: `message::Dict`):
```json
{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": "",
"reasoning_content": "I need to call getWeather...",
"tool_calls": [
{
"type": "function",
"function": {
"name": "getWeather",
"arguments": "{\"city\":\"Bangkok\"}" // JSON string
},
"id": "call_1"
}
]
}
}
```
**When no tool calls**:
```json
{
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "It's 22°C in Bangkok"
}
}
```
**Traceability**:
- Requirement: FR-002
- Solution Design: SD-009
### SP-018: format 2 — OpenAI API style (content blocks array)
**Content blocks with tool calls** (`SP-018.1`):
```json
{
"content": [
{
"type": "tool_calls",
"tool_calls": [
{
"type": "function",
"function": {
"name": "getWeather",
"arguments": {"city": "Bangkok"} // already parsed dict
},
"id": "call_1"
}
]
}
]
}
```
**Content blocks with tool_call (already parsed)** (`SP-018.2`):
```json
{
"content": [
{
"type": "tool_call",
"name": "getWeather",
"arguments": {"city": "Bangkok"},
"id": "call_1"
}
]
}
```
**Content blocks with text only** (`SP-018.3`):
```json
{
"content": [
{"type": "text", "text": "It's 22°C in Bangkok"}
]
}
```
**Response dict extras** (common across all formats, `SP-018.4`):
```json
{
"api": "openai",
"provider": "anthropic",
"model": "gpt-4",
"usage": {"inputTokens": 100, "outputTokens": 50},
"error_message": "Optional error"
}
```
**Traceability**:
- Requirement: FR-002
- Solution Design: SD-009
## 6. Agent Loop Configuration (`src/type.jl` + `src/agentCore.jl`)
### SP-019: agentContext
```julia
struct agentContext
systemPrompt::String
messages::Vector{agentMessage}
tools::Union{OrderedDict{String, agentTool}, Nothing} # O(1) lookup by name
end
```
**Traceability**:
- Requirement: FR-003 (context preparation)
- Solution Design: SD-003, SD-004
### SP-020: agentState
```julia
mutable struct agentState
systemPrompt::String
model::Union{llmModel, Nothing}
tools::OrderedDict{String, agentTool} # insertion-ordered
messages::Vector{agentMessage} # history (no system prompt)
pendingToolCalls::Vector{String} # tool call IDs waiting for results
errorMessage::Union{String, Nothing} # last error message
end
```
**Traceability**:
- Requirement: FR-001 (agent state management)
- Solution Design: SD-006
### SP-021: agentLoopConfig
```julia
struct agentLoopConfig
beforeToolCall::Union{Function, Nothing}
afterToolCall::Union{Function, Nothing}
toolExecution::String # "sequential" or "parallel"
end
```
**Traceability**:
- Requirement: FR-005 (lifecycle hooks)
- Solution Design: SD-005
### SP-022: abortSignal
```julia
struct abortSignal
aborted::Bool
end
```
**Traceability**:
- Requirement: NFR-101 (timeout handling)
- Solution Design: SD-008
## 7. Tool Validation Hooks (`src/utils.jl`)
### SP-023: validateRequiredArgs (default hook)
**Signature**: `validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing, String}`
**Behavior**:
- Extract `required = get(inputSchema, "required", Any[])`
- Check each required field exists in `keys(args)`
- Return `nothing` if all present
- Return `"Missing required arguments: $(join(missing, ", "))"` if any missing
**Traceability**:
- Requirement: NFR-301 (input validation)
- Solution Design: SD-005
### SP-024: validateToolArguments (wrapper hook)
**Signature**: `validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}`
**Behavior**:
- If `tool.validateRequiredArgs !== nothing`: call `tool.validateRequiredArgs(prepared.arguments)`
- Else: call `validateRequiredArgs(prepared.arguments, tool.inputSchema)`
- If returns `!nothing`: throw `ArgumentError(result)`
- Caller (`prepareToolCall`) catches and returns `immediateOutcome`
**Traceability**:
- Requirement: FR-005 (tool validation)
- Solution Design: SD-005
+192
View File
@@ -0,0 +1,192 @@
# Validation: YiemAgent Agent Framework
## Test Architecture Overview
Because YiemAgent is a Julia library (not a web service), validation tests focus on:
- **Unit tests**: message type conversion, tool execution logic, format extraction
- **Integration tests**: full agent loop with stubbed LLM and MCP
- **Contract tests**: response format compatibility with OpenAI/LMStudio specs
- **Performance tests**: agent loop overhead for sequential tool calls
Every specification rule (SP-XXX), walkthrough journey (WT-XXX), and requirement (FR-XXX) must have a corresponding test.
---
## Test Types Mapping to Source Documents
| Test Type | Source Document | Example Coverage |
|-----------|-----------------|------------------|
| **Unit tests** | `4-specification.md` (SP-001..SP-024), `src/utils.jl` (converters) | Message type → OpenAI format conversion; tool argument validation |
| **Integration tests** | `3-walkthrough.md` (WT-001..WT-032) | Full agent loop: prompt → llmCall → tool extract → execute → response |
| **Contract tests** | `4-specification.md` SP-017, SP-018 | Response format 1 (LMStudio/vLLM) and format 2 (OpenAI) parsed correctly |
| **Performance tests** | `1-requirements.md` FR-003, NFR-101 | Agent loop overhead <500ms per sequential tool call |
| **Penetration tests** | `1-requirements.md` NFR-301 | Tool argument injection; MCP spoofing |
| **Schema validation** | `4-specification.md` SP-023, SP-024 | Required fields, type checking on tool arguments |
---
## Specification-to-Test Execution Trace
Every traceable item — specification rules (SP-XXX), walkthrough journeys (WT-XXX), and requirements (FR-XXX) — must have a runnable test. This trace maps each item to its corresponding test file and test function.
**Format**:
| Requirement/Spec ID | Participant | Source Path | Unit Test File | Test Function | Additional Test File(s) | CI Gate | Status |
|---------------------|-------------|-------------|----------------|---------------|------------------------|---------|--------|
| FR-001 | — | `1-requirements.md:3.1` | `tests/unit/channel-handling.jl` | `test_input_channel_priority` | `tests/integration/full-agent-loop.jl` | `ci/julia-unit.yaml` | **TODO** |
| FR-002 | — | `1-requirements.md:3.2` | `tests/unit/llm-format-extraction.jl` | `test_format1_tool_calls` | `tests/contract/response-format.jl` | `ci/julia-unit.yaml` | **TODO** |
| FR-003 | — | `1-requirements.md:3.3` | `tests/integration/tool-call-loop.jl` | `test_loop_exits_on_no_tool_calls` | — | `ci/julia-integration.yaml` | **TODO** |
| FR-004 | — | `1-requirements.md:3.4` | `tests/unit/mcp-tool-discovery.jl` | `test_tools_list_with_pagination` | `tests/contract/mcp-json-rpc.jl` | `ci/julia-unit.yaml` | **TODO** |
| FR-005 | — | `1-requirements.md:3.5` | `tests/unit/tool-hooks.jl` | `test_beforeToolCall_blocks` | `tests/integration/agent-loop.jl` | `ci/julia-unit.yaml` | **TODO** |
| FR-006 | — | `1-requirements.md:3.6` | `tests/unit/message-conversion.jl` | `test_userMessage_to_OpenAI` | — | `ci/julia-unit.yaml` | **TODO** |
| WT-001 | Caller | `3-walkthrough.md:#agent-creation` | `integration/agent-creation.jl` | `test_agent_construction_and_lifecycle` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-002 | Caller | `3-walkthrough.md:#send-message` | `integration/message-flow.jl` | `test_promptAgent_and_takeResponse` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-003 | Caller | `3-walkthrough.md:#receive-response` | `integration/response-format.jl` | `test_output_channel_delivery` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-004 | Caller | `3-walkthrough.md:#follow-up` | `integration/followup.jl` | `test_followUp_while_busy` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-005 | Caller | `3-walkthrough.md:#stop-agent` | `integration/shutdown.jl` | `test_stopAgent_channel_cleanup` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-010 | LLM Provider | `3-walkthrough.md:#receive-formatted` | `integration/llm-call.jl` | `test_formatMsgForLLM_output_structure` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-011 | LLM Provider | `3-walkthrough.md:#response-no-tool-calls` | `integration/non-tool-response.jl` | `test_lmm_response_no_tool_calls_breaks_loop` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-012 | LLM Provider | `3-walkthrough.md:#response-with-tool` | `integration/tool-response.jl` | `test_lmm_response_with_tool_calls` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-020 | MCP Server | `3-walkthrough.md:#discover-tools` | `unit/mcp-discovery.jl` | `test_mcp_tools_list_parsed` | `contract/mcp-json-rpc.jl` | `ci/julia-unit.yaml` | **TODO** |
| WT-021 | MCP Server | `3-walkthrough.md:#execute-tool` | `unit/mcp-tool-execute.jl` | `test_mcp_tools_call_response_parsed` | — | `ci/julia-unit.yaml` | **TODO** |
| WT-022 | MCP Server | `3-walkthrough.md:#mcp-error` | `unit/mcp-error-handling.jl` | `test_mcp_response_error_returned` | — | `ci/julia-unit.yaml` | **TODO** |
| WT-030 | Error | `3-walkthrough.md:#llm-timeout` | `integration/llm-timeout.jl` | `test_LLM_timeout_agent_loop_exits` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-031 | Error | `3-walkthrough.md:#tool-throw` | `integration/tool-error.jl` | `test_tool_execute_throws_returns_error_result` | — | `ci/julia-integration.yaml` | **TODO** |
| WT-032 | Error | `3-walkthrough.md:#beforeToolCall-block` | `unit/beforeToolCall-block.jl` | `test_beforeToolCall_blocks_execution` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-001 | — | `4-specification.md:#SP-001:userMessage` | `unit/message-types.jl` | `test_userMessage_struct_default_roles` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-002 | — | `4-specification.md:#SP-002:assistantMessage` | `unit/message-types.jl` | `test_assistantMessage_struct_default_stopReason_end_turn` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-003 | — | `4-specification.md:#SP-003:assistantMessageToolCall` | `unit/message-types.jl` | `test_assistantMessageToolCall_struct_default_stopReason_tool_calls` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-004 | — | `4-specification.md:#SP-004:toolResultMessage` | `unit/message-types.jl` | `test_toolResultMessage_struct_defaults` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-005 | — | `4-specification.md:#SP-005:agentToolCall` | `unit/message-types.jl` | `test_agentToolCall_type_is_always_function` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-006.1,2,3 | — | `4-specification.md:#SP-006:content-types` | `unit/content-types.jl` | `test_textContent_imageContent_reasoningContent` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-017 | — | `4-specification.md:#SP-017` | `contract/response-format.jl` | `test_lmStudio_vLLM_format_parsed` | — | `ci/julia-contract.yaml` | **TODO** |
| SP-018.1,2,3,4 | — | `4-specification.md:#SP-018` | `contract/response-format.jl` | `test_openAI_format_blocks_parsed` | — | `ci/julia-contract.yaml` | **TODO** |
| SP-023 | — | `4-specification.md:#SP-023` | `unit/tool-validation.jl` | `test_validateRequiredArgs_missing_fields` | — | `ci/julia-unit.yaml` | **TODO** |
| SP-024 | — | `4-specification.md:#SP-024` | `unit/tool-validation.jl` | `test_validateToolArguments_throws_on_missing` | — | `ci/julia-unit.yaml` | **TODO** |
| NFR-101 | — | `1-requirements.md:4.1` | `perf/agent-loop.jl` | `perf_agent_loop_overhead_<500ms` | `tests/perf/concurrency.jl` | `ci/julia-perf.yaml` | **TODO** |
| NFR-102 | — | `1-requirements.md:4.1` | `perf/mcp-discovery.jl` | `perf_mcp_discovery_50_tools_<2s` | — | `ci/julia-perf.yaml` | **TODO** |
| NFR-201 | — | `1-requirements.md:4.2` | `unit/error-handling.jl` | `test_agent_loop_catches_and_logs_errors` | `integration/llm-timeout.jl` | `ci/julia-unit.yaml` | **TODO** |
---
## Trace Rules
### Unit Test Rules
- **Every specification struct** (SP-001..SP-008, SP-019..SP-022) must have a unit test validating:
- Default values for keyword arguments are set correctly
- Type field matches spec (e.g., `agentToolCall.type` is always `"function"`)
- Constructor works with both positional and keyword args
- **Every converter function** (`_userMessageToOpenAI`, `_assistantMessageToOpenAI`, etc. SP-011..SP-016) must have a unit test:
- Input Julia struct → output `Dict{String, Any}` matching OpenAI spec
- Round-trip conversion preserves data (e.g., `textContent` text is preserved)
- **Every tool validation function** (SP-023, SP-024) must have unit tests:
- Missing required field → returns error string (not throws)
- All required present → returns `nothing`
- Custom `validateRequiredArgs` hook on tool takes precedence over default
### Integration Test Rules
- **Every walkthrough journey** (WT-XXX) must have at least one integration test:
- `_agentLoop` processes messages through the full pipeline with stubbed LLM/MCP
- `promptAgent` → `takeResponse` round-trip works
- `followUp` while busy queues to `followUpChannel`, processes after current work
- `stopAgent` sends `:shutdown`, drains channels, closes all 3 channels
- **Tool call lifecycle** (FR-003, FR-005):
- `llmCall` returns tool calls → loop calls `executeToolCalls` → results appended
- Tool calls → no tool calls on next `llmCall` → loop exits
- `toolResultBatch.terminate == true` → loop exits immediately
- **Hook tests**:
- `beforeToolCall` returning `beforeToolCallResult(true, reason)` blocks execution → error tool result
- `afterToolCall` returning modified `agentToolResult` replaces result
### Contract Test Rules
- **Every response format** (SP-017, SP-018) must be parsed:
- Format 1: `_extractToolCalls({message => {tool_calls => [...]}})` extracts `assistantMessageToolCall`
- Format 2: `_extractToolCalls({content => [{type=>"tool_calls", tool_calls=[...]}}])` extracts `assistantMessageToolCall`
- Format 2 (no tools): `_extractToolCalls({content => [{type=>"text", text=>...}]})` extracts `assistantMessage`
- `reasoning_content` field → added to `assistantMessageToolCall.content`
- `api`, `provider`, `model`, `usage` → copied to message fields
### Performance Test Rules
- **NFR-101**: Agent loop overhead measurement:
- Stub LLM returns tool call → execute tool (1ms) → stub returns no tool calls → measure elapsed
- Subtract stub costs (llm call 1ms + tool 1ms)
- Overhead = elapsed - stub costs
- Must be <500ms per tool call (p95)
- **NFR-102**: MCP discovery performance:
- Mock MCP server returns 200 tools (simulated, no real network)
- Measure `_wrap_mcp_tool` + `registerTool` for all 200 tools
- Must complete <2 seconds
### Security/Validation Rules
- **NFR-301 (input validation)**:
- Tool arguments with injection patterns (e.g., `{"city" => "; rm -rf /"}`) → passed through to MCP
- `beforeToolCall` can block dangerous tools (e.g., `toolCall.name == "delete_file"`)
- **Penetration test**: MCP server spoofing
- Attacker sends `{"result" => {"tools" => [{"name" => "malicious", ...}]}}` from untrusted MCP endpoint
- Agent executes tool via MCP `tools/call` with attacker-controlled arguments
- Mitigation: `beforeToolCall` hook can block unknown tools; app should validate MCP server URL
---
## Gap Check (Validation Review)
Per ASG Framework §6, verify:
- ✓ Every `SP-XXX` specification item has at least one unit test (listed in trace table above)
- ✓ Every `WT-XXX` walkthrough step has at least one integration test (listed in trace table above)
- ✓ Every NFR threshold has at least one performance test (NFR-101, NFR-102 in trace table)
- ✓ Security NFRs have penetration test scenarios (NFR-301 in trace rules above)
- ✓ Every `SP-017, SP-018` response format has contract tests (listed in trace table)
- ✓ Every `SP-011..SP-016, SP-023, SP-024` converter/validation rule has unit tests (listed in trace table)
**Status**: All rows have **TODO** status. Tests must be written before integration into production CI pipeline.
---
## CI Pipeline
```yaml
# ci/julia-pipeline.yaml (placeholder)
name: Julia YiemAgent CI
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: julia-actions/julia-buildpkg@latest
- run: julia --project=. -e 'using Pkg; Pkg.instantiate()'
- run: julia --project=./tests -e 'using Test; Test.@testset "Unit tests" # include tests/unit/run_tests.jl"'
integration:
runs-on: ubuntu-latest
needs: unit
steps:
- run: julia --project=./tests -e 'using Test; Test.@testset "Integration" # include tests/integration/run_tests.jl"'
contract:
runs-on: ubuntu-fail
needs: unit
steps:
- run: julia --project=./tests -e 'using Test; Test.@testset "Contract" # include tests/contract/run_tests.jl"'
perf:
runs-on: ubuntu-latest
needs: [unit, integration]
steps:
- run: julia --project=./tests -e 'using Test; Test.@testset "Performance" # include tests/perf/run_tests.jl"'
# Fail if NFR-101 or NFR-102 thresholds breached
lint:
runs-on: ubuntu-latest
steps:
- run: julia --eval 'using Aqua; Aqua.test_all(YiemAgent)'
```
**Gap Check**: Per ASG §5, every spec item maps to a CI gate. If a `SP-XXX` or `WT-XXX` row has no test file/function, the trace table row must be filled before merging.
+217
View File
@@ -0,0 +1,217 @@
# Implementation Plan: YiemAgent Agent Framework
## 1. Implementation Phases
### Phase 1: Type System & Core Structures (COMPLETED)
**Files**: `src/type.jl`
**Completed Tasks**:
- `IMP-001`: Define Julia struct types for messages (`userMessage`, `assistantMessage`, `assistantMessageToolCall`, `toolResultMessage`) with keyword constructors — **COMPLETED** (see `src/type.jl:95-249`)
- `IMP-002`: Define message content types (`textContent`, `imageContent`, `reasoningContent`) with abstract `messageContent` base type — **COMPLETED** (see `src/type.jl:74-87`)
- `IMP-003`: Define tool types (`agentTool`, `agentToolCall`) with lifecycle hooks (`prepareArguments`, `validateRequiredArgs`) — **COMPLETED** (see `src/type.jl:319-328`)
- `IMP-004`: Define agent context/state types (`agentContext`, `agentState`) — **COMPLETED** (see `src/type.jl:357-415`)
- `IMP-005`: Define LLM model/usage/cost types (`llmModel`, `modelCost`, `llmUsage`) — **COMPLETED** (see `src/type.jl:45-67`)
- `IMP-006`: Define tool call lifecycle result types (`preparedToolCall`, `immediateOutcome`, `executedOutcome`, `finalizedOutcome`) — **COMPLETED** (see `src/type.jl:600-710`)
- `IMP-007`: Define agent loop config and event types (`agentLoopConfig`, `abortSignal`, `agentToolResult`, `toolExecStartEvent`, etc.) — **COMPLETED** (see `src/type.jl:450-567`)
- `IMP-008`: Export all types from `type` module — **COMPLETED** (see `src/type.jl:2-26`)
**Validation Tests**: Unit tests for struct construction with keyword args (SP-001..SP-008, SP-019..SP-022)
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006
---
### Phase 2: Utility & Formatting Functions (COMPLETED)
**Files**: `src/utils.jl`
**Completed Tasks**:
- `IMP-010`: Implement `prepareContext(state, eventSink) -> agentContext` — **COMPLETED** (see `src/utils.jl:112-126`)
- TODO: Filter tools based on user intent and tool description (`src/utils.jl:114`)
- TODO: Inject system prompt context (e.g., time, session state) (`src/utils.jl:118`)
- TODO: Prune/reorder messages (`src/utils.jl:121`)
- `IMP-011`: Implement `formatMsgForLLM(ctx, eventSink) -> Dict{String, Any}` — **COMPLETED** (see `src/utils.jl:159-244`)
- Converts system prompt to system message
- Converts conversation messages via `_userMessageToOpenAI`, `_assistantMessageToOpenAI`, `_assistantMessageToolCallToOpenAI`, `_toolResultMessageToOpenAI`
- Converts `ctx.tools` to OpenAI function tool format via `_toolsToOpenAI`
- `IMP-012`: Implement message converters (`_userMessageToOpenAI`, `_assistantMessageToOpenAI`, `_assistantMessageToolCallToOpenAI`, `_toolResultMessageToOpenAI`) — **COMPLETED** (see `src/utils.jl:321-387`)
- `IMP-013`: Implement `_messageContentToBlocks` — converts `textContent`/`imageContent` to OpenAI content blocks — **COMPLETED** (see `src/utils.jl:396-413`)
- `IMP-014`: Implement `_toolsToOpenAI` — converts `OrderedDict{String, agentTool}` to OpenAI tools array — **COMPLETED** (see `src/utils.jl:429-447`)
- `IMP-015`: Implement `beforeToolCall` hook default (returns `beforeToolCallResult(false, "N/A")`) — **COMPLETED** (see `src/utils.jl:278-284`)
- `IMP-016`: Implement `afterToolCall` hook default (returns `nothing`) — **COMPLETED** (see `src/utils.jl:310-316`)
- `IMP-017`: Implement `validateRequiredArgs(args, inputSchema)` — validates required fields in tool arguments — **COMPLETED** (see `src/utils.jl:479-497`)
- `IMP-018`: Implement `validateToolArguments(tool, prepared)` — wrapper that calls tool-specific or default hook — **COMPLETED** (see `src/utils.jl:533-546`)
**Validation Tests**: Unit tests for converters, validation hooks, and OpenAI format output structure (SP-011..SP-016, SP-023, SP-024)
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006
---
### Phase 3: Tool Registry & MCP Integration (COMPLETED)
**Files**: `src/toolRegistry.jl`
**Completed Tasks**:
- `IMP-020`: Implement `_extract_text_content(result)` — extracts text from MCP tool result content array — **COMPLETED** (see `src/toolRegistry.jl:19-33`)
- `IMP-021`: Implement `_wrap_mcp_tool(mcpserver, tool_def)` — wraps MCP tool as `agentTool` with `execute` calling `mcpserver("tools/call", ...)` — **COMPLETED** (see `src/toolRegistry.jl:41-106`)
- Handles MCP error responses (`haskey(response, "error")`)
- Extracts text and `isError` from result
- `IMP-022`: Implement `register_mcp_tools(tools, mcpserver)` — queries `tools/list` with pagination (`nextCursor`) — **COMPLETED** (see `src/toolRegistry.jl:118-186`)
- Handles both `result.tools` and `result.error` response shapes
- Skips already registered tools (idempotent)
- `IMP-023`: Implement `registerTool(tools, tool)` — adds tool to OrderedDict — **COMPLETED** (see `src/toolRegistry.jl:200-205`)
- `IMP-024`: Implement `listTool(tools, mcpserver)` — builtin tool that triggers runtime discovery and lists all tools — **COMPLETED** (see `src/toolRegistry.jl:235-271`)
- `IMP-025`: Implement `registerAllTools(tools, mcpserver)` — discovers existing tools + registers `listTools` — **COMPLETED** (see `src/toolRegistry.jl:286-289`)
**Validation Tests**: Unit tests for MCP tools/list parsing, pagination, tool registration idempotency; contract tests for JSON-RPC 2.0 format (SP-009, SP-010)
**Traceability**: FR-002, FR-004
---
### Phase 4: Agent Core & Loop (COMPLETED)
**Files**: `src/agentCore.jl`
**Completed Tasks**:
- `IMP-030`: Define `yiemAgent` struct with channels, callbacks, MCP server — **COMPLETED** (see `src/agentCore.jl:17-95`)
- `IMP-031`: Implement `yiemAgent` constructor — creates channels, discovers MCP tools, spawns `_agentLoop` task — **COMPLETED** (see `src/agentCore.jl:126-199`)
- `IMP-032`: Implement `_agentLoop(agent)` — background `@spawn` task managing `inputChannel`/`followUpChannel` priority dispatch — **COMPLETED** (see `src/agentCore.jl:225-364`)
- Handles shutdown signal: drains all channels, breaks loop
- On error: catches, calls `eventSink(errMsg)`, exits loop
- `IMP-033`: Implement `_processMessage(...)` — drains input, calls `llmCall`, extracts tool calls, executes, loops until no tool calls — **COMPLETED** (see `src/agentCore.jl:394-544`)
- `while true` loop: `llmCall()` → `_extractToolCalls()` → if `hasToolCalls && length > 0` execute → append results → repeat
- On `toolResultBatch.terminate == true`: build final response and break
- Catches all errors and calls `eventSink(err_msg)`
- `IMP-034`: Implement `_extractToolCalls(response)` — handles **both** formats:
- Format 1: `response["message"]["tool_calls"]` (LMStudio/vLLM style) — **COMPLETED** (see `src/agentCore.jl:671-683`)
- Format 2: `response.content` blocks with `type => "tool_calls"` or `type => "tool_call"` (OpenAI API style) — **COMPLETED** (see `src/agentCore.jl:685-719`)
- Extracts `reasoning_content`, `content` text blocks, `api`, `provider`, `model`, `usage`, `finish_reason` — **COMPLETED** (see `src/agentCore.jl:723-797`)
- Returns `(hasToolCalls, toolCallList, assistant_message)` — **COMPLETED** (see `src/agentCore.jl:799-827`)
- `IMP-035`: Implement `prepareToolCall(context, assistantMsg, toolCall, config, signal, eventSink)` — resolves tool, validates args, runs `beforeToolCall` hook, never throws — **COMPLETED** (see `src/agentCore.jl:974-1035`)
- `IMP-036`: Implement `executePreparedToolCall(prep, signal, eventSink)` — calls `tool.execute(toolCallId, args, ...)` — **COMPLETED** (see `src/agentCore.jl:1078-1100`)
- `IMP-037`: Implement `finalizeExecutedToolCall(context, msg, prep, executed, config, signal, eventSink)` — runs `afterToolCall` hook, catches errors — **COMPLETED** (see `src/agentCore.jl:1159-1198`)
- `IMP-038`: Implement `prepareToolCallArguments(tool, toolCall)` — calls `tool.prepareArguments` if present — **COMPLETED** (see `src/agentCore.jl:908-917`)
- `IMP-039`: Implement `createErrorToolResult(msg)` — builds `agentToolResult` with error content — **COMPLETED** (see `src/agentCore.jl:572-574`)
- `IMP-040`: Implement `createToolResultMessage(f)` — builds `toolResultMessage` from `finalizedOutcome` — **COMPLETED** (see `src/agentCore.jl:606-612`)
**Note**: Sequential and parallel execution implementations continue beyond line 1207 of `src/agentCore.jl` — implementation incomplete, requires review to ensure execution methods properly call hooks and emit lifecycle events. See IMP-041.
**Validation Tests**: Integration tests for full agent loop with mocked LLM and MCP; contract tests for response format extraction (WT-001..WT-032, SP-017, SP-018)
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005
---
### Phase 5: Public API (COMPLETED)
**Files**: `src/api.jl`
**Completed Tasks**:
- `IMP-050`: Implement `promptAgent(agent, msg::AbstractDict)` — put to `inputChannel`, returns agent for chaining — **COMPLETED** (see `src/api.jl:36-39`)
- `IMP-051`: Implement `promptAgent(agent, text::String)` — wraps text as OpenAI `Dict` and put to `inputChannel` — **COMPLETED** (see `src/api.jl:41-44`)
- `IMP-052`: Implement `takeResponse(agent)` — blocks on `outputChannel` `take!`, returns OpenAI dict response — **COMPLETED** (see `src/api.jl:66-68`)
- `IMP-053`: Implement `followUp(agent, msg::AbstractDict)` — put to `followUpChannel`, returns agent — **COMPLETED** (see `src/api.jl:94-97`)
- `IMP-054`: Implement `followUp(agent, text::String)` — wraps text as OpenAI `Dict` and put to `followUpChannel` — **COMPLETED** (see `src/api.jl:99-102`)
- `IMP-055`: Implement `stopAgent(agent)` — puts `:shutdown`, `fetch(_agentLoop)`, closes all channels — **COMPLETED** (see `src/api.jl:126-139`)
**Validation Tests**: Integration tests for send/receive/stop lifecycle (WT-001..WT-005)
**Traceability**: FR-001, FR-006
---
## 2. Module Structure
```
src/
├── YiemAgent.jl # Parent module, includes submodules in dependency order
├── type.jl # All Julia struct types (exports 30+ types)
├── utils.jl # Formatting, validation, hooks (default implementations)
├── toolRegistry.jl # MCP tool discovery, registration, listTools builtin
├── agentCore.jl # yiemAgent struct, _agentLoop, _processMessage, extractToolCalls
└── api.jl # Public: promptAgent, takeResponse, followUp, stopAgent
```
**Include order** (per `src/YiemAgent.jl` lines 6-19):
1. `type.jl` (no dependencies)
2. `utils.jl` (depends on `type`)
3. `toolRegistry.jl` (depends on `type`)
4. `agentCore.jl` (depends on `type`, `utils`, `toolRegistry`)
5. `api.jl` (depends on `type`, `utils`, `agentCore`, `toolRegistry`)
---
## 3. Task Dependencies
| Task ID | Title | Depends On | Status |
|---------|-------|------------|--------|
| IMP-001..IMP-008 | Type system | — | **COMPLETED** |
| IMP-010..IMP-018 | Utils (formatMsgForLLM, converters, hooks) | IMP-001..IMP-008 | **COMPLETED** |
| IMP-020..IMP-025 | Tool registry (MCP discovery) | IMP-003, IMP-017 | **COMPLETED** |
| IMP-030..IMP-040 | Agent core (agent loop, tool call execution) | IMP-001..IMP-008, IMP-010..IMP-018, IMP-020..IMP-025 | **INCOMPLETE*** (see note) |
| IMP-050..IMP-055 | Public API | IMP-030..IMP-040 | **COMPLETED** |
**NOTE on IMP-030..IMP-040**: The sequential and parallel execution functions (`executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls`) begin after line 1207 of `src/agentCore.jl` but the full implementation is truncated in file reads and requires review. The exported functions (`executePreparedToolCall`, `prepareToolCall`, `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls`) are declared (`src/agentCore.jl:4-5`) but their full bodies may be incomplete. **Requires manual validation.**
**Traceability**: IMP-001..IMP-008 → SP-001..SP-008, IMP-020..IMP-025 → SP-009, SP-010; IMP-010..IMP-018 → SP-011..SP-016, SP-023, SP-024; IMP-030..IMP-040 → SP-017, SP-018, SP-019..SP-022
---
## 4. Remaining Work (TODOs)
| File | Line | Description | Priority |
|------|------|-------------|----------|
| `src/utils.jl` | 114 | Filter tools from `state.tools` based on user intent and tool description | High |
| `src/utils.jl` | 118 | Add system prompt context: time, session state, user preferences | Medium |
| `src/utils.jl` | 121 | Prune/reorder/conversation messages before sending to LLM | Medium |
| `src/agentCore.jl` | post-1207 | Complete and validate `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls` implementations (currently exported but body may be incomplete) | **Critical** |
| `tests/unit/...` | — | Write unit tests for all struct types (IMP-001..IMP-008) and converters (SP-011..SP-016) | High |
| `tests/unit/...` | — | Write unit tests for tool validation hooks (SP-023, SP-024) | High |
| `tests/integration/...` | — | Write integration tests for agent loop with mocked LLM/MCP (WT-001..WT-032) | High |
| `tests/contract/...` | — | Write contract tests for response formats (SP-017, SP-018) | Medium |
| `tests/perf/...` | — | Write performance tests (NFR-101, NFR-102) | Medium |
---
## 5. Build & Deployment
### Build
```bash
cd /home/ton/docker-apps/sommpanion/YiemAgent
julia --project=. -e 'using Pkg; Pkg.instantiate(); using Pkg; Pkg.precompile()'
```
### Test
```bash
julia --project=./tests -e 'using Pkg; Pkg.instantiate()'
julia --project=./tests -e 'using Test; Test.@testset "Unit" begin include("tests/unit/run_tests.jl") end'
julia --project=./tests -e 'using Test; Test.@testset "Integration" begin include("tests/integration/run_tests.jl") end'
```
### CI/CD Preparation
- Add `.github/workflows/julia.yaml` referencing `ci/julia-pipeline.yaml` steps
- Add `Project.toml` dependencies:
```toml
[deps]
JSON = "682c8566-3430-5439-8f4e-b64832e8aaba"
DataStructures = "892a3cad-33e6-51e5-808c-f3d90072be70"
HTTP = "cd6eee30-4dd5-5329-8bad-3259ec48d0f4"
# plus: UUIDs, Dates, PrettyPrinting, Serialization, DataFrames, NATS, LibPQ
```
---
## 6. Risk Mitigation
| Risk | Mitigation |
|------|------------|
| Line 1207+ of `agentCore.jl` (sequential/parallel execution) is incomplete or broken | Manually review and validate `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls` against spec SP-021 (agentLoopConfig) and trace to test `test_tool_call_execution_sequence` |
| `GeneralUtils` internal package unavailable in CI | Ensure `GeneralUtils` is installed as dependency (via `Pkg.develop` or registry) |
| NATS `mcpServer` struct is abstract — no concrete implementation | Provide at least one mock `mcpServer` for tests that returns canned JSON responses |
| Message history grows unbounded | Implement `prepareContext` pruning (TODO in code); add NFR-104 for max history tokens |
+445
View File
@@ -0,0 +1,445 @@
# Runbook: YiemAgent Agent Framework
**Severity**: N/A (library — not a deployed service)
**Estimated Time**: N/A
This runbook documents operational guidance for developers working with the YiemAgent agent framework. It is not an operations runbook for a production service, but a developer handbook for debugging, troubleshooting, and extending the framework.
---
## 1. Agent Lifecycle
### 1.1: Create an Agent
**Steps**:
```julia
using YiemAgent
# 1. Implement llmCall function
function my_llmCall(messages; eventSink=nothing)
# Call LLM API (e.g., OpenAI, Anthropic, LMStudio)
# Return dict with keys: "message", "content", "finish_reason", "api", "provider", "model", "usage"
eventSink("my_llmCall: sending $(length(messages["messages"])) messages"; publish=true)
# ... API call ...
return response_dict
end
# 2. Implement mcpServer callable struct
struct MyMCPClient
conn::NATS.Connection # or HTTP.Client
topic::String
senderID::String
end
function (mcp::MyMCPClient)(method::String, args...; eventSink=nothing)
# JSON-RPC 2.0 calls:
# mcp("tools/list", sessionId; eventSink=eventSink)
# mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)
# ... NATS/HTTP call ...
return jsonrpc_response
end
# 3. Create agent
mcp = MyMCPClient(nats_conn, "yiemagent:tools", "agent_1")
agent = yiemAgent(
my_llmCall;
systemPrompt="You are a wine assistant.\nYou have tools for searching wine inventory.",
tools=mcp_tools, # optional: pass pre-discovered tools
prepareContext=my_prepareContext, # optional: custom context preparation
formatMsgForLLM=my_formatMsgForLLM, # optional: custom formatting for provider
beforeToolCall=my_beforeToolCall, # optional: approval gate
afterToolCall=my_afterToolCall, # optional: result sanitization
mcpServer=mcp,
parallelToolExecute=false,
eventSink=my_eventSink,
)
```
> **Reference**: `2-solution_design.md:SD-001` (channel-based async), `SD-002` (background task), `SD-004` (MCP struct)
### 1.2: Send a Message
```julia
# Option 1: Send a string message (automatically wrapped as OpenAI dict)
promptAgent(agent, "What wines have notes of oak and vanilla?")
# Option 2: Send a full OpenAI-format dict with images
msg = Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "What wines match these notes?"),
Dict("type" => "image_url", "image_url" => Dict("url" => "data:image/png;base64,..."))
]
)
promptAgent(agent, msg)
```
### 1.3: Receive a Response
```julia
# Blocks until agent finishes processing (no tool calls or tool calls complete)
response = takeResponse(agent)
# response is a Dict in OpenAI format:
# Dict("role" => "assistant", "content" => [Dict("type" => "text", "text" => "...")])
# For text-only response:
if response["content"][1]["type"] == "text"
println(response["content"][1]["text"])
end
```
### 1.4: Send Follow-up While Agent is Busy
```julia
# While the agent is processing tool calls (between promptAgent(...) and takeResponse(...))
followUp(agent, "Also check European wines")
```
### 1.5: Stop the Agent
```julia
stopAgent(agent)
# Sends :shutdown, closes all channels. Agent object is destroyed.
```
---
## 2. Troubleshooting
### 2.1: Agent Loop Exits Silently
**Symptoms**:
- `takeResponse(agent)` hangs indefinitely
- No output in eventSink
**Steps**:
1. Check if `eventSink` is receiving error messages:
```julia
global error_count = 0
my_eventSink(msg; publish=false) = begin
if occursin("error", lowercase(msg)) || occursin("failed", lowercase(msg))
println("ERROR EVENT: $msg")
global error_count += 1
end
end
```
2. Check network connectivity to LLM API endpoint
3. Verify `formatMsgForLLM` produces valid OpenAI-format message structure (`4-specification.md:SP-011..SP-016`)
> **Reference**: `4-specification.md` (OpenAI format specs), `3-walkthrough.md:WT-030` (LLM timeout)
### 2.2: Tool Call Not Executing
**Symptoms**:
- LLM returns `tool_calls` but agent returns text response immediately
- `beforeToolCall` hook is missing or incorrectly implemented
**Steps**:
1. Verify `prepareContext` returns `agentContext` with `tools` field (not `nothing`) (`src/utils.jl:123`)
2. Check `beforeToolCall` hook is not blocking execution:
```julia
function my_beforeToolCall(ctx, signal, eventSink)
# Returning nothing → allows execution
# Returning beforeToolCallResult(true, "blocked") → blocks
eventSink("beforeToolCall for $(ctx.toolCall.name)"; publish=true)
nothing # ← must return nothing (not beforeToolCallResult(false, "N/A"))
end
```
3. Verify tool exists in `context.tools[toolCall.name]` (`src/agentCore.jl:985`)
4. Check `validateToolArguments` is not throwing (catches and returns error tool result) (`src/utils.jl:533`)
> **Reference**: `4-specification.md:SP-021` (agentLoopConfig with beforeToolCall hook), `3-walkthrough.md:WT-032` (beforeToolCall blocks)
### 2.3: MCP Server Returns No Tools
**Symptoms**:
- `println("[toolRegistry] Discovered 0 MCP tools."` on startup
- Agent has no tools available at all times
**Steps**:
1. Verify `mcpServer` callable struct correctly handles `mcp("tools/list", sessionId; eventSink)`
2. Check JSON-RPC 2.0 response shape matches `4-specification.md:SP-009`:
```json
{
"result": {
"tools": [{"name": "...", ...}],
"nextCursor": "" # empty string = no more pages
}
}
```
3. Verify `tools/list` error response uses `haskey(response, "error")` check (`src/toolRegistry.jl:128`)
> **Reference**: `4-specification.md:SP-009` (tools/list format), `3-walkthrough.md:WT-020` (MCP discovery)
### 2.4: LLM Response Format Not Parsed
**Symptoms**:
- Agent always treats LLM response as text, never extracts tool calls
- LLM correctly returns `tool_calls` but agent ignores them
**Steps**:
1. Verify `llmCall` returns a dict containing `message` (LMStudio/vLLM format, `4-specification.md:SP-017`) or `content` (OpenAI format, `4-specification.md:SP-018`)
2. Check for both format variations:
- Format 1: `response["message"]["tool_calls"]` (LMStudio style, see `src/agentCore.jl:671-683`)
- Format 2: `response.content[{type => "tool_calls"}]` (OpenAI style, see `src/agentCore.jl:700-705`)
3. Verify `finish_reason` field matches: `"tool_calls"` triggers tool execution, `"stop"`/`"end_turn"` breaks loop (`src/agentCore.jl:723-724`)
> **Reference**: `4-specification.md:SP-017, SP-018`, `3-walkthrough.md:WT-011, WT-012` (LLM response handling)
### 2.5: Tool Execution Hangs
**Symptoms**:
- `tool.execute(...)` starts but never returns
- EventSink receives `toolExecutionStart` but never `toolExecutionEnd`
**Steps**:
1. Implement `abortSignal` checking in tool execute function:
```julia
function my_tool_execute(toolCallId, args, signal, eventSink)
while !signal.aborted && !all_conditions_met
# Long-running operation with periodic abort checks
sleep(0.1)
end
if signal.aborted
return agentToolResult([textContent("Cancelled")], Dict(), nothing, false)
end
return agentToolResult([textContent("Done")], Dict(), nothing, false)
end
```
2. External caller sets `signal.aborted = true` (if `abortSignal` is shared via closure)
3. Add timeout logic to tool execute function:
```julia
start_time = time()
timeout_seconds = 30.0
while time() - start_time < timeout_seconds && !signal.aborted
# operation...
sleep(0.5)
end
```
> **Reference**: `4-specification.md:SP-022` (abortSignal), `3-walkthrough.md:WT-030` (LLM timeout)
### 2.6: Message History Grows Unbounded
**Symptoms**:
- Memory usage grows linearly with conversation length
- LLM context window exceeded for long sessions
**Steps**:
1. Implement pruning in `prepareContext` (`src/utils.jl:112-126`):
```julia
function my_prepareContext(state::agentState, eventSink)
# Prune old messages, keep last N turns
max_turns = 20
recent_msgs = isempty(state.messages) ? state.messages :
state.messages[max(1, end-2*max_turns):end]
# Optionally inject system context (time, user prefs)
preparedMessages = recent_msgs
preparedSystemPrompt = state.systemPrompt * "\nCurrent time: $(now())"
# Filter tools (optional: keep only relevant subset)
filteredTools = state.tools # or filter by intent...
return agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
end
```
2. Add hard limit in `_agentLoop`:
```julia
if length(agent._state.messages) > 200
# Truncate oldest messages
agent._state.messages = agent._state.messages[end-100:end]
end
```
> **Reference**: `3-walkthrough.md` (agent message flow), `1-requirements.md:NFR-103` (channel buffer limits)
---
## 3. Extending the Framework
### 3.1: Add a Custom LLM Provider
**Steps**:
1. Implement `llmCall` function that accepts `FormattedMessages::Dict{String, Any}` and returns response dict
2. Ensure response dict contains at least one of:
- `message.tool_calls` format (LMStudio/vLLM) — `4-specification.md:SP-017`
- `content[{type=>"tool_calls"}]` format (OpenAI) — `4-specification.md:SP-018`
3. Response dict may optionally contain: `api`, `provider`, `model`, `usage` (`llmUsage` or dict), `finish_reason`, `error_message` (`4-specification.md:SP-017`, `SP-018.4`)
```julia
function my_openai_llmCall(messages::Dict{String, Any}; eventSink=nothing)
# messages["messages"] contains OpenAI-format messages
# messages["temperature"] (default 0.7)
# messages["tools"] (if tools are set)
body = Dict(
"model" => "gpt-4",
"messages" => messages["messages"],
"tools" => get(messages, "tools", Any[])
)
resp = HTTP.post("https://api.openai.com/v1/chat/completions",
headers, JSON.json(body))
json_resp = JSON.parse(String(resp.body))
# Extract first completion
choice = json_resp["choices"][1]["message"]
# Build uniform response
return Dict(
"api" => "openai",
"provider" => "openai",
"model" => "gpt-4",
"message" => choice,
"usage" => json_resp["usage"],
"finish_reason" => json_resp["choices"][1]["finish_reason"]
)
end
```
> **Reference**: `1-requirements.md:FR-002`, `4-specification.md:SP-017, SP-018`
### 3.2: Add a Custom Tool
**Steps**:
1. Define tool schema in `inputSchema` (JSON Schema format):
```julia
schema = Dict{String, Any}(
"type" => "object",
"properties" => Dict(
"query" => Dict("type" => "string", "description" => "Search query"),
"max_results" => Dict("type" => "integer", "default" => 5)
),
"required" => ["query"]
)
```
2. Implement `execute` function:
```julia
tool = agentTool(
name="searchWine",
label="Search Wine Inventory",
description="Search for wines matching a query",
inputSchema=schema,
execute=(toolCallId, args, signal, eventSink) -> begin
query = args["query"]
max_results = get(args, "max_results", Dict{Any,Any}()["max_results"] => 5...) # handle default
wines = run_search(query, max_results)
result_text = join(["- $(w.name) (\$$(w.price))" for w in wines], "\n")
return agentToolResult(
[textContent("Found $(length(wines)) wines:\n$result_text")],
Dict("count" => length(wines)),
nothing, false # content, details, usage, terminate
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing, # uses default validator
parallelToolExecute = false
)
# Register: push!(tools, tool.name => tool)
```
3. Optionally override validation:
```julia
function my_validate(args)
if haskey(args, "query") && length(args["query"]) < 3
return "Query must be at least 3 characters"
end
return nothing
end
agentTool(
...,
validateRequiredArgs = my_validate, # replaces default validator
)
```
> **Reference**: `1-requirements.md:FR-004`, `4-specification.md:SP-009, SP-010`
### 3.3: Implement User Approval Gate (beforeToolCall Hook)
**Steps**:
1. Implement `beforeToolCall` that blocks expensive/destructive operations:
```julia
function my_approval_toolCall(ctx::beforeToolCallContext, signal::abortSignal, eventSink)
if ctx.toolCall.name in ("delete_wine", "update_price_bulk")
eventSink("Approval required for $(ctx.toolCall.name) with args $(ctx.args)")
# In production, send to UI and wait for user input
approved = ask_user_for_approval(ctx.toolCall.name, ctx.args) # blocking call
if !approved
return beforeToolCallResult(true, "User denied approval for $(ctx.toolCall.name)")
end
end
return nothing # allow execution
end
```
> **Reference**: `2-solution_design.md:SD-005` (lifecycle hooks), `3-walkthrough.md:WT-032` (beforeToolCall blocks)
### 3.4: Implement Result Sanitization (afterToolCall Hook)
**Steps**:
1. Implement `afterToolCall` to mask sensitive data:
```julia
function my_sanitize_toolCall(ctx::afterToolCallContext, signal::abortSignal, eventSink)
if ctx.toolCall.name == "getCustomerInfo"
# Mask sensitive fields before LLM sees result
result = ctx.result
for (i, block) in enumerate(result.content)
if block isa textContent
masked = replace(block.text, r"\d{12}" => "***-***-****") # mask SSN
result = agentToolResult(
[replace(block.text, r"\d{12}" => "***-***-****") for block in result.content],
result.details,
result.usage,
result.terminate
)
end
end
return result
end
return nothing # pass through for other tools
end
```
> **Reference**: `2-solution_design.md:SD-005` (lifecycle hooks)
---
## 4. Common Error Codes
| Error Condition | Response Code | Description |
|-----------------|---------------|-------------|
| Tool not found | `immediateOutcome("Tool <name> not found", true)` | `context.tools[toolCall.name]` returns `nothing` |
| Missing required args | `immediateOutcome("Missing required arguments: <fields>", true)` | `validateRequiredArgs` returns error string |
| Custom validation failed | `immediateOutcome("<tool.validateRequiredArgs result>", true)` | Tool-specific validation hook returns error |
| beforeToolCall blocked | `immediateOutcome("Tool execution was blocked: <reason>", true)` | `beforeToolCall` returns `beforeToolCallResult(true, reason)` |
| Operation aborted | `immediateOutcome("Operation aborted", true)` | `signal.aborted == true` after `beforeToolCall` |
| Tool execution crashed | `immediateOutcome("<exception message>", true)` | `preptool.execute()` throws exception |
| MCP error | `agentToolResult(["MCP error: <err_msg>"], {..., "error"=>err_msg}, ...)` | Server returns `response["error"]` JSON-RPC envelope |
| MCP network error | `agentToolResult(["MCP call error: <err_msg>"], {..., "error"=>err_msg}, ...)` | Exception during `mcp("tools/call", ...)` |
> **Reference**: `4-specification.md:SP-009, SP-010`, `src/agentCore.jl:989, 1013, 1019, 1033, 1097`
---
## 5. Debugging Checklist
Per ASG Framework §Pre-Code Debugging Habit:
| Stage | Debug Question |
|-------|----------------|
| **Implementation** | If a tool is not found, does `prepareToolCall` return `immediateOutcome` with "Tool not found"? |
| **Testing** | If an LLM response has `tool_calls`, does `_extractToolCalls` return `(true, toolCallList, assistantMessageToolCall)`? |
| **Deploying** | If the agent loop exits, does `eventSink` contain the error message? |
---
## 6. References
- **API contract**: [`4-specification.md`](4-specification.md) — message types, MCP JSON-RPC format, response extraction formats
- **Solution decisions**: [`2-solution_design.md`](2-solution_design.md) — component architecture, lifecycle hooks, format-agnostic LLM responses
- **Walkthroughs**: [`3-walkthrough.md`](3-walkthrough.md) — end-to-end agent flow, error paths, hook sequences
- **Validation tests**: [`6-validation.md`](6-validation.md) — test traceability matrix mapping requirements/specs to test files
+10
View File
@@ -0,0 +1,10 @@
# check if this column has vector embedding. if there is one, seach vector version instead
column_name_embedding = column_name * "_embedding"
if occursin(column_name_embedding, tables_schema[column_name_embedding])
vector_column = Dict(
"table_name"=> table_name,
"column_name"=> column_name_embedding,
"operator"=> "vector_similarity",
"value"=> column_obj["value"]
)
end
+585
View File
@@ -0,0 +1,585 @@
using Revise
using JSON, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures
using YiemAgent, GeneralUtils
using Base.Threads
# ---------------------------------------------- 100 --------------------------------------------- #
# load config
config = JSON.parsefile("/appfolder/app/dev/YiemAgent/test/config.json")
# config = copy(JSON.parsefile("../mountvolume/config.json"))
function executeSQL(sql::T) where {T<:AbstractString}
host = config[:externalservice][:wineDB][:host]
port = config[:externalservice][:wineDB][:port]
dbname = config[:externalservice][:wineDB][:dbname]
user = config[:externalservice][:wineDB][:user]
password = config[:externalservice][:wineDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function executeSQLVectorDB(sql)
host = config[:externalservice][:SQLVectorDB][:host]
port = config[:externalservice][:SQLVectorDB][:port]
dbname = config[:externalservice][:SQLVectorDB][:dbname]
user = config[:externalservice][:SQLVectorDB][:user]
password = config[:externalservice][:SQLVectorDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function text2textInstructLLM(prompt::String; maxattempt::Integer=10, modelsize::String="medium",
senderId=GeneralUtils.uuid4snakecase(), timeout=90,
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.5,
)
)
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="inference",
senderName="yiemagent",
senderId=senderId,
receiverName="text2textinstruct_$modelsize",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => prompt,
:kwargs => llmkwargs
)
)
response = nothing
for attempts in 1:maxattempt
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; responsetimeout=timeout, responsemaxattempt=maxattempt)
payload = _response[:response]
if _response[:success] && payload[:text] !== nothing
response = _response[:response][:text]
break
else
println("\n<text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(outgoingMsg)
println("</text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
sleep(3)
end
end
return response
end
# get text embedding from a LLM service
function getEmbedding(text::T) where {T<:AbstractString}
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="embedding",
senderName="yiemagent",
senderId=sessionId,
receiverName="textembedding",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => [text] # must be a vector of string
)
)
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; responsetimeout=120, responsemaxattempt=3)
embedding = response[:response][:embeddings]
return embedding
end
function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3,
vectorDB::Function; limit::Integer=1
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
# get embedding from LLM service
embedding = getEmbedding(text)[1]
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
sql = """
SELECT *, $embeddingColumnName <-> '$embedding' as distance
FROM $tablename
ORDER BY distance LIMIT $limit;
"""
response = vectorDB(sql)
df = DataFrame(response)
return df
end
function similarSQLVectorDB(query; maxdistance::Integer=100)
tablename = "sqlllm_decision_repository"
# get embedding of the query
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
# println(df[1, [:id, :function_output]])
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
# distance = 100 # CHANGE this is for testing only
if row != 0 && distance < maxdistance
# if there is usable SQL, return it.
output_b64 = df[1, :function_output_base64] # pick the closest match
output_str = String(base64decode(output_b64))
rowid = df[1, :id]
println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=output_str, distance=distance)
else
println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=nothing, distance=nothing)
end
end
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString}
tablename = "sqlllm_decision_repository"
# get embedding of the query
# query = state[:thoughtHistory][:question]
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
query_embedding = getEmbedding(query)[1]
query = replace(query, "'" => "")
sql_base64 = base64encode(SQL)
sql_ = replace(SQL, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
"""
# println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# println(sql)
_ = executeSQLVectorDB(sql)
end
end
function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
tablename = "sommelier_decision_repository"
# find similar
println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__)
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable decision, return it.
rowid = df[1, :id]
println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
output_b64 = df[1, :function_output_base64] # pick the closest match
_output_str = String(base64decode(output_b64))
output = copy(JSON.parsefile(_output_str))
return output
else
println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return nothing
end
end
function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5
) where {T1<:AbstractString, T2<:AbstractDict}
tablename = "sommelier_decision_repository"
# find similar
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
recentevents_embedding = getEmbedding(recentevents)[1]
recentevents = replace(recentevents, "'" => "")
decision_json = JSON.json(decision)
decision_base64 = base64encode(decision_json)
decision = replace(decision_json, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
else
println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
end
end
sessionId = GeneralUtils.uuid4snakecase()
externalFunction = (
getEmbedding=getEmbedding,
text2textInstructLLM=text2textInstructLLM,
executeSQL=executeSQL,
similarSQLVectorDB=similarSQLVectorDB,
insertSQLVectorDB=insertSQLVectorDB,
similarSommelierDecision=similarSommelierDecision,
insertSommelierDecision=insertSommelierDecision,
)
# s = "full-bodied red wine, budget 1500 USD"
# r = YiemAgent.extractWineAttributes_1(agent, s)
# println(r)
# --------------------------- generating scenario and customer profile --------------------------- #
function rolegenerator()
rolegenerator_systemmsg =
"""
Your role:
- You are a helpful assistant
Your mission:
- Create one random role of a potential customer of an internet wine store.
You must follow the following guidelines:
- the user only need the role, do not add your own words.
- the role should be detailed and realistic.
You should then respond to the user with:
Name: a name of the potential customer
Situation: a situation that the potential customer may be facing
Mission: a mission of the potential customer
Profile: a profile of the potential customer, including their age, gender, occupation, and other relevant information
You should only respond in format as described below:
Name: ...
Situation: ...
Mission: ...
Profile: ...
Additional_information: ...
Here are some examples:
Name: Jimmy
Situation:
- Your relationship with your boss is not that good. You need to improve your relationship with your boss.
- Your boss's wedding anniversary is coming up.
- You are at a wine store and start talking with the store's sommelier.
Mission:
- Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list.
Profile:
- You are a young professional in a big company.
- You are avid party goer
- You like beer.
- You know nothing about wine.
- You have a budget of 1500usd.
Additional_information:
- your boss like spicy food.
- your boss is a middle-aged man.
- your boss likes Australian wine.
Name: Kate
Situation:
- Your husband asked you to get him a bottle of wine. He will gift the wine to his business client while dining at a German restaurant.
- Your husband is a business client and he will gift the wine to his business
- You are at a wine store and start talking with the store's sommelier.
Mission:
- Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list.
Profile:
- You are a CEO in a startup company.
- You are a nerd
- You don't like alcohol.
- You have a budget of 150usd.
- You don't care about organic, sulfite, gluten-free, or sustainability certified wines
Additional_information:
- your husband like spicy food.
- your husband is a middle-aged man.
Name: John
Situation:
- A local newspaper club wants to have a scoop about wine with local food in the U.S.
- You are at a wine store and start talking with the store's sommelier.
Mission:
- Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list.
Profile:
- I'm a young guy.
- I prefer to express my ideas in a succinct and clear manner.
Additional_information:
- N/A
Name: Jane
Situation:
- You have catering a dinner party with French cuisine.
- You want to serve wine with your guests.
- You are at a wine store and start talking with the store's sommelier.
Mission:
- Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list.
Profile:
- You are a young French restaurant owner.
- You like dry, full-bodied red wine with high tannin
- You don't care about organic, sulfite, gluten-free, or sustainability certified wines.
- You have a budget of 200 usd.
Additional_information:
- N/A
Let's begin!
"""
header = ["Name:", "Situation:", "Mission:", "Profile:", "Additional_information:"]
dictkey = ["name", "situation", "mission", "profile", "additional_information"]
errornote = "N/A"
for attempt in 1:10
_prompt =
[
Dict(:name => "system", :text => rolegenerator_systemmsg),
]
prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3")
response = text2textInstructLLM(prompt) # generated role
response = GeneralUtils.deFormatLLMtext(response, "qwen3")
think, response = GeneralUtils.extractthink(response)
# check whether response has all header
detected_kw = GeneralUtils.detect_keyword(header, response)
kwvalue = [i for i in values(detected_kw)]
zeroind = findall(x -> x == 0, kwvalue)
missingkeys = [header[i] for i in zeroind]
if 0 ∈ values(detected_kw)
errornote = "$missingkeys are missing from your previous response"
println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
elseif sum(values(detected_kw)) > length(header)
errornote = "\nYour previous attempt has duplicated points according to the required response format"
println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
responsedict = GeneralUtils.textToDict(response, header;
dictKey=dictkey, symbolkey=true)
responsedict[:id] = GeneralUtils.uuid4snakecase()
responsedict[:systemmsg] =
"""
You are role playing as a CUSTOMER of a wine store and you are currently talking with a sommelier of a wine store.
Your profile is as follows:
Situation: $(responsedict[:situation])
Mission: $(responsedict[:mission])
Profile: $(responsedict[:profile])
Additional_information: $(responsedict[:additional_information])
You should follow the following guidelines:
- Focus on the lastest conversation
- Your like to be short and concise
- If you don't know an answer to sommelier's question, you should say: I don't know.
- If you think the store can't provide what you seek, you can leave.
You should then respond to the user with:
Dialogue: what you want to say to the user
Role: Verify that the dialogue is intended for the customer of a wine store. Can be "yes" or "no"
You should only respond in format as described below:
Dialogue: ...
Role: ...
Let's begin!
"""
println("\nrolegenerator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
println(responsedict)
return responsedict
end
error("ERROR rolegenerator() failed to generate customer role: ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
# Define the external functions for the customer agent in named tuple format
customer_externalFunction = (
text2textInstructLLM=text2textInstructLLM,
)
function main()
agent = YiemAgent.sommelier(
externalFunction;
name="Jane",
id=sessionId, # agent instance id
retailername="Yiem",
llmFormatName="qwen3"
)
customerDict = rolegenerator()
customer = YiemAgent.virtualcustomer(
customer_externalFunction;
systemmsg=customerDict[:systemmsg],
name=customerDict[:name],
id=sessionId, # agent instance id
llmFormatName="qwen3"
)
# customer_chat = "hello"
# YiemAgent.addNewMessage(customer, "assistant", customer_chat)
# # add user activity to events memory
# push!(customer.memory[:events],
# YiemAgent.eventdict(;
# event_description="the assistant talks to the user.",
# timestamp=Dates.now(),
# subject="assistant",
# action_name="CHAT_BOX",
# action_input=customer_chat,
# )
# )
# println("\ncustomer respond:\n $customer_chat")
agent_response = YiemAgent.conversation(agent; maximumMsg=50)
println("\nagent respond:\n $agent_response")
while true
customer_chat = nothing
while customer_chat === nothing
customer_response = YiemAgent.conversation(customer, Dict(:text=> agent_response);
converPartnerName=agent.name,
maximumMsg=50)
customer_response = GeneralUtils.deFormatLLMtext(customer_response, customer.llmFormatName)
customer_chat = customer_response
#[WORKING] check whether customer response the same before
end
println("\ncustomer respond:\n $customer_chat")
agent_response = YiemAgent.conversation(agent;
userinput=Dict(:text=> customer_chat),
maximumMsg=50)
println("\nagent respond:\n $agent_response")
if haskey(agent.memory[:events][end], :thought)
lastAssistantAction = agent.memory[:events][end][:thought][:action_name]
if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict
# save a.memory[:shortmem][:decisionlog] to disk using JSON
println("\nsaving agent.memory[:shortmem][:decisionlog] to disk")
date = "$(Dates.now())"
date = replace(date, ':'=>'.')
filename = "agent_decision_log_$(date)_$(agent.id).json"
filepath = "/appfolder/mountvolume/appdata/log/$filename"
open(filepath, "w") do io
JSON.pretty(io, agent.memory[:shortmem][:decisionlog])
end
# check how many file in /appfolder/mountvolume/appdata/log/ folder now
logfilesnumber = length(readdir("/appfolder/mountvolume/appdata/log/"))
println("\nCaching conversation process done. Total $logfilesnumber files in /appfolder/mountvolume/appdata/log/ folder now.\n")
break
end
end
end
end
for i in 1:100
main()
println("\n Round $i/100 done.")
end
println("done")
# prompt =
# """
# <|im_start|>system
# You are a role playing agent acting as:
# Name: Emily
# Situation: - Emily is planning her upcoming birthday party and wants to make it extra special. She has invited close friends and family, and she's looking for a unique wine that will impress them.
# Mission: - Emily needs to find a rare and high-quality wine that matches the theme of her party, which is a mix of classic and modern flavors. She also wants to ensure that the wine is not too expensive so that it won't break her budget.
# Profile: - Emily is in her late 20s, works as a marketing executive for a tech company, and has a passion for trying new things. She's organized and detail-oriented but can be spontaneous when it comes to planning events.
# Additional_information: - Emily loves experimenting with different types of food and wine pairings.
# Your are currently talking with a sommelier.
# You should follow the following guidelines:
# - Focus on the lastest conversation
# - If you satisfy with the sommelier's recommendation for bottle of wine(s), you should say: Thanks for you help. I will buy the wine you recommended.
# - If you don't satisfy with the sommelier's questions or can't get a good wine recommendation, you can continue the conversation.
# Let's begin!
# <|im_end|>
# <|im_start|>Jane
# Hello! Welcome to Yiem's Wine Store. I'm Jane, your friendly sommelier. How can I assist you today? What type of wine are you in the mood for, and is there a special occasion or event on your mind?
# <|im_end|>
# <|im_start|>Emily
# Hi Jane! Thank you so much for welcoming me. For my birthday party, I'm looking for something that combines classic and modern flavors. It's a mix of guests who enjoy both traditional tastes and more contemporary ones. Also, I want to make sure it won't break the bank. Any suggestions?
# <|im_end|>
# <|im_start|>Jane
# Thank you for sharing your preferences, Jane! To better assist you, could you please let me know if there are any specific characteristics of wine you're looking for, such as tannin, sweetness, intensity, or acidity? Additionally, do you have any food items in mind that this wine should pair well with?
# <|im_end|>
# <|im_start|>Emily
# """
# llmkwargs=Dict(
# :num_ctx => 32768,
# :temperature => 0.3,
# )
# r = text2textInstructLLM(prompt, llmkwargs=llmkwargs)
# println(r)
# println(555)
# response = YiemAgent.conversation(agent, Dict(:text=> "I want to get a French red wine under 100."))
# while true
# println("your respond: ")
# user_answer = readline()
# response = YiemAgent.conversation(agent, Dict(:text=> user_answer))
# println("\n$response")
# end
# """
# Hello
# I would like to get a bottle of wine for my boss but I don't know much about wine. Can you help me?
# well actually, my boss is going to offer the wine to his client as a gift in a business meeting. All I know is his client like spicy food and French wine. I have a budget about 1000.
# """
# input = "French wine, bordeaux, under USD100, pairs with spicy food"
# r = YiemAgent.extractWineAttributes_1(a, input)
# inventory_order = "French Syrah, Viognier, full bodied, under 100"
# r = YiemAgent.extractWineAttributes_2(a, inventory_order)
# pprintln(r)
# cron job
# @reboot sleep 50 && nvidia-smi -pm 1
# @reboot sleep 51 && nvidia-smi -i 0 -pl 150
# @reboot sleep 52 && nvidia-smi -i 1 -pl 150
# @reboot sleep 53 && nvidia-smi -i 2 -pl 150
# @reboot sleep 54 && nvidia-smi -i 3 -pl 150
# @reboot sleep 55 && julia -t 2 /home/ton/work/restartContainer/main.jl
# using GeneralUtils
# msgMeta = GeneralUtils.generate_msgMeta(
# "/tonpc_containerServices",
# senderName= "somename",
# senderId= "1230",
# mqttBrokerAddress= "mqtt.yiem.cc",
# mqttBrokerPort= 1883,
# )
# outgoingMsg = Dict(
# :msgMeta=> msgMeta,
# :payload=> "docker container restart playground-app",
# )
# GeneralUtils.sendMqttMsg(outgoingMsg)
+76
View File
@@ -0,0 +1,76 @@
{
"mqttServerInfo": {
"description": "mqtt server info",
"port": 1883,
"broker": "mqtt.yiem.cc"
},
"testingOrProduction": {
"value": "testing",
"description": "agent status, couldbe testing or production"
},
"agentid": {
"value": "2b74b87a-5413-4fe2-a4d3-405891051680",
"description": "a unique id for this agent"
},
"agentCentralConfigTopic": {
"mqtttopic": "/yiem_branch_1/agent/sommelier/backend/config/api/v1.1",
"description": "a central agent server's topic to get this agent config"
},
"servicetopic": {
"mqtttopic": [
"/yiem/hq/agent/sommelier/backend/prompt/api_v1/testing"
],
"description": "a topic this agent are waiting for service request"
},
"role": {
"value": "sommelier",
"description": "agent role"
},
"organization": {
"value": "yiem_branch_1",
"description": "organization name"
},
"externalservice": {
"loadbalancer": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM"
},
"text2textinstruct": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"virtualWineCustomer_1": {
"mqtttopic": "/virtualenvironment/winecustomer",
"description": "text to text service with instruct LLM that act as wine customer",
"llminfo": {
"name": "llama3instruct"
}
},
"text2textchat": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"wineDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10201,
"dbname": "wineDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
},
"SQLVectorDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10203,
"dbname": "SQLVectorDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
}
}
}
+706
View File
@@ -0,0 +1,706 @@
using JSON, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures
using YiemAgent, GeneralUtils
using Base.Threads
# ---------------------------------------------- 100 --------------------------------------------- #
""" Expected incomming MQTT message format for this service:
{
"msgMeta": {
"msgPurpose": "updateStatus",
"requestresponse": "request",
"timestamp": "2024-03-29T05:8:48.362",
"replyToMsgId": null,
"receiverId": null,
"getpost": "get",
"msgId": "e5c09bd8-7100-4e4e-bb43-05bee589a22c",
"acknowledgestatus": null,
"sendTopic": "/agent/wine/backend/chat/api/v1/prompt",
"receiverName": "agent-wine-backend",
"replyTopic": "/agent/wine/frontend/chat/api/v1/txt/receive",
"senderName": "agent-wine-frontend-chat",
"senderId": "0938a757-e0ee-40a9-8355-5e24906a87cd"
},
"payload" : {
"text": "hello"
}
}
"""
# load config
config = copy(JSON.parsefile("../mountvolume/config/config.json"))
""" Instantiate an agent. One need to specify startmessage and one of gpu location info,
Mqtt or Rest. start message must be comply with GeneralUtils's message format
Arguments\n
-----
channel::Channel
communication channel
sessionId::String
sesstion ID of the agent
agentName::String
Name of the agent
mqttBroker::String
mqtt broker e.g. "tcp://127.0.0.1:1883"
agentConfigTopic::String
main communication topic for an agent to ask for config
timeout::Int64
inactivity timeout in minutes. If timeout is reached, an agent will be terminated.
Return\n
-----
a task represent an agent
Example\n
-----
```jldoctest
julia> using YiemAgent, GeneralUtils
julia> msg = GeneralUtils.generate_msgMeta("/agent")
julia> incoming_msg = msg # assuming 1st msg was sent from other app
julia> agentConfigTopic = "/agent/wine/backend/config"
julia> task = runAgentInstance(incoming_msg, mqttBroker, agentConfigTopic, 60)
```
TODO\n
-----
[] update docstringLAMA_CONTEXT_LENGTH=40960 since the default size is 2048 as you can see in your debug log:
[] change how to get result of YiemAgent from let YiemAgent send msg directly to frontend,
to
response = YiemAgent.conversation()
then send response to frontend
Signature\n
-----
"""
function runAgentInstance(
receiveUserMsgChannel::Channel,
outputchannel::Channel,
sessionId::String,
config::Dict,
timeout::Int64,
)
function executeSQL(sql::T) where {T<:AbstractString}
host = config[:externalservice][:wineDB][:host]
port = config[:externalservice][:wineDB][:port]
dbname = config[:externalservice][:wineDB][:dbname]
user = config[:externalservice][:wineDB][:user]
password = config[:externalservice][:wineDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function executeSQLVectorDB(sql)
host = config[:externalservice][:SQLVectorDB][:host]
port = config[:externalservice][:SQLVectorDB][:port]
dbname = config[:externalservice][:SQLVectorDB][:dbname]
user = config[:externalservice][:SQLVectorDB][:user]
password = config[:externalservice][:SQLVectorDB][:password]
DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function text2textInstructLLM(prompt::String; maxattempt::Integer=3, modelsize::String="medium",
senderId=GeneralUtils.uuid4snakecase(), timeout=180,
llmkwargs=Dict(
:num_ctx => 32768,
:temperature => 0.5,
))
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="inference",
senderName="yiemagent",
senderId=senderId,
receiverName="text2textinstruct_$modelsize",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => prompt,
:kwargs => llmkwargs
)
)
response = nothing
for attempts in 1:maxattempt
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=timeout, maxattempt=maxattempt)
payload = _response[:response]
if _response[:success] && payload[:text] !== nothing
response = _response[:response][:text]
break
else
println("\n<text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(outgoingMsg)
println("</text2textInstructLLM()> attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n")
sleep(3)
end
end
return response
end
# get text embedding from a LLM service
function getEmbedding(text::T) where {T<:AbstractString}
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:loadbalancer][:mqtttopic];
msgPurpose="embedding",
senderName="yiemagent",
senderId=sessionId,
receiverName="textembedding",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => [text] # must be a vector of string
)
)
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120, maxattempt=3)
embedding = response[:response][:embeddings]
return embedding
end
function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3,
vectorDB::Function; limit::Integer=1
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
# get embedding from LLM service
embedding = getEmbedding(text)[1]
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
sql = """
SELECT *, $embeddingColumnName <-> '$embedding' as distance
FROM $tablename
ORDER BY distance LIMIT $limit;
"""
response = vectorDB(sql)
df = DataFrame(response)
return df
end
function similarSQLVectorDB(query; maxdistance::Integer=100)
tablename = "sqlllm_decision_repository"
# get embedding of the query
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
# println(df[1, [:id, :function_output]])
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
# distance = 100 # CHANGE this is for testing only
if row != 0 && distance < maxdistance
# if there is usable SQL, return it.
output_b64 = df[1, :function_output_base64] # pick the closest match
output_str = String(base64decode(output_b64))
rowid = df[1, :id]
println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=output_str, distance=distance)
else
println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
return (dict=nothing, distance=nothing)
end
end
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString}
tablename = "sqlllm_decision_repository"
# get embedding of the query
# query = state[:thoughtHistory][:question]
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
query_embedding = getEmbedding(query)[1]
query = replace(query, "'" => "")
sql_base64 = base64encode(SQL)
sql_ = replace(SQL, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
"""
# println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# println(sql)
_ = executeSQLVectorDB(sql)
end
end
function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
tablename = "sommelier_decision_repository"
# find similar
println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__)
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable decision, return it.
rowid = df[1, :id]
println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
output_b64 = df[1, :function_output_base64] # pick the closest match
_output_str = String(base64decode(output_b64))
output = copy(JSON.parsefile(_output_str))
return output
else
println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return nothing
end
end
function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5
) where {T1<:AbstractString, T2<:AbstractDict}
tablename = "sommelier_decision_repository"
# find similar
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
recentevents_embedding = getEmbedding(recentevents)[1]
recentevents = replace(recentevents, "'" => "")
decision_json = JSON.json(decision)
decision_base64 = base64encode(decision_json)
decision = replace(decision_json, "'" => "")
sql =
"""
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
else
println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
end
end
# keepaliveChannel_2::Channel{Dict} = Channel{Dict}(8)
latestUserMsgTimeStamp::DateTime = Dates.now()
externalFunction = (
getEmbedding=getEmbedding,
text2textInstructLLM=text2textInstructLLM,
executeSQL=executeSQL,
similarSQLVectorDB=similarSQLVectorDB,
insertSQLVectorDB=insertSQLVectorDB,
similarSommelierDecision=similarSommelierDecision,
insertSommelierDecision=insertSommelierDecision,
)
agent = YiemAgent.sommelier(
externalFunction;
name="Jane",
id=sessionId, # agent instance id
retailername="Yiem",
llmFormatName="qwen3"
)
# user chat loop
while true
# check for new user message
if isready(receiveUserMsgChannel)
incomingMsg = take!(receiveUserMsgChannel)
incoming_msgMeta = incomingMsg[:msgMeta]
incomingPayload = incomingMsg[:payload]
latestUserMsgTimeStamp = Dates.now()
# make sure the message has :text key because YiemAgent use this key for incoming user msg
if haskey(incomingPayload, :text)
# skip, msg already has correct key name
elseif haskey(incomingPayload, :txt)
# change key name to text
incomingPayload[:text] = incomingPayload[:txt]
else
error("\n no :txt or :text key in the message.")
end
# reset agent
if occursin("newtopic", incomingPayload[:text]) ||
occursin("Newtopic", incomingPayload[:text]) ||
occursin("New topic", incomingPayload[:text]) ||
occursin("new topic", incomingPayload[:text])
# YiemAgent.clearhistory(agent)
agent = YiemAgent.sommelier(
externalFunction;
name="Janie",
id=sessionId, # agent instance id
retailername="Yiem",
)
# sending msg back to sender i.e. LINE
msgMeta = GeneralUtils.generate_msgMeta(
incomingMsg[:msgMeta][:replyTopic];
senderName="wine_assistant_backend",
senderId=sessionId,
replyToMsgId=incomingMsg[:msgMeta][:msgId],
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:alias => agent.name, # will be shown in frontend as agent name
:text => "Okay. What shall we talk about?"
)
)
_ = GeneralUtils.sendMqttMsg(outgoingMsg)
println("--> outgoingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(outgoingMsg)
else
usermsg = incomingPayload
if incoming_msgMeta[:msgPurpose] == "initialize"
println("\n-- Initializing... ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
# send prompt
result = YiemAgent.conversation(agent;
userinput=usermsg,
maximumMsg=50)
# Ken's bot use [br] for newline character '\n'
# result = replace(result, '\n'=>"[br]")
if incoming_msgMeta[:msgPurpose] == "initialize"
println("\n-- Initialized. Ready! waiting for request at:\n$(config[:servicetopic][:mqtttopic]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
msgMeta = GeneralUtils.generate_msgMeta(
incomingMsg[:msgMeta][:replyTopic];
senderName="wine_assistant_backend",
senderId=string(uuid4()),
replyToMsgId=incomingMsg[:msgMeta][:msgId],
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:alias => agent.name, # will be shown in frontend as agent name
:text => result
)
)
_ = GeneralUtils.sendMqttMsg(outgoingMsg)
println("\n--> outgoingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(outgoingMsg)
# jpg_as_juliaStr = nothing
# prompt = nothing
# if haskey(payload, "img")
# url_or_base64 = payload["img"]
# if startswith(url_or_base64, "http")
# # img in http
# julia_rgb_img, cv2_bgr_img = ImageUtils.url_to_cv2_image(url_or_base64)
# _, buffer = cv2.imencode(".jpg", cv2_bgr_img)
# jpg_as_pyStr = base64.b64encode(buffer).decode("utf-8")
# jpg_as_juliaStr = pyconvert(String, jpg_as_pyStr)
# else
# # img in base64
# cv2_bgr_img = payload["img"]
# jpg_as_juliaStr = pyconvert(String, jpg_as_pyStr)
# end
# end
end
else
# println("\n no msg")
end
if haskey(agent.memory[:events][end], :thought)
lastAssistantAction = agent.memory[:events][end][:thought][:action_name]
if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict
# save a.memory[:shortmem][:decisionlog] to disk using JSON
println("\nsaving agent.memory[:shortmem][:decisionlog] to disk")
filename = "agent_decision_log_$(Dates.now())_$(agent.id).json"
filepath = "/appfolder/app/log/$filename"
open(filepath, "w") do io
JSON.pretty(io, agent.memory[:shortmem][:decisionlog])
end
# for (i, event) in enumerate(agent.memory[:events])
# if event[:subject] == "assistant"
# # create timeline of the last 3 conversation except the last one.
# # The former will be used as caching key and the latter will be the caching target
# # in vector database
# all_recapkeys = keys(agent.memory[:recap]) #[TESTING] recap as caching
# all_recapkeys_vec = [r for r in all_recapkeys] # convert to a vector
# # select from 1 to 2nd-to-lase event (i.e. excluding the latest which is assistant's response)
# _recapkeys_vec = all_recapkeys_vec[1:i-1]
# # select only previous 3 recaps
# recapkeys_vec =
# if length(_recapkeys_vec) <= 3 # 1st message is a user's hello msg
# _recapkeys_vec # choose all
# else
# _recapkeys_vec[end-2:end]
# end
# #[PENDING] if there is specific data such as number, donot store in database
# tempmem = DataStructures.OrderedDict()
# for k in recapkeys_vec
# tempmem[k] = agent.memory[:recap][k]
# end
# recap = GeneralUtils.dictToString_noKey(tempmem)
# thoughtDict = agent.memory[:events][i][:thought] # latest assistant thoughtDict
# insertSommelierDecision(recap, thoughtDict)
# else
# # skip
# end
# end
println("\nCaching conversation process done")
break
end
end
# self terminate if too long inactivity
timediff = GeneralUtils.timedifference(latestUserMsgTimeStamp, Dates.now(), "minutes")
if timediff > timeout
result = Dict(:exitreason => "timeout", :timestamp => Dates.now())
put!(outputchannel, result)
println("Agent ID $(agent.id) timeout has been reached $timediff/$timeout minutes Send delete session msg ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# send "delete session" message to inform the main loop that this session can be deleted
sendto =
if typeof(config[:servicetopic][:mqtttopic]) <: Array
config[:servicetopic][:mqtttopic][1]
else
config[:servicetopic][:mqtttopic]
end
msgMeta = GeneralUtils.generate_msgMeta(
sendto;
senderName="session",
senderId=sessionId,
msgPurpose="delete session",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => nothing
)
_ = GeneralUtils.sendMqttMsg(outgoingMsg)
try disconnect(agent.mqttClient) catch end
break
end
sleep(1) # allowing on_msg_2, asyncmove above and other process to run
end
end
sessionDict = Dict{String,Any}()
incomingMsgChannel = (ch1=Channel(8),) # store msg that coming into servicetopic
# incommingInternalMsg = [] # st ore msg that coming into servicetopic internal management
keepaliveChannel::Channel{Dict} = Channel{Dict}(8)
# Define the callback for receiving messages.
function onMsgCallback_1(topic, payload)
jobj = JSON.parsefile(String(payload))
incomingMqttMsg = copy(jobj) # convert json object into julia dictionary recursively
if occursin("keepalive", topic)
put!(keepaliveChannel, incomingMqttMsg)
else
put!(incomingMsgChannel[:ch1], incomingMqttMsg)
end
end
mqttInstance = GeneralUtils.mqttClientInstance_v2(
config[:mqttServerInfo][:broker],
config[:servicetopic][:mqtttopic],
incomingMsgChannel,
keepaliveChannel,
onMsgCallback_1
)
# ------------------------------------------------------------------------------------------------ #
# this service main loop #
# ------------------------------------------------------------------------------------------------ #
function main()
sessiontimeout = 1 * 1 * 60 # timeout in minute for each instance (day * hour * minute)
initializing = false
while true
# check if mqtt connection is still up
_ = GeneralUtils.checkMqttConnection!(mqttInstance; keepaliveCheckInterval=30)
# initialize session 0
if initializing == false # send init msg
sendto =
if typeof(config[:servicetopic][:mqtttopic]) <: Array
config[:servicetopic][:mqtttopic][1]
else
config[:servicetopic][:mqtttopic]
end
msgMeta = GeneralUtils.generate_msgMeta(
sendto;
msgPurpose="initialize",
senderName="initializer",
senderId="0",
msgId= "initMsg",
replyTopic=sendto,
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict( # will be shown in frontend as agent name
:text => "Do you have full-bodied red wines under 100 USD. I don't have any other preferences."
)
)
_ = GeneralUtils.sendMqttMsg(outgoingMsg)
initializing = true
println("\n--> Initializing msg sent ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
# check for new message
if !isempty(incomingMsgChannel[:ch1])
msg = popfirst!(incomingMsgChannel[:ch1])
println("\n<-- incomingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
pprintln(msg)
# @spawn new runAgentInstance and store it in sessionDict
# use agent's frontend id because 1 backend agent per 1 frontend session
sessionId = msg[:msgMeta][:senderId]
sessionId = replace(sessionId, "-" => "_") # julia can't use "-" in a dict key
# check for delete session msg
if msg[:msgMeta][:msgPurpose] == "delete session"
delete!(sessionDict, sessionId)
println("sessionId $(sessionId) has been terminated ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# no session yet, create new session
elseif sessionId ∉ keys(sessionDict)
inputch = Channel{Dict}(8)
outputch = Channel{Dict}(8)
process = @spawn runAgentInstance(inputch, outputch, sessionId, config, sessiontimeout)
# process = runAgentInstance(inputch, outputch, sessionId, config, sessiontimeout) #XXX use spawn version
println("\ninstantiate agent success ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# call runAgentInstance() and store it in sessionDict to be able to check on it later
sessionDict[sessionId] = Dict(
:inputchannel => inputch,
:outputchannel => outputch,
:process => process,
)
put!(sessionDict[sessionId][:inputchannel], msg)
# ongoing session
else
println("sessionId $(sessionId) existing session ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
put!(sessionDict[sessionId][:inputchannel], msg)
end
end
# sleep is needed because MQTTClient use async. "while true" loop leave no
# chance for control to switch to on_msg()
sleep(1)
end
end
main()
+72
View File
@@ -0,0 +1,72 @@
To make **LLM-driven inference** fast while maintaining its dynamic capabilities, there are a few practices or approaches to avoid, as they could lead to performance bottlenecks or inefficiencies. Here's what *not* to do:
---
### **1. Avoid Using Overly Large Models for Every Query**
While larger LLMs like GPT-4 provide high accuracy and nuanced responses, they may slow down real-time processing due to their computational complexity. Instead:
- Use distilled or smaller models (e.g., GPT-3.5 Turbo or fine-tuned versions) for faster inference without compromising much on quality.
---
### **2. Avoid Excessive Entity Preprocessing**
Don’t rely on overly complicated preprocessing steps (like advanced NER models or regex-heavy pipelines) to extract entities from the query before invoking the LLM. This could add latency. Instead:
- Design efficient prompts that allow the LLM to extract entities and generate responses simultaneously.
---
### **3. Avoid Asking the LLM Multiple Separate Questions**
Running the LLM for multiple subtasks—for example, entity extraction first and response generation second—can significantly slow down the pipeline. Instead:
- Create prompts that combine tasks into one pass, e.g., *"Identify the city name and generate a weather response for this query: 'What's the weather in London?'"*.
---
### **4. Don’t Overload the LLM with Context History**
Excessively lengthy conversation history or irrelevant context in your prompts can slow down inference times. Instead:
- Provide only the relevant context for each query, trimming unnecessary parts of the conversation.
---
### **5. Avoid Real-Time Dependence on External APIs**
Using external APIs to fetch supplementary data (e.g., weather details or location info) during every query can introduce latency. Instead:
- Pre-fetch API data asynchronously and use the LLM to integrate it dynamically into responses.
---
### **6. Avoid Running LLM on Underpowered Hardware**
Running inference on CPUs or low-spec GPUs will result in slower response times. Instead:
- Deploy the LLM on optimized infrastructure (e.g., high-performance GPUs like NVIDIA A100 or cloud platforms like Azure AI) to reduce latency.
---
### **7. Skip Lengthy Generative Prompts**
Avoid prompts that encourage the LLM to produce overly detailed or verbose responses, as these take longer to process. Instead:
- Use concise prompts that focus on generating actionable or succinct answers.
---
### **8. Don’t Ignore Optimization Techniques**
Failing to optimize your LLM setup can drastically impact performance. For example:
- Avoid skipping techniques like model quantization (reducing numerical precision to speed up inference) or distillation (training smaller models).
---
### **9. Don’t Neglect Response Caching**
While you may not want a full caching system to avoid sunk costs, dismissing lightweight caching entirely can impact speed. Instead:
- Use temporary session-based caching for very frequent queries, without committing to a full-fledged cache infrastructure.
---
### **10. Avoid One-Size-Fits-All Solutions**
Applying the same LLM inference method to all queries—whether simple or complex—will waste processing resources. Instead:
- Route basic queries to faster, specialized models and use the LLM for nuanced or multi-step queries only.
---
### Summary: Focus on Efficient Design
By avoiding these pitfalls, you can ensure that LLM-driven inference remains fast and responsive:
- Optimize prompts.
- Use smaller models for simpler queries.
- Run the LLM on high-performance hardware.
- Trim unnecessary preprocessing or contextual steps.
Would you like me to help refine a prompt or suggest specific tools to complement your implementation? Let me know!
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

-537
View File
@@ -1,537 +0,0 @@
https://github.com/andyz245/LanguageAgentTreeSearch/blob/main/hotpot/hotpot.py
standard_prompt = '''
Write a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: {input}
'''
reflection_prompt = '''You are an advanced reasoning agent that can improve based on self refection. You will be given a previous reasoning trial in which you were given access to an Docstore API environment and a question to answer. You were unsuccessful in answering the question either because you guessed the wrong answer with Finish[<answer>], or you used up your set number of reasoning steps. In a few sentences, Diagnose a possible reason for failure and devise a new, concise, high level plan that aims to mitigate the same failure. Use complete sentences.
Here are some examples:
Previous Trial:
Question: The Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of what?
Thought 1: I need to search Rome Protocols, find the three Prime Ministers, then find what they were assassinated as part of.
Action 1: Search[Rome Protocols]
Observation 1: The Rome Protocols were a series of three international agreements concluded in Rome on 17 March 1934 between the governments of Austria, Hungary and Italy. They were signed by Italian Prime Minister Benito Mussolini, Austrian Chancellor Engelbert Dollfuss and Hungarian Prime Minister Gyula Gömbös. All of the protocols went into effect on 12 July 1934 and were registered in League of Nations Treaty Series on 12 December 1934.
Thought 2: The three Prime Ministers were Benito Mussolini, Engelbert Dollfuss, and Gyula Gömbös. I need to search Benito Mussolini and find what he was assassinated as part of.
Action 2: Search[Benito Mussolini]
Observation 2: Benito Amilcare Andrea Mussolini (UK: MU(U)SS-ə-LEE-nee, US: MOOSS-, Italian: [beˈniːto aˈmilkare anˈdrɛːa mussoˈliːni]; 29 July 1883 – 28 April 1945) was an Italian politician and journalist who founded and led the National Fascist Party (PNF). He was Prime Minister of Italy from the March on Rome in 1922 until his deposition in 1943, as well as "Duce" of Italian fascism from the establishment of the Italian Fasces of Combat in 1919 until his summary execution in 1945 by Italian partisans. As dictator of Italy and principal founder of fascism, Mussolini inspired and supported the international spread of fascist movements during the inter-war period.Mussolini was originally a socialist politician and a journalist at the Avanti! newspaper. In 1912, he became a member of the National Directorate of the Italian Socialist Party (PSI), but he was expelled from the PSI for advocating military intervention in World War I, in opposition to the party's stance on neutrality. In 1914, Mussolini founded a new journal, Il Popolo d'Italia, and served in the Royal Italian Army during the war until he was wounded and discharged in 1917. Mussolini denounced the PSI, his views now centering on Italian nationalism instead of socialism, and later founded the fascist movement which came to oppose egalitarianism and class conflict, instead advocating "revolutionary nationalism" transcending class lines. On 31 October 1922, following the March on Rome (28–30 October), Mussolini was appointed prime minister by King Victor Emmanuel III, becoming the youngest individual to hold the office up to that time. After removing all political opposition through his secret police and outlawing labor strikes, Mussolini and his followers consolidated power through a series of laws that transformed the nation into a one-party dictatorship. Within five years, Mussolini had established dictatorial authority by both legal and illegal means and aspired to create a totalitarian state. In 1929, Mussolini signed the Lateran Treaty with the Holy See to establish Vatican City.
Mussolini's foreign policy aimed to restore the ancient grandeur of the Roman Empire by expanding Italian colonial possessions and the fascist sphere of influence. In the 1920s, he ordered the Pacification of Libya, instructed the bombing of Corfu over an incident with Greece, established a protectorate over Albania, and incorporated the city of Fiume into the Italian state via agreements with Yugoslavia. In 1936, Ethiopia was conquered following the Second Italo-Ethiopian War and merged into Italian East Africa (AOI) with Eritrea and Somalia. In 1939, Italian forces annexed Albania. Between 1936 and 1939, Mussolini ordered the successful Italian military intervention in Spain in favor of Francisco Franco during the Spanish Civil War. Mussolini's Italy initially tried to avoid the outbreak of a second global war, sending troops at the Brenner Pass to delay Anschluss and taking part in the Stresa Front, the Lytton Report, the Treaty of Lausanne, the Four-Power Pact and the Munich Agreement. However, Italy then alienated itself from Britain and France by aligning with Germany and Japan. Germany invaded Poland on 1 September 1939, resulting in declarations of war by France and the UK and the start of World War II.
On 10 June 1940, Mussolini decided to enter the war on the Axis side. Despite initial success, the subsequent Axis collapse on multiple fronts and eventual Allied invasion of Sicily made Mussolini lose the support of the population and members of the Fascist Party. As a consequence, early on 25 July 1943, the Grand Council of Fascism passed a motion of no confidence in Mussolini; later that day King Victor Emmanuel III dismissed him as head of government and had him placed in custody, appointing Pietro Badoglio to succeed him as Prime Minister. After the king agreed to an armistice with the Allies, on 12 September 1943 Mussolini was rescued from captivity in the Gran Sasso raid by German paratroopers and Waffen-SS commandos led by Major Otto-Harald Mors. Adolf Hitler, after meeting with the rescued former dictator, then put Mussolini in charge of a puppet regime in northern Italy, the Italian Social Republic (Italian: Repubblica Sociale Italiana, RSI), informally known as the Salò Republic, causing a civil war. In late April 1945, in the wake of near total defeat, Mussolini and his mistress Clara Petacci attempted to flee to Switzerland, but both were captured by Italian communist partisans and summarily executed by firing squad on 28 April 1945 near Lake Como. The bodies of Mussolini and his mistress were then taken to Milan, where they were hung upside down at a service station to publicly confirm their demise.
Thought 3: Benito Mussolini was assassinated as part of the collapse of the Axis powers during World War II. So the Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of World War II.
Action 3: Finish[World War II]
Reflection: I searched one of the prime ministers involved in the signing, then attemted to answer right away. I should have searched each of the prime ministers, then looked up 'death' on each of their pages in order to get more information before answering.
Previous Trial:
Question: Kam Heskin plays Paige Morgan in a 2004 film directed by who?
Thought 1: I need to search Kam Heskin and Paige Morgan, find the 2004 film they are in, then find the director of the film.
Action 1: Search[Kam Heskin]
Observation 1: Kam Erika Heskin (born May 8, 1973) is an American actress. She began her career playing Caitlin Deschanel on the NBC daytime soap opera Sunset Beach (1998–1999), before appearing in films Planet of the Apes (2001) and Catch Me If You Can (2002). Heskin went on in 2003 to play Elizabeth Bennet in an independent film Pride & Prejudice: A Latter-Day Comedy, Robin in Charmed (2004), and Paige Morgan in The Prince and Me film franchise (2006–2010).
Thought 2: Kam Heskin plays Paige Morgan in The Prince and Me film franchise. I need to search The Prince and Me film franchise and find the director of the 2004 film.
Action 2: Search[The Prince and Me film franchise]
Observation 2: Could not find [The Prince and Me film franchise]. Similar: ['Kam Heskin', 'Prince of Persia', 'Harry Potter and the Half-Blood Prince (film)', 'Prince of Persia: The Sands of Time (film)', 'The Chronicles of Narnia (film series)', 'Majestic Prince (franchise)', 'Kingsman (franchise)', 'The Little Prince (2015 film)', 'Shrek (franchise)', 'Harry Potter (film series)']
Thought 3: I need to search The Prince and Me (2004 film) instead.
Action 3: Search[The Prince and Me (2004 film)]
Observation 3: Could not find [The Prince and Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Kam Heskin', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince of Persia', 'Brooklynn Prince', 'Prince (musician)']
Thought 4: I need to search The Prince & Me (2004 film).
Action 4: Search[The Prince & Me (2004 film)]
Observation 4: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince']
Thought 5: I need to search The Prince & Me (2004 film) instead.
Action 5: Search[The Prince & Me (2004 film)]
Observation 5: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince']
Thought 6: I need to search The Prince & Me (2004 film).
Action 6: Search[The Prince & Me (2004 film)]
Observation 6: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince']
Reflection: I got stuck in a loop where I kept trying to search 'The Prince & Me (2004 film)' but the page could not be found. Instead I should have tried to search the similar results that had a similar name to see and they were made in 2004.
Previous Trial:
Question: The Oberoi family is part of a hotel company that has a head office in what city?
Thought 1: I need to search the Oberoi family and find the city where the hotel company's head office is located.
Action 1: Search[Oberoi family]
Observation 1: The Oberoi Group is a luxury hotel group with its head office in New Delhi, India.[2] Founded in 1934, the company owns and operates 32 luxury hotels and two river cruise ships in 7 countries, primarily under its Oberoi Hotels & Resorts and Trident brands.[3] The group also operates The Oberoi Centre for Learning and Development, which is regarded as one of Asia's top institutions for hospitality education.[4]. The foundations of the Oberoi Group date back to 1934 when The Rai Bahadur Mohan Singh Oberoi, the founder of the group, bought two properties: the Maidens in Delhi and the Clarke's in Shimla.[5] In the following years Oberoi, assisted by his two sons, Tilak Raj Singh Oberoi and Prithvi Raj Singh Oberoi (P.R.S. Oberoi
Thought 2: The Oberoi Group has its head office in New Delhi, India. I just need to finish with New Delhi.
Action 2: Finish[New Delhi]
Observation 2: Episode finished, reward = 0
Reflection: I appeared to have retrieved the correct information about The Oberoi Family and the location of it's head office, and provided a corresponding answer. However this answer does not exactly match the ground truth answer so I should try a different wording, such as Delhi.
Previous trial:
{trajectory}Reflection:'''
cot_prompt = '''
Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county?
Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded.
Action 1: Search[Guitars for Wounded Warriors]
Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales.
Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in
Action 2: Search[New Paltz (village), New York]
Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3]
Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County.
Action 3: Finish[Ulster County]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
{input}
'''
cot_prompt_short = '''
Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
{input}
'''
cot_prompt_feedback_short = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
You have attempted to answer the following question before and failed. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question.
{trajectories}
{input}
'''
cot_prompt_feedback = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county?
Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded.
Action 1: Search[Guitars for Wounded Warriors]
Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales.
Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in
Action 2: Search[New Paltz (village), New York]
Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3]
Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County.
Action 3: Finish[Ulster County]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
You have attempted to answer the following question before and failed, either because your reasoning for the answer was incorrect or the phrasing of your response did not exactly match the answer. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question.
{trajectories}
When providing the thought and action for the current trial, that into account these failed trajectories and make sure not to repeat the same mistakes and incorrect answers.
{input}
'''
vote_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a list of trajectories, decide which trajectory is most promising. Analyze each trajectory in detail and consider possible errors, then conclude in the last line "The best trajectory is {s}", where s the integer id of the trajectory.
'''
compare_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Briefly analyze the correctness of the following two trajectories. Conclude in the last line "The more correct trajectory is 1", "The more correct trajectory is 2", or "The two trajectories are similarly correct".
'''
score_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, analyze the following trajectory, then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
'''
value_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
Thus the correctness score is 3
Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?
Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United.
Action 1: Search[David Beckham's football manager]
Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park'].
Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager.
Action 2: Search[Manchester United]
Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields.
Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe.
Action 3: Search[Manchester United managers]
Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers.
Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers.
Action 4: Search[list of Manchester United managers]
Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers.
Thus the correctness score is 4
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
Thus the correctness score is 10
{input}
'''
value_prompt_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
Thus the correctness score is 3
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
Thus the correctness score is 10
{trajectories}
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
Thus the correctness score is 10
{input}
'''
value_prompt_reasoning = '''You are an advanced reasoning agent that can improve based on self refection. Analyze the trajectories of your previous solutions to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested.
Thus the correctness score is 3
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action.
Thus the correctness score is 10
Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?
Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United.
Action 1: Search[David Beckham's football manager]
Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park'].
Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager.
Action 2: Search[Manchester United]
Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields.
Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe.
Action 3: Search[Manchester United managers]
Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers.
Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers.
Action 4: Search[list of Manchester United managers]
Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers.
This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager.
Thus the correctness score is 4
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search.
Thus the correctness score is 10
{input}
'''
value_prompt_reasoning_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners it is reasonable to checkof the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested.
Thus the correctness score is 3
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action.
Thus the correctness score is 10
Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?
Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United.
Action 1: Search[David Beckham's football manager]
Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park'].
Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager.
Action 2: Search[Manchester United]
Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields.
Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe.
Action 3: Search[Manchester United managers]
Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers.
Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers.
Action 4: Search[list of Manchester United managers]
Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers.
This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager.
Thus the correctness score is 4
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search.
Thus the correctness score is 10
{trajectories}
{input}
'''
value_prompt_reasoning_feedback_short = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested.
Thus the correctness score is 3
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search.
Thus the correctness score is 10
{trajectories}
{input}
'''
rap_prompt = '''
Solve a question answering task with interleaving Thought and Action steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Thought 2: Then I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county?
Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded.
Action 1: Search[Guitars for Wounded Warriors]
Thought 2: I need to search New Paltz (village), New York and find which county it is in
Action 2: Search[New Paltz (village), New York]
Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County.
Action 3: Finish[Ulster County]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Thought 2: I can look up "named after" for finding the specific individual Milhouse is named after.
Action 2: Lookup[named after]
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
{input}
'''
+14 -31
View File
@@ -1,23 +1,25 @@
module YiemAgent module YiemAgent
# export agent
"""Order by dependencies of each file. The 1st included file must not depend on any other """Order by dependencies of each file. The 1st included file must not depend on any other
files and each file can only depend on the file included before it. files and each file can only depend on the file included before it."""
"""
include("type.jl") include("type.jl")
using .type using .type
include("util.jl") include("utils.jl")
using .util using .utils
include("llmfunction.jl") include("toolRegistry.jl")
using .llmfunction using .toolRegistry
include("agentCore.jl")
using .agentCore
include("api.jl")
using .api
export promptAgent, takeResponse, followUp, stopAgent
include("interface.jl")
using .interface
# ---------------------------------------------- 100 --------------------------------------------- # # ---------------------------------------------- 100 --------------------------------------------- #
@@ -25,23 +27,4 @@ module YiemAgent
end # end module
end # module YiemAgent_v1
+1593
View File
File diff suppressed because it is too large Load Diff
+410
View File
@@ -0,0 +1,410 @@
module api
export promptAgent, takeResponse, followUp, stopAgent
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames
using GeneralUtils
using ..type, ..utils, ..agentCore, ..toolRegistry
# ---------------------------------------------- 100 --------------------------------------------- #
"""
Send a message to the agent's input channel.
Blocks if the input channel buffer is full (capacity 16 by default).
The agent processes messages from `inputChannel` in the background task.
# Arguments
- `agent::yiemAgent`: The agent instance to send a message to
- `msg`: The message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Hello!")`)
# Returns
- The same `agent` instance for chaining
# Notes
- Use `takeResponse(agent)` to receive the agent's response after sending a message.
- Use `followUp(agent, msg)` to send messages while the agent is still processing.
# Examples
```jldoctest
julia> promptAgent(agent, Dict("role" => "user", "content" => "Hello!"))
yiemAgent(...)
```
"""
function promptAgent(agent::yiemAgent, msg::AbstractDict{String, Any})
put!(agent.inputChannel, msg)
return agent
end
function promptAgent(agent::yiemAgent, text::String)
put!(agent.inputChannel, Dict("role" => "user", "content" => [Dict("type" => "text", "text" => text)]))
return agent
end
"""
Take a response from the agent's output channel.
Blocks until the agent sends a response.
# Arguments
- `agent::yiemAgent`: The agent instance to receive a response from
# Returns
- An `assistantMessage` instance representing the agent's response
# Notes
- Use `promptAgent(agent, msg)` to send a message before calling this function.
# Examples
```jldoctest
julia> response = takeResponse(agent)
assistantMessage(...)
```
"""
function takeResponse(agent::yiemAgent)
return take!(agent.outputChannel)
end
"""
Send a follow-up message while the agent is still processing.
Follow-up messages are queued and processed after all `inputChannel` messages
and before any tool call results are sent.
# Arguments
- `agent::yiemAgent`: The agent instance to send a follow-up message to
- `msg`: The follow-up message to send, in OpenAI message format (e.g. `Dict("role" => "user", "content" => "Also consider red wines")`)
# Returns
- The same `agent` instance for chaining
# Notes
- Use `promptAgent(agent, msg)` for the primary message and `followUp(agent, msg)` for additional
messages while the agent is processing.
- Follow-up messages are buffered in a separate channel (capacity 32 by default).
# Examples
```jldoctest
julia> followUp(agent, Dict("role" => "user", "content" => "Also consider red wines"))
yiemAgent(...)
```
"""
function followUp(agent::yiemAgent, msg::AbstractDict{String, Any})
put!(agent.followUpChannel, msg)
return agent
end
function followUp(agent::yiemAgent, text::String)
put!(agent.followUpChannel, Dict("role" => "user", "content" => [Dict("type" => "text", "text" => text)]))
return agent
end
"""
Gracefully stop the agent.
Sends a `:shutdown` signal to the input channel, waits for the background task to finish,
then closes all channels (`inputChannel`, `outputChannel`, `followUpChannel`).
# Arguments
- `agent::yiemAgent`: The agent instance to stop
# Returns
- `nothing`
# Notes
- After calling `stopAgent`, the agent is no longer usable. A new agent must be created
for further interaction.
- If the background task throws a `TaskFailedException`, it is rethrown.
# Examples
```jldoctest
julia> stopAgent(agent)
```
"""
function stopAgent(agent::yiemAgent)
put!(agent.inputChannel, :shutdown)
try
fetch(agent._agentLoop)
catch e
if e isa TaskFailedException
rethrow(e)
end
end
close(agent.inputChannel)
close(agent.outputChannel)
close(agent.followUpChannel)
return nothing
end
""" Recursively convert dictionary-like variable (e.g. JSON.Object) into an OrderedDict.
The function walks any nested structure composed of `AbstractDict` (e.g., `JSON.Object`,
`Dict`, `OrderedDict`) and `AbstractArray` and produces a new tree where
every dictionary-like node is an `OrderedDict` and every array-like node is a `Vector{Any}`.
Scalar values (numbers, strings, booleans, `nothing`, etc.) are returned unchanged.
Does **not** mutate the input; it always allocates new containers.
# Arguments
- `x`
Any Julia value. If `x` is an `AbstractDict` it will be converted to an `OrderedDict`;
if it is an `AbstractArray` its elements will be processed recursively.
# Keyword Arguments
- `keytype::Type=Any`
The key type for the output OrderedDict. Use `String` for `OrderedDict{String,Any}`,
`Symbol` for `OrderedDict{Symbol,Any}`, or `Any` to preserve original key types.
- `sort_order::Union{Nothing, Vector}=nothing`
Vector of keys specifying the desired order. Keys are arranged in the specified order
first, followed by any remaining keys.
# Return
- A newly allocated nested structure composed of `OrderedDict{keytype,Any}` and `Vector{Any}`
that mirrors the input shape but uses ordered Julia containers.
# Notes
- The function treats any `AbstractDict` as a mapping source, so it works with
`JSON.Object`, `Dict`, `OrderedDict`, etc.
- Arrays are returned as `Vector{Any}` with their elements processed recursively.
# Examples
```jldoctest
julia> using JSON, DataStructures
julia> d = Dict(
"a" => 4,
"b" => 6,
"c" => Dict(
"d"=>7,
:e=>Dict(
"f"=>"hey",
"g"=>Dict(
"world"=>[1, "2", 3, Dict(:dd=>4.7)]
)
)
)
)
julia> jsonstring = JSON.json(d)
julia> A1 = JSON.parse(jsonstring) # A1 type is JSON.Object
julia> A2 = dictify(A1; keytype=String)
OrderedDict{String,Any} with 3 entries:
"a" => 4
"b" => 6
"c" => OrderedDict("d"=>7, "e"=>Dict("f"=>"hey", "g"=>Dict("world"=>[1, "2", 3, 4.7])))
julia> A3 = dictify(A1; keytype=Symbol)
OrderedDict{Symbol,Any} with 3 entries:
:a => 4
:b => 6
:c => OrderedDict(:d=>7, :e=>Dict("f"=>"hey", "g"=>Dict("world"=>[1, "2", 3, 4.7])))
julia> B1 = dictify(d; keytype=String)
OrderedDict{String, Any} with 3 entries:
```
**With sort_order:**
```jldoctest
julia> d = Dict("a"=>1, "b"=>2, "c"=>3)
julia> dictify(d; sort_order=["c", "a"])
OrderedDict{String,Int} with 3 entries:
"c" => 3
"a" => 1
"b" => 2
```
"""
function dictify(x::T; keytype::Type=Any, sort_order::Union{Nothing, Vector}=nothing
)::OrderedDict where {T<:AbstractDict}
# this function is example
end
end # module interface
-1563
View File
File diff suppressed because it is too large Load Diff
-1037
View File
File diff suppressed because it is too large Load Diff
+290
View File
@@ -0,0 +1,290 @@
module toolRegistry
export registerTool, registerAllTools, clearTools, listTool
using Dates
using JSON, DataStructures
using ..type
# ── MCP helper functions ────────────────────────────────────────────
"""
Extract text from MCP tool result content array.
Handles JSON-RPC 2.0 result content format:
{"content": [{"type": "text", "text": "..."}], "isError": false}
"""
function _extract_text_content(result::AbstractDict)::String
content = get(result, "content", Any[])
if content isa Vector && !isempty(content)
lines = String[]
for block in content
if block isa Dict && get(block, "type", "") == "text"
push!(lines, string(get(block, "text", "")))
end
end
if !isempty(lines)
return join(lines, "\n")
end
end
return JSON.json(result)
end
"""
Wrap an MCP tool definition as an `agentTool`.
The returned tool's `execute` function calls the MCP server's "tools/call"
method with the validated arguments.
"""
function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSink=nothing, sessionId=nothing)::agentTool
name = tool_def["name"]
title = get(tool_def, "title", get(tool_def, "label", name))
desc = get(tool_def, "description", "")
input_schema = get(tool_def, "inputSchema", Dict{String,Any}())
# Normalize inputSchema to OpenAI function format
if haskey(input_schema, "properties") && input_schema["type"] == "object"
params = Dict(
"type" => "object",
"properties" => input_schema["properties"],
"required" => get(input_schema, "required", Any[]),
)
else
params = Dict(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[],
)
end
return agentTool(
name=name,
label=title,
description=desc,
inputSchema=params,
execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
eventSink) -> begin
try
response = mcpserver("tools/call", name, args, sessionId; eventSink=eventSink)
# Parse JSON-RPC 2.0 response envelope
if haskey(response, "error")
rpc_error = response["error"]
err_msg = get(rpc_error, "message", "Unknown MCP error")
return agentToolResult(
[textContent("MCP error: $err_msg")],
Dict{Any,Any}("error" => err_msg),
nothing, false
)
end
result_data = get(response, "result", response)
content_text = _extract_text_content(result_data)
is_error = get(result_data, "isError", false)
return agentToolResult(
[textContent(content_text)],
Dict{Any,Any}("isError" => is_error),
nothing, false
)
catch e
errMsg = sprint(showerror, e)
return agentToolResult(
[textContent("MCP call error: $errMsg")],
Dict{Any,Any}("error" => errMsg),
nothing, false
)
end
end,
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
"""
Discover and register MCP tools into `store.tools`.
Queries the MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0 format),
parses the response envelope, and registers each discovered tool.
Handles pagination via `nextCursor`. Skips tools already registered.
# Returns
- `Int`: number of new tools registered
"""
function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::Int
if mcpserver === nothing
return 0
end
new_count = 0
try
response = mcpserver("tools/list", sessionId; eventSink=eventSink)
# Parse JSON-RPC 2.0 response envelope
if haskey(response, "error")
rpc_error = response["error"]
err_msg = get(rpc_error, "message", "Unknown MCP error")
println("[toolRegistry] MCP tools/list failed: $err_msg")
return 0
end
if haskey(response, "result")
response = response["result"]
end
tools_array = response["tools"]
cursor = get(response, "nextCursor", nothing)
for tool_def in tools_array
name = tool_def["name"]
if haskey(tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
tools[name] = wrapped
new_count += 1
end
# Paginate: fetch remaining tools if nextCursor is present
while cursor !== nothing && cursor !== ""
response = sessionId !== nothing ? mcpserver("tools/list", sessionId; eventSink=eventSink) : mcpserver("tools/list"; eventSink=eventSink)
if haskey(response, "result")
response = response["result"]
end
tools_array = get(response, "tools", Any[])
cursor = get(response, "nextCursor", nothing)
for tool_def in tools_array
name = tool_def["name"]
if haskey(tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
tools[name] = wrapped
new_count += 1
end
end
println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(tools))")
catch e
bt = catch_backtrace()
err_msg = sprint() do io
showerror(io, e, bt)
println(io)
end
eventSink(err_msg)
errMsg = sprint(showerror, e)
println("[toolRegistry] MCP tools/list failed: $errMsg")
end
return new_count
end
"""
registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool) -> OrderedDict{String, agentTool}
Add `tool` to `tools`, overwriting any existing tool with the same name.
# Arguments
- `tools`: Tool dict to modify
- `tool`: The `agentTool` to register
# Returns
- The same `tools` dict (modified in place)
"""
function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
)::OrderedDict{String, agentTool}
tools[tool.name] = tool
eventSink("[toolRegistry] Registered tool: $(tool.name)")
return tools
end
"""
Remove all tools from the dict.
# Arguments
- `tools`: Tool dict to clear
# Returns
- `nothing`
"""
function clearTools(tools::OrderedDict{String, agentTool})::Nothing
empty!(tools)
println("[toolRegistry] Registry cleared")
return nothing
end
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
"""
listTool(tools::OrderedDict{String, agentTool}, mcpserver) -> agentTool
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into `tools` (in-place mutation, with
pagination via nextCursor), then returns the full tool list.
Subsequent calls: returns the current list (tools remain registered).
"""
function listTool(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::agentTool
return agentTool(
name="listTools",
label="List Tools",
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
inputSchema=Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
eventSink) -> begin
# Discover and register MCP tools (idempotent — skips already registered)
new_count = register_mcp_tools(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
# Always include listTools itself in the count
total = length(tools)
lines = String[
"- $(t.name): $(t.label) — $(t.description)"
for (k, t) in tools
]
result_text = "Available tools ($total):\n" * join(lines, "\n")
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => total),
nothing, false
)
end,
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
# ── High-level API ──────────────────────────────────────────────────
"""
registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver)
Register all tools for an agent:
1. Auto-discover existing tools from the MCP server
2. Register the `listTools` tool so the agent can discover new tools at runtime
# Arguments
- `tools`: The tool dict to populate
- `mcpserver`: A callable struct that communicates with the MCP server
"""
function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing)
list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
eventSink("registering tools")
registerTool(tools, list_t; eventSink=eventSink)
eventSink("registering tools done")
end
end # module
+749 -184
View File
@@ -1,209 +1,774 @@
module type module type
export Timestamp,
# Abstract types
messageContent, agentMessage, agent,
# Model types
modelCost, llmModel, llmUsage,
# Message content types
textContent, imageContent, reasoningContent,
# Message types
userMessage, assistantMessageToolCall, assistantMessage, toolResultMessage,
# Tool types
agentTool, validateRequiredArgs,
# Context types
agentContext, agentState, agentToolCall, prepareNextTurnContext,
# Loop & execution types
agentLoopConfig, abortSignal, agentToolResult,beforeToolCallContext,
beforeToolCallResult, afterToolCallContext,
# Event types
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
# Agent
yiemAgent,
# Tool call lifecycle types
preparedToolCall, immediateOutcome, executedOutcome, finalizedOutcome,
agentToolCallBatch,
# Functions (defined elsewhere)
promptAgent, takeResponse, followUp, stopAgent
export agent, sommelier, companion
using Dates, UUIDs, DataStructures, JSON3 using Dates, UUIDs, DataStructures, JSON, NATS, Base.Threads
using GeneralUtils using GeneralUtils
# ---------------------------------------------- 100 --------------------------------------------- # const Timestamp = DateTime
struct agentToolCall # A tool invocation from the LLM
type::String # Always "function"
id::String # Unique tool call identifier
name::String # Tool name
arguments::Dict{String, Any} # Parsed tool arguments
end
# ------------------------------------------------------------------------------------------------ #
# LLM model info #
# ------------------------------------------------------------------------------------------------ #
struct modelCost # Model pricing per 1M tokens
input::Float64 # Price per 1M input tokens
output::Float64 # Price per 1M output tokens
cache_read::Float64 # Price per 1M cached read tokens
cache_write::Float64 # Price per 1M cache write tokens
end
struct llmModel # LLM model configuration
id::String # Unique model identifier
name::String # Human-readable model name
provider::String # Provider name (e.g., "anthropic", "openai")
baseUrl::String # API endpoint base URL
reasoning::Bool # Whether the model supports chain-of-thought
input::Vector{String} # Supported input modalities (e.g., "text", "image")
cost::modelCost # Pricing information
contextWindow::Int64 # Maximum context length in tokens
maxTokens::Int64 # Maximum output tokens per completion
end
struct llmUsage
inputTokens::Int64
outputTokens::Int64
end
# ------------------------------------------------------------------------------------------------ #
# Message content types #
# ------------------------------------------------------------------------------------------------ #
abstract type messageContent end # Base type for message content
struct textContent <: messageContent # Plain text message content
text::String # The text content
end
struct imageContent <: messageContent # Image message content
data::String # Base64-encoded image data
mimeType::String # MIME type (e.g., "image/png")
end
struct reasoningContent <: messageContent # LLM reasoning/thinking content
text::String # The reasoning text
end
# ------------------------------------------------------------------------------------------------ #
# Message types #
# ------------------------------------------------------------------------------------------------ #
abstract type agentMessage end # Base type for all agent messages
struct userMessage <: agentMessage # Message from the user
role::String # Always "user"
content::Vector{messageContent} # Text and/or image content
timestamp::Timestamp # When the message was sent
end
"""
Create a new user message.
# Arguments
- `role::String`: Always "user"
- `content::Vector{messageContent}`: Text and/or image content
- `timestamp::Timestamp`: When the message was sent
# Returns
- A new `userMessage` instance
# Examples
```julia
julia> msg = userMessage(content=[textContent("Hello")])
userMessage("user", [textContent("Hello")], DateTime(...))
```
"""
function userMessage(; role="user", content=Vector{messageContent}(), timestamp=now())
return userMessage(role, content, timestamp)
end
struct assistantMessageToolCall <: agentMessage # Assistant message containing tool calls
role::String # Always "assistant"
toolCalls::Vector{agentToolCall} # Tool calls to execute
content::Vector{messageContent} # Reasoning/thinking content blocks
api::String # API name used (e.g., "openai")
provider::String # Provider name (e.g., "anthropic")
model::String # Model identifier
usage::llmUsage # Token usage for this message
stopReason::String # Why generation stopped (e.g., "tool_calls")
errorMessage::Union{String, Nothing} # Error if generation failed
timestamp::Timestamp # When the message was received
end
"""
Create a new assistant message containing tool calls.
# Arguments
- `role::String`: Always "assistant"
- `toolCalls::Vector{agentToolCall}`: Tool calls to execute
- `content::Vector{messageContent}`: Reasoning/thinking content blocks
- `api::String`: API name used
- `provider::String`: Provider name
- `model::String`: Model identifier
- `usage::llmUsage`: Token usage
- `stopReason::String`: Why generation stopped
- `errorMessage::Union{String, Nothing}`: Error if generation failed
- `timestamp::Timestamp`: When the message was received
# Returns
- A new `assistantMessageToolCall` instance
# Examples
```julia
julia> tc = agentToolCall("function", "call_1", "getWeather", Dict("city" => "Tokyo"))
julia> msg = assistantMessageToolCall(toolCalls=[tc], stopReason="tool_calls")
assistantMessageToolCall("assistant", [agentToolCall(...)], messageContent[], "", "", "", llmUsage(0, 0), "tool_calls", nothing, DateTime(...))
```
"""
function assistantMessageToolCall(; role="assistant", toolCalls=agentToolCall[],
content=Vector{messageContent}(), api="", provider="", model=nothing, usage=llmUsage(0, 0),
stopReason="tool_calls", errorMessage=nothing, timestamp=now())
model_str = model isa AbstractString ? String(model) : ""
return assistantMessageToolCall(role, toolCalls, content, api, provider, model_str,
usage, stopReason, errorMessage, timestamp)
end
struct assistantMessage <: agentMessage # Message from the AI assistant
role::String # Always "assistant"
content::Vector{messageContent} # Text and/or image content
api::String # API name used (e.g., "openai")
provider::String # Provider name (e.g., "anthropic")
model::String # Model identifier
usage::llmUsage # Token usage for this message
stopReason::String # Why generation stopped (e.g., "end_turn")
errorMessage::Union{String, Nothing} # Error if generation failed
timestamp::Timestamp # When the message was received
end
"""
Create a new assistant message.
# Arguments
- `role::String`: Always "assistant"
- `content::Vector{messageContent}`: Text and/or image content
- `api::String`: API name used (e.g., "openai")
- `provider::String`: Provider name (e.g., "anthropic")
- `model::String`: Model identifier
- `usage::llmUsage`: Token usage for this message
- `stopReason::String`: Why generation stopped (e.g., "end_turn")
- `errorMessage::Union{String, Nothing}`: Error if generation failed
- `timestamp::Timestamp`: When the message was received
# Returns
- A new `assistantMessage` instance
# Examples
```julia
julia> msg = assistantMessage(content=[textContent("Hello!")], model="gpt-4")
assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "end_turn", nothing, DateTime(...))
```
"""
function assistantMessage(; role="assistant", content=Vector{messageContent}(),
api="", provider="", model=nothing, usage=llmUsage(0, 0), stopReason="end_turn",
errorMessage=nothing, timestamp=now())
model_str = model isa AbstractString ? String(model) : ""
return assistantMessage(role, content, api, provider, model_str, usage, stopReason, errorMessage, timestamp)
end
struct toolResultMessage <: agentMessage # Result returned from a tool execution
role::String # Always "tool"
toolCallId::String # ID matching the tool call
toolName::String # Name of the executed tool
content::Vector{messageContent} # Tool output content
details::Any # Additional tool-specific details
usage::Union{llmUsage, Nothing} # Token usage if applicable
addedToolNames::Union{Vector{String}, Nothing} # Tools added during execution
isError::Bool # Whether the tool call resulted in an error
timestamp::Timestamp # When the result was recorded
end
"""
Create a new tool result message.
# Arguments
- `role::String`: Always "tool"
- `toolCallId::String`: ID matching the tool call
- `toolName::String`: Name of the executed tool
- `content::Vector{messageContent}`: Tool output content
- `details::Any`: Additional tool-specific details
- `usage::Union{llmUsage, Nothing}`: Token usage if applicable
- `addedToolNames::Union{Vector{String}, Nothing}`: Tools added during execution
- `isError::Bool`: Whether the tool call resulted in an error
- `timestamp::Timestamp`: When the result was recorded
# Returns
- A new `toolResultMessage` instance
# Examples
```julia
julia> msg = toolResultMessage(toolCallId="call_123", toolName="search", content=[textContent("results")])
toolResultMessage("tool", "call_123", "search", [textContent("results")], nothing, nothing, nothing, false, DateTime(...))
```
"""
function toolResultMessage(; role="tool", toolCallId="", toolName="",
content=Vector{messageContent}(), details=nothing, usage=nothing,
addedToolNames=nothing, isError=false, timestamp=now())
return toolResultMessage(role, toolCallId, toolName, content, details, usage, addedToolNames, isError, timestamp)
end
# ------------------------------------------------------------------------------------------------ #
# Tool types #
# ------------------------------------------------------------------------------------------------ #
"""
A tool available to the agent.
Maps MCP server tool definitions to an executable Julia tool.
# Arguments
- `name::String`: Tool identifier (from MCP `name`)
- `label::String`: Human-readable tool name (from MCP `title`)
- `description::String`: What the tool does (from MCP `description`)
- `inputSchema::Any`: Tool parameters schema (from MCP `inputSchema`, JSON Schema format)
- `execute::Function`: Tool execution function, signature:
`execute(toolCallId::String, args::Dict, signal::Union{Nothing,AbortSignal}, onPartialResult::Function)`
- `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback
- `validateRequiredArgs::Union{Function, Nothing}`: Optional validation hook, signature:
`validateRequiredArgs(args::Dict) -> Union{Nothing, String}` where `String` is an error message
- `parallelToolExecute::Bool`: Override: run tool calls sequentially or in parallel
# Returns
- A new `agentTool` instance
# MCP Tool Example
```
{
"name": "getWeather",
"title": "Weather Lookup",
"description": "Fetch current weather and forecast for a given city.",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City and state/country" },
"units": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" }
},
"required": ["city"]
}
}
```
# Example
```julia
tool = agentTool(
name="getWeather",
label="Weather Lookup",
description="Fetch current weather and forecast for a given city.",
inputSchema=Dict(
"type" => "object",
"properties" => Dict(
"city" => Dict("type" => "string", "description" => "City name"),
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"])
),
"required" => ["city"]
),
execute=(toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Sunny, 22C in Bangkok")],
Dict{Any,Any}(), nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
)
```
"""
struct agentTool # A tool available to the agent
name::String # Tool identifier
label::String # Human-readable tool name
description::String # What the tool does
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
execute # Tool execution function
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
end
"""
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
"""
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
execute, prepareArguments::Union{Function, Nothing}=nothing,
validateRequiredArgs::Union{Function, Nothing}=nothing,
parallelToolExecute::Bool=false)
return agentTool(name, label, description, inputSchema, execute,
prepareArguments, validateRequiredArgs, parallelToolExecute)
end
# ------------------------------------------------------------------------------------------------ #
# Agent context #
# ------------------------------------------------------------------------------------------------ #
"""
Snapshot of the agent's conversation context.
# Arguments
- `systemPrompt::String`: System prompt for the agent
- `messages::Vector{agentMessage}`: Conversation messages
- `tools::Union{OrderedDict{String, agentTool}, Nothing}`: Available tools keyed by name for O(1) lookup
# Returns
- A new `agentContext` instance
"""
struct agentContext # Snapshot of the agent's conversation context
systemPrompt::String # System prompt for the agent
messages::Vector{agentMessage} # Conversation messages
tools::Union{OrderedDict{String, agentTool}, Nothing} # Available tools keyed by name
end
# ------------------------------------------------------------------------------------------------ #
# Agent state #
# ------------------------------------------------------------------------------------------------ #
mutable struct agentState # Mutable runtime state of an agent
systemPrompt::String # System prompt for the agent
model::Union{llmModel, Nothing} # LLM model to use
tools::OrderedDict{String, agentTool} # Available tools keyed by name, insertion-ordered
# messages history includes userMessage, assistantMessage, toolResultMessage. NO system prompt
messages::Vector{agentMessage}
pendingToolCalls::Vector{String} # Tool call IDs waiting for results
errorMessage::Union{String, Nothing} # Last error message
end
"""
Create a new mutable agent state.
Creates a deep copy of the provided tools and messages to isolate the
new state from external references.
# Arguments
- `systemPrompt::String`: System prompt text
- `model::llmModel`: LLM model to use (defaults to an unknown model)
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (deep copied)
- `messages::Vector{agentMessage}`: Conversation messages (deep copied)
# Returns
- A new `agentState` instance with an empty pending tool calls list and no error
# Examples
```julia
julia> state = agentState(systemPrompt="You are a helpful assistant")
agentState("You are a helpful assistant", OrderedDict{String, agentTool}(), agentMessage[], String[], nothing)
"""
function agentState(
systemPrompt::String="",
model=llmModel("model_1", "unknown", "unknown", "", false, String[],
modelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
messages::Vector{agentMessage}=agentMessage[],
)
agentState(
systemPrompt,
model,
tools,
deepcopy(messages),
Vector{String}(),
nothing,
)
end
"""
Context for preparing the next conversation turn.
# Arguments
- `message::assistantMessage`: The assistant's message that just completed
- `toolResults::Vector{toolResultMessage}`: Tool results from this turn
- `context::agentContext`: Current conversation context
- `newMessages::Vector{agentMessage}`: Messages to append to the context
# Returns
- A new `prepareNextTurnContext` instance
"""
struct prepareNextTurnContext # Context for preparing the next conversation turn
message::assistantMessage # The assistant's message that just completed
toolResults::Vector{toolResultMessage} # Tool results from this turn
context::agentContext # Current conversation context
newMessages::Vector{agentMessage} # Messages to append to the context
end
# ------------------------------------------------------------------------------------------------ #
# Agent loop configuration & tool execution types #
# ------------------------------------------------------------------------------------------------ #
"""
Configuration for the agent tool execution loop.
# Fields
- `beforeToolCall::Union{Function, Nothing}`: Callback before tool execution
- `afterToolCall::Union{Function, Nothing}`: Callback after tool execution
- `toolExecution::String`: Execution mode — "sequential" or "parallel"
"""
struct agentLoopConfig
beforeToolCall::Union{Function, Nothing}
afterToolCall::Union{Function, Nothing}
toolExecution::String
end
"""
Signal for aborting ongoing operations.
# Arguments
- `aborted::Bool`: Whether the operation has been aborted
"""
struct abortSignal
aborted::Bool
end
"""
Result returned by tool execution before the `afterToolCall` hook.
# Arguments
- `content::Vector{messageContent}`: Tool output content
- `details::Dict{Any,Any}`: Tool-specific details
- `usage::Union{llmUsage, Nothing}`: Token usage if applicable
- `terminate::Bool`: Whether tool requests termination of the agent loop
"""
struct agentToolResult
content::Vector{messageContent}
details::Dict{Any,Any}
usage::Union{llmUsage, Nothing}
terminate::Bool
end
"""
Context passed to the `beforeToolCall` hook.
# Arguments
- `message::assistantMessageToolCall`: The assistant message containing the tool call
- `toolCall::agentToolCall`: The tool call being prepared
- `args::AbstractDict{String, Any}`: Validated tool arguments
- `context::agentContext`: Current conversation context
"""
struct beforeToolCallContext
message::assistantMessageToolCall
toolCall::agentToolCall
args::AbstractDict{String, Any}
context::agentContext
end
struct beforeToolCallResult
block::Bool
reason::String
end
"""
Context passed to the `afterToolCall` hook.
# Arguments
- `message::assistantMessageToolCall`: The assistant message containing the tool call
- `toolCall::agentToolCall`: The tool call that was executed
- `args::AbstractDict{String, Any}`: Tool arguments
- `result::agentToolResult`: The raw tool result
- `isError::Bool`: Whether execution resulted in an error
- `context::agentContext`: Current conversation context
"""
struct afterToolCallContext
message::assistantMessageToolCall
toolCall::agentToolCall
args::AbstractDict{String, Any}
result::agentToolResult
isError::Bool
context::agentContext
end
"""
Event emitted when a tool call execution starts.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `arguments::AbstractDict{String, Any}`: Tool arguments
"""
struct toolExecStartEvent
toolCallId::String
toolName::String
arguments::AbstractDict{String, Any}
end
"""
Event emitted with partial results during tool execution.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `arguments::AbstractDict{String, Any}`: Tool arguments
- `partialResult::Any`: The partial result data
"""
struct toolExecUpdateEvent
toolCallId::String
toolName::String
arguments::AbstractDict{String, Any}
partialResult::Any
end
"""
Event emitted when a tool call execution ends.
# Arguments
- `toolCallId::String`: ID of the tool call
- `toolName::String`: Name of the tool
- `result::agentToolResult`: The final tool result
- `isError::Bool`: Whether execution resulted in an error
"""
struct toolExecEndEvent
toolCallId::String
toolName::String
result::agentToolResult
isError::Bool
end
# ------------------------------------------------------------------------------------------------ #
# Agent struct #
# ------------------------------------------------------------------------------------------------ #
abstract type agent end abstract type agent end
mutable struct companion <: agent
id::String # agent id
systemmsg::Union{String, Nothing}
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
""" Memory
Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
NO "system" message in chathistory because I want to add it at the inference time
chathistory= [
Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()),
Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()),
]
""" """
chathistory::Vector{Dict{Symbol, Any}} preparedToolCall(tool, toolCall, args)
memory::Dict{Symbol, Any}
# communication function Intermediate state between tool call validation and execution.
text2textInstructLLM::Function Created after `prepareToolCall` succeeds; serves as the bridge to
end the execution phase. Keeping the resolved tool, original call
metadata, and validated args together avoids repeated lookups and
allows the execution phase to access all necessary data without
carrying the full context through the call chain.
function companion( # Fields
text2textInstructLLM::Function - `tool::agentTool`: The resolved tool definition from the context
; - `toolCall::agentToolCall`: The original tool call from the assistant
id::String= string(uuid4()), - `args::any`: Validated (and coerced) argument values
systemmsg::Union{String, Nothing}= nothing,
maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(),
)
memory = Dict{Symbol, Any}( # Examples
:chatbox=> "", ```julia
:shortmem=> OrderedDict{Symbol, Any}(), # After prepareToolCall succeeds, the agent holds a preparedToolCall
:events=> Vector{Dict{Symbol, Any}}(), prep = preparedToolCall(
:state=> Dict{Symbol, Any}(), tool, # agentTool found in context.tools
) toolCall, # {id: "call_1", name: "search_wine", arguments: "{\"query\": \"red wine\"}"}
validatedArgs # Dict("query" => "red wine")
newAgent = companion(
id,
systemmsg,
maxHistoryMsg,
chathistory,
memory,
text2textInstructLLM
)
return newAgent
end
""" A sommelier agent.
# Arguments
- `mqttClient::Client`
MQTTClient's client
- `msgMeta::Dict{Symbol, Any}`
A dict contain info about a message.
- `config::Dict{Symbol, Any}`
Config info for an agent. Contain mqtt topic for internal use and other info.
# Keyword Arguments
- `name::String`
Agent's name
- `id::String`
Agent's ID
- `tools::Dict{Symbol, Any}`
Agent's tools
- `maxHistoryMsg::Integer`
max history message
# Return
- `nothing`
# Example
```jldoctest
julia> using YiemAgent, MQTTClient, GeneralUtils
julia> msgMeta = GeneralUtils.generate_msgMeta(
"N/A",
replyTopic = "/testtopic/prompt"
)
julia> tools= Dict(
:chatbox=>Dict(
:name => "chatbox",
:description => "Useful only for when you need to ask the user for more info or context. Do not ask the user their own question.",
:input => "Input should be a text.",
:output => "" ,
:func => nothing,
),
)
julia> agentConfig = Dict(
:receiveprompt=>Dict(
:mqtttopic=> "/testtopic/prompt", # topic to receive prompt i.e. frontend send msg to this topic
),
:receiveinternal=>Dict(
:mqtttopic=> "/testtopic/internal", # receive topic for model's internal
),
:text2text=>Dict(
:mqtttopic=> "/text2text/receive",
),
)
julia> client, connection = MakeConnection("test.mosquitto.org", 1883)
julia> agent = YiemAgent.bsommelier(
client,
msgMeta,
agentConfig,
name= "assistant",
id= "555", # agent instance id
tools=tools,
) )
``` ```
# TODO
- [] update docstring
- [x] implement the function
# Signature
""" """
mutable struct sommelier <: agent struct preparedToolCall
name::String # agent name tool::agentTool # The resolved tool definition from the context
id::String # agent id toolCall::agentToolCall # The original tool call from the assistant
retailername::String args::Any # Validated (and coerced) argument values
tools::Dict
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
""" Memory
Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3
NO "system" message in chathistory because I want to add it at the inference time
chathistory= [
Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()),
Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()),
]
"""
chathistory::Vector{Dict{Symbol, Any}}
memory::Dict{Symbol, Any}
func # NamedTuple of functions
end end
function sommelier( """
func, # NamedTuple of functions immediateOutcome(result, isError)
;
name::String= "Assistant", A tool call that was resolved without actual execution — either
id::String= string(uuid4()), because the tool was not found, validation failed, or a
retailername::String= "retailer_name", `beforeToolCall` hook blocked the call. The result is produced
maxHistoryMsg::Integer= 20, immediately and emitted as a tool result message.
chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(),
Returning an outcome instead of throwing an exception is intentional:
it lets the agent feed the error back to the LLM as a tool result so
the model can recover — for example, by re-issuing a tool call with
corrected arguments after a validation failure.
# Fields
- `result::agentToolResult`: The pre-computed tool result
- `isError::Bool`: Whether this outcome represents an error
# Examples
```julia
# Tool not found — immediate error
immediateOutcome(
createErrorToolResult("Tool search_wine not found"),
true
) )
tools = Dict( # update input format # beforeToolCall hook blocked execution
"chatbox"=> Dict( immediateOutcome(
:description => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>", createErrorToolResult("Tool execution was blocked"),
:input => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""", true
:output => "" ,
),
"winestock"=> Dict(
:description => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
:input => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
:output => """<output>Output are wines that match the search query in JSON format.""",
),
# "finalanswer"=> Dict(
# :description => "<tool description>Useful for when you are ready to recommend wines to the user.</tool description>",
# :input => """<input format>{\"finalanswer\": \"some text\"}.</input format><input example>{\"finalanswer\": \"I recommend Zena Crown Vista\"}</input example>""",
# :output => "" ,
# :func => nothing,
# ),
) )
```
memory = Dict{Symbol, Any}( """
:chatbox=> "", struct immediateOutcome
:shortmem=> OrderedDict{Symbol, Any}( result::agentToolResult # The pre-computed tool result
:available_wine=> [], isError::Bool # Whether this outcome represents an error
:found_wine=> [], # used by decisionMaker(). This is to prevent decisionMaker() keep presenting the same wines
),
:events=> Vector{Dict{Symbol, Any}}(),
:state=> Dict{Symbol, Any}(
),
:recap=> OrderedDict{Symbol, Any}(),
)
newAgent = sommelier(
name,
id,
retailername,
tools,
maxHistoryMsg,
chathistory,
memory,
func
)
return newAgent
end end
"""
executedOutcome(result, isError)
A tool call that has been executed by `tool.execute()` but has not
yet been through the `afterToolCall` hook. This intermediate state
is necessary because the hook may mutate the result (content, usage,
termination, error status). Keeping execution and finalization
separate allows the hook to inspect the raw result and decide
whether to transform it or replace it entirely.
# Fields
- `result::agentToolResult`: The tool's execution result
- `isError::Bool`: Whether execution raised an error
# Examples
```julia
# Successful execution
executedOutcome(
agentToolResult([textContent("text", "Found 3 wines")], dict{any,any}(), dict{any,any}()),
false
)
# Execution error
executedOutcome(
createErrorToolResult("Connection timeout"),
true
)
```
"""
struct executedOutcome
result::agentToolResult # The tool's execution result
isError::Bool # Whether execution raised an error
end
"""
finalizedOutcome(toolCall, result, isError)
The complete outcome of a tool call after both execution and the
`afterToolCall` hook. This is the final form used to construct
the `toolResultMessage` emitted to the agent loop.
The three-phase design (prepare → execute → finalize) exists so
that each phase has a single responsibility: preparation handles
validation and gating, execution performs the actual work, and
finalization applies post-processing hooks. This separation allows
the agent loop to emit `toolExecutionEnd` events with the
finalized data while keeping each phase independently testable
and swappable.
# Fields
- `toolCall::agentToolCall`: The original tool call reference
- `result::agentToolResult`: The final tool result (post-afterToolCall)
- `isError::Bool`: Whether the call failed or was blocked
# Examples
```julia
# Normal successful finalization
finalizedOutcome(tc, agentToolResult(content, details, usage, false), false)
# afterToolCall mutated result and set terminate
finalizedOutcome(tc, agentToolResult(content, details, usage, true), false)
```
"""
struct finalizedOutcome
toolCall::agentToolCall # The original tool call reference
result::agentToolResult # The final tool result (post-afterToolCall)
isError::Bool # Whether the call failed or was blocked
end
"""
agentToolCallBatch(messages, terminate)
A batch of tool result messages from executing one or more tool calls.
The `terminate` flag indicates whether all tools in the batch
requested termination, which causes the agent loop to stop
processing further turns.
This flag is set by the tool implementation (not the end user) to
signal that the agent should not call the LLM again. Typical use
cases:
- Task completion: a tool like `deploy` or `submit` finishes its
work and returns `terminate: true` so the agent stops instead
of asking the LLM what to do next.
- Unrecoverable error: a tool hits a fatal condition (e.g.
database connection lost, auth token expired) and returns
`terminate: true` so the agent stops with an error message
rather than retrying.
- Async handoff: a tool triggers a long-running external
operation and wants the agent to stop now; the external system
will later resume the agent via `continue()`.
If `terminate` is `false` (default), the agent loop feeds the tool
results back to the LLM for another turn.
# Fields
- `messages::Vector{toolResultMessage}`: Tool result messages for this batch
- `terminate::Bool`: Whether the batch should terminate the loop
# Examples
```julia
# Batch of 3 tool results, no termination
agentToolCallBatch(resultMessages, false)
# All tools requested termination
agentToolCallBatch(resultMessages, true)
# Empty batch — terminate is false regardless
agentToolCallBatch(toolResultMessage[], false)
```
"""
struct agentToolCallBatch
messages::Vector{toolResultMessage} # Tool result messages for this batch
terminate::Bool # Whether the batch should terminate the loop
end
-496
View File
@@ -1,496 +0,0 @@
module util
export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText
using UUIDs, Dates, DataStructures, HTTP, JSON3
using GeneralUtils
using ..type
# ---------------------------------------------- 100 --------------------------------------------- #
""" Clear agent chat history.
# Arguments
- `a::agent`
an agent
# Return
- nothing
# Example
```jldoctest
julia> using YiemAgent, MQTTClient, GeneralUtils
julia> client, connection = MakeConnection("test.mosquitto.org", 1883)
julia> connect(client, connection)
julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic")
julia> agentConfig = Dict(
:receiveprompt=>Dict(
:mqtttopic=> "testtopic/receive",
),
:receiveinternal=>Dict(
:mqtttopic=> "testtopic/internal",
),
:text2text=>Dict(
:mqtttopic=> "testtopic/text2text",
),
)
julia> a = YiemAgent.sommelier(
client,
msgMeta,
agentConfig,
)
julia> YiemAgent.addNewMessage(a, "user", "hello")
julia> YiemAgent.clearhistory(a)
```
# TODO
- [PENDING] clear memory
# Signature
"""
function clearhistory(a::T) where {T<:agent}
empty!(a.chathistory)
empty!(a.memory[:shortmem])
empty!(a.memory[:events])
a.memory[:chatbox] = ""
end
""" Add new message to agent.
Arguments\n
-----
a::agent
an agent
role::String
message sender role i.e. system, user or assistant
text::String
message text
Return\n
-----
nothing
Example\n
-----
```jldoctest
julia> using YiemAgent, MQTTClient, GeneralUtils
julia> client, connection = MakeConnection("test.mosquitto.org", 1883)
julia> connect(client, connection)
julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic")
julia> agentConfig = Dict(
:receiveprompt=>Dict(
:mqtttopic=> "testtopic/receive",
),
:receiveinternal=>Dict(
:mqtttopic=> "testtopic/internal",
),
:text2text=>Dict(
:mqtttopic=> "testtopic/text2text",
),
)
julia> a = YiemAgent.sommelier(
client,
msgMeta,
agentConfig,
)
julia> YiemAgent.addNewMessage(a, "user", "hello")
```
Signature\n
-----
"""
function addNewMessage(a::T1, name::String, text::T2;
maximumMsg::Integer=20) where {T1<:agent, T2<:AbstractString}
if name ∉ ["system", "user", "assistant"] # guard against typo
error("name is not in agent.availableRole $(@__LINE__)")
end
#[PENDING] summarize the oldest 10 message
if length(a.chathistory) > maximumMsg
summarize(a.chathistory)
else
d = Dict(:name=> name, :text=> text, :timestamp=> Dates.now())
push!(a.chathistory, d)
end
end
""" Converts a vector of dictionaries to a formatted string.
This function takes in a vector of dictionaries and outputs a single string where each dictionary's keys are prefixed by their values.
# Arguments
- `vecd::Vector`
a vector of dictionaries
- `withkey::Bool`
whether to include the key in the output text. Default is true
# Return
a string with the formatted dictionaries
# Example
```jldoctest
julia> using Revise
julia> using GeneralUtils
julia> vecd = [Dict(:name => "John", :text => "Hello"), Dict(:name => "Jane", :text => "Goodbye")]
julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true)
"John> Hello\nJane> Goodbye\n"
```
# Signature
"""
function chatHistoryToText(vecd::Vector; withkey=true)::String
# Initialize an empty string to hold the final text
text = ""
# Determine whether to include the key in the output text or not
if withkey
# Loop through each dictionary in the input vector
for d in vecd
# Extract the 'name' and 'text' keys from the dictionary
name = d[:name]
_text = d[:text]
# Append the formatted string to the text variable
text *= "$name> $_text \n"
end
else
# Loop through each dictionary in the input vector
for d in vecd
# Iterate over all key-value pairs in the dictionary
for (k, v) in d
# Append the formatted string to the text variable
text *= "$v \n"
end
end
end
# Return the final text
return text
end
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
function eventdict(;
event_description::Union{String, Nothing}=nothing,
timestamp::Union{DateTime, Nothing}=nothing,
subject::Union{String, Nothing}=nothing,
thought::Union{AbstractDict, Nothing}=nothing,
actionname::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENTBOX", etc
actioninput::Union{String, Nothing}=nothing,
location::Union{String, Nothing}=nothing,
equipment_used::Union{String, Nothing}=nothing,
material_used::Union{String, Nothing}=nothing,
outcome::Union{String, Nothing}=nothing,
note::Union{String, Nothing}=nothing,
)
return Dict{Symbol, Any}(
:event_description=> event_description,
:timestamp=> timestamp,
:subject=> subject,
:thought=> thought,
:actionname=> actionname,
:actioninput=> actioninput,
:location=> location,
:equipment_used=> equipment_used,
:material_used=> material_used,
:outcome=> outcome,
:note=> note,
)
end
function createTimeline(memory::T1; skiprecent::Integer=0) where {T1<:AbstractVector}
events = memory[1:end-skiprecent]
timeline = ""
for (i, event) in enumerate(events)
if event[:outcome] === nothing
timeline *= "$i) $(event[:subject])> $(event[:actioninput])\n"
else
timeline *= "$i) $(event[:subject])> $(event[:actioninput]) $(event[:outcome])\n"
end
end
return timeline
end
# """ Convert a single chat dictionary into LLM model instruct format.
# # Llama 3 instruct format example
# <|system|>
# You are a helpful AI assistant.<|end|>
# <|user|>
# I am going to Paris, what should I see?<|end|>
# <|assistant|>
# Paris, the capital of France, is known for its stunning architecture, art museums."<|end|>
# <|user|>
# What is so great about #1?<|end|>
# <|assistant|>
# # Arguments
# - `name::T`
# message owner name e.f. "system", "user" or "assistant"
# - `text::T`
# # Return
# - `formattedtext::String`
# text formatted to model format
# # Example
# ```jldoctest
# julia> using Revise
# julia> using YiemAgent
# julia> d = Dict(:name=> "system",:text=> "You are a helpful, respectful and honest assistant.",)
# julia> formattedtext = YiemAgent.formatLLMtext_phi3instruct(d[:name], d[:text])
# ```
# Signature
# """
# function formatLLMtext_phi3instruct(name::T, text::T) where {T<:AbstractString}
# formattedtext =
# """
# <|$name|>
# $text<|end|>\n
# """
# return formattedtext
# end
# """ Convert a single chat dictionary into LLM model instruct format.
# # Llama 3 instruct format example
# <|begin_of_text|>
# <|start_header_id|>system<|end_header_id|>
# You are a helpful assistant.
# <|eot_id|>
# <|start_header_id|>user<|end_header_id|>
# Get me an icecream.
# <|eot_id|>
# <|start_header_id|>assistant<|end_header_id|>
# Go buy it yourself at 7-11.
# <|eot_id|>
# # Arguments
# - `name::T`
# message owner name e.f. "system", "user" or "assistant"
# - `text::T`
# # Return
# - `formattedtext::String`
# text formatted to model format
# # Example
# ```jldoctest
# julia> using Revise
# julia> using YiemAgent
# julia> d = Dict(:name=> "system",:text=> "You are a helpful, respectful and honest assistant.",)
# julia> formattedtext = YiemAgent.formatLLMtext_llama3instruct(d[:name], d[:text])
# "<|begin_of_text|>\n <|start_header_id|>system<|end_header_id|>\n You are a helpful, respectful and honest assistant.\n <|eot_id|>\n"
# ```
# Signature
# """
# function formatLLMtext_llama3instruct(name::T, text::T) where {T<:AbstractString}
# formattedtext =
# if name == "system"
# """
# <|begin_of_text|>
# <|start_header_id|>$name<|end_header_id|>
# $text
# <|eot_id|>
# """
# else
# """
# <|start_header_id|>$name<|end_header_id|>
# $text
# <|eot_id|>
# """
# end
# return formattedtext
# end
# """ Convert a chat messages in vector of dictionary into LLM model instruct format.
# # Arguments
# - `messages::Vector{Dict{Symbol, T}}`
# message owner name e.f. "system", "user" or "assistant"
# - `formatname::T`
# format name to be used
# # Return
# - `formattedtext::String`
# text formatted to model format
# # Example
# ```jldoctest
# julia> using Revise
# julia> using YiemAgent
# julia> chatmessage = [
# Dict(:name=> "system",:text=> "You are a helpful, respectful and honest assistant.",),
# Dict(:name=> "user",:text=> "list me all planets in our solar system.",),
# Dict(:name=> "assistant",:text=> "I'm sorry. I don't know. You tell me.",),
# ]
# julia> formattedtext = YiemAgent.formatLLMtext(chatmessage, "llama3instruct")
# "<|begin_of_text|>\n <|start_header_id|>system<|end_header_id|>\n You are a helpful, respectful and honest assistant.\n <|eot_id|>\n <|start_header_id|>user<|end_header_id|>\n list me all planets in our solar system.\n <|eot_id|>\n <|start_header_id|>assistant<|end_header_id|>\n I'm sorry. I don't know. You tell me.\n <|eot_id|>\n"
# ```
# # Signature
# """
# function formatLLMtext(messages::Vector{Dict{Symbol, T}},
# formatname::String="llama3instruct") where {T<:Any}
# f = if formatname == "llama3instruct"
# formatLLMtext_llama3instruct
# elseif formatname == "mistral"
# # not define yet
# elseif formatname == "phi3instruct"
# formatLLMtext_phi3instruct
# else
# error("$formatname template not define yet")
# end
# str = ""
# for t in messages
# str *= f(t[:name], t[:text])
# end
# # add <|assistant|> so that the model don't generate it and I don't need to clean it up later
# if formatname == "phi3instruct"
# str *= "<|assistant|>\n"
# end
# return str
# end
# """
# Arguments\n
# -----
# Return\n
# -----
# Example\n
# -----
# ```jldoctest
# julia>
# ```
# TODO\n
# -----
# [] update docstring
# [PENDING] implement the function
# Signature\n
# -----
# """
# function iterativeprompting(a::T, prompt::String, verification::Function) where {T<:agent}
# msgMeta = GeneralUtils.generate_msgMeta(
# a.config[:externalService][:text2textinstruct],
# senderName= "iterativeprompting",
# senderId= a.id,
# receiverName= "text2textinstruct",
# )
# outgoingMsg = Dict(
# :msgMeta=> msgMeta,
# :payload=> Dict(
# :text=> prompt,
# )
# )
# success = nothing
# result = nothing
# critique = ""
# # iteration loop
# while true
# # send prompt to LLM
# response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg)
# error("--> iterativeprompting")
# # check for correctness and get feedback
# success, _critique = verification(response)
# if success
# result = response
# break
# else
# # add critique to prompt
# critique *= _critique * "\n"
# replace!(prompt, "Critique: ..." => "Critique: $critique")
# end
# end
# return (success=success, result=result)
# end
end # module util
+598
View File
@@ -0,0 +1,598 @@
module utils
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
validateToolArguments, _userMessageToOpenAI,
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks,
_toolsToOpenAI, beforeToolCall, afterToolCall
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
using GeneralUtils
using ..type
# ---------------------------------------------- 100 --------------------------------------------- #
"""
Clear agent chat history.
Empties the conversation history, short-term memory, events log, and chatbox.
# Arguments
- `a::T`: An agent instance (subtype of `agent`)
# Returns
- `nothing`
# Notes
- Does not clear long-term memory; use `[PENDING] clear memory` when implemented.
# Examples
```jldoctest
julia> YiemAgent.clearhistory(agent)
```
"""
function clearhistory(a::T) where {T<:agent}
# empty!(a.chathistory)
# empty!(a.memory["shortmem"])
# empty!(a.memory["events"])
# a.memory["chatbox"] = ""
end
"""
Convert a vector of wine dictionaries to a formatted text string.
# Arguments
- `vecd::Vector`: A vector of dictionaries, each representing a wine with key-value pairs
# Returns
- A formatted string where each wine is numbered and each key-value pair is comma-separated
in the format: `"1) key1:value1,key2:value2 key3:value3 2) ..."`
# Examples
```jldoctest
julia> vecd = [Dict("wine_name" => "Chateau A", "price" => "50")]
julia> YiemAgent.availableWineToText(vecd)
"1) wine_name:Chateau A,price:50 "
```
"""
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
"""
prepareContext(state::agentState) -> agentContext
Prepares an `agentContext` from the given `agentState` for sending to
the LLM. By default, it deep copies the system prompt, messages, and
tools from `state` into a new `agentContext`.
Override this function to customize the context — such as filtering
tools based on the user's intent, modifying the system prompt, injecting
additional context (retrieved documents, current time, user preferences),
or pruning and reordering messages before formatting and calling the LLM.
# Arguments
- `state::agentState`: The current agent state containing conversation history,
system prompt, tools, and other configuration
# Returns
- `agentContext`: An `agentContext` containing the prepared system prompt,
messages, and tools to be sent to the LLM
# Examples
```jldoctest
# Default: returns an agentContext with deep copies of system prompt, messages, and tools
prepareContext(state).messages == deepcopy(state.messages)
# Override to filter tools and inject system context:
# function prepareContext(state::agentState)
# msgs = deepcopy(state.messages)
# sysPrompt = state.systemPrompt * "\\nCurrent time: $(now())"
# tools = filter(t -> contains(t.description, "wine"), state.tools)
# return agentContext(sysPrompt, msgs, tools)
# end
```
"""
function prepareContext(state::agentState, eventSink)::agentContext
#TODO filter tools from state.tools based on user intend in user message and tool description
filteredTools = state.tools
#TODO add filtered tools to the current system prompt / modify systemPrompt here
preparedSystemPrompt = state.systemPrompt
#TODO add system prompt, adjust/modify and inject additional context into messages
preparedMessages = deepcopy(state.messages) # messages that will be send to LLM
agentCtx = agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
return agentCtx
end
"""
formatMsgForLLM(ctx::agentContext) -> Dict{String, Any}
Converts an `agentContext` into OpenAI-compatible message format
ready to be sent to the LLM. The system prompt is converted into
a system role message, followed by user, assistant, and tool result
messages.
This function can be overridden in `yiemAgent` to produce custom
LLM message formats for different APIs/providers.
# Arguments
- `ctx::agentContext`: The prepared context containing system prompt,
messages, and tools
# Returns
- `Dict{String, Any}`: A dictionary with `"messages"` key containing
an array of OpenAI-format message dicts
# Examples
```jldoctest
# Default output:
formatMsgForLLm(ctx) == Dict("messages" => [
Dict("role" => "system", "content" => [...]),
Dict("role" => "user", "content" => [...]),
Dict("role" => "assistant", "content" => [...]),
Dict("role" => "tool", "tool_call_id" => "...", "content" => [...]),
])
```
"""
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
""" openai message format example
msg = Dict(
"model" => "gemma-4-E4B-it-UD-Q4_K_XL",
"messages" => [
Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => systemmsg),
]
),
Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Do you have something similar to the one in the image?"),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data_uri)
)
]
),
Dict(
"role" => "assistant",
"content" => [
Dict("type" => "text", "text" => "let me check."),
]
),
],
"tools"=> [
Dict(
"type" => "function",
"function" => Dict(
"name" => "getWeather",
"description" => "Get current weather",
"parameters" => Dict(
"type" => "object",
"properties" => Dict(
"city" => Dict("type" => "string")
),
"required" => ["city"]
)
)
)
],
"temperature" => 0.7
)
"""
openaiReadyMsg = Dict{String, Any}()
# openaiReadyMsg["model"] = "gemma-4-E4B-it-UD-Q4_K_XL"
messages = Vector{Dict{String, Any}}()
# System prompt as system message
if !isempty(ctx.systemPrompt)
push!(messages, Dict(
"role" => "system",
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
))
end
# Conversation messages
for msg in ctx.messages
if msg isa userMessage
push!(messages, _userMessageToOpenAI(msg))
elseif msg isa assistantMessageToolCall
push!(messages, _assistantMessageToolCallToOpenAI(msg))
elseif msg isa assistantMessage
push!(messages, _assistantMessageToOpenAI(msg))
elseif msg isa toolResultMessage
push!(messages, _toolResultMessageToOpenAI(msg))
end
end
# Convert ctx.tools into OpenAI tools format
tools_array = _toolsToOpenAI(ctx.tools, eventSink)
openaiReadyMsg["messages"] = messages
openaiReadyMsg["temperature"] = 0.7
if !isempty(tools_array)
openaiReadyMsg["tools"] = tools_array
end
return openaiReadyMsg
end
"""
beforeToolCall(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult
Callback invoked before executing a tool call. Use this hook to inspect
the tool call and decide whether to allow, block, or modify it.
Common use cases:
- Request user approval via UI before running destructive tools.
- Validate business rules that cannot be expressed in the JSON schema.
- Check final context (e.g. session state, rate limits, permissions).
# Arguments
- `context::beforeToolCallContext`: Contains the assistant message, tool call,
validated arguments, and current conversation context.
- `signal::abortSignal`: Signal that may be set to abort the operation.
# Returns
- `beforeToolCallResult(false, "N/A")` to allow the call to proceed.
- `beforeToolCallResult(true, "Reason")` to block the call with a reason.
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
# Example
```jldoctest
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
if context.toolCall.name == "deleteFile"
# Block file deletion unless explicitly approved
return beforeToolCallResult(true, "User must approve file deletion")
end
return beforeToolCallResult(false, "N/A")
end
```
"""
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal, eventSink
)::beforeToolCallResult
# eventSink("beforeToolCall: $(context.toolCall.name) args=$(context.args)")
return beforeToolCallResult(false, "N/A")
end
"""
afterToolCall(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}
Callback invoked after a tool call finishes executing (before and after errors).
Use this hook to post-process the tool result before it is fed back to the LLM.
Common use cases:
- Mask sensitive data (API keys, tokens) from result content.
- Normalize usage tracking data into a consistent format.
- Inspect the result and set `terminate: true` based on business logic
(e.g. "if deployment failed, stop the agent rather than retrying").
- Wrap error results in friendlier messages for the LLM to understand.
# Arguments
- `context::afterToolCallContext`: Contains the assistant message, tool call,
arguments, raw result, error status, and current conversation context.
- `signal::abortSignal`: Signal that may be set to abort the operation.
# Returns
- `nothing` to pass the result through unchanged.
- `agentToolResult(...)` to return a modified result (content, details, usage,
terminate flag can all be overridden).
""" # arrange result from searchWine tool
function afterToolCall(context::afterToolCallContext, signal::abortSignal, eventSink
)::Union{agentToolResult, Nothing}
# eventSink("afterToolCall: $(context.toolCall.name) result=$(context.result) error=$(context.isError)")
return nothing
end
"""
Convert a userMessage to OpenAI message format.
"""
function _userMessageToOpenAI(msg::userMessage)::Dict{String, Any}
return Dict(
"role" => "user",
"content" => _messageContentToBlocks(msg.content)
)
end
"""
Convert an assistantMessageToolCall to OpenAI message format.
Produces a message with role="assistant", content=null, and a tool_calls array:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\": \"San Francisco, CA\"}"
}
}
]
}
"""
function _assistantMessageToolCallToOpenAI(msg::assistantMessageToolCall)::Dict{String, Any}
tool_calls = Dict{String, Any}[]
for tc in msg.toolCalls
push!(tool_calls, Dict(
"id" => tc.id,
"type" => tc.type,
"function" => Dict(
"name" => tc.name,
"arguments" => JSON.json(tc.arguments)
)
))
end
return Dict(
"role" => "assistant",
"content" => nothing,
"tool_calls" => tool_calls
)
end
"""
Convert an assistantMessage to OpenAI message format.
"""
function _assistantMessageToOpenAI(msg::assistantMessage)::Dict{String, Any}
return Dict(
"role" => "assistant",
"content" => _messageContentToBlocks(msg.content)
)
end
"""
Convert a toolResultMessage to OpenAI message format.
"""
function _toolResultMessageToOpenAI(msg::toolResultMessage)::Dict{String, Any}
return Dict(
"role" => "tool",
"tool_call_id" => msg.toolCallId,
"content" => _messageContentToBlocks(msg.content)
)
end
"""
Convert a vector of messageContent to OpenAI content blocks.
Each textContent becomes a text block, each imageContent becomes
an image_url block.
"""
function _messageContentToBlocks(contents::Vector{messageContent})::Vector{Dict{String, Any}}
blocks = Vector{Dict{String, Any}}()
for c in contents
if c isa textContent
push!(blocks, Dict("type" => "text", "text" => c.text))
elseif c isa imageContent
push!(blocks, Dict(
"type" => "image_url",
"image_url" => Dict(
"url" => "data:$(c.mimeType);base64,$(c.data)"
)
))
end
end
return blocks
end
"""
_toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}) -> Vector{Dict{String, Any}}
Convert an OrderedDict of agentTool definitions into OpenAI function tool format.
Returns an empty vector when `tools` is `nothing` or empty.
# Examples
```jldoctest
_toolsToOpenAI(nothing) # => Dict{String, Any}[]
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
```
"""
function _toolsToOpenAI(tools::Union{OrderedDict{String, agentTool}, Nothing}, eventSink)::Vector{Dict{String, Any}}
tools_array = Vector{Dict{String, Any}}()
if tools !== nothing
for (_, tool) in tools
push!(tools_array, Dict(
"type" => "function",
"function" => Dict(
"name" => tool.name,
"description" => tool.description,
"parameters" => tool.inputSchema
)
))
end
end
return tools_array
end
"""
validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any}) -> Union{Nothing,String}
Validates that all required fields listed in the tool's JSON Schema are present
in `args`. Returns `nothing` if validation passes, or a descriptive error string
listing the missing required fields.
This is the default `validateRequiredArgs` hook. Tool authors can override it
with a custom validation function that performs additional checks (e.g. type
coercion, format validation, cross-field constraints).
# Arguments
- `args::AbstractDict{String, Any}`: The arguments provided by the LLM
- `inputSchema::AbstractDict{String, Any}`: The tool's `inputSchema` (JSON Schema format)
# Returns
- `nothing` if all required args are present
- `String` error message listing missing fields otherwise
# Examples
```jldoctest
schema = Dict("required" => ["city"])
args = Dict{String,Any}()
validateRequiredArgs(args, schema) # => "Missing required arguments: city"
args2 = Dict("city" => "Tokyo")
validateRequiredArgs(args2, schema) # => nothing
```
"""
function validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing,String}
required = get(inputSchema, "required", Any[])
if isempty(required)
return nothing
end
missing = String[]
for field in required
if !(field in keys(args))
push!(missing, field)
end
end
if !isempty(missing)
return "Missing required arguments: $(join(missing, ", "))"
end
return nothing
end
"""
validateToolArguments(tool::agentTool, prepared::agentToolCall) -> Dict{String,Any}
Validates the prepared tool call arguments by calling the tool's
`validateRequiredArgs` hook (or the default implementation). If validation
fails, returns a modified `agentToolCall` with an empty arguments dict
so downstream code can detect the failure. If the hook exists on the tool
and returns an error string, that error is returned.
This runs **before** the `beforeToolCall` hook, allowing the agent to
reject invalid calls without invoking lifecycle callbacks or logging
false `toolExecutionStart` events.
# Arguments
- `tool::agentTool`: The resolved tool definition
- `prepared::agentToolCall`: The prepared tool call with potentially transformed arguments
# Returns
- `Dict{String,Any}`: The validated arguments if successful
# Errors
- Throws `ArgumentError` if validation fails — this is caught by `prepareToolCall`
and converted to an `immediateOutcome`
# Examples
```jldoctest
# With validateRequiredArgs hook set on the tool
validateToolArguments(toolWithHook, tc) # => validated args or throws
# With default validation (nothing on tool)
validateToolArguments(toolDefault, tc) # => args or throws
```
"""
function validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}
# Use default (2-arg: args + schema) or tool-specific hook (1-arg: args only)
if isnothing(tool.validateRequiredArgs)
result = validateRequiredArgs(prepared.arguments, tool.inputSchema)
else
result = tool.validateRequiredArgs(prepared.arguments)
end
if result !== nothing
throw(ArgumentError(result))
end
return prepared.arguments
end
end # module util
+642
View File
@@ -0,0 +1,642 @@
using Test
using YiemAgent
using YiemAgent.agentCore
using YiemAgent.type
using JSON
# Import the function from the private module scope
import YiemAgent.agentCore: _extractToolCalls
@testset "_extractToolCalls" begin
# ------------------------------------------------------------------ #
# Format 1: response["message"]["tool_calls"] (LMStudio.jl style) #
# ------------------------------------------------------------------ #
@testset "single tool call via message format" begin
response = Dict{String,Any}(
"finish_reason" => "tool_calls",
"index" => 0,
"message" => Dict{String,Any}(
"role" => "assistant",
"content" => "",
"reasoning_content" => "Let me check the weather.",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => "{\"city\":\"Bangkok, Thailand\"}",
),
"id" => "tc_001",
)
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getWeather"
@test tc_list[1].id == "tc_001"
@test tc_list[1].type == "function"
@test tc_list[1].arguments["city"] == "Bangkok, Thailand"
@test assistant_msg isa assistantMessage
@test assistant_msg.role == "assistant"
@test assistant_msg.stopReason == "tool_calls"
@test length(assistant_msg.content) == 1
@test assistant_msg.content[1] isa reasoningContent
@test assistant_msg.content[1].text == "Let me check the weather."
end
@testset "multiple tool calls via message format" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"content" => "",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => "{\"city\":\"Tokyo, Japan\"}",
),
"id" => "tc_001",
),
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getTime",
"arguments" => "{\"timezone\":\"Asia/Tokyo\"}",
),
"id" => "tc_002",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 2
@test tc_list[1].name == "getWeather"
@test tc_list[1].arguments["city"] == "Tokyo, Japan"
@test tc_list[2].name == "getTime"
@test tc_list[2].arguments["timezone"] == "Asia/Tokyo"
@test assistant_msg.role == "assistant"
end
@testset "tool call with empty arguments string" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "listTools",
"arguments" => "{}",
),
"id" => "tc_empty",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "listTools"
@test tc_list[1].arguments == Dict{String,Any}()
@test assistant_msg.stopReason == "end_turn"
end
@testset "tool call with missing id falls back to uuid" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getTime",
"arguments" => "{\"city\":\"NYC\"}",
),
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test !isempty(tc_list[1].id)
@test tc_list[1].name == "getTime"
end
@testset "tool call with non-string arguments (pre-parsed dict)" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => Dict{String,Any}("city" => "London", "units" => "fahrenheit"),
),
"id" => "tc_parsed",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].arguments["city"] == "London"
@test tc_list[1].arguments["units"] == "fahrenheit"
end
@testset "tool call with api/provider/model/usage metadata" begin
response = Dict{String,Any}(
"api" => "openai",
"provider" => "anthropic",
"model" => "claude-3-opus",
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getTime",
"arguments" => "{}",
),
"id" => "tc_meta",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test assistant_msg.api == "openai"
@test assistant_msg.provider == "anthropic"
@test assistant_msg.model == "claude-3-opus"
end
# --------------------------------------------------------------- #
# Format 2: response.content blocks (OpenAI API style) #
# --------------------------------------------------------------- #
@testset "content blocks with tool_calls" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}("type" => "text", "text" => "Let me check."),
Dict{String,Any}(
"type" => "tool_calls",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => "{\"city\":\"Paris\"}",
),
"id" => "tc_block_1",
),
],
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getWeather"
@test tc_list[1].arguments["city"] == "Paris"
# text block before tool_calls should be included in content
@test length(assistant_msg.content) == 1
@test assistant_msg.content[1].text == "Let me check."
end
@testset "content blocks with tool_call (single-call format)" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}(
"type" => "tool_call",
"id" => "tc_single",
"name" => "getTime",
"arguments" => Dict{String,Any}("timezone" => "Europe/London"),
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getTime"
@test tc_list[1].id == "tc_single"
@test tc_list[1].arguments["timezone"] == "Europe/London"
end
@testset "content blocks with reasoning and text" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}("type" => "reasoning", "text" => "Thinking..."),
Dict{String,Any}("type" => "text", "text" => "Here's the answer."),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
@test length(assistant_msg.content) == 2
@test assistant_msg.content[1] isa reasoningContent
@test assistant_msg.content[1].text == "Thinking..."
@test assistant_msg.content[2] isa textContent
@test assistant_msg.content[2].text == "Here's the answer."
end
@testset "content blocks with text and tool_call (tool_call not in content)" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}("type" => "text", "text" => "Sure, I'll check."),
Dict{String,Any}(
"type" => "tool_call",
"id" => "tc_mix",
"name" => "getWeather",
"arguments" => Dict{String,Any}("city" => "London"),
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getWeather"
# text block included, tool_call block excluded from content
@test length(assistant_msg.content) == 1
@test assistant_msg.content[1].text == "Sure, I'll check."
end
# ------------------------------------------------------------------ #
# assistantMessage construction #
# ------------------------------------------------------------------ #
@testset "assistantMessage with error_message and errorMessage fallback" begin
response = Dict{String,Any}(
"error_message" => "rate limit",
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fail")],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test assistant_msg.errorMessage == "rate limit"
end
@testset "assistantMessage with usage tracking" begin
response = Dict{String,Any}(
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
"usage" => llmUsage(100, 50),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test assistant_msg.usage.inputTokens == 100
@test assistant_msg.usage.outputTokens == 50
end
@testset "assistantMessage with invalid usage defaults to zero" begin
response = Dict{String,Any}(
"content" => Any[Dict{String,Any}("type" => "text", "text" => "hi")],
"usage" => "invalid",
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test assistant_msg.usage.inputTokens == 0
@test assistant_msg.usage.outputTokens == 0
end
@testset "reasoning_content as textContent" begin
response = Dict{String,Any}(
"reasoning_content" => textContent("internal thought"),
"content" => Any[Dict{String,Any}("type" => "text", "text" => "output")],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test length(assistant_msg.content) == 2
@test assistant_msg.content[1] isa reasoningContent
@test assistant_msg.content[1].text == "internal thought"
@test assistant_msg.content[2] isa textContent
@test assistant_msg.content[2].text == "output"
end
# ------------------------------------------------------------------ #
# No tool call cases #
# ------------------------------------------------------------------ #
@testset "no tool calls found" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}("type" => "text", "text" => "Hello world."),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
@test assistant_msg.stopReason == "end_turn"
end
@testset "empty message" begin
response = Dict{String,Any}()
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
@test assistant_msg.role == "assistant"
@test assistant_msg.stopReason == "end_turn"
@test length(assistant_msg.content) == 0
end
@testset "message with empty tool_calls array" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
@test assistant_msg.role == "assistant"
end
@testset "message field is not a Dict" begin
response = Dict{String,Any}(
"message" => "not a dict",
"content" => Any[Dict{String,Any}("type" => "text", "text" => "fallback")],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
@test length(assistant_msg.content) == 1
end
@testset "Format 1 takes priority over Format 2" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => "{\"city\":\"Format1\"}",
),
"id" => "tc_fmt1",
),
],
),
"content" => Any[
Dict{String,Any}(
"type" => "tool_call",
"id" => "tc_fmt2",
"name" => "getTime",
"arguments" => Dict{String,Any}("city" => "Format2"),
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getWeather"
end
# ------------------------------------------------------------------ #
# Edge cases #
# ------------------------------------------------------------------ #
@testset "tool call with null arguments" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getTime",
"arguments" => nothing,
),
"id" => "tc_null",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getTime"
end
@testset "tool call with missing function key" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"id" => "tc_nofunc",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == ""
end
@testset "tool call with missing name in function block" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "assistant",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}("arguments" => "{}"),
"id" => "tc_noname",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == ""
end
@testset "message format with JSON.Object (JSON.parse result)" begin
json_str = JSON.json(Dict(
"message" => Dict(
"role" => "assistant",
"tool_calls" => [
Dict(
"type" => "function",
"function" => Dict("name" => "getWeather", "arguments" => "{\"city\":\"Test\"}"),
"id" => "tc_jsonobj",
),
],
),
))
parsed = JSON.parse(json_str)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(parsed)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getWeather"
@test tc_list[1].arguments["city"] == "Test"
end
@testset "tool_calls block with mixed content types (text + tool_calls)" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}("type" => "text", "text" => "I'll check both."),
Dict{String,Any}(
"type" => "tool_calls",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => "{\"city\":\"London\"}",
),
"id" => "tc_mix1",
),
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getTime",
"arguments" => "{\"timezone\":\"UTC\"}",
),
"id" => "tc_mix2",
),
],
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 2
@test tc_list[1].name == "getWeather"
@test tc_list[2].name == "getTime"
@test length(assistant_msg.content) == 1
@test assistant_msg.content[1].text == "I'll check both."
end
@testset "tool_call block without arguments field" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}(
"type" => "tool_call",
"id" => "tc_noargs",
"name" => "getTime",
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].arguments == Dict{String,Any}()
end
@testset "tool_calls block with empty tool_calls array" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}(
"type" => "tool_calls",
"tool_calls" => Any[],
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
end
@testset "tool_calls block with non-AbstractDict elements" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}(
"type" => "tool_calls",
"tool_calls" => Any["not a dict", 42, nothing],
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
end
@testset "content field is not a Vector" begin
response = Dict{String,Any}(
"content" => "not a vector",
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(tc_list) == 0
@test length(assistant_msg.content) == 0
end
@testset "Dict-based response with all metadata fields" begin
response = Dict{String,Any}(
"api" => "openai",
"provider" => "anthropic",
"model" => "claude-3-sonnet",
"content" => Any[
Dict{String,Any}(
"type" => "tool_call",
"id" => "tc_meta",
"name" => "getTime",
"arguments" => Dict{String,Any}("city" => "Seoul"),
),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == true
@test length(tc_list) == 1
@test tc_list[1].name == "getTime"
@test tc_list[1].arguments["city"] == "Seoul"
@test assistant_msg.api == "openai"
@test assistant_msg.provider == "anthropic"
@test assistant_msg.model == "claude-3-sonnet"
end
@testset "default role is assistant" begin
response = Dict{String,Any}(
"content" => Any[Dict{String,Any}("type" => "text", "text" => "no role specified")],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test assistant_msg.role == "assistant"
end
@testset "tool call with custom role in message format" begin
response = Dict{String,Any}(
"message" => Dict{String,Any}(
"role" => "custom_role",
"tool_calls" => Any[
Dict{String,Any}(
"type" => "function",
"function" => Dict{String,Any}(
"name" => "getWeather",
"arguments" => "{}",
),
"id" => "tc_role",
),
],
),
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test assistant_msg.role == "custom_role"
end
@testset "image content block handling" begin
response = Dict{String,Any}(
"content" => Any[
Dict{String,Any}(
"type" => "image_url",
"image_url" => Dict("url" => "data:image/png;base64,abc123"),
),
Dict{String,Any}("type" => "text", "text" => "What is this?"),
],
)
has_toolcalls, tc_list, assistant_msg = _extractToolCalls(response)
@test has_toolcalls == false
@test length(assistant_msg.content) == 2
@test assistant_msg.content[1] isa textContent
@test assistant_msg.content[1].text == ""
@test assistant_msg.content[2].text == "What is this?"
end
end
-56
View File
@@ -1,56 +0,0 @@
{
"mqttServerInfo": {
"description": "mqtt server info",
"port": 1883,
"broker": "mqtt.yiem.cc"
},
"testingOrProduction": {
"value": "testing",
"description": "agent status, couldbe testing or production"
},
"agentid": {
"value": "2b74b87a-5413-4fe2-a4d3-405891051680",
"description": "a unique id for this agent"
},
"agentCentralConfigTopic": {
"mqtttopic": "/yiem_branch_1/agent/sommelier/backend/config/api/v1.1",
"description": "a central agent server's topic to get this agent config"
},
"servicetopic": {
"mqtttopic": [
"/yiem/hq/agent/sommelier/backend/prompt/api_v1/testing"
],
"description": "a topic this agent are waiting for service request"
},
"role": {
"value": "sommelier",
"description": "agent role"
},
"organization": {
"value": "yiem_hq",
"description": "organization name"
},
"externalservice": {
"text2textinstruct": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"virtualWineCustomer_1": {
"mqtttopic": "/virtualenvironment/winecustomer",
"description": "text to text service with instruct LLM that act as wine customer",
"llminfo": {
"name": "llama3instruct"
}
},
"text2textchat": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
}
}
}
-9
View File
@@ -1,9 +0,0 @@
using GeneralUtils
response = "trajectory_evaluation:\nThe trajectory is correct so far. The thought accurately reflects the user's question, and the action taken is a valid attempt to retrieve data from the database that matches the specified criteria.\n\nanswer_evaluation:\nThe observation provides information about two red wines from Bordeaux rive droite in France, which partially answers the question. However, it does not provide a complete answer as it only lists the wine names and characteristics, but does not explicitly state whether there are any other wines that match the criteria.\n\naccepted_as_answer: No\n\nscore: 6\nThe trajectory is mostly correct, but the observation does not fully address the question.\n\nsuggestion: Consider adding more filters or parameters to the database query to retrieve a complete list of wines that match the specified criteria."
responsedict = GeneralUtils.textToDict(response,
["trajectory_evaluation", "answer_evaluation", "accepted_as_answer", "score", "suggestion"],
rightmarker=":", symbolkey=true)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

-159
View File
@@ -1,159 +0,0 @@
using Revise
using YiemAgent, GeneralUtils, JSON3, DataStructures
thoughtDict = OrderedDict(
:Question=> "Hello, I would like a get a bottle of wine",
:Thought_1=> "The customer wants to buy a bottle of wine, but we need more information about their preferences.",
:Action_1=> Dict(
:name=> "chatbox",
:input=> "What occasion are you buying the wine for?",
),
:Observation_1=> "We are having a wedding pary this weekend.",
:Thought_2=> "A wedding party is a great occasion to have a good bottle of wine.",
:Action_2=> Dict(
:name=> "chatbox",
:input=> "What type of food will you be serving with the wine?",
),
:Observation_2=> "I think it is Thai dishes",
:Thought_3=> "Now that I know the occasion and food, I need to ask about the budget.",
:Action_3=> Dict(
:name=> "chatbox",
:input=> "What is your budget for this wine?",
),
:Observation_3=> "50 bucks",
:Thought_4=> "With a budget of \$50, we have a wide range of options. Now that I know it's a wedding party and Thai dishes, I need to ask about the type of wine they prefer.",
:Action_4=> Dict(
:name=> "chatbox",
:input=> "What type of wine are you looking for? (Red, White, Sparkling, Rose, Dessert, Fortified)",
),
:Observation_4=> "Sparkling please.",
:Thought_5=> "Now that I know the occasion, food, budget and preferred type of wine, it's time to check our inventory for the best matching wine.",
:Action_5=> Dict(
:name=> "winestock",
:input=> "wine with budget \$50, Thai dishes, sparkling, wedding party",
),
:Observation_5=> "I found the following wine in stock {1 : Zena Crown Vista, 2 : Schrader Cabernet Sauvignon}",
:Thought_6=> "Now that I have all the information, it's time to recommend a wine that fits their preferences.",
:Action_6=> Dict(
:name=> "recommendation",
:input=> "I recommend Zena Crown Vista for its sparkling and affordable price.",
),
:Observation_6=> "I don't like it. Do you have another option?",
)
_thoughtJsonStr = JSON3.write(thoughtDict)
thoughtJsonStr = _thoughtJsonStr[1:end-1] # remove } at the end
# @show thoughtJsonStr
_, latestThoughtIndice = GeneralUtils.findHighestIndexKey(thoughtDict, "Thought")
nextThoughtIndice = latestThoughtIndice + 1
_prompt =
"""
You are a helpful sommelier working for a wine store.
Your goal is to reccommend the best wine from your inventory that match the user preferences.
You must follow the following criteria:
1) Get to know what occasion the user is buying wine for
2) Get to know what food the user will have with wine
3) Get to know how much the user willing to spend
4) Get to know type of wine the user is looking for e.g. Red, White, Sparkling, Rose, Dessert, Fortified
5) Get to know what characteristics of wine the user is looking for
e.g. tannin, sweetness, intensity, acidity
6) Check your inventory for the best wine that match the user preference
7) Recommend wine to the user
You should only respond with interleaving Thought, Action, Observation steps.
Thought can reason about the current situation, and Action can be three types:
1) winestock[query], which you can use to find wine in your inventory. The more input data the better.
2) chatbox[text], which you can use to interact with the user.
3) recommendation[answer], which returns your wine reccommendation to the user.
You should only respond in JSON format as describe below:
{
"Thought": "your reasoning",
"Action": {"name": "action to take", "input": "Action input"},
"Observation": "result of the action"
}
Here are some examples:
{
"Question": "I would like to buy a sedan with 8 seats.",
"Thought_1": "Our showroom carries various vehicle model. But I'm not sure whether we have a models that fits the user demand, I need to check our inventory.",
"Action_1": {"name": "inventory", "input": "sedan with 8 seats."},
"Observation_1": "Several model has 8 seats. Available color are black, red green"
}
{
"Thought_2": "I have to ask the user what color he likes.",
"Action_2": {"name": "chatbox", "input": "Which color do you like?"}
"Observation_2": "I'll take black."
}
{
"Thought_3": "There is only one model that fits the user preference. It's Yiem model A",
"Action_3": {"name": "recommendation", "input": "I recommend a Yiem model A"}
}
Let's begin!
$(JSON3.write(thoughtDict))
{Thought_$nextThoughtIndice
"""
prompt = YiemAgent.formatLLMtext_llama3instruct("system", _prompt)
@show prompt
msgMeta = Dict(:requestResponse => nothing,
:msgPurpose => nothing,
:receiverId => nothing,
:getPost => nothing,
:msgId => "4c7111e0-c30e-44c3-8f85-1c8b3f03a8be",
:acknowledgestatus => nothing,
:replyToMsgId => nothing,
:msgFormatVersion => nothing,
:mqttServerInfo => Dict(:port => 1883, :broker => "mqtt.yiem.cc"),
:sendTopic => "/loadbalancer/requestingservice",
:receiverName => "text2textinstruct",
:replyTopic => nothing,
:senderName => "decisionMaker",
:senderSelfnote => nothing,
:senderId => "testingSessionID",
:timeStamp => "2024-05-04T08:06:23.561"
)
outgoingMsg = Dict(
:msgMeta=> msgMeta,
:payload=> Dict(
:text=> prompt,
)
)
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg)
thoughtJsonStr = _response[:response][:text]
-87
View File
@@ -1,87 +0,0 @@
using Revise # remove when this package is completed
using YiemAgent, GeneralUtils, JSON3, MQTTClient, Dates, UUIDs, DataStructures
using Base.Threads
# ---------------------------------------------- 100 --------------------------------------------- #
config = copy(JSON3.read("config.json"))
instanceInternalTopic = config[:serviceInternalTopic][:mqtttopic] * "/1"
client, connection = MakeConnection(config[:mqttServerInfo][:broker],
config[:mqttServerInfo][:port])
receiveUserMsgChannel = Channel{Dict}(4)
receiveInternalMsgChannel = Channel{Dict}(4)
msgMeta = GeneralUtils.generate_msgMeta(
"N/A",
replyTopic = config[:servicetopic][:mqtttopic] # ask frontend reply to this instance_chat_topic
)
agentConfig = Dict(
:mqttServerInfo=> config[:mqttServerInfo],
:receivemsg=> Dict(
:prompt=> config[:servicetopic][:mqtttopic], # topic to receive prompt i.e. frontend send msg to this topic
:internal=> instanceInternalTopic,
),
:externalservice=> config[:externalservice],
)
# Instantiate an agent
tools=Dict( # update input format
"askbox"=> Dict(
:description => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
:input => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
:output => "" ,
:func => nothing,
),
# "winestock"=> Dict(
# :description => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
# :input => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
# :output => """<output>Output are wines that match the search query in JSON format.""",
# :func => ChatAgent.winestock,
# ),
"finalanswer"=> Dict(
:description => "<tool description>Useful for when you are ready to recommend wines to the user.</tool description>",
:input => """<input format>{\"finalanswer\": \"some text\"}.</input format><input example>{\"finalanswer\": \"I recommend Zena Crown Vista\"}</input example>""",
:output => "" ,
:func => nothing,
),
)
a = YiemAgent.sommelier(
receiveUserMsgChannel,
receiveInternalMsgChannel,
agentConfig,
name= "assistant",
id= "testingSessionID", # agent instance id
tools=tools,
)
input =
OrderedDict{Symbol, Any}(:question => "Hello, I would like a get a bottle of wine", :thought_1 => "It's great that the user is looking for a bottle of wine. To give them a personalized recommendation, I need to know more about their preferences.", :action_1 => Dict{Symbol, Any}(:name => "chatbox", :input => "What occasion are you planning to use this wine for?"), :observation_1 => "We are holding a wedding party", :thought_2 => "A wedding party is a great occasion for a special bottle of wine. I need to know what type of food will be served, and how much the user is willing to spend.", :action_2 => Dict{Symbol, Any}(:name => "chatbox", :input => "What type of food will you be serving at the wedding?"), :observation_2 => "It will be Thai dishes.", :thought_3 => "The type of wine that pairs well with Thai dishes is usually a crisp and refreshing white wine, but I also need to consider the budget and personal preferences.", :action_3 => Dict{Symbol, Any}(:name => "chatbox", :input => "How much are you willing to spend on this bottle of wine?"), :observation_3 => "I would spend up to 50 bucks.", :thought_4 => "I have a good idea of the occasion, food, and budget. Now I need to know what type of wine the user is looking for.", :action_4 => Dict{Symbol, Any}(:name => "chatbox", :input => "What type of wine are you usually looking for? Red, White, Sparkling, Rose, Dessert or Fortified?"), :observation_4 => "I like full-bodied Red wine with low tannin.", :thought_5 => "Now that I have all the necessary information, I can start searching for a suitable wine in our inventory.", :action_5 => Dict{Symbol, Any}(:name => "winestock", :input => "red wine with low tannins"), :observation_5 => "I found the following wines in our stock: \n{\n 1: El Enemigo Cabernet Franc 2019\n2: Tantara Chardonnay 2017\n\n}\n", :thought_6 => "Now that I have the information about the wine, it's time to make a recommendation.", :action_6 => Dict{Symbol, Any}(:name => "recommendbox", :input => "El Enemigo Cabernet Franc 2019"), :observation_6 => "I don't like the one you recommend. I want dry wine.")
result = YiemAgent.jsoncorrection(a, input)
-119
View File
@@ -1,119 +0,0 @@
using Revise
using YiemAgent, GeneralUtils, JSON3, DataStructures, LibPQ
using SQLLLM
# _prompt =
# """
# You are a helpful assistant.
# answer the following question:
# From the following CSV text:
# "{\"tabledescription\":[\"The customer table stores information about customers. It includes details such as first name, last name, display name, username, password, gender, country, telephone number, email, birthdate, additional_search_term, other attributes (in JSON format) and a description.\",\"The wine table stores information about different wines. It includes details namely id, name, brand, manufacturer, region, country, wine_type, grape_variety, serving_temperature, intensity, sweetness, tannin, acidity, fizziness, additional_search_term, other attributes (in JSON format) and a description.\",\"The wine_food table represents the association between wines and food items. It estab" ⋯ 477 bytes ⋯ "ed to retailer names, usernames, passwords, addresses, contact persons, telephone numbers, email addresses, additional_search_term, other attributes (in JSON format) and a description.\",\"The retailer_wine table represents the relationship between retailers and wines. It stores information about the wines available from which retailers, including vintage, their price, and the currency.\",\"The retailer_food table represents the relationship between retailers and food items. It stores information about the food items available from which retailers, including their price and the currency.\"],\"tablename\":[\"customer\",\"wine\",\"wine_food\",\"food\",\"retailer\",\"retailer_wine\",\"retailer_food\"]}"
# What is the description of table wine?
# """
# prompt = YiemAgent.formatLLMtext_llama3instruct("system", _prompt)
# @show prompt
# msgMeta = Dict(:requestResponse => nothing,
# :msgPurpose => nothing,
# :receiverId => nothing,
# :getPost => nothing,
# :msgId => "4c7111e0-c30e-44c3-8f85-1c8b3f03a8be",
# :acknowledgestatus => nothing,
# :replyToMsgId => nothing,
# :msgFormatVersion => nothing,
# :mqttServerInfo => Dict(:port => 1883, :broker => "mqtt.yiem.cc"),
# :sendTopic => "/loadbalancer/requestingservice",
# :receiverName => "text2textinstruct",
# :replyTopic => nothing,
# :senderName => "decisionMaker",
# :senderSelfnote => nothing,
# :senderId => "testingSessionID",
# :timeStamp => "2024-05-04T08:06:23.561"
# )
# outgoingMsg = Dict(
# :msgMeta=> msgMeta,
# :payload=> Dict(
# :text=> prompt,
# )
# )
# _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg)
# result = _response[:response][:text]
DBconnection = LibPQ.Connection("host=192.168.88.12 port=5432 dbname=yiem_wine_assistant user=yiem password=yiem@Postgres_0.0")
tableinfo, df1, df2, df3 = SQLLLM.tableinfo(DBconnection, "wine")
_prompt =
"""
You are a helpful assistant helping to answer user question from a database table.
$tableinfo
Are there any chardonnay?
"""
prompt = YiemAgent.formatLLMtext_llama3instruct("system", _prompt)
@show prompt
msgMeta = Dict(:requestResponse => nothing,
:msgPurpose => nothing,
:receiverId => nothing,
:getPost => nothing,
:msgId => "4c7111e0-c30e-44c3-8f85-1c8b3f03a8be",
:acknowledgestatus => nothing,
:replyToMsgId => nothing,
:msgFormatVersion => nothing,
:mqttServerInfo => Dict(:port => 1883, :broker => "mqtt.yiem.cc"),
:sendTopic => "/loadbalancer/requestingservice",
:receiverName => "text2textinstruct",
:replyTopic => nothing,
:senderName => "decisionMaker",
:senderSelfnote => nothing,
:senderId => "testingSessionID",
:timeStamp => "2024-05-04T08:06:23.561"
)
outgoingMsg = Dict(
:msgMeta=> msgMeta,
:payload=> Dict(
:text=> prompt,
)
)
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg)
result2 = _response[:response][:text]
+4 -271
View File
@@ -1,272 +1,5 @@
using Revise using Test
using JSON, JSON3, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames using YiemAgent
using YiemAgent, GeneralUtils
using Base.Threads
# ---------------------------------------------- 100 --------------------------------------------- #
# load config
config = JSON3.read("./test/config.json")
# config = copy(JSON3.read("../mountvolume/config.json"))
function executeSQL(sql::T) where {T<:AbstractString}
DBconnection = LibPQ.Connection("host=192.168.88.12 port=10201 dbname=wineDB user=yiemtechnologies password=yiemtechnologies@Postgres_0.0")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function executeSQLVectorDB(sql)
DBconnection = LibPQ.Connection("host=192.168.88.12 port=10203 dbname=SQLVectorDB user=yiemtechnologies password=yiemtechnologies@Postgres_0.0")
result = LibPQ.execute(DBconnection, sql)
close(DBconnection)
return result
end
function text2textInstructLLM(prompt::String)
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:text2textinstruct][:mqtttopic];
msgPurpose="inference",
senderName="yiemagent",
senderId=string(uuid4()),
receiverName="text2textinstruct",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => prompt,
:kwargs => Dict(
:num_ctx => 16384,
:temperature => 0.2,
)
)
)
_response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=6000)
response = _response[:response][:text]
return response
end
# get text embedding from a LLM service
function getEmbedding(text::T) where {T<:AbstractString}
msgMeta = GeneralUtils.generate_msgMeta(
config[:externalservice][:text2textinstruct][:mqtttopic];
msgPurpose="embedding",
senderName="yiemagent",
senderId=string(uuid4()),
receiverName="text2textinstruct",
mqttBrokerAddress=config[:mqttServerInfo][:broker],
mqttBrokerPort=config[:mqttServerInfo][:port],
)
outgoingMsg = Dict(
:msgMeta => msgMeta,
:payload => Dict(
:text => [text] # must be a vector of string
)
)
response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=6000)
embedding = response[:response][:embeddings]
return embedding
end
function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3,
vectorDB::Function; limit::Integer=1
)::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString}
# get embedding from LLM service
embedding = getEmbedding(text)[1]
# check whether there is close enough vector already store in vectorDB. if no, add, else skip
sql = """
SELECT *, $embeddingColumnName <-> '$embedding' as distance
FROM $tablename
ORDER BY distance LIMIT $limit;
"""
response = vectorDB(sql)
df = DataFrame(response)
return df
end
function similarSQLVectorDB(query; maxdistance::Integer=100)
tablename = "sqlllm_decision_repository"
# get embedding of the query
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable SQL, return it.
output_b64 = df[1, :function_output_base64] # pick the closest match
output_str = String(base64decode(output_b64))
rowid = df[1, :id]
println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
return (dict=output_str, distance=distance)
else
println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return (dict=nothing, distance=nothing)
end
end
function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=1) where {T1<:AbstractString, T2<:AbstractString}
tablename = "sqlllm_decision_repository"
# get embedding of the query
# query = state[:thoughtHistory][:question]
df = findSimilarTextFromVectorDB(query, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
query_embedding = getEmbedding(query)[1]
query = replace(query, "'" => "")
sql_base64 = base64encode(SQL)
sql_ = replace(SQL, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
end
end
function similarSommelierDecision(recentevents::T1; maxdistance::Integer=5
)::Union{AbstractDict, Nothing} where {T1<:AbstractString}
tablename = "sommelier_decision_repository"
# find similar
println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__)
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row != 0 && distance < maxdistance
# if there is usable decision, return it.
rowid = df[1, :id]
println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__)
output_b64 = df[1, :function_output_base64] # pick the closest match
_output_str = String(base64decode(output_b64))
output = copy(JSON3.read(_output_str))
return output
else
println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__)
return nothing
end
end
function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5
) where {T1<:AbstractString, T2<:AbstractDict}
tablename = "sommelier_decision_repository"
# find similar
df = findSimilarTextFromVectorDB(recentevents, tablename,
"function_input_embedding", executeSQLVectorDB)
row, col = size(df)
distance = row == 0 ? Inf : df[1, :distance]
if row == 0 || distance > maxdistance # no close enough SQL stored in the database
recentevents_embedding = a.func[:getEmbedding](recentevents)[1]
recentevents = replace(recentevents, "'" => "")
decision_json = JSON3.write(decision)
decision_base64 = base64encode(decision_json)
decision = replace(decision_json, "'" => "")
sql = """
INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding');
"""
println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__)
println(sql)
_ = executeSQLVectorDB(sql)
else
println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__)
end
end
sessionId = "12345"
externalFunction = (
getEmbedding=getEmbedding,
text2textInstructLLM=text2textInstructLLM,
executeSQL=executeSQL,
similarSQLVectorDB=similarSQLVectorDB,
insertSQLVectorDB=insertSQLVectorDB,
similarSommelierDecision=similarSommelierDecision,
insertSommelierDecision=insertSommelierDecision,
)
a = YiemAgent.sommelier(
externalFunction;
name="Ton",
id=sessionId, # agent instance id
retailername="Yiem",
)
while true
println("your respond: ")
user_answer = readline()
response = YiemAgent.conversation(a, Dict(:text=> user_answer))
println("\n$response")
end
# response = YiemAgent.conversation(a, Dict(:text=> "I want to get a French red wine under 100."))
include("toolTest.jl")
include("_extractToolCalls.jl")
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB