mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-14 09:43:03 +08:00
Compare commits
606 Commits
0.5.0
...
codex-revi
| Author | SHA1 | Date | |
|---|---|---|---|
| e9eaaa24db | |||
| 2ec2d8e096 | |||
| eea72c747c | |||
| 137b1ee917 | |||
| c5aa558319 | |||
| 8c8b0883bb | |||
| dd8e20838d | |||
| 15d7eb6850 | |||
| 657023694c | |||
| 9b97fffc5c | |||
| 14df95fc59 | |||
| 864bda95e4 | |||
| bfa3562335 | |||
| 9b24b662da | |||
| 41c611cb70 | |||
| 6ca77bdb74 | |||
| e5ce6d6722 | |||
| a7ef9bff49 | |||
| 92c8e01000 | |||
| e0543a8966 | |||
| 0c079c127e | |||
| 96301dc64a | |||
| 3fd90c0f5b | |||
| 8928ec9e07 | |||
| 503afecbe2 | |||
| 281984bb05 | |||
| 062af4b7d2 | |||
| 380c674d06 | |||
| 347560d979 | |||
| 999a6a8d7e | |||
| f510dc5a42 | |||
| d8ff0b5ea4 | |||
| dc147000ba | |||
| 247057e100 | |||
| 33ffd782c4 | |||
| a847f3bff8 | |||
| 28d0a43ef3 | |||
| cb4380eb1e | |||
| 737f9d122a | |||
| e65d3302c6 | |||
| fece67135f | |||
| d30e58ff83 | |||
| 93304878ad | |||
| e37a282141 | |||
| 04d6ad6d80 | |||
| 9755672750 | |||
| e0f955694d | |||
| c14e6f84e7 | |||
| 19f56d11f0 | |||
| 94d91d9746 | |||
| fac6e24e49 | |||
| 2aae1b1f14 | |||
| 1b48e955bd | |||
| b8cb2afbcf | |||
| 3732555f02 | |||
| ec52d64e73 | |||
| 7b963df991 | |||
| 4876d621b2 | |||
| 663d66fdea | |||
| d29dad4691 | |||
| 291371f8da | |||
| 3e5d6ebeb4 | |||
| 56daaf0b60 | |||
| d6593c10f9 | |||
| 328befecca | |||
| 1e46149d0a | |||
| 427d224f65 | |||
| 0528890d60 | |||
| f233c5ce32 | |||
| 91a595d073 | |||
| 6d27e55a1e | |||
| 2935daeffa | |||
| e04b2c4fe8 | |||
| 21a9d2114f | |||
| 3640cc2108 | |||
| a114044c23 | |||
| 874488ea79 | |||
| b80ff8400c | |||
| 6fd12cd19f | |||
| 2cbaccedba | |||
| 006e7c61e5 | |||
| 2458ea4849 | |||
| eab8eec46e | |||
| a242723727 | |||
| 57bc63c57b | |||
| 9aa793df8e | |||
| 8c04566243 | |||
| b1ba86be57 | |||
| f79a0521b2 | |||
| 332792daa2 | |||
| 3b236286b9 | |||
| 50db3fa7b2 | |||
| 6503ec32b4 | |||
| 9771aa1de5 | |||
| 660bb01440 | |||
| 188b5e8b53 | |||
| db4dc6d040 | |||
| ef9d177adc | |||
| 5a41e02602 | |||
| f387c36b17 | |||
| 03532d3d97 | |||
| f37bef57ba | |||
| 29d6546b07 | |||
| b9fd9bcaac | |||
| 4ae65b885e | |||
| 632e47ec13 | |||
| 4f200cadfc | |||
| e4df68ea5d | |||
| 81aa4b7933 | |||
| b4c44c7d98 | |||
| 1301a0f8b1 | |||
| 7d82ec7238 | |||
| d70054cd9b | |||
| def48fd0ce | |||
| 0b750f8028 | |||
| 6e37ed032b | |||
| b6c86c6640 | |||
| 71956b4dce | |||
| c9466f4359 | |||
| cb09b86b23 | |||
| d4a5c5a0ed | |||
| 274d007ba1 | |||
| c7df35beb4 | |||
| 1c995923a1 | |||
| 247052e318 | |||
| eb1723de97 | |||
| c2fbfe751f | |||
| 4a1828ed94 | |||
| db20e0ca78 | |||
| 09f90d8ad5 | |||
| baa6870ccf | |||
| fd43466dd5 | |||
| 7628d40645 | |||
| d806693e08 | |||
| dbfd0b1fc3 | |||
| f5b9f8fc58 | |||
| 7bbb7745f4 | |||
| b65f5f37fb | |||
| fb9b3860af | |||
| c5256cbc90 | |||
| 1592c876c7 | |||
| 453a1cfe84 | |||
| 54320dbfde | |||
| 201addbc64 | |||
| 5219142b5c | |||
| 750bef0fd8 | |||
| a9b925c614 | |||
| 264a45c85f | |||
| 155d06df45 | |||
| 7879836b91 | |||
| 82db27484c | |||
| d11a431614 | |||
| 50b07488a6 | |||
| 313cea0d3b | |||
| 4494d58ff9 | |||
| d7179364a1 | |||
| b05ef8683d | |||
| 556b8a5348 | |||
| 027ebf860e | |||
| 8cd12f7379 | |||
| 617daea17c | |||
| 61d5270625 | |||
| 93b0199c9e | |||
| 977d92881c | |||
| 6aa96c33ac | |||
| e85094670b | |||
| 21ea6a25c8 | |||
| ad65cd4c09 | |||
| ff46d97eed | |||
| 1a83075031 | |||
| 809d38bd07 | |||
| f7d46c8ef1 | |||
| e1ee6589ef | |||
| 17108f3239 | |||
| 59a5e22f35 | |||
| 30373cbc02 | |||
| 0e164115c0 | |||
| 3cc6e8df99 | |||
| ccb7041093 | |||
| e0b20ce414 | |||
| 1029624dc7 | |||
| 5b9b9c3c09 | |||
| 67d7fd34f8 | |||
| 12368ded53 | |||
| 7d9ec976e3 | |||
| d8b19ebcea | |||
| 7f4b82204a | |||
| 664bd5a0fb | |||
| 6474eb8dc6 | |||
| 4439e5c04b | |||
| ef2eb909b7 | |||
| fd20af3e1c | |||
| d30ff322a2 | |||
| cb63bbf570 | |||
| 4da4e5f161 | |||
| 5a717dbdda | |||
| a6682a7719 | |||
| 1465d7687b | |||
| d31cac70a6 | |||
| 108cf9b071 | |||
| c4b8c2a858 | |||
| d55b849747 | |||
| a9993299a5 | |||
| 4c47cac3a5 | |||
| cba9ff4f0b | |||
| 603b0835c5 | |||
| fc0d4ef03b | |||
| 97bb897407 | |||
| 8463eb9dae | |||
| 9794b39572 | |||
| 0506149f5f | |||
| 9325b84d14 | |||
| 6c8a1b2636 | |||
| 3c550af33d | |||
| 229c7a0edb | |||
| a067e058fb | |||
| cdfae8ab1a | |||
| c1998f61b6 | |||
| 3f6283d12a | |||
| 3d1c258944 | |||
| 36dfd65046 | |||
| ee1a033c1b | |||
| 027e8bddc0 | |||
| 0903b8482b | |||
| f51401e2f2 | |||
| 5ad80d8b86 | |||
| 760ba8300b | |||
| 0f3d4659ae | |||
| 331d415925 | |||
| f5f64971f3 | |||
| 1cd6ba11bb | |||
| 5db533f823 | |||
| 1b2ccaee9c | |||
| 8dc7ed080b | |||
| 3348557352 | |||
| 4bb83f86a8 | |||
| b256af0f58 | |||
| ec429db4da | |||
| 145aeed600 | |||
| 5e8ef6d66f | |||
| 65690c65f8 | |||
| 7df0f208b5 | |||
| 4484d4a06b | |||
| 25ddc6f181 | |||
| d905c4ace1 | |||
| c3c4c88c9a | |||
| ae1b7fc033 | |||
| d9e384960f | |||
| 2baa3971a8 | |||
| 4ee7a52f42 | |||
| 28400545a7 | |||
| 1ce2038ab8 | |||
| 0b63ec8523 | |||
| 28a1bf8d1f | |||
| 77059f84c4 | |||
| d3eb5e8ee3 | |||
| 01cfc04dc7 | |||
| 6919fe656e | |||
| df25f488fa | |||
| 036fd9e051 | |||
| 383a49b855 | |||
| 7e88b8b926 | |||
| c6c8a83dad | |||
| 6635d7aca2 | |||
| facc49a799 | |||
| 3c6076ee0a | |||
| 40bd2deeba | |||
| 839f19f15b | |||
| da1abbdc88 | |||
| e6a071fc93 | |||
| 56688785ba | |||
| 8a9892f039 | |||
| 06f5ae9aac | |||
| f8d90fbcee | |||
| b02f29b1b1 | |||
| f1848fece4 | |||
| 85cc5cace8 | |||
| d462f02960 | |||
| 5ae8b713d7 | |||
| cf25fce09e | |||
| 868b17b56b | |||
| fe8031d9ac | |||
| 5847b38f2b | |||
| 6920bc6130 | |||
| fa9512db3b | |||
| 51d51937ed | |||
| 23026d6dc8 | |||
| 661dd625e3 | |||
| 33fdc61eff | |||
| 0870d7bc0e | |||
| baf0b05e60 | |||
| 51efb55259 | |||
| d5095359db | |||
| 1abfc729f8 | |||
| 528e88f613 | |||
| 333d087979 | |||
| a863b43563 | |||
| dde01a6253 | |||
| fa50f4aeb7 | |||
| b87ede0e8b | |||
| 010860de8d | |||
| 379cabe042 | |||
| 1b164cedf1 | |||
| 7ee698768c | |||
| 2e29e5ca7f | |||
| e0a62053b5 | |||
| f13e45327d | |||
| f56ff7d719 | |||
| fd9b376afa | |||
| 542de84640 | |||
| 907bb626f2 | |||
| 2cdeaa1c30 | |||
| 833fe4deb3 | |||
| 3c26e77b76 | |||
| 2825f7f1de | |||
| 86b7e5c492 | |||
| b1e4d3c2e4 | |||
| 997616e45f | |||
| 6733984843 | |||
| 0f2052c507 | |||
| 23bfb8bac1 | |||
| 15c11ac500 | |||
| deffc91dd1 | |||
| b2d44668da | |||
| f8399d2280 | |||
| 2870e79f79 | |||
| 3c9ace8e56 | |||
| e510725e71 | |||
| f963cae4ed | |||
| 45c4e8169a | |||
| 6bf4d95b05 | |||
| 00ef090d2f | |||
| e62cddfe44 | |||
| 115a8d5446 | |||
| ef5d5802a7 | |||
| 87c34cc699 | |||
| bbace28d7a | |||
| c47d2b2285 | |||
| 38c618a222 | |||
| e00441faa8 | |||
| c3b0a9dd25 | |||
| 6b7c9db5b1 | |||
| e2ef92ce43 | |||
| 051b6450e7 | |||
| 2a3d33a61e | |||
| e57c03e213 | |||
| 14e6d71ac9 | |||
| dc9f9417bc | |||
| 5051c2f662 | |||
| c30ec35f85 | |||
| c7f113b59a | |||
| 8735660830 | |||
| 18b2bb8812 | |||
| 7fccea5b91 | |||
| a9bf7ca1c2 | |||
| 1685d148c4 | |||
| 73ab40416d | |||
| 1244d59fa4 | |||
| 11ea1045f4 | |||
| a1bc784da5 | |||
| 747d3e47d6 | |||
| 5f0165fa3a | |||
| 2b0682b9e0 | |||
| 16a92de377 | |||
| cbba183b60 | |||
| 8e642b07d9 | |||
| 66d8a95c73 | |||
| 0850f8403d | |||
| 24c29a6dc6 | |||
| f703cc8157 | |||
| d52f48f132 | |||
| f6afe21b43 | |||
| d381a97731 | |||
| 940beb2587 | |||
| 69d9f04f11 | |||
| e2bd9eb0af | |||
| 9ec03c4c95 | |||
| ecbbbc9954 | |||
| a5d26769e8 | |||
| 2db1bdf3e9 | |||
| 656d6b65e3 | |||
| 7c46f1d1ff | |||
| 406b4250aa | |||
| eab3d00fe8 | |||
| d47e9fbf95 | |||
| 4b77f26e7b | |||
| 650f9b27a1 | |||
| 9f479c5f6f | |||
| 227c735667 | |||
| b05b665960 | |||
| 882ec43f2b | |||
| 7cb565fd1b | |||
| 84b96b6645 | |||
| 2169376062 | |||
| 9bff74c8c7 | |||
| 76c9c27532 | |||
| 8524ca6f9f | |||
| 7dd2104689 | |||
| 6ba5784a7f | |||
| cdea8d6322 | |||
| 8ca2b9998d | |||
| d098b28f31 | |||
| 98e4d4cf1b | |||
| 70489e57f7 | |||
| a43c87006e | |||
| be43b7eec6 | |||
| 3bc2ce839a | |||
| fe5a366527 | |||
| 9f724cee5d | |||
| ad58b83020 | |||
| c9b64fec2a | |||
| 0eb4765235 | |||
| 050c39cbc7 | |||
| 08100aea8a | |||
| 2cd0774834 | |||
| 12df938d85 | |||
| 277c0d437f | |||
| 6b861f4b77 | |||
| d33b6617c1 | |||
| a1dcf4a6fa | |||
| 9c38719514 | |||
| 33df0fa017 | |||
| 08bda84471 | |||
| 76da3c29f8 | |||
| 558b589830 | |||
| 80d7c283c5 | |||
| b0bb40c5f0 | |||
| eec8f71096 | |||
| fbd30d1a96 | |||
| 346f925b66 | |||
| 04e8d9e531 | |||
| 63d1552de2 | |||
| 77eb9b92a4 | |||
| a1b4743eeb | |||
| 0768cddd2d | |||
| 75145cc547 | |||
| d1ca1cda7d | |||
| fac6609d6b | |||
| dce8825e58 | |||
| cd641ac8dd | |||
| 5ffdab9e4a | |||
| 830503eee4 | |||
| 96e74ec877 | |||
| 420d51af15 | |||
| 8ead306b7b | |||
| c793851107 | |||
| fb5cabc747 | |||
| c5f6c4e0ae | |||
| 200c0f3f13 | |||
| fdf398b86e | |||
| 774e2b6cd5 | |||
| 837a4c92d1 | |||
| ddd999d47b | |||
| 9694a022c7 | |||
| 31968c7076 | |||
| abec141e4e | |||
| cdb6ae9d01 | |||
| dd8d86d3c4 | |||
| 99b42620d0 | |||
| 70b8335d49 | |||
| 8ca475beeb | |||
| 4f36c0dd2d | |||
| 00993bd763 | |||
| a0bca668cb | |||
| c6118c41b0 | |||
| 872d21170a | |||
| 44ab6cfac8 | |||
| ec30ac1922 | |||
| 74b6d0c653 | |||
| de462866b2 | |||
| 4ea8926363 | |||
| 04c98c7856 | |||
| 0757856187 | |||
| 19ec93f248 | |||
| 5877b9e80d | |||
| 5db0b5fad1 | |||
| 623a86daab | |||
| 64f24d3fc3 | |||
| 3097efe453 | |||
| b58eeffd2f | |||
| 62cec79005 | |||
| 03a5935107 | |||
| 0ecaec0545 | |||
| 74f2c6c950 | |||
| f35a467ebc | |||
| 64b907707a | |||
| a6e33edc7a | |||
| 94ef79c67d | |||
| a222015abb | |||
| 1c562f0e7b | |||
| 89535a6b1c | |||
| 6e90bc8d67 | |||
| 0e741802d1 | |||
| db3435fccf | |||
| e3294ec302 | |||
| bf99e01b51 | |||
| 1bd23b20c4 | |||
| 442dd55686 | |||
| abe5dd5251 | |||
| 1f737c0e29 | |||
| d41074c814 | |||
| 621441601a | |||
| e00d77f076 | |||
| d614ac0b15 | |||
| 592e2604d9 | |||
| dcbd2c6569 | |||
| 476acb0641 | |||
| 88a14f36b2 | |||
| 05d1fff125 | |||
| 49a4c9eb01 | |||
| 9e76c3e7ad | |||
| 9762739138 | |||
| 1f5509c17d | |||
| ed042cfffa | |||
| 128592e23c | |||
| 5ba36ed3e8 | |||
| 4dea948f82 | |||
| dc4074715e | |||
| 225802c1a8 | |||
| e851e33b2e | |||
| cb28a5b068 | |||
| ad58567ada | |||
| 0eee12d685 | |||
| 1e6ff1b30c | |||
| 0413fc281d | |||
| 8a7681ae31 | |||
| 1947f25ed6 | |||
| 488246525f | |||
| 534dcd5ade | |||
| ad58c0cc7c | |||
| d546148d69 | |||
| bf2d5ac707 | |||
| 628234f6e2 | |||
| 4b852e0049 | |||
| 6e3deced77 | |||
| 6a351413a1 | |||
| ad973d4230 | |||
| 1d315a9b62 | |||
| 4e32129b31 | |||
| 3f59719e16 | |||
| c548cceec6 | |||
| b3098310b4 | |||
| f48d559a7b | |||
| 14a57f0be6 | |||
| dff7b69b51 | |||
| d77ffd1db6 | |||
| 264cdb09e5 | |||
| fea7f9c81f | |||
| a1520f117b | |||
| ae5caf8475 | |||
| 980d9384d1 | |||
| 9ba0d1363a | |||
| f6d5cad5cd | |||
| c3ca4145b8 | |||
| 5419722c40 | |||
| 31ebee3ded | |||
| 746fda1a5e | |||
| ec4fbb7f19 | |||
| f9c3cacfea | |||
| e35e18f3b7 | |||
| 83832d2060 | |||
| 4757425a15 | |||
| 21b3a0e846 | |||
| 6bfa941c35 | |||
| 456a7e04e8 | |||
| 5864760f35 | |||
| aee6d879e9 | |||
| d1ea8dde79 | |||
| 7094a8a68b | |||
| e78048f66d | |||
| 2f09c0cd71 | |||
| 8c43d6594f | |||
| 2d052442b1 | |||
| 84f7befb75 | |||
| 85818556f8 | |||
| cb1a25e9d5 | |||
| a10a149edb | |||
| 41bf19f43e | |||
| 941943f696 | |||
| a7d54349e4 | |||
| 3c2ac32708 | |||
| 7f9d007f07 | |||
| c1018d6b54 | |||
| 47684c78e0 | |||
| 10fb689c83 | |||
| 86548903a0 | |||
| cf1578fd14 | |||
| 35b7dc7cda | |||
| b1ed79ae9d | |||
| 507917157d | |||
| ca3ffca4ea | |||
| 3c41abbba8 | |||
| 64a7ed261e | |||
| ade922cbc2 | |||
| effa1df7fa | |||
| 954095aa55 | |||
| c9c9b05f18 | |||
| dd10b00fb6 | |||
| 98d830d08b | |||
| 192ae1bf5f | |||
| a1d3c1e295 | |||
| 9302417e58 | |||
| e9053a4e88 | |||
| f5c37f26a5 | |||
| d11d39ea81 | |||
| 407181db05 |
38
.gitignore
vendored
38
.gitignore
vendored
@@ -8,6 +8,8 @@ target/
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
.idea/db-forest-config.xml
|
||||
.idea/markdown.xml
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
@@ -36,16 +38,28 @@ build/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
/data/
|
||||
/config/
|
||||
/src/test/java/memory/test.json
|
||||
/src/test/java/memory/result/input1.json
|
||||
/src/test/java/memory/result/input2.json
|
||||
/src/test/java/memory/result/output1.json
|
||||
/src/test/java/memory/result/output2.json
|
||||
/src/test/java/memory/result/total_input.json
|
||||
/src/test/java/memory/result/input3.json
|
||||
/src/test/java/memory/result/input4.json
|
||||
/src/test/java/memory/result/primary_input.json
|
||||
/src/main/resources/prompt/module/memory/topic_extractor.json.bak
|
||||
/backup/data/
|
||||
/backup/config/
|
||||
/Partner-Core/src/main/java/src/test/java/memory/test.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/input1.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/input2.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/output1.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/output2.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/total_input.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/input3.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/input4.json
|
||||
/Partner-Core/src/main/java/src/test/java/memory/result/primary_input.json
|
||||
/Partner-Core/src/main/java/src/main/resources/prompt/module/memory/topic_extractor.json.bak
|
||||
/backup/
|
||||
/Partner-Main/src/test/java/text/test.json
|
||||
/CLAUDE.md
|
||||
/config/
|
||||
/data/
|
||||
/generated-classes/
|
||||
/.idea/copilot.data.migration.ask2agent.xml
|
||||
/Partner-Main/data/
|
||||
/AGENTS.md
|
||||
/.serena/
|
||||
/Partner-Core/data/
|
||||
/.ai/mcp/mcp.json
|
||||
/.codex
|
||||
|
||||
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@@ -6,3 +6,4 @@
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
/inspectionProfiles/Project_Default.xml
|
||||
|
||||
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AskMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EditMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/dictionaries/project.xml
generated
Normal file
8
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>openai</w>
|
||||
<w>zuper</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
11
.idea/encodings.xml
generated
11
.idea/encodings.xml
generated
@@ -1,6 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/Partner-Common/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Common/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Framework/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Framework/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-SandboxRunner/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
|
||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.2.0" />
|
||||
</component>
|
||||
</project>
|
||||
36
.idea/misc.xml
generated
36
.idea/misc.xml
generated
@@ -1,12 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<list size="19">
|
||||
<item index="0" class="java.lang.String" itemvalue="lombok.Data" />
|
||||
<item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" />
|
||||
<item index="2" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
|
||||
<item index="3" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
|
||||
<item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
|
||||
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
|
||||
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
|
||||
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
|
||||
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
|
||||
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
|
||||
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
|
||||
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
|
||||
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
|
||||
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
|
||||
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
|
||||
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
|
||||
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
|
||||
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
|
||||
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" />
|
||||
</list>
|
||||
<writeAnnotations>
|
||||
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />
|
||||
<writeAnnotation name="work.slhaf.partner.api.agent.factory.component.annotation.InjectModule" />
|
||||
<writeAnnotation name="work.slhaf.partner.api.agent.factory.module.annotation.InjectModule" />
|
||||
<writeAnnotation name="work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability" />
|
||||
<writeAnnotation name="work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule" />
|
||||
</writeAnnotations>
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
<option value="$PROJECT_DIR$/PartnerExecutor/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="ignoredFiles">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/Partner-Test-Demo/pom.xml" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
<component name="PWA">
|
||||
<option name="enabled" value="true" />
|
||||
|
||||
28
Partner-Common/pom.xml
Normal file
28
Partner-Common/pom.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>Partner-Common</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/io.modelcontextprotocol.sdk/mcp -->
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,155 @@
|
||||
package work.slhaf.partner.common.mcp;
|
||||
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.server.McpStatelessServerHandler;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class InProcessMcpTransport implements McpClientTransport, McpStatelessServerTransport {
|
||||
|
||||
// 每个 transport 只处理一条 inbound 流
|
||||
private final Sinks.Many<McpSchema.JSONRPCMessage> inbound =
|
||||
Sinks.many().unicast().onBackpressureBuffer();
|
||||
|
||||
private final AtomicBoolean clientConnected = new AtomicBoolean(false);
|
||||
private final AtomicBoolean serverConnected = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* 对端
|
||||
*/
|
||||
private volatile InProcessMcpTransport peer;
|
||||
|
||||
private volatile McpStatelessServerHandler serverHandler;
|
||||
|
||||
public static Pair pair() {
|
||||
InProcessMcpTransport client = new InProcessMcpTransport();
|
||||
InProcessMcpTransport server = new InProcessMcpTransport();
|
||||
|
||||
client.peer = server;
|
||||
server.peer = client;
|
||||
|
||||
return new Pair(client, server);
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* Internal receive: peer.sendMessage -> this.receive
|
||||
* ====================================================== */
|
||||
private void receive(McpSchema.JSONRPCMessage message) {
|
||||
if (inbound.tryEmitNext(message).isFailure()) {
|
||||
throw new RuntimeException("Failed to receive message: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* Client → Server sendMessage
|
||||
* ====================================================== */
|
||||
@Override
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
InProcessMcpTransport p = this.peer;
|
||||
if (p == null) {
|
||||
return Mono.error(new IllegalStateException("Transport is not linked"));
|
||||
}
|
||||
return Mono.fromRunnable(() -> p.receive(message));
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* Client connect(handler) 处理 server → client 消息
|
||||
* ====================================================== */
|
||||
@Override
|
||||
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
|
||||
if (!clientConnected.compareAndSet(false, true)) {
|
||||
return Mono.error(new IllegalStateException("Client already connected"));
|
||||
}
|
||||
|
||||
return inbound.asFlux()
|
||||
.concatMap(msg ->
|
||||
handler.apply(Mono.just(msg))
|
||||
// handler may emit response message → send back to server
|
||||
.flatMap(resp -> resp != null ? sendMessage(resp) : Mono.empty())
|
||||
)
|
||||
.doFinally(sig -> clientConnected.set(false))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExceptionHandler(Consumer<Throwable> handler) {
|
||||
McpClientTransport.super.setExceptionHandler(handler);
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* Server: bind stateless handler = process client → server inbound
|
||||
* ====================================================== */
|
||||
@Override
|
||||
public void setMcpHandler(McpStatelessServerHandler handler) {
|
||||
this.serverHandler = handler;
|
||||
|
||||
if (!serverConnected.compareAndSet(false, true)) {
|
||||
throw new IllegalStateException("Server already connected");
|
||||
}
|
||||
|
||||
// 订阅 client → server 消息
|
||||
inbound.asFlux()
|
||||
.concatMap(this::handleServerMessage)
|
||||
.doFinally(sig -> serverConnected.set(false))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Server 端处理 JSONRPCMessage
|
||||
*/
|
||||
private Mono<Void> handleServerMessage(McpSchema.JSONRPCMessage msg) {
|
||||
// 创建 transport context(简单实现即可)
|
||||
McpTransportContext ctx = key -> null;
|
||||
|
||||
if (msg instanceof McpSchema.JSONRPCRequest req) {
|
||||
return serverHandler.handleRequest(ctx, req)
|
||||
.flatMap(this::sendMessage);
|
||||
}
|
||||
|
||||
if (msg instanceof McpSchema.JSONRPCNotification noti) {
|
||||
return serverHandler.handleNotification(ctx, noti);
|
||||
}
|
||||
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
McpClientTransport.super.close();
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
* other boilerplate
|
||||
* ====================================================== */
|
||||
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
inbound.tryEmitComplete();
|
||||
clientConnected.set(false);
|
||||
serverConnected.set(false);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||
return McpJsonMapper.getDefault().convertValue(data, typeRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> protocolVersions() {
|
||||
return McpClientTransport.super.protocolVersions();
|
||||
}
|
||||
|
||||
public record Pair(InProcessMcpTransport clientSide, InProcessMcpTransport serverSide) {
|
||||
}
|
||||
}
|
||||
44
Partner-Core/dependency-reduced-pom.xml
Normal file
44
Partner-Core/dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>Partner</artifactId>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<version>0.5.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>Partner-Main</artifactId>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>work.slhaf.partner.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<properties>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
140
Partner-Core/pom.xml
Normal file
140
Partner-Core/pom.xml
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>Partner-Core</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner-Framework</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api -->
|
||||
<dependency>
|
||||
<groupId>org.nd4j</groupId>
|
||||
<artifactId>nd4j-api</artifactId>
|
||||
<version>1.0.0-M2.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.onnxruntime</groupId>
|
||||
<artifactId>onnxruntime</artifactId>
|
||||
<version>1.23.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.huggingface</groupId>
|
||||
<artifactId>tokenizers</artifactId>
|
||||
<version>0.34.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/io.modelcontextprotocol.sdk/mcp -->
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner-Common</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.20.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>5.20.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>5.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cronutils</groupId>
|
||||
<artifactId>cron-utils</artifactId>
|
||||
<version>9.2.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>work.slhaf.partner.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>process-sources</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/main/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/test/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<jvmTarget>${maven.compiler.target}</jvmTarget>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
14
Partner-Core/src/main/java/work/slhaf/partner/Main.java
Normal file
14
Partner-Core/src/main/java/work/slhaf/partner/Main.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package work.slhaf.partner;
|
||||
|
||||
import work.slhaf.partner.common.vector.VectorClientRegistry;
|
||||
import work.slhaf.partner.framework.agent.Agent;
|
||||
import work.slhaf.partner.runtime.gateway.WebSocketGatewayRegistration;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
Agent.newAgent(Main.class)
|
||||
.addGatewayRegistration(WebSocketGatewayRegistration.INSTANCE)
|
||||
.addConfigurable(new VectorClientRegistry())
|
||||
.launch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package work.slhaf.partner.common.base
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.io.StringWriter
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
abstract class Block(
|
||||
val blockName: String
|
||||
) {
|
||||
|
||||
fun encodeToXml(): Element {
|
||||
val document = DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.newDocument()
|
||||
|
||||
val root = document.createElement(blockName)
|
||||
document.appendChild(root)
|
||||
appendRootAttributes().forEach { attribute, value -> root.setAttribute(attribute, value) }
|
||||
fillXml(document, root)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
protected open fun appendRootAttributes(): Map<String, String> {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
fun encodeToXmlString(): String {
|
||||
val transformer = TransformerFactory.newInstance().newTransformer().apply {
|
||||
setOutputProperty(OutputKeys.INDENT, "yes")
|
||||
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
|
||||
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
|
||||
}
|
||||
|
||||
return StringWriter().use { writer ->
|
||||
transformer.transform(DOMSource(encodeToXml()), StreamResult(writer))
|
||||
writer.toString()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun fillXml(document: Document, root: Element)
|
||||
|
||||
protected fun appendTextElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
tagName: String,
|
||||
value: Any?
|
||||
): Element {
|
||||
val element = document.createElement(tagName)
|
||||
element.textContent = value?.toString() ?: ""
|
||||
parent.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
protected fun appendChildElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
tagName: String,
|
||||
block: Element.() -> Unit = {}
|
||||
): Element {
|
||||
val element = document.createElement(tagName)
|
||||
parent.appendChild(element)
|
||||
element.block()
|
||||
return element
|
||||
}
|
||||
|
||||
protected fun appendCDataElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
tagName: String,
|
||||
value: String?
|
||||
): Element {
|
||||
val element = document.createElement(tagName)
|
||||
element.appendChild(document.createCDATASection(value ?: ""))
|
||||
parent.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
protected fun <T> appendListElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
wrapperTagName: String,
|
||||
itemTagName: String,
|
||||
values: Iterable<T>,
|
||||
block: Element.(T) -> Unit = { value ->
|
||||
textContent = value?.toString() ?: ""
|
||||
}
|
||||
): Element {
|
||||
val wrapper = document.createElement(wrapperTagName)
|
||||
parent.appendChild(wrapper)
|
||||
|
||||
for (value in values) {
|
||||
val item = document.createElement(itemTagName)
|
||||
wrapper.appendChild(item)
|
||||
item.block(value)
|
||||
}
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
protected fun <T> appendRepeatedElements(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
itemTagName: String,
|
||||
values: Iterable<T>,
|
||||
block: Element.(T) -> Unit = { value ->
|
||||
textContent = value?.toString() ?: ""
|
||||
}
|
||||
) {
|
||||
for (value in values) {
|
||||
val item = document.createElement(itemTagName)
|
||||
parent.appendChild(item)
|
||||
item.block(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package work.slhaf.partner.common.vector;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class OllamaVectorClient extends VectorClient {
|
||||
|
||||
private final String ollamaEmbeddingUrl;
|
||||
private final String ollamaEmbeddingModel;
|
||||
|
||||
protected OllamaVectorClient(String url, String model) {
|
||||
this.ollamaEmbeddingUrl = url;
|
||||
this.ollamaEmbeddingModel = model;
|
||||
|
||||
compute("test");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float[] doCompute(String input) {
|
||||
Map<String, String> param = Map.of("model", ollamaEmbeddingModel, "input", input);
|
||||
HttpRequest request = HttpRequest.get(ollamaEmbeddingUrl).body(JSONObject.toJSONString(param));
|
||||
try (HttpResponse response = request.execute()) {
|
||||
if (!response.isOk())
|
||||
throw new VectorClientExecutionException(
|
||||
"Failed to execute embedding model",
|
||||
"ollama",
|
||||
"COMPUTE",
|
||||
ollamaEmbeddingUrl
|
||||
);
|
||||
String resStr = response.body();
|
||||
EmbeddingModelResponse embeddingResponse = JSONObject.parseObject(resStr, EmbeddingModelResponse.class);
|
||||
return embeddingResponse.getEmbeddings()[0];
|
||||
} catch (Exception e) {
|
||||
throw new VectorClientExecutionException(
|
||||
"Failed to execute embedding model",
|
||||
"ollama",
|
||||
"COMPUTE",
|
||||
ollamaEmbeddingUrl,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class EmbeddingModelResponse {
|
||||
private String model;
|
||||
private float[][] embeddings;
|
||||
private long total_duration;
|
||||
private long load_duration;
|
||||
private int prompt_eval_count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package work.slhaf.partner.common.vector;
|
||||
|
||||
import ai.djl.huggingface.tokenizers.Encoding;
|
||||
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
|
||||
import ai.onnxruntime.OnnxTensor;
|
||||
import ai.onnxruntime.OrtEnvironment;
|
||||
import ai.onnxruntime.OrtSession;
|
||||
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
public class OnnxVectorClient extends VectorClient {
|
||||
|
||||
private String tokenizerPath;
|
||||
private String modelPath;
|
||||
|
||||
private HuggingFaceTokenizer tokenizer;
|
||||
private OrtSession session;
|
||||
private OrtEnvironment env;
|
||||
|
||||
protected OnnxVectorClient(String tokenizer, String model) {
|
||||
this.tokenizerPath = tokenizer;
|
||||
this.modelPath = model;
|
||||
|
||||
loadTokenizer();
|
||||
loadModel();
|
||||
|
||||
compute("test");
|
||||
}
|
||||
|
||||
private void loadModel() {
|
||||
try {
|
||||
env = OrtEnvironment.getEnvironment();
|
||||
OrtSession.SessionOptions ops = new OrtSession.SessionOptions();
|
||||
session = env.createSession(modelPath, ops);
|
||||
} catch (Exception e) {
|
||||
throw new VectorClientExecutionException(
|
||||
"Failed to load ONNX model",
|
||||
"onnx",
|
||||
"MODEL_LOAD",
|
||||
modelPath,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadTokenizer() {
|
||||
try {
|
||||
tokenizer = HuggingFaceTokenizer.newInstance(Path.of(tokenizerPath));
|
||||
} catch (Exception e) {
|
||||
throw new VectorClientExecutionException(
|
||||
"Failed to load tokenizer",
|
||||
"onnx",
|
||||
"TOKENIZER_LOAD",
|
||||
tokenizerPath,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float[] doCompute(String input) {
|
||||
try {
|
||||
Encoding encode = tokenizer.encode(input);
|
||||
long[] ids = encode.getIds();
|
||||
long[] attentionMask = encode.getAttentionMask();
|
||||
|
||||
long[][] inputIdsBatch = {ids};
|
||||
long[][] attentionMaskBatch = {attentionMask};
|
||||
long[][] tokenTypeIdsBatch = {new long[ids.length]}; // 初始化全 0
|
||||
for (int i = 0; i < ids.length; i++)
|
||||
tokenTypeIdsBatch[0][i] = 0;
|
||||
|
||||
OnnxTensor inputTensor = OnnxTensor.createTensor(env, inputIdsBatch);
|
||||
OnnxTensor maskTensor = OnnxTensor.createTensor(env, attentionMaskBatch);
|
||||
OnnxTensor tokenTypeTensor = OnnxTensor.createTensor(env, tokenTypeIdsBatch);
|
||||
|
||||
Map<String, OnnxTensor> inputs = new HashMap<>();
|
||||
inputs.put("input_ids", inputTensor);
|
||||
inputs.put("attention_mask", maskTensor);
|
||||
inputs.put("token_type_ids", tokenTypeTensor);
|
||||
|
||||
OrtSession.Result result = session.run(inputs);
|
||||
OnnxTensor embeddingTensor = (OnnxTensor) result.get(0);
|
||||
return embeddingTensor.getFloatBuffer().array();
|
||||
} catch (Exception e) {
|
||||
throw new VectorClientExecutionException(
|
||||
"Failed to execute embedding model",
|
||||
"onnx",
|
||||
"COMPUTE",
|
||||
modelPath,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package work.slhaf.partner.common.vector;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nd4j.linalg.api.ndarray.INDArray;
|
||||
import org.nd4j.linalg.factory.Nd4j;
|
||||
import org.nd4j.linalg.ops.transforms.Transforms;
|
||||
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
|
||||
import work.slhaf.partner.common.vector.exception.VectorClientStartupException;
|
||||
|
||||
@Slf4j
|
||||
public abstract class VectorClient {
|
||||
|
||||
public static boolean status = false;
|
||||
public static VectorClient INSTANCE;
|
||||
|
||||
public static void startClient(VectorConfig config) {
|
||||
try {
|
||||
if (config instanceof VectorConfig.Ollama ollama) {
|
||||
INSTANCE = new OllamaVectorClient(ollama.ollamaEmbeddingUrl, ollama.ollamaEmbeddingModel);
|
||||
} else if (config instanceof VectorConfig.Onnx onnx) {
|
||||
INSTANCE = new OnnxVectorClient(onnx.tokenizerPath, onnx.embeddingModelPath);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
status = true;
|
||||
} catch (VectorClientStartupException e) {
|
||||
throw e;
|
||||
} catch (VectorClientExecutionException e) {
|
||||
throw new VectorClientStartupException(
|
||||
"Vector client startup failed",
|
||||
e.getClientType(),
|
||||
"COMPUTE".equals(e.getPhase()) ? "STARTUP_SELF_TEST" : e.getPhase(),
|
||||
e.getTarget(),
|
||||
e
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new VectorClientStartupException(
|
||||
"Vector client startup failed",
|
||||
resolveClientType(config),
|
||||
"STARTUP",
|
||||
resolveTarget(config),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public float[] compute(String input) {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
return doCompute(input);
|
||||
}
|
||||
|
||||
protected abstract float[] doCompute(String input);
|
||||
|
||||
private static String resolveClientType(VectorConfig config) {
|
||||
if (config instanceof VectorConfig.Onnx) {
|
||||
return "onnx";
|
||||
}
|
||||
if (config instanceof VectorConfig.Ollama) {
|
||||
return "ollama";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private static String resolveTarget(VectorConfig config) {
|
||||
if (config instanceof VectorConfig.Onnx onnx) {
|
||||
return onnx.embeddingModelPath;
|
||||
}
|
||||
if (config instanceof VectorConfig.Ollama ollama) {
|
||||
return ollama.ollamaEmbeddingUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public double compare(float[] v1, float[] v2) {
|
||||
if (!status) {
|
||||
return 0;
|
||||
}
|
||||
try (INDArray a1 = Nd4j.create(v1); INDArray a2 = Nd4j.create(v2)) {
|
||||
return Transforms.cosineSim(a1, a2);
|
||||
}
|
||||
}
|
||||
|
||||
public float[] weightedAverage(float[] newVector, float[] primaryVector) {
|
||||
try (INDArray primary = Nd4j.create(primaryVector);
|
||||
INDArray latest = Nd4j.create(newVector)) {
|
||||
|
||||
// 1️⃣ 计算余弦相似度
|
||||
double similarity = Transforms.cosineSim(primary, latest);
|
||||
|
||||
// 2️⃣ 根据相似度决定更新比例 α(差异越大,新输入影响越强)
|
||||
double alpha = (1.0 - similarity) * 0.5;
|
||||
alpha = Math.clamp(alpha, 0.05, 0.5);
|
||||
|
||||
// 3️⃣ 按比例混合旧向量与新向量
|
||||
INDArray updated = primary.mul(1 - alpha).add(latest.mul(alpha));
|
||||
|
||||
// 4️⃣ 归一化结果(保持方向空间一致)
|
||||
updated = updated.div(updated.norm2Number());
|
||||
|
||||
return updated.toFloatVector();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package work.slhaf.partner.common.vector;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.framework.agent.config.Config;
|
||||
import work.slhaf.partner.framework.agent.config.ConfigRegistration;
|
||||
import work.slhaf.partner.framework.agent.config.Configurable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
public class VectorClientRegistry implements Configurable, ConfigRegistration<VectorConfig> {
|
||||
|
||||
@Override
|
||||
public void init(@NotNull VectorConfig config, @Nullable JSONObject json) {
|
||||
if (!config.enabled) {
|
||||
return;
|
||||
}
|
||||
if (config.type == null) {
|
||||
return;
|
||||
}
|
||||
if (json == null) {
|
||||
return;
|
||||
}
|
||||
config = switch (config.type) {
|
||||
case ONNX -> json.toJavaObject(VectorConfig.Onnx.class);
|
||||
case OLLAMA -> json.toJavaObject(VectorConfig.Ollama.class);
|
||||
};
|
||||
VectorClient.startClient(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Class<VectorConfig> type() {
|
||||
return VectorConfig.class;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public VectorConfig defaultConfig() {
|
||||
return new VectorConfig(false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Map<Path, ConfigRegistration<? extends Config>> declare() {
|
||||
return Map.of(Path.of("vector.json"), this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package work.slhaf.partner.common.vector;
|
||||
|
||||
import work.slhaf.partner.framework.agent.config.Config;
|
||||
|
||||
public sealed class VectorConfig extends Config permits VectorConfig.Ollama, VectorConfig.Onnx {
|
||||
final boolean enabled;
|
||||
final Type type;
|
||||
|
||||
public VectorConfig(boolean enabled, Type type) {
|
||||
this.enabled = enabled;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
ONNX,
|
||||
OLLAMA
|
||||
}
|
||||
|
||||
static final class Onnx extends VectorConfig {
|
||||
|
||||
final String tokenizerPath;
|
||||
final String embeddingModelPath;
|
||||
|
||||
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath) {
|
||||
super(enabled, type);
|
||||
this.tokenizerPath = tokenizerPath;
|
||||
this.embeddingModelPath = embeddingModelPath;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Ollama extends VectorConfig {
|
||||
|
||||
final String ollamaEmbeddingUrl;
|
||||
final String ollamaEmbeddingModel;
|
||||
|
||||
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel) {
|
||||
super(enabled, type);
|
||||
this.ollamaEmbeddingUrl = ollamaEmbeddingUrl;
|
||||
this.ollamaEmbeddingModel = ollamaEmbeddingModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package work.slhaf.partner.common.vector.exception;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
|
||||
|
||||
public class VectorClientExecutionException extends AgentRuntimeException {
|
||||
|
||||
private final String clientType;
|
||||
private final String phase;
|
||||
private final String target;
|
||||
|
||||
public VectorClientExecutionException(String message, String clientType, String phase, @Nullable String target) {
|
||||
super(message);
|
||||
this.clientType = clientType;
|
||||
this.phase = phase;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public VectorClientExecutionException(String message, String clientType, String phase, @Nullable String target, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.clientType = clientType;
|
||||
this.phase = phase;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public String getClientType() {
|
||||
return clientType;
|
||||
}
|
||||
|
||||
public String getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExceptionReport toReport() {
|
||||
ExceptionReport report = super.toReport();
|
||||
report.getExtra().put("clientType", clientType);
|
||||
report.getExtra().put("phase", phase);
|
||||
if (target != null) {
|
||||
report.getExtra().put("target", target);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package work.slhaf.partner.common.vector.exception;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
|
||||
|
||||
public class VectorClientStartupException extends AgentStartupException {
|
||||
|
||||
private final String clientType;
|
||||
private final String phase;
|
||||
private final String target;
|
||||
|
||||
public VectorClientStartupException(String message, String clientType, String phase, @Nullable String target) {
|
||||
super(message, "vector-client-registry");
|
||||
this.clientType = clientType;
|
||||
this.phase = phase;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public VectorClientStartupException(String message, String clientType, String phase, @Nullable String target, Throwable cause) {
|
||||
super(message, "vector-client-registry", cause);
|
||||
this.clientType = clientType;
|
||||
this.phase = phase;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExceptionReport toReport() {
|
||||
ExceptionReport report = super.toReport();
|
||||
report.getExtra().put("clientType", clientType);
|
||||
report.getExtra().put("phase", phase);
|
||||
if (target != null) {
|
||||
report.getExtra().put("target", target);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package work.slhaf.partner.core.action;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.core.action.entity.ExecutableAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Capability(value = "action")
|
||||
public interface ActionCapability {
|
||||
|
||||
void putAction(@NonNull ExecutableAction executableAction);
|
||||
|
||||
Set<ExecutableAction> listActions(@Nullable ExecutableAction.Status status, @Nullable String source);
|
||||
|
||||
ExecutorService getExecutor(ActionCore.ExecutorType type);
|
||||
|
||||
Result<MetaAction> loadMetaAction(@NonNull String actionKey);
|
||||
|
||||
Result<MetaActionInfo> loadMetaActionInfo(@NonNull String actionKey);
|
||||
|
||||
void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions);
|
||||
|
||||
Map<String, MetaActionInfo> listAvailableMetaActions();
|
||||
|
||||
boolean checkExists(String... actionKeys);
|
||||
|
||||
RunnerClient runnerClient();
|
||||
|
||||
void handleInterventions(List<MetaIntervention> interventions, ExecutableAction data);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package work.slhaf.partner.core.action;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.core.action.entity.ExecutableAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
|
||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
import work.slhaf.partner.core.action.exception.ActionLookupException;
|
||||
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
|
||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||
import work.slhaf.partner.framework.agent.factory.context.Shutdown;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@CapabilityCore(value = "action")
|
||||
@Slf4j
|
||||
public class ActionCore implements StateSerializable {
|
||||
public static final String BUILTIN_LOCATION = "builtin";
|
||||
public static final String ORIGIN_LOCATION = "origin";
|
||||
|
||||
// 由于当前的执行器逻辑实现,平台线程池大小不得小于 2,这里规定为最小为 4
|
||||
private final ExecutorService platformExecutor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4));
|
||||
private final ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
/**
|
||||
* 已存在的行动程序,键格式为‘<MCP-ServerName>::<Tool-Name>’,值为 MCP Server 通过 Resources 相关渠道传递的行动程序元信息
|
||||
*/
|
||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 持久行动池
|
||||
*/
|
||||
private CopyOnWriteArraySet<ExecutableAction> actionPool = new CopyOnWriteArraySet<>();
|
||||
|
||||
private RunnerClient runnerClient;
|
||||
|
||||
public ActionCore() throws IOException, ClassNotFoundException {
|
||||
String baseActionPath = ConfigCenter.INSTANCE.getPaths().getResourcesDir().resolve("action").normalize().toAbsolutePath().toString();
|
||||
// TODO 通过 Config 指定采用何种 runnerClient,当前只提供 LocalRunnerClient
|
||||
runnerClient = new LocalRunnerClient(existedMetaActions, virtualExecutor, baseActionPath);
|
||||
register();
|
||||
}
|
||||
|
||||
@Shutdown
|
||||
public void shutdown() {
|
||||
try {
|
||||
runnerClient.close();
|
||||
} catch (Exception e) {
|
||||
log.warn("runner client close error", e);
|
||||
}
|
||||
try {
|
||||
platformExecutor.shutdown();
|
||||
virtualExecutor.shutdown();
|
||||
|
||||
int count = 0;
|
||||
if (!platformExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
|
||||
count += platformExecutor.shutdownNow().size();
|
||||
}
|
||||
if (!virtualExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
|
||||
count += virtualExecutor.shutdownNow().size();
|
||||
}
|
||||
if (count != 0) {
|
||||
log.warn("{} tasks still running", count);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void putAction(@NonNull ExecutableAction executableAction) {
|
||||
actionPool.removeIf(data -> data.getUuid().equals(executableAction.getUuid())); // 用来应对 ScheduledActionData 的重新排列
|
||||
actionPool.add(executableAction);
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Set<ExecutableAction> listActions(@Nullable ExecutableAction.Status status, @Nullable String source) {
|
||||
return actionPool.stream()
|
||||
.filter(actionData -> status == null || actionData.getStatus().equals(status))
|
||||
.filter(actionData -> source == null || actionData.getSource().equals(source))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public ExecutorService getExecutor(ExecutorType type) {
|
||||
return switch (type) {
|
||||
case VIRTUAL -> virtualExecutor;
|
||||
case PLATFORM -> platformExecutor;
|
||||
};
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions) {
|
||||
existedMetaActions.putAll(metaActions);
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Map<String, MetaActionInfo> listAvailableMetaActions() {
|
||||
return existedMetaActions;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Result<MetaAction> loadMetaAction(@NonNull String actionKey) {
|
||||
MetaActionInfo metaActionInfo = existedMetaActions.get(actionKey);
|
||||
if (metaActionInfo == null) {
|
||||
return Result.failure(new ActionLookupException(
|
||||
"Meta action info not found for action key: " + actionKey,
|
||||
actionKey,
|
||||
"META_ACTION"
|
||||
));
|
||||
}
|
||||
|
||||
String[] split = actionKey.split("::", 2);
|
||||
if (split.length < 2) {
|
||||
return Result.failure(new ActionLookupException(
|
||||
"Invalid action key format: " + actionKey,
|
||||
actionKey,
|
||||
"META_ACTION"
|
||||
));
|
||||
}
|
||||
MetaAction.Type type = switch (split[0]) {
|
||||
case BUILTIN_LOCATION -> MetaAction.Type.BUILTIN;
|
||||
case ORIGIN_LOCATION -> MetaAction.Type.ORIGIN;
|
||||
default -> MetaAction.Type.MCP;
|
||||
};
|
||||
return Result.success(new MetaAction(
|
||||
split[1],
|
||||
metaActionInfo.getIo(),
|
||||
metaActionInfo.getLauncher(),
|
||||
type,
|
||||
split[0]
|
||||
));
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Result<MetaActionInfo> loadMetaActionInfo(@NonNull String actionKey) {
|
||||
MetaActionInfo info = existedMetaActions.get(actionKey);
|
||||
if (info == null) {
|
||||
return Result.failure(new ActionLookupException(
|
||||
"Meta action description not found for action key: " + actionKey,
|
||||
actionKey,
|
||||
"META_ACTION_INFO"
|
||||
));
|
||||
}
|
||||
return Result.success(info);
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public boolean checkExists(String... actionKeys) {
|
||||
return existedMetaActions.keySet().containsAll(Arrays.asList(actionKeys));
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public RunnerClient runnerClient() {
|
||||
return runnerClient;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void handleInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
|
||||
// 加载数据
|
||||
if (executableAction == null) {
|
||||
return;
|
||||
}
|
||||
// 加锁确保同步
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
applyInterventions(interventions, executableAction);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
|
||||
boolean[] rebuildCleanTag = {false};
|
||||
|
||||
interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder));
|
||||
|
||||
for (MetaIntervention intervention : interventions) {
|
||||
Result<List<MetaAction>> actionsResult = resolveInterventionActions(intervention);
|
||||
actionsResult
|
||||
.onFailure(ExceptionReporterHandler.INSTANCE::report)
|
||||
.onSuccess(actions -> {
|
||||
switch (intervention.getType()) {
|
||||
case InterventionType.APPEND ->
|
||||
handleAppend(executableAction, intervention.getOrder(), actions);
|
||||
case InterventionType.INSERT ->
|
||||
handleInsert(executableAction, intervention.getOrder(), actions);
|
||||
case InterventionType.DELETE ->
|
||||
handleDelete(executableAction, intervention.getOrder(), actions);
|
||||
case InterventionType.CANCEL -> handleCancel(executableAction);
|
||||
case InterventionType.REBUILD -> {
|
||||
if (!rebuildCleanTag[0]) {
|
||||
cleanActionData(executableAction);
|
||||
rebuildCleanTag[0] = true;
|
||||
}
|
||||
handleRebuild(executableAction, intervention.getOrder(), actions);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Result<List<MetaAction>> resolveInterventionActions(MetaIntervention intervention) {
|
||||
List<MetaAction> actions = new ArrayList<>();
|
||||
for (String actionKey : intervention.getActions()) {
|
||||
Result<MetaAction> metaActionResult = loadMetaAction(actionKey);
|
||||
AgentRuntimeException failure = metaActionResult.onSuccess(actions::add).exceptionOrNull();
|
||||
if (failure != null) {
|
||||
return Result.failure(failure);
|
||||
}
|
||||
}
|
||||
return Result.success(actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在未进入执行阶段的行动单元组新增新的行动
|
||||
*/
|
||||
private void handleAppend(ExecutableAction executableAction, int order, List<MetaAction> actions) {
|
||||
if (order <= executableAction.getExecutingStage())
|
||||
return;
|
||||
|
||||
executableAction.getActionChain().put(order, actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在未进入执行阶段和正处于行动阶段的行动单元组插入新的行动
|
||||
*/
|
||||
private void handleInsert(ExecutableAction executableAction, int order, List<MetaAction> actions) {
|
||||
if (order < executableAction.getExecutingStage())
|
||||
return;
|
||||
|
||||
List<MetaAction> stageActions = executableAction.getActionChain().computeIfAbsent(order, k -> new ArrayList<>());
|
||||
synchronized (stageActions) {
|
||||
stageActions.addAll(actions);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDelete(ExecutableAction executableAction, int order, List<MetaAction> actions) {
|
||||
if (order <= executableAction.getExecutingStage())
|
||||
return;
|
||||
|
||||
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
|
||||
if (actionChain.containsKey(order)) {
|
||||
actionChain.get(order).removeAll(actions);
|
||||
if (actionChain.get(order).isEmpty()) {
|
||||
actionChain.remove(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCancel(ExecutableAction executableAction) {
|
||||
executableAction.setStatus(ExecutableAction.Status.FAILED);
|
||||
executableAction.setResult("行动取消");
|
||||
}
|
||||
|
||||
private void handleRebuild(ExecutableAction executableAction, int order, List<MetaAction> actions) {
|
||||
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
|
||||
actionChain.put(order, actions);
|
||||
}
|
||||
|
||||
private void cleanActionData(ExecutableAction executableAction) {
|
||||
executableAction.getActionChain().clear();
|
||||
executableAction.setExecutingStage(0);
|
||||
executableAction.setStatus(ExecutableAction.Status.PREPARE);
|
||||
executableAction.getHistory().clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("core", "action.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
actionPool = ActionPoolStateCodec.decode(state.getJSONArray("action_pool"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
List<StateValue.Obj> actionPoolState = ActionPoolStateCodec.encode(actionPool);
|
||||
state.append("action_pool", StateValue.arr(actionPoolState));
|
||||
return state;
|
||||
}
|
||||
|
||||
public enum ExecutorType {
|
||||
VIRTUAL, PLATFORM
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package work.slhaf.partner.core.action;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
@Slf4j
|
||||
final class ActionPoolStateCodec {
|
||||
|
||||
private ActionPoolStateCodec() {
|
||||
}
|
||||
|
||||
static List<StateValue.Obj> encode(CopyOnWriteArraySet<ExecutableAction> actionPool) {
|
||||
return actionPool.stream()
|
||||
.map(ActionPoolStateCodec::encodeExecutableAction)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static CopyOnWriteArraySet<ExecutableAction> decode(@Nullable JSONArray actionPoolArray) {
|
||||
CopyOnWriteArraySet<ExecutableAction> restored = new CopyOnWriteArraySet<>();
|
||||
if (actionPoolArray == null) {
|
||||
return restored;
|
||||
}
|
||||
for (int i = 0; i < actionPoolArray.size(); i++) {
|
||||
JSONObject actionObject = actionPoolArray.getJSONObject(i);
|
||||
if (actionObject == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ExecutableAction executableAction = decodeExecutableAction(actionObject);
|
||||
if (executableAction != null) {
|
||||
restored.add(executableAction);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Skip invalid action_pool item at index {}", i, e);
|
||||
}
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
private static StateValue.Obj encodeExecutableAction(ExecutableAction action) {
|
||||
Map<String, StateValue> actionMap = new LinkedHashMap<>();
|
||||
actionMap.put("kind", StateValue.str(action instanceof SchedulableExecutableAction ? "schedulable" : "immediate"));
|
||||
actionMap.put("uuid", StateValue.str(action.getUuid()));
|
||||
actionMap.put("source", StateValue.str(action.getSource()));
|
||||
actionMap.put("reason", StateValue.str(action.getReason()));
|
||||
actionMap.put("description", StateValue.str(action.getDescription()));
|
||||
actionMap.put("status", StateValue.str(action.getStatus().name()));
|
||||
actionMap.put("tendency", StateValue.str(action.getTendency()));
|
||||
actionMap.put("executing_stage", StateValue.num(action.getExecutingStage()));
|
||||
|
||||
String result = resolveExecutableResult(action);
|
||||
if (result != null) {
|
||||
actionMap.put("result", StateValue.str(result));
|
||||
}
|
||||
if (action instanceof SchedulableExecutableAction schedulableAction) {
|
||||
actionMap.put("schedule_type", StateValue.str(schedulableAction.getScheduleType().name()));
|
||||
actionMap.put("schedule_content", StateValue.str(schedulableAction.getScheduleContent()));
|
||||
actionMap.put("enabled", StateValue.bool(schedulableAction.getEnabled()));
|
||||
actionMap.put("schedule_histories", StateValue.arr(encodeScheduleHistories(schedulableAction)));
|
||||
}
|
||||
|
||||
List<StateValue> chainStates = action.getActionChain().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.<StateValue>map(entry -> {
|
||||
Map<String, StateValue> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", StateValue.num(entry.getKey()));
|
||||
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
|
||||
.map(metaAction -> (StateValue) encodeMetaAction(metaAction))
|
||||
.toList()));
|
||||
return StateValue.obj(stageMap);
|
||||
}).toList();
|
||||
actionMap.put("action_chain", StateValue.arr(chainStates));
|
||||
|
||||
actionMap.put("history", StateValue.arr(encodeHistoryStages(action.getHistory())));
|
||||
|
||||
return StateValue.obj(actionMap);
|
||||
}
|
||||
|
||||
private static StateValue.Obj encodeMetaAction(MetaAction metaAction) {
|
||||
Map<String, StateValue> metaMap = new LinkedHashMap<>();
|
||||
metaMap.put("name", StateValue.str(metaAction.getName()));
|
||||
metaMap.put("io", StateValue.bool(metaAction.getIo()));
|
||||
if (metaAction.getLauncher() != null) {
|
||||
metaMap.put("launcher", StateValue.str(metaAction.getLauncher()));
|
||||
}
|
||||
metaMap.put("type", StateValue.str(metaAction.getType().name()));
|
||||
metaMap.put("location", StateValue.str(metaAction.getLocation()));
|
||||
metaMap.put("params_json", StateValue.str(JSONObject.toJSONString(metaAction.getParams())));
|
||||
metaMap.put("result_status", StateValue.str(metaAction.getResult().getStatus().name()));
|
||||
if (metaAction.getResult().getData() != null) {
|
||||
metaMap.put("result_data", StateValue.str(metaAction.getResult().getData()));
|
||||
}
|
||||
return StateValue.obj(metaMap);
|
||||
}
|
||||
|
||||
private static StateValue.Obj encodeHistoryAction(HistoryAction historyAction) {
|
||||
Map<String, StateValue> historyMap = new LinkedHashMap<>();
|
||||
historyMap.put("action_key", StateValue.str(historyAction.actionKey()));
|
||||
historyMap.put("description", StateValue.str(historyAction.description()));
|
||||
historyMap.put("result", StateValue.str(historyAction.result()));
|
||||
return StateValue.obj(historyMap);
|
||||
}
|
||||
|
||||
private static ExecutableAction decodeExecutableAction(JSONObject actionObject) {
|
||||
String kind = actionObject.getString("kind");
|
||||
String uuid = actionObject.getString("uuid");
|
||||
String source = actionObject.getString("source");
|
||||
String reason = actionObject.getString("reason");
|
||||
String description = actionObject.getString("description");
|
||||
String tendency = actionObject.getString("tendency");
|
||||
String status = actionObject.getString("status");
|
||||
Integer executingStage = actionObject.getInteger("executing_stage");
|
||||
if (kind == null || uuid == null || source == null || reason == null || description == null || tendency == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<Integer, List<MetaAction>> restoredChain = decodeActionChain(actionObject.getJSONArray("action_chain"));
|
||||
ExecutableAction executableAction;
|
||||
if ("schedulable".equals(kind)) {
|
||||
String scheduleType = actionObject.getString("schedule_type");
|
||||
String scheduleContent = actionObject.getString("schedule_content");
|
||||
if (scheduleType == null || scheduleContent == null) {
|
||||
return null;
|
||||
}
|
||||
SchedulableExecutableAction schedulableAction = new SchedulableExecutableAction(
|
||||
tendency,
|
||||
restoredChain,
|
||||
reason,
|
||||
description,
|
||||
source,
|
||||
Schedulable.ScheduleType.valueOf(scheduleType),
|
||||
scheduleContent,
|
||||
uuid
|
||||
);
|
||||
Boolean enabled = actionObject.getBoolean("enabled");
|
||||
if (enabled != null) {
|
||||
schedulableAction.setEnabled(enabled);
|
||||
}
|
||||
schedulableAction.getScheduleHistories().addAll(decodeScheduleHistories(actionObject.getJSONArray("schedule_histories")));
|
||||
executableAction = schedulableAction;
|
||||
} else if ("immediate".equals(kind)) {
|
||||
executableAction = new ImmediateExecutableAction(
|
||||
tendency,
|
||||
restoredChain,
|
||||
reason,
|
||||
description,
|
||||
source,
|
||||
uuid
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status != null) {
|
||||
executableAction.setStatus(Action.Status.valueOf(status));
|
||||
}
|
||||
if (executingStage != null) {
|
||||
executableAction.setExecutingStage(executingStage);
|
||||
}
|
||||
String result = actionObject.getString("result");
|
||||
if (result != null) {
|
||||
executableAction.setResult(result);
|
||||
}
|
||||
executableAction.getHistory().putAll(decodeHistory(actionObject.getJSONArray("history")));
|
||||
return executableAction;
|
||||
}
|
||||
|
||||
private static Map<Integer, List<MetaAction>> decodeActionChain(@Nullable JSONArray actionChainArray) {
|
||||
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
|
||||
if (actionChainArray == null) {
|
||||
return toMutableActionChain(restored);
|
||||
}
|
||||
for (int i = 0; i < actionChainArray.size(); i++) {
|
||||
JSONObject stageObject = actionChainArray.getJSONObject(i);
|
||||
if (stageObject == null) {
|
||||
continue;
|
||||
}
|
||||
Integer stage = stageObject.getInteger("stage");
|
||||
JSONArray actions = stageObject.getJSONArray("actions");
|
||||
if (stage == null || actions == null) {
|
||||
continue;
|
||||
}
|
||||
List<MetaAction> metaActions = new ArrayList<>();
|
||||
for (int j = 0; j < actions.size(); j++) {
|
||||
JSONObject actionObject = actions.getJSONObject(j);
|
||||
MetaAction metaAction = decodeMetaAction(actionObject);
|
||||
if (metaAction != null) {
|
||||
metaActions.add(metaAction);
|
||||
}
|
||||
}
|
||||
restored.put(stage, metaActions);
|
||||
}
|
||||
return toMutableActionChain(restored);
|
||||
}
|
||||
|
||||
private static MetaAction decodeMetaAction(@Nullable JSONObject actionObject) {
|
||||
if (actionObject == null) {
|
||||
return null;
|
||||
}
|
||||
String name = actionObject.getString("name");
|
||||
Boolean io = actionObject.getBoolean("io");
|
||||
String type = actionObject.getString("type");
|
||||
String location = actionObject.getString("location");
|
||||
if (name == null || io == null || type == null || location == null) {
|
||||
return null;
|
||||
}
|
||||
MetaAction metaAction = new MetaAction(
|
||||
name,
|
||||
io,
|
||||
actionObject.getString("launcher"),
|
||||
MetaAction.Type.valueOf(type),
|
||||
location
|
||||
);
|
||||
String paramsJson = actionObject.getString("params_json");
|
||||
if (paramsJson != null && !paramsJson.isBlank()) {
|
||||
JSONObject paramsObject = JSONObject.parseObject(paramsJson);
|
||||
if (paramsObject != null) {
|
||||
metaAction.getParams().putAll(paramsObject);
|
||||
}
|
||||
}
|
||||
String resultStatus = actionObject.getString("result_status");
|
||||
if (resultStatus != null) {
|
||||
metaAction.getResult().setStatus(MetaAction.Result.Status.valueOf(resultStatus));
|
||||
}
|
||||
metaAction.getResult().setData(actionObject.getString("result_data"));
|
||||
return metaAction;
|
||||
}
|
||||
|
||||
private static Map<Integer, List<HistoryAction>> decodeHistory(@Nullable JSONArray historyArray) {
|
||||
Map<Integer, List<HistoryAction>> restored = new LinkedHashMap<>();
|
||||
if (historyArray == null) {
|
||||
return restored;
|
||||
}
|
||||
for (int i = 0; i < historyArray.size(); i++) {
|
||||
JSONObject stageObject = historyArray.getJSONObject(i);
|
||||
if (stageObject == null) {
|
||||
continue;
|
||||
}
|
||||
Integer stage = stageObject.getInteger("stage");
|
||||
JSONArray actions = stageObject.getJSONArray("actions");
|
||||
if (stage == null || actions == null) {
|
||||
continue;
|
||||
}
|
||||
List<HistoryAction> historyActions = new ArrayList<>();
|
||||
for (int j = 0; j < actions.size(); j++) {
|
||||
JSONObject historyObject = actions.getJSONObject(j);
|
||||
if (historyObject == null) {
|
||||
continue;
|
||||
}
|
||||
String actionKey = historyObject.getString("action_key");
|
||||
String description = historyObject.getString("description");
|
||||
String result = historyObject.getString("result");
|
||||
if (actionKey == null || description == null || result == null) {
|
||||
continue;
|
||||
}
|
||||
historyActions.add(new HistoryAction(actionKey, description, result));
|
||||
}
|
||||
restored.put(stage, historyActions);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
private static List<StateValue> encodeHistoryStages(Map<Integer, ? extends List<HistoryAction>> historyMap) {
|
||||
return historyMap.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.<StateValue>map(entry -> {
|
||||
Map<String, StateValue> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", StateValue.num(entry.getKey()));
|
||||
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
|
||||
.map(historyAction -> (StateValue) encodeHistoryAction(historyAction))
|
||||
.toList()));
|
||||
return StateValue.obj(stageMap);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private static List<StateValue> encodeScheduleHistories(SchedulableExecutableAction schedulableAction) {
|
||||
return schedulableAction.getScheduleHistories().stream()
|
||||
.<StateValue>map(scheduleHistory -> {
|
||||
Map<String, StateValue> historyMap = new LinkedHashMap<>();
|
||||
historyMap.put("end_time", StateValue.str(scheduleHistory.getEndTime().toString()));
|
||||
historyMap.put("result", StateValue.str(scheduleHistory.getResult()));
|
||||
historyMap.put("history", StateValue.arr(encodeHistoryStages(scheduleHistory.getHistory())));
|
||||
return StateValue.obj(historyMap);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<SchedulableExecutableAction.ScheduleHistory> decodeScheduleHistories(@Nullable JSONArray scheduleHistoriesArray) {
|
||||
List<SchedulableExecutableAction.ScheduleHistory> restored = new ArrayList<>();
|
||||
if (scheduleHistoriesArray == null) {
|
||||
return restored;
|
||||
}
|
||||
for (int i = 0; i < scheduleHistoriesArray.size(); i++) {
|
||||
JSONObject historyObject = scheduleHistoriesArray.getJSONObject(i);
|
||||
if (historyObject == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String endTime = historyObject.getString("end_time");
|
||||
String result = historyObject.getString("result");
|
||||
if (endTime == null || result == null) {
|
||||
continue;
|
||||
}
|
||||
restored.add(new SchedulableExecutableAction.ScheduleHistory(
|
||||
ZonedDateTime.parse(endTime),
|
||||
result,
|
||||
decodeHistory(historyObject.getJSONArray("history"))
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.warn("Skip invalid schedule_history item at index {}", i, e);
|
||||
}
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
private static Map<Integer, List<MetaAction>> toMutableActionChain(Map<Integer, List<MetaAction>> actionChain) {
|
||||
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
|
||||
actionChain.forEach((stage, actions) -> restored.put(stage, new ArrayList<>(actions)));
|
||||
return restored;
|
||||
}
|
||||
|
||||
private static String resolveExecutableResult(ExecutableAction action) {
|
||||
try {
|
||||
return action.getResult();
|
||||
} catch (RuntimeException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package work.slhaf.partner.core.action.entity
|
||||
|
||||
import work.slhaf.partner.module.action.executor.entity.HistoryAction
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
sealed class Action(
|
||||
open val uuid: String = UUID.randomUUID().toString()
|
||||
) {
|
||||
/**
|
||||
* 行动ID
|
||||
*/
|
||||
abstract val source: String
|
||||
|
||||
/**
|
||||
* 行动原因
|
||||
*/
|
||||
abstract val reason: String
|
||||
|
||||
/**
|
||||
* 行动描述
|
||||
*/
|
||||
abstract val description: String
|
||||
|
||||
abstract val timeout: Duration
|
||||
|
||||
val timeoutMills: Long
|
||||
get() = timeout.inWholeMilliseconds
|
||||
|
||||
/**
|
||||
* 行动状态
|
||||
*/
|
||||
var status: Status = Status.PREPARE
|
||||
|
||||
enum class Status {
|
||||
/**
|
||||
* 执行成功
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* 执行失败
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* 执行中
|
||||
*/
|
||||
EXECUTING,
|
||||
|
||||
/**
|
||||
* 暂时中断
|
||||
*/
|
||||
INTERRUPTED,
|
||||
|
||||
/**
|
||||
* 预备执行
|
||||
*/
|
||||
PREPARE
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Schedulable {
|
||||
|
||||
val scheduleType: ScheduleType
|
||||
val scheduleContent: String
|
||||
val uuid: String
|
||||
val timeout: Duration
|
||||
val timeoutMills: Long
|
||||
var enabled: Boolean
|
||||
|
||||
enum class ScheduleType {
|
||||
CYCLE,
|
||||
ONCE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行动模块传递的行动数据,包含行动uuid、倾向、状态、行动链、结果、发起原因、行动描述等信息。
|
||||
*/
|
||||
sealed class ExecutableAction(
|
||||
override val uuid: String = UUID.randomUUID().toString()
|
||||
) : Action(uuid) {
|
||||
val executionLock: Any = Any()
|
||||
|
||||
/**
|
||||
* 行动倾向
|
||||
*/
|
||||
abstract val tendency: String
|
||||
|
||||
/**
|
||||
* 行动链
|
||||
*/
|
||||
abstract val actionChain: MutableMap<Int, MutableList<MetaAction>>
|
||||
|
||||
/**
|
||||
* 行动阶段(当前阶段)
|
||||
*/
|
||||
var executingStage: Int = 0
|
||||
|
||||
/**
|
||||
* 行动结果
|
||||
*/
|
||||
var result: String? = null
|
||||
|
||||
val history: MutableMap<Int, MutableList<HistoryAction>> = mutableMapOf()
|
||||
|
||||
override val timeout: Duration = 10.minutes
|
||||
|
||||
/**
|
||||
* @param timeout 最长打断时间
|
||||
* @return 是否超时结束
|
||||
*/
|
||||
fun interrupt(timeout: Int): Boolean {
|
||||
status = Status.INTERRUPTED
|
||||
val interruptAt = Instant.now().epochSecond
|
||||
|
||||
while (status == Status.INTERRUPTED) {
|
||||
Thread.sleep(500)
|
||||
if (Instant.now().epochSecond - interruptAt > timeout) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
status = Status.EXECUTING
|
||||
}
|
||||
|
||||
fun snapshot(): ExecutableActionSnapshot {
|
||||
val schedulable = this as? Schedulable
|
||||
|
||||
return ExecutableActionSnapshot(
|
||||
uuid = uuid,
|
||||
source = source,
|
||||
reason = reason,
|
||||
description = description,
|
||||
timeoutMills = timeoutMills,
|
||||
status = status,
|
||||
tendency = tendency,
|
||||
actionChainSize = actionChain.size,
|
||||
executingStage = executingStage,
|
||||
result = result,
|
||||
history = history.mapValues { (_, value) -> value.toList() },
|
||||
scheduleType = schedulable?.scheduleType,
|
||||
scheduleContent = schedulable?.scheduleContent,
|
||||
enabled = schedulable?.enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划行动数据类,继承自[Action],扩展了[Schedulable]相关调度属性,用于标识计划类型(单次还是周期性任务)和计划内容
|
||||
*/
|
||||
data class SchedulableExecutableAction @JvmOverloads constructor(
|
||||
override val tendency: String,
|
||||
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
|
||||
override val reason: String,
|
||||
override val description: String,
|
||||
override val source: String,
|
||||
override val scheduleType: Schedulable.ScheduleType,
|
||||
override val scheduleContent: String,
|
||||
override val uuid: String = UUID.randomUUID().toString(),
|
||||
) : ExecutableAction(uuid), Schedulable {
|
||||
|
||||
override var enabled = true
|
||||
val scheduleHistories = ArrayList<ScheduleHistory>()
|
||||
|
||||
fun recordAndReset() {
|
||||
val newHistory = ScheduleHistory(ZonedDateTime.now(), result ?: "null", history.toMap())
|
||||
scheduleHistories.add(newHistory)
|
||||
|
||||
executingStage = 0
|
||||
for (entry in actionChain) {
|
||||
for (action in entry.value) {
|
||||
action.params.clear()
|
||||
action.result.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ScheduleHistory(
|
||||
val endTime: ZonedDateTime,
|
||||
val result: String,
|
||||
val history: Map<Int, List<HistoryAction>>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 即时行动数据类
|
||||
*/
|
||||
data class ImmediateExecutableAction @JvmOverloads constructor(
|
||||
override val tendency: String,
|
||||
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
|
||||
override val reason: String,
|
||||
override val description: String,
|
||||
override val source: String,
|
||||
override val uuid: String = UUID.randomUUID().toString(),
|
||||
) : ExecutableAction(uuid)
|
||||
|
||||
/**
|
||||
* 用于计时的一次性或周期性触发或者针对某一数据源进行内容更新的行动
|
||||
*/
|
||||
data class StateAction @JvmOverloads constructor(
|
||||
override val source: String,
|
||||
override val reason: String,
|
||||
override val description: String,
|
||||
|
||||
override val scheduleType: Schedulable.ScheduleType,
|
||||
override val scheduleContent: String,
|
||||
|
||||
val trigger: Trigger,
|
||||
|
||||
override var enabled: Boolean = true,
|
||||
override val timeout: Duration = 5.minutes,
|
||||
) : Action(), Schedulable {
|
||||
|
||||
fun snapshot(): StateActionSnapshot {
|
||||
return StateActionSnapshot(
|
||||
uuid = uuid,
|
||||
source = source,
|
||||
reason = reason,
|
||||
description = description,
|
||||
timeoutMills = timeoutMills,
|
||||
status = status,
|
||||
scheduleType = scheduleType,
|
||||
scheduleContent = scheduleContent,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Trigger {
|
||||
|
||||
fun onTrigger()
|
||||
|
||||
/**
|
||||
* State 更新触发
|
||||
*/
|
||||
class Update<T>(val stateSource: T, val update: (stateSource: T) -> Unit) : Trigger {
|
||||
override fun onTrigger() {
|
||||
update(stateSource)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 常规逻辑触发
|
||||
*/
|
||||
class Call(val call: () -> Unit) : Trigger {
|
||||
override fun onTrigger() {
|
||||
call()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
data class ExecutableActionSnapshot(
|
||||
val uuid: String,
|
||||
val source: String,
|
||||
val reason: String,
|
||||
val description: String,
|
||||
val timeoutMills: Long,
|
||||
val status: Action.Status,
|
||||
|
||||
val tendency: String,
|
||||
val actionChainSize: Int,
|
||||
val executingStage: Int,
|
||||
val result: String?,
|
||||
val history: Map<Int, List<HistoryAction>>,
|
||||
|
||||
val scheduleType: Schedulable.ScheduleType? = null,
|
||||
val scheduleContent: String? = null,
|
||||
val enabled: Boolean? = null
|
||||
)
|
||||
|
||||
data class StateActionSnapshot(
|
||||
val uuid: String,
|
||||
val source: String,
|
||||
val reason: String,
|
||||
val description: String,
|
||||
val timeoutMills: Long,
|
||||
val status: Action.Status,
|
||||
val scheduleType: Schedulable.ScheduleType,
|
||||
val scheduleContent: String,
|
||||
val enabled: Boolean
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package work.slhaf.partner.core.action.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ActionFileMetaData {
|
||||
private String content;
|
||||
private String name;
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package work.slhaf.partner.core.action.entity
|
||||
|
||||
|
||||
/**
|
||||
* 行动链中的单一元素,封装了调用外部行动程序的必要信息与结果容器,可被[work.slhaf.partner.core.action.ActionCapability]执行
|
||||
*/
|
||||
data class MetaAction(
|
||||
/**
|
||||
* 行动name,用于标识行动程序
|
||||
*/
|
||||
val name: String,
|
||||
/**
|
||||
* 是否IO密集,用于决定使用何种线程池
|
||||
*/
|
||||
val io: Boolean = false,
|
||||
/**
|
||||
* 启动器/解释器,对于原生 MCP Tool 、Dynamic Action 来说可忽略,目前仅用于 ORIGIN 类型
|
||||
*/
|
||||
val launcher: String? = null,
|
||||
/**
|
||||
* 行动程序类型,可分为 MCP、ORIGIN、BUILTIN 三种,
|
||||
* 分别对应读取到的 MCP Tool、生成的临时行动程序、内置行动
|
||||
*/
|
||||
val type: Type,
|
||||
/**
|
||||
* 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id;
|
||||
* 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串;
|
||||
* 当类型为 BUILTIN 时,该字段固定为 builtin
|
||||
*/
|
||||
val location: String,
|
||||
) {
|
||||
|
||||
/**
|
||||
* 行动程序可接受的参数,由调用处设置
|
||||
*/
|
||||
val params: MutableMap<String, Any> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* 行动结果,包括执行状态和相应内容(执行结果或者错误信息)
|
||||
*/
|
||||
val result = Result()
|
||||
|
||||
val key: String
|
||||
/**
|
||||
* actionKey 将由 location+name 共同定位
|
||||
*
|
||||
* @return actionKey
|
||||
*/
|
||||
get() = "$location::$name"
|
||||
|
||||
class Result {
|
||||
var status = Status.WAITING
|
||||
var data: String? = null
|
||||
|
||||
fun reset() {
|
||||
status = Status.WAITING
|
||||
data = null
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
WAITING
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
/**
|
||||
* 将调用的 MCP 工具,可包括远程、本地任意服务
|
||||
*/
|
||||
MCP,
|
||||
|
||||
/**
|
||||
* 适用于‘临时生成’的行动程序,在生成后根据序列化选项及执行情况,进行持久化
|
||||
*/
|
||||
ORIGIN,
|
||||
|
||||
/**
|
||||
* 由本地内置注册表直接执行的行动
|
||||
*/
|
||||
BUILTIN
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package work.slhaf.partner.core.action.entity
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
|
||||
|
||||
data class MetaActionInfo(
|
||||
/**
|
||||
* 是否 IO 密集
|
||||
*/
|
||||
val io: Boolean,
|
||||
/**
|
||||
* 所需的启动器/解释器
|
||||
*/
|
||||
val launcher: String?,
|
||||
/**
|
||||
* 参数描述
|
||||
*/
|
||||
val params: Map<String, String>,
|
||||
/**
|
||||
* 行动功能描述
|
||||
*/
|
||||
val description: String,
|
||||
/**
|
||||
* 行动标签
|
||||
*/
|
||||
val tags: Set<String>,
|
||||
/**
|
||||
* 前置行动依赖
|
||||
*/
|
||||
val preActions: Set<String>,
|
||||
/**
|
||||
* 后置行动依赖
|
||||
*/
|
||||
val postActions: Set<String>,
|
||||
|
||||
/**
|
||||
* 是否严格依赖前置行动的成功执行,若为true且前置行动失败则不执行该行动,后置任务多为触发式。默认即执行。
|
||||
*/
|
||||
val strictDependencies: Boolean,
|
||||
/**
|
||||
* 响应格式说明
|
||||
*/
|
||||
val responseSchema: JSONObject,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package work.slhaf.partner.core.action.entity.intervention;
|
||||
|
||||
public enum InterventionType {
|
||||
/**
|
||||
* 追加行动: 追加至指定行动链序列之后才执行
|
||||
*/
|
||||
APPEND,
|
||||
|
||||
/**
|
||||
* 插入行动: 指定行动链序列执行过程中即时新增并执行
|
||||
*/
|
||||
INSERT,
|
||||
|
||||
/**
|
||||
* 重建行动: 重建指定行动链序列之后的所有行动内容
|
||||
*/
|
||||
REBUILD,
|
||||
|
||||
/**
|
||||
* 删除行动: 删除指定行动链序列上的指定行动单元
|
||||
*/
|
||||
DELETE,
|
||||
|
||||
/**
|
||||
* 取消行动链: 中断并取消指定行动链的执行
|
||||
*/
|
||||
CANCEL
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package work.slhaf.partner.core.action.entity.intervention;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class MetaIntervention {
|
||||
/**
|
||||
* 干预数据类型
|
||||
*/
|
||||
private InterventionType type;
|
||||
/**
|
||||
* 干预数据对应的行动链序列
|
||||
*/
|
||||
private int order;
|
||||
/**
|
||||
* 干预数据所需的行动key列表
|
||||
*/
|
||||
private List<String> actions;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package work.slhaf.partner.core.action.exception;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
|
||||
|
||||
public class ActionInfrastructureStartupException extends AgentStartupException {
|
||||
|
||||
private final String component;
|
||||
private final String path;
|
||||
private final String command;
|
||||
|
||||
public ActionInfrastructureStartupException(String message, String component, @Nullable String path, @Nullable String command) {
|
||||
super(message, "action-core");
|
||||
this.component = component;
|
||||
this.path = path;
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public ActionInfrastructureStartupException(String message, String component, @Nullable String path, @Nullable String command, Throwable cause) {
|
||||
super(message, "action-core", cause);
|
||||
this.component = component;
|
||||
this.path = path;
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExceptionReport toReport() {
|
||||
ExceptionReport report = super.toReport();
|
||||
report.getExtra().put("component", component);
|
||||
if (path != null) {
|
||||
report.getExtra().put("path", path);
|
||||
}
|
||||
if (command != null) {
|
||||
report.getExtra().put("command", command);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package work.slhaf.partner.core.action.exception;
|
||||
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
|
||||
|
||||
public class ActionLookupException extends AgentRuntimeException {
|
||||
|
||||
private final String actionKey;
|
||||
private final String lookupTarget;
|
||||
|
||||
public ActionLookupException(String message, String actionKey, String lookupTarget) {
|
||||
super(message);
|
||||
this.actionKey = actionKey;
|
||||
this.lookupTarget = lookupTarget;
|
||||
}
|
||||
|
||||
public ActionLookupException(String message, String actionKey, String lookupTarget, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.actionKey = actionKey;
|
||||
this.lookupTarget = lookupTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExceptionReport toReport() {
|
||||
ExceptionReport report = super.toReport();
|
||||
report.getExtra().put("actionKey", actionKey);
|
||||
report.getExtra().put("lookupTarget", lookupTarget);
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package work.slhaf.partner.core.action.exception;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
|
||||
|
||||
public class ActionSerializationException extends AgentRuntimeException {
|
||||
|
||||
private final String actionName;
|
||||
private final String baseDir;
|
||||
private final String fileExt;
|
||||
private final String stage;
|
||||
|
||||
public ActionSerializationException(
|
||||
String message,
|
||||
@Nullable String actionName,
|
||||
@Nullable String baseDir,
|
||||
@Nullable String fileExt,
|
||||
String stage
|
||||
) {
|
||||
super(message);
|
||||
this.actionName = actionName;
|
||||
this.baseDir = baseDir;
|
||||
this.fileExt = fileExt;
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
public ActionSerializationException(
|
||||
String message,
|
||||
@Nullable String actionName,
|
||||
@Nullable String baseDir,
|
||||
@Nullable String fileExt,
|
||||
String stage,
|
||||
Throwable cause
|
||||
) {
|
||||
super(message, cause);
|
||||
this.actionName = actionName;
|
||||
this.baseDir = baseDir;
|
||||
this.fileExt = fileExt;
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExceptionReport toReport() {
|
||||
ExceptionReport report = super.toReport();
|
||||
report.getExtra().put("stage", stage);
|
||||
if (actionName != null) {
|
||||
report.getExtra().put("actionName", actionName);
|
||||
}
|
||||
if (baseDir != null) {
|
||||
report.getExtra().put("baseDir", baseDir);
|
||||
}
|
||||
if (fileExt != null) {
|
||||
report.getExtra().put("fileExt", fileExt);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package work.slhaf.partner.core.action.runner;
|
||||
|
||||
import cn.hutool.system.OsInfo;
|
||||
import cn.hutool.system.SystemUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
|
||||
import work.slhaf.partner.core.action.runner.execution.McpActionExecutor;
|
||||
import work.slhaf.partner.core.action.runner.execution.OriginExecutionService;
|
||||
import work.slhaf.partner.core.action.runner.mcp.*;
|
||||
import work.slhaf.partner.core.action.runner.policy.BwrapPolicyProvider;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||
import work.slhaf.partner.core.action.runner.support.ActionSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
public class LocalRunnerClient extends RunnerClient {
|
||||
|
||||
public static final String MCP_NAME_DESC = "mcp-desc";
|
||||
public static final String MCP_NAME_DYNAMIC = "mcp-dynamic";
|
||||
|
||||
private final String tmpActionPath;
|
||||
private final String dynamicActionPath;
|
||||
private final String mcpServerPath;
|
||||
private final String mcpDescPath;
|
||||
|
||||
private final McpClientRegistry mcpClientRegistry;
|
||||
private final McpTransportFactory mcpTransportFactory;
|
||||
private final ActionSerializer actionSerializer;
|
||||
private final OriginExecutionService originExecutionService;
|
||||
private final McpActionExecutor mcpActionExecutor;
|
||||
private final McpMetaRegistry mcpMetaRegistry;
|
||||
private final McpDescWatcher mcpDescWatcher;
|
||||
private final DynamicActionMcpManager dynamicActionMcpManager;
|
||||
private final McpConfigWatcher mcpConfigWatcher;
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
|
||||
public LocalRunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @Nullable String baseActionPath) {
|
||||
super(existedMetaActions, executor, baseActionPath);
|
||||
this.tmpActionPath = buildPathStr(ACTION_PATH, "tmp");
|
||||
this.dynamicActionPath = buildPathStr(ACTION_PATH, "dynamic");
|
||||
this.mcpServerPath = buildPathStr(ACTION_PATH, "mcp");
|
||||
this.mcpDescPath = buildPathStr(mcpServerPath, "desc");
|
||||
|
||||
createPath(tmpActionPath);
|
||||
createPath(dynamicActionPath);
|
||||
createPath(mcpServerPath);
|
||||
createPath(mcpDescPath);
|
||||
|
||||
McpClientRegistry clientRegistry = new McpClientRegistry();
|
||||
McpTransportFactory transportFactory = new McpTransportFactory();
|
||||
ActionSerializer serializer = new ActionSerializer(tmpActionPath, dynamicActionPath);
|
||||
OriginExecutionService originService = new OriginExecutionService();
|
||||
McpActionExecutor actionExecutor = new McpActionExecutor(clientRegistry);
|
||||
|
||||
McpMetaRegistry metaRegistry = null;
|
||||
McpDescWatcher descWatcher = null;
|
||||
DynamicActionMcpManager dynamicManager = null;
|
||||
McpConfigWatcher configWatcher = null;
|
||||
|
||||
try {
|
||||
|
||||
registerPolicyProviders();
|
||||
|
||||
metaRegistry = new McpMetaRegistry(existedMetaActions);
|
||||
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DESC, metaRegistry.clientConfig(MCP_NAME_DESC, 10));
|
||||
log.info("DescMcp 注册完毕");
|
||||
|
||||
descWatcher = new McpDescWatcher(Path.of(mcpDescPath), metaRegistry, executor);
|
||||
descWatcher.start();
|
||||
|
||||
dynamicManager = new DynamicActionMcpManager(
|
||||
Path.of(dynamicActionPath),
|
||||
existedMetaActions,
|
||||
executor
|
||||
);
|
||||
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DYNAMIC, dynamicManager.clientConfig(10));
|
||||
log.info("DynamicActionMcp 注册完毕");
|
||||
dynamicManager.start();
|
||||
|
||||
configWatcher = new McpConfigWatcher(
|
||||
Path.of(mcpServerPath),
|
||||
existedMetaActions,
|
||||
clientRegistry,
|
||||
transportFactory,
|
||||
metaRegistry,
|
||||
executor
|
||||
);
|
||||
configWatcher.start();
|
||||
configWatcher.registerPolicyListener();
|
||||
} catch (ActionInfrastructureStartupException e) {
|
||||
closeQuietly(configWatcher);
|
||||
closeQuietly(dynamicManager);
|
||||
closeQuietly(descWatcher);
|
||||
closeQuietly(metaRegistry);
|
||||
closeQuietly(clientRegistry);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
closeQuietly(configWatcher);
|
||||
closeQuietly(dynamicManager);
|
||||
closeQuietly(descWatcher);
|
||||
closeQuietly(metaRegistry);
|
||||
closeQuietly(clientRegistry);
|
||||
throw new ActionInfrastructureStartupException(
|
||||
"LocalRunnerClient initialization failed",
|
||||
"local-runner-client",
|
||||
ACTION_PATH,
|
||||
null,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
this.mcpClientRegistry = clientRegistry;
|
||||
this.mcpTransportFactory = transportFactory;
|
||||
this.actionSerializer = serializer;
|
||||
this.originExecutionService = originService;
|
||||
this.mcpActionExecutor = actionExecutor;
|
||||
this.mcpMetaRegistry = metaRegistry;
|
||||
this.mcpDescWatcher = descWatcher;
|
||||
this.dynamicActionMcpManager = dynamicManager;
|
||||
this.mcpConfigWatcher = configWatcher;
|
||||
}
|
||||
|
||||
private void registerPolicyProviders() {
|
||||
OsInfo os = SystemUtil.getOsInfo();
|
||||
if (os.isLinux()) {
|
||||
ExecutionPolicyRegistry.INSTANCE.registerPolicyProvider(BwrapPolicyProvider.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RunnerResponse doRun(MetaAction metaAction) {
|
||||
RunnerResponse response;
|
||||
response = switch (metaAction.getType()) {
|
||||
case MCP -> mcpActionExecutor.run(metaAction);
|
||||
case ORIGIN -> originExecutionService.run(metaAction);
|
||||
case BUILTIN -> doRunWithBuiltin(metaAction);
|
||||
};
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildTmpPath(String actionKey, String codeType) {
|
||||
return actionSerializer.buildTmpPath(actionKey, codeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
|
||||
actionSerializer.tmpSerialize(tempAction, code, codeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
|
||||
actionSerializer.persistSerialize(metaActionInfo, fileMetaData);
|
||||
}
|
||||
|
||||
private void registerMcpClient(McpClientRegistry clientRegistry, McpTransportFactory transportFactory, String id, McpTransportConfig transportConfig) {
|
||||
val client = io.modelcontextprotocol.client.McpClient.sync(transportFactory.create(transportConfig))
|
||||
.requestTimeout(java.time.Duration.ofSeconds(transportConfig.timeout()))
|
||||
.clientInfo(new io.modelcontextprotocol.spec.McpSchema.Implementation(id, "PARTNER"))
|
||||
.build();
|
||||
clientRegistry.register(id, client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
mcpConfigWatcher.unregisterPolicyListener();
|
||||
closeQuietly(mcpConfigWatcher);
|
||||
closeQuietly(dynamicActionMcpManager);
|
||||
closeQuietly(mcpDescWatcher);
|
||||
closeQuietly(mcpMetaRegistry);
|
||||
closeQuietly(mcpClientRegistry);
|
||||
}
|
||||
|
||||
private void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public String buildPathStr(String... path) {
|
||||
StringBuilder str = new StringBuilder();
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
str.append(path[i]);
|
||||
if (i < path.length - 1) {
|
||||
str.append("/");
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package work.slhaf.partner.core.action.runner;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction.Result;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
|
||||
import work.slhaf.partner.module.action.builtin.BuiltinActionRegistry;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* 执行客户端抽象类
|
||||
* <br/>
|
||||
* 只负责暴露序列化、执行等相应接口,具体逻辑交给下游实现
|
||||
* <br/>
|
||||
* 默认存在两类实现,{@link LocalRunnerClient} 和 {@link SandboxRunnerClient}
|
||||
* <ol>
|
||||
* LocalRunnerClient:
|
||||
* <li>
|
||||
* 对应本地运行环境,可在本地启动 MCP 客户端将 RunnerClient 暴露的能力接口转发至本地 MCP Client 并执行
|
||||
* </li>
|
||||
* SandboxRunnerClient:
|
||||
* <li>
|
||||
* 对应沙盒运行环境,该 Client 仅作为沙盒环境的客户端,不持有额外能力,仅保持远端连接已存在行动的内容更新
|
||||
* </li>
|
||||
* </ol>
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class RunnerClient implements AutoCloseable {
|
||||
|
||||
protected final String ACTION_PATH;
|
||||
|
||||
protected final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||
protected final ExecutorService executor;
|
||||
@Setter
|
||||
protected BuiltinActionRegistry builtinActionRegistry;
|
||||
|
||||
/**
|
||||
* ActionCore 将注入虚拟线程池
|
||||
*/
|
||||
public RunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @NotNull String baseActionPath) {
|
||||
this.existedMetaActions = existedMetaActions;
|
||||
this.executor = executor;
|
||||
this.ACTION_PATH = baseActionPath;
|
||||
|
||||
createPath(ACTION_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行行动程序
|
||||
*/
|
||||
public void submit(MetaAction metaAction) {
|
||||
log.debug("执行行动: {}", metaAction);
|
||||
// 获取已存在行动列表
|
||||
Result result = metaAction.getResult();
|
||||
if (!result.getStatus().equals(Result.Status.WAITING)) {
|
||||
return;
|
||||
}
|
||||
RunnerResponse response = work.slhaf.partner.framework.agent.support.Result.runCatching(() -> doRun(metaAction)).fold(
|
||||
runnerResponse -> runnerResponse,
|
||||
ex -> {
|
||||
RunnerResponse r = new RunnerResponse();
|
||||
r.setOk(false);
|
||||
r.setData(ex.getLocalizedMessage());
|
||||
return r;
|
||||
}
|
||||
);
|
||||
result.setData(response.getData());
|
||||
result.setStatus(response.isOk() ? Result.Status.SUCCESS : Result.Status.FAILED);
|
||||
log.debug("行动执行结果: {}", response);
|
||||
}
|
||||
|
||||
protected abstract RunnerResponse doRun(MetaAction metaAction);
|
||||
|
||||
public abstract String buildTmpPath(String actionKey, String codeType);
|
||||
|
||||
public abstract void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException;
|
||||
|
||||
public abstract void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData);
|
||||
|
||||
protected RunnerResponse doRunWithBuiltin(MetaAction metaAction) {
|
||||
RunnerResponse response = new RunnerResponse();
|
||||
if (builtinActionRegistry == null) {
|
||||
response.setOk(false);
|
||||
response.setData("BuiltinActionRegistry 未初始化");
|
||||
return response;
|
||||
}
|
||||
response.setData(builtinActionRegistry.call(metaAction.getKey(), metaAction.getParams()));
|
||||
response.setOk(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
protected void createPath(String pathStr) {
|
||||
val path = Path.of(pathStr);
|
||||
try {
|
||||
Files.createDirectory(path);
|
||||
} catch (IOException e) {
|
||||
if (!Files.exists(path)) {
|
||||
throw new ActionInfrastructureStartupException(
|
||||
"Failed to create action directory: " + pathStr,
|
||||
"runner-client",
|
||||
pathStr,
|
||||
null,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class RunnerResponse {
|
||||
private boolean ok;
|
||||
private String data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package work.slhaf.partner.core.action.runner;
|
||||
|
||||
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* 基于 Http 与 WebSocket 的沙盒执行器客户端,负责:
|
||||
* <ul>
|
||||
* <li>
|
||||
* 发送行动单元数据
|
||||
* </li>
|
||||
* <li>
|
||||
* 实时更新获取已存在行动列表
|
||||
* </li>
|
||||
* <li>
|
||||
* 向传入的 MetaAction 回写执行结果
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class SandboxRunnerClient extends RunnerClient {
|
||||
|
||||
public SandboxRunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor) { // 连接沙盒执行器(websocket)
|
||||
super(existedMetaActions, executor, null);
|
||||
}
|
||||
|
||||
protected RunnerResponse doRun(MetaAction metaAction) {
|
||||
return switch (metaAction.getType()) {
|
||||
case BUILTIN -> doRunWithBuiltin(metaAction);
|
||||
case MCP, ORIGIN -> {
|
||||
// 调用沙盒执行器
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildTmpPath(String actionKey, String codeType) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'buildTmpPath'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'tmpSerialize'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'persistSerialize'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package work.slhaf.partner.core.action.runner.execution;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class CommandExecutionService {
|
||||
|
||||
public static final CommandExecutionService INSTANCE = new CommandExecutionService();
|
||||
|
||||
private CommandExecutionService() {
|
||||
}
|
||||
|
||||
public String[] buildFileExecutionCommands(String launcher, Map<String, Object> params, String absolutePath) {
|
||||
int paramSize = params == null ? 0 : params.size();
|
||||
String[] commands = new String[paramSize + 2];
|
||||
commands[0] = launcher;
|
||||
commands[1] = absolutePath;
|
||||
AtomicInteger paramCount = new AtomicInteger(2);
|
||||
if (params != null) {
|
||||
params.forEach((param, value) -> commands[paramCount.getAndIncrement()] = "--" + param + "=" + value);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
public Result exec(List<String> commands) {
|
||||
return exec(commands.toArray(new String[0]));
|
||||
}
|
||||
|
||||
public Result exec(WrappedLaunchSpec launchSpec) {
|
||||
Result result = new Result();
|
||||
List<String> output = Collections.synchronizedList(new ArrayList<>());
|
||||
List<String> error = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
try {
|
||||
Process process = startProcess(launchSpec);
|
||||
|
||||
Thread stdoutThread = Thread.startVirtualThread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.add(line);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
|
||||
Thread stderrThread = Thread.startVirtualThread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
error.add(line);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
stdoutThread.join();
|
||||
stderrThread.join();
|
||||
|
||||
result.setOk(exitCode == 0);
|
||||
List<String> stdoutLines = List.copyOf(output);
|
||||
List<String> stderrLines = List.copyOf(error);
|
||||
result.setStdoutLines(stdoutLines);
|
||||
result.setStderrLines(stderrLines);
|
||||
result.setResultList(stdoutLines.isEmpty() ? stderrLines : stdoutLines);
|
||||
result.setTotal(buildDisplayText(stdoutLines, stderrLines));
|
||||
} catch (Exception e) {
|
||||
result.setOk(false);
|
||||
result.setTotal(e.getMessage());
|
||||
result.setStdoutLines(List.of());
|
||||
result.setStderrLines(List.of(e.getMessage()));
|
||||
result.setResultList(result.getStderrLines());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Result exec(String... commands) {
|
||||
return exec(defaultLaunchSpec(commands));
|
||||
}
|
||||
|
||||
public CommandSession createSessionTask(List<String> commands) {
|
||||
return createSessionTask(commands.toArray(new String[0]));
|
||||
}
|
||||
|
||||
public CommandSession createSessionTask(WrappedLaunchSpec launchSpec) {
|
||||
try {
|
||||
Process process = startProcess(launchSpec);
|
||||
CommandSession session = new CommandSession();
|
||||
StringBuilder stdoutBuffer = new StringBuilder();
|
||||
StringBuilder stderrBuffer = new StringBuilder();
|
||||
session.setProcess(process);
|
||||
session.setStdoutBuffer(stdoutBuffer);
|
||||
session.setStderrBuffer(stderrBuffer);
|
||||
|
||||
Thread.startVirtualThread(() -> readToBuffer(process.getInputStream(), stdoutBuffer));
|
||||
Thread.startVirtualThread(() -> readToBuffer(process.getErrorStream(), stderrBuffer));
|
||||
|
||||
return session;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("创建命令会话失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public CommandSession createSessionTask(String... commands) {
|
||||
return createSessionTask(defaultLaunchSpec(commands));
|
||||
}
|
||||
|
||||
private void readToBuffer(java.io.InputStream inputStream, StringBuilder buffer) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
synchronized (buffer) {
|
||||
if (!buffer.isEmpty()) {
|
||||
buffer.append('\n');
|
||||
}
|
||||
buffer.append(line);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private Process startProcess(WrappedLaunchSpec launchSpec) throws Exception {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(launchSpec.getCommand());
|
||||
command.addAll(launchSpec.getArgs());
|
||||
processBuilder.command(command);
|
||||
processBuilder.redirectErrorStream(false);
|
||||
if (launchSpec.getWorkingDirectory() != null && !launchSpec.getWorkingDirectory().isBlank()) {
|
||||
processBuilder.directory(new File(launchSpec.getWorkingDirectory()));
|
||||
}
|
||||
Map<String, String> environment = processBuilder.environment();
|
||||
environment.clear();
|
||||
environment.putAll(launchSpec.getEnvironment());
|
||||
return processBuilder.start();
|
||||
}
|
||||
|
||||
private WrappedLaunchSpec defaultLaunchSpec(String... commands) {
|
||||
return new WrappedLaunchSpec(
|
||||
commands[0],
|
||||
List.of(commands).subList(1, commands.length),
|
||||
null,
|
||||
System.getenv()
|
||||
);
|
||||
}
|
||||
|
||||
private String buildDisplayText(List<String> stdoutLines, List<String> stderrLines) {
|
||||
if (stdoutLines.isEmpty()) {
|
||||
return String.join("\n", stderrLines);
|
||||
}
|
||||
if (stderrLines.isEmpty()) {
|
||||
return String.join("\n", stdoutLines);
|
||||
}
|
||||
return String.join("\n", stdoutLines) + "\n" + String.join("\n", stderrLines);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Result {
|
||||
private boolean ok;
|
||||
private String total;
|
||||
private List<String> resultList;
|
||||
private List<String> stdoutLines;
|
||||
private List<String> stderrLines;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CommandSession {
|
||||
private Process process;
|
||||
private StringBuilder stdoutBuffer;
|
||||
private StringBuilder stderrBuffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package work.slhaf.partner.core.action.runner.execution;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class McpActionExecutor {
|
||||
|
||||
private final McpClientRegistry mcpClientRegistry;
|
||||
|
||||
public McpActionExecutor(McpClientRegistry mcpClientRegistry) {
|
||||
this.mcpClientRegistry = mcpClientRegistry;
|
||||
}
|
||||
|
||||
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
|
||||
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
|
||||
McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation());
|
||||
if (mcpClient == null) {
|
||||
response.setOk(false);
|
||||
response.setData("MCP client not found: " + metaAction.getLocation());
|
||||
return response;
|
||||
}
|
||||
McpSchema.CallToolRequest callToolRequest = McpSchema.CallToolRequest.builder()
|
||||
.name(metaAction.getName())
|
||||
.arguments(metaAction.getParams())
|
||||
.build();
|
||||
McpSchema.CallToolResult callToolResult;
|
||||
try {
|
||||
callToolResult = mcpClient.callTool(callToolRequest);
|
||||
} catch (Exception e) {
|
||||
response.setOk(false);
|
||||
response.setData("MCP tool call failed: " + e.getMessage());
|
||||
return response;
|
||||
}
|
||||
Boolean error = callToolResult.isError();
|
||||
response.setOk(error == null || !error);
|
||||
response.setData(extractResponseData(callToolResult));
|
||||
return response;
|
||||
}
|
||||
|
||||
private String extractResponseData(McpSchema.CallToolResult callToolResult) {
|
||||
Object structuredContent = callToolResult.structuredContent();
|
||||
if (structuredContent != null) {
|
||||
return String.valueOf(structuredContent);
|
||||
}
|
||||
|
||||
List<McpSchema.Content> contents = callToolResult.content();
|
||||
if (contents != null && !contents.isEmpty()) {
|
||||
String contentSummary = contents.stream()
|
||||
.map(this::renderContent)
|
||||
.filter(text -> text != null && !text.isBlank())
|
||||
.collect(Collectors.joining("\n"));
|
||||
if (!contentSummary.isBlank()) {
|
||||
return contentSummary;
|
||||
}
|
||||
}
|
||||
|
||||
return callToolResult.toString();
|
||||
}
|
||||
|
||||
private String renderContent(McpSchema.Content content) {
|
||||
if (content instanceof McpSchema.TextContent textContent) {
|
||||
return textContent.text();
|
||||
}
|
||||
return String.valueOf(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package work.slhaf.partner.core.action.runner.execution;
|
||||
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.ORIGIN_LOCATION;
|
||||
|
||||
public class OriginExecutionService {
|
||||
|
||||
public OriginExecutionService() {
|
||||
}
|
||||
|
||||
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
|
||||
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
|
||||
File file = new File(resolveOriginPath(metaAction));
|
||||
String[] commands = CommandExecutionService.INSTANCE.buildFileExecutionCommands(metaAction.getLauncher(), metaAction.getParams(), file.getAbsolutePath());
|
||||
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(Arrays.stream(commands).toList());
|
||||
CommandExecutionService.Result execResult = CommandExecutionService.INSTANCE.exec(wrapped);
|
||||
response.setOk(execResult.isOk());
|
||||
response.setData(execResult.getTotal());
|
||||
return response;
|
||||
}
|
||||
|
||||
private String resolveOriginPath(MetaAction metaAction) {
|
||||
if (ORIGIN_LOCATION.equals(metaAction.getLocation())) {
|
||||
return metaAction.getName();
|
||||
}
|
||||
return metaAction.getLocation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
|
||||
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
|
||||
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Slf4j
|
||||
public class DynamicActionMcpManager implements AutoCloseable {
|
||||
|
||||
private final Path root;
|
||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||
private final CommandExecutionService commandExecutionService;
|
||||
private final McpStatelessAsyncServer server;
|
||||
private final InProcessMcpTransport clientTransport;
|
||||
private final DirectoryWatchSupport watchSupport;
|
||||
|
||||
public DynamicActionMcpManager(Path root,
|
||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
||||
ExecutorService executor) throws IOException {
|
||||
this.root = root;
|
||||
this.existedMetaActions = existedMetaActions;
|
||||
this.commandExecutionService = CommandExecutionService.INSTANCE;
|
||||
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
|
||||
this.clientTransport = pair.clientSide();
|
||||
McpSchema.ServerCapabilities serverCapabilities = McpSchema.ServerCapabilities.builder()
|
||||
.tools(true)
|
||||
.build();
|
||||
this.server = McpServer.async(pair.serverSide())
|
||||
.capabilities(serverCapabilities)
|
||||
.jsonMapper(McpJsonMapper.getDefault())
|
||||
.build();
|
||||
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 1, this::loadExisting)
|
||||
.onCreate(this::handleCreate)
|
||||
.onModify(this::handleModify)
|
||||
.onDelete(this::handleDelete)
|
||||
.onOverflow((thisDir, context) -> reconcile());
|
||||
}
|
||||
|
||||
public McpTransportConfig.InProcess clientConfig(int timeout) {
|
||||
return new McpTransportConfig.InProcess(timeout, clientTransport);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
watchSupport.start();
|
||||
log.info("DynamicActionMcp 文件监听注册完毕");
|
||||
}
|
||||
|
||||
private void loadExisting() {
|
||||
File file = root.toFile();
|
||||
if (file.isFile()) {
|
||||
throw new ActionInfrastructureStartupException(
|
||||
"Expected a directory but found a file: " + root,
|
||||
"dynamic-action-mcp-manager",
|
||||
root.toString(),
|
||||
null
|
||||
);
|
||||
}
|
||||
File[] files = file.listFiles();
|
||||
if (files == null) {
|
||||
throw new ActionInfrastructureStartupException(
|
||||
"Failed to read action directory: " + root,
|
||||
"dynamic-action-mcp-manager",
|
||||
root.toString(),
|
||||
null
|
||||
);
|
||||
}
|
||||
for (File dir : files) {
|
||||
if (!normalPath(dir.toPath())) {
|
||||
continue;
|
||||
}
|
||||
addAction(dir.getName(), dir.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTmp(Path context) {
|
||||
return context.getFileName().endsWith(".tmp");
|
||||
}
|
||||
|
||||
private void handleModify(Path thisDir, Path context) {
|
||||
if (context == null || isTmp(context) || !normalPath(thisDir)) {
|
||||
return;
|
||||
}
|
||||
modify(thisDir, context);
|
||||
}
|
||||
|
||||
private void handleCreate(Path thisDir, Path context) {
|
||||
if (context == null || isTmp(context)) {
|
||||
return;
|
||||
}
|
||||
if (thisDir.equals(root) && Files.isDirectory(context)) {
|
||||
try {
|
||||
watchSupport.registerDirectory(context);
|
||||
} catch (IOException e) {
|
||||
log.error("监听目录注册失败: {}", context);
|
||||
}
|
||||
}
|
||||
if (normalPath(thisDir)) {
|
||||
modify(thisDir, context);
|
||||
}
|
||||
if (Files.isDirectory(context) && normalPath(context)) {
|
||||
File[] files = context.toFile().listFiles();
|
||||
if (files == null) {
|
||||
log.warn("目录无法访问: {}", context);
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
modify(context, file.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDelete(Path thisDir, Path context) {
|
||||
if (context == null || isTmp(context)) {
|
||||
return;
|
||||
}
|
||||
if (thisDir.equals(root)) {
|
||||
String name = context.getFileName().toString();
|
||||
Path candidate = root.resolve(name);
|
||||
if (Files.isDirectory(candidate)) {
|
||||
return;
|
||||
}
|
||||
removeAction(name);
|
||||
AtomicReference<java.nio.file.WatchKey> toRemove = new AtomicReference<>();
|
||||
watchSupport.context().watchKeys().forEach((key, path) -> {
|
||||
if (path.getFileName().toString().equals(name)) {
|
||||
key.cancel();
|
||||
toRemove.set(key);
|
||||
}
|
||||
});
|
||||
if (toRemove.get() != null) {
|
||||
watchSupport.context().watchKeys().remove(toRemove.get());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!thisDir.equals(root) && !normalPath(thisDir)) {
|
||||
removeAction(thisDir.getFileName().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void reconcile() {
|
||||
Set<String> existed = existedMetaActions.keySet().stream()
|
||||
.filter(actionKey -> actionKey.startsWith("local::"))
|
||||
.map(actionKey -> actionKey.split("::")[1])
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> currentDirs = new HashSet<>();
|
||||
try (Stream<Path> stream = Files.list(root).filter(Files::isDirectory)) {
|
||||
stream.forEach(path -> {
|
||||
String name = path.getFileName().toString();
|
||||
currentDirs.add(name);
|
||||
boolean contains = existed.contains(name);
|
||||
boolean normal = normalPath(path);
|
||||
if (contains && !normal) {
|
||||
removeAction(name);
|
||||
}
|
||||
if (!contains) {
|
||||
boolean alreadyWatching = watchSupport.isWatching(path);
|
||||
if (!alreadyWatching) {
|
||||
try {
|
||||
watchSupport.registerDirectory(path);
|
||||
} catch (IOException e) {
|
||||
log.error("监听目录注册失败: {}", path);
|
||||
}
|
||||
}
|
||||
if (normal) {
|
||||
addAction(name, path);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.error("目录无法读取: {}", root);
|
||||
return;
|
||||
}
|
||||
for (String existedName : existed) {
|
||||
if (!currentDirs.contains(existedName)) {
|
||||
removeAction(existedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void modify(Path thisDir, Path context) {
|
||||
String fileName = context.getFileName().toString();
|
||||
if (fileName.equals("desc.json")) {
|
||||
handleMetaModify(thisDir);
|
||||
}
|
||||
if (fileName.startsWith("run.")) {
|
||||
handleProgramModify(thisDir);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleProgramModify(Path thisDir) {
|
||||
String name = thisDir.getFileName().toString();
|
||||
String actionKey = "local::" + name;
|
||||
if (existedMetaActions.containsKey(actionKey)) {
|
||||
return;
|
||||
}
|
||||
if (!addAction(name, thisDir)) {
|
||||
removeAction(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMetaModify(Path thisDir) {
|
||||
String name = thisDir.getFileName().toString();
|
||||
if (!addAction(name, thisDir)) {
|
||||
removeAction(name);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean addAction(String name, Path dir) {
|
||||
File program = null;
|
||||
try (Stream<Path> stream = Files.list(dir)) {
|
||||
for (Path path : stream.toList()) {
|
||||
if (isTmp(path)) {
|
||||
continue;
|
||||
}
|
||||
if (path.getFileName().toString().startsWith("run.")) {
|
||||
program = path.toFile();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("添加 action 失败", e);
|
||||
return false;
|
||||
}
|
||||
MetaActionInfo info;
|
||||
try {
|
||||
info = JSON.parseObject(Files.readString(dir.resolve("desc.json"), StandardCharsets.UTF_8), MetaActionInfo.class);
|
||||
} catch (Exception e) {
|
||||
log.error("desc.json 加载失败: {}", dir);
|
||||
return false;
|
||||
}
|
||||
String actionKey = "local::" + name;
|
||||
existedMetaActions.put(actionKey, info);
|
||||
server.addTool(buildAsyncToolSpecification(info, program, actionKey, name)).subscribe();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void removeAction(String name) {
|
||||
existedMetaActions.remove("local::" + name);
|
||||
server.removeTool(name).subscribe();
|
||||
}
|
||||
|
||||
private boolean normalPath(Path path) {
|
||||
File[] files = loadFiles(path);
|
||||
if (files == null || files.length < 2) {
|
||||
return false;
|
||||
}
|
||||
boolean desc = false;
|
||||
int run = 0;
|
||||
for (File file : files) {
|
||||
String fileName = file.getName();
|
||||
if (fileName.endsWith(".tmp")) {
|
||||
continue;
|
||||
}
|
||||
if (fileName.equals("desc.json")) {
|
||||
desc = true;
|
||||
}
|
||||
if (fileName.startsWith("run.")) {
|
||||
run++;
|
||||
}
|
||||
}
|
||||
return run == 1 && desc;
|
||||
}
|
||||
|
||||
private File[] loadFiles(Path path) {
|
||||
if (!Files.isDirectory(path)) {
|
||||
return null;
|
||||
}
|
||||
return path.toFile().listFiles();
|
||||
}
|
||||
|
||||
private McpStatelessServerFeatures.AsyncToolSpecification buildAsyncToolSpecification(MetaActionInfo info, File program, String actionKey, String name) {
|
||||
Map<String, Object> additional = Map.of(
|
||||
"pre", info.getPreActions(),
|
||||
"post", info.getPostActions(),
|
||||
"strict_pre", info.getStrictDependencies(),
|
||||
"io", info.getIo()
|
||||
);
|
||||
McpSchema.Tool tool = McpSchema.Tool.builder()
|
||||
.name(name)
|
||||
.description(info.getDescription())
|
||||
.inputSchema(McpJsonMapper.getDefault(), JSONObject.toJSONString(info.getParams()))
|
||||
.outputSchema(info.getResponseSchema())
|
||||
.title(actionKey)
|
||||
.meta(additional)
|
||||
.build();
|
||||
return McpStatelessServerFeatures.AsyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(buildToolHandler(program, info.getLauncher()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private BiFunction<McpTransportContext, McpSchema.CallToolRequest, Mono<McpSchema.CallToolResult>> buildToolHandler(File program, String launcher) {
|
||||
return (mcpTransportContext, callToolRequest) -> {
|
||||
Map<String, Object> arguments = callToolRequest.arguments();
|
||||
if (arguments == null) {
|
||||
arguments = Map.of();
|
||||
}
|
||||
String[] commands = commandExecutionService.buildFileExecutionCommands(launcher, arguments, program.getAbsolutePath());
|
||||
if (commands == null) {
|
||||
return Mono.just(McpSchema.CallToolResult.builder()
|
||||
.addTextContent("未知文件类型: " + program.getName())
|
||||
.isError(true)
|
||||
.build());
|
||||
}
|
||||
return Mono.fromCallable(() -> {
|
||||
CommandExecutionService.Result execResult = commandExecutionService.exec(
|
||||
ExecutionPolicyRegistry.INSTANCE.prepare(List.of(commands))
|
||||
);
|
||||
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder()
|
||||
.isError(!execResult.isOk());
|
||||
List<String> resultList = execResult.getResultList();
|
||||
if (resultList != null && !resultList.isEmpty()) {
|
||||
builder.textContent(resultList);
|
||||
builder.structuredContent(resultList);
|
||||
} else {
|
||||
builder.addTextContent(execResult.getTotal());
|
||||
builder.structuredContent(execResult.getTotal());
|
||||
}
|
||||
return builder.build();
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
watchSupport.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class McpClientRegistry implements AutoCloseable {
|
||||
|
||||
private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>();
|
||||
|
||||
public McpSyncClient get(String serverName) {
|
||||
return clients.get(serverName);
|
||||
}
|
||||
|
||||
public void register(String serverName, McpSyncClient client) {
|
||||
McpSyncClient old = clients.put(serverName, client);
|
||||
if (old != null && old != client) {
|
||||
old.close();
|
||||
}
|
||||
}
|
||||
|
||||
public McpSyncClient remove(String serverName) {
|
||||
McpSyncClient client = detach(serverName);
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
public McpSyncClient detach(String serverName) {
|
||||
return clients.remove(serverName);
|
||||
}
|
||||
|
||||
public boolean contains(String serverName) {
|
||||
return clients.containsKey(serverName);
|
||||
}
|
||||
|
||||
public Set<String> listIds() {
|
||||
return new HashSet<>(clients.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
clients.forEach((id, client) -> client.close());
|
||||
clients.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
||||
import work.slhaf.partner.core.action.runner.policy.RunnerExecutionPolicyListener;
|
||||
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Slf4j
|
||||
public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyListener {
|
||||
|
||||
private final Path root;
|
||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||
private final McpClientRegistry mcpClientRegistry;
|
||||
private final McpTransportFactory mcpTransportFactory;
|
||||
private final McpMetaRegistry mcpMetaRegistry;
|
||||
private final DirectoryWatchSupport watchSupport;
|
||||
private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>();
|
||||
|
||||
public McpConfigWatcher(Path root,
|
||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
||||
McpClientRegistry mcpClientRegistry,
|
||||
McpTransportFactory mcpTransportFactory,
|
||||
McpMetaRegistry mcpMetaRegistry,
|
||||
ExecutorService executor) throws IOException {
|
||||
this.root = root;
|
||||
this.existedMetaActions = existedMetaActions;
|
||||
this.mcpClientRegistry = mcpClientRegistry;
|
||||
this.mcpTransportFactory = mcpTransportFactory;
|
||||
this.mcpMetaRegistry = mcpMetaRegistry;
|
||||
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, this::loadInitial)
|
||||
.onCreate(this::handleCreate)
|
||||
.onModify((thisDir, context) -> checkAndReload(true))
|
||||
.onDelete(this::handleDelete)
|
||||
.onOverflow((thisDir, context) -> checkAndReload(false));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
watchSupport.start();
|
||||
log.info("CommonMcp 文件监听注册完毕");
|
||||
}
|
||||
|
||||
private void loadInitial() {
|
||||
File[] files = loadFiles(root);
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
if (!normalFile(file)) {
|
||||
continue;
|
||||
}
|
||||
loadAndRegisterMcpClientsFromFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCreate(Path thisDir, Path context) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
File file = context.toFile();
|
||||
if (!normalFile(file)) {
|
||||
return;
|
||||
}
|
||||
loadAndRegisterMcpClientsFromFile(file);
|
||||
}
|
||||
|
||||
private void handleDelete(Path thisDir, Path context) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
File file = context.toFile();
|
||||
if (!file.getName().endsWith(".json")) {
|
||||
return;
|
||||
}
|
||||
McpConfigFileRecord fileRecord = mcpConfigFileCache.remove(file);
|
||||
if (fileRecord == null) {
|
||||
return;
|
||||
}
|
||||
for (String clientId : fileRecord.paramsCacheMap().keySet()) {
|
||||
unregisterClient(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean normalFile(File file) {
|
||||
return file.exists() && file.isFile() && file.getName().endsWith(".json");
|
||||
}
|
||||
|
||||
private void registerMcpClient(String id, McpTransportConfig transportConfig) {
|
||||
McpSyncClient client = McpClient.sync(mcpTransportFactory.create(transportConfig))
|
||||
.requestTimeout(Duration.ofSeconds(transportConfig.timeout()))
|
||||
.clientInfo(new McpSchema.Implementation(id, "PARTNER"))
|
||||
.build();
|
||||
try {
|
||||
for (McpSchema.Tool tool : client.listTools().tools()) {
|
||||
existedMetaActions.put(id + "::" + tool.name(), mcpMetaRegistry.buildMetaActionInfo(id, tool));
|
||||
}
|
||||
mcpClientRegistry.register(id, client);
|
||||
} catch (Exception e) {
|
||||
log.warn("[{}] MCP client init failed, skipped (probably non-stdio-safe)", id, e);
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterClient(String clientId) {
|
||||
McpSyncClient client = mcpClientRegistry.detach(clientId);
|
||||
removeClientActions(clientId, client);
|
||||
if (client != null) {
|
||||
try {
|
||||
client.close();
|
||||
} catch (Exception e) {
|
||||
log.warn("[{}] MCP client close failed", clientId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeClientActions(String clientId, McpSyncClient client) {
|
||||
boolean removedByListing = false;
|
||||
if (client != null) {
|
||||
try {
|
||||
List<McpSchema.Tool> tools = client.listTools().tools();
|
||||
if (tools != null) {
|
||||
for (McpSchema.Tool tool : tools) {
|
||||
existedMetaActions.remove(clientId + "::" + tool.name());
|
||||
}
|
||||
removedByListing = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[{}] MCP client listTools failed during unregister, fallback to key scan", clientId, e);
|
||||
}
|
||||
}
|
||||
if (!removedByListing) {
|
||||
String prefix = clientId + "::";
|
||||
existedMetaActions.keySet().removeIf(actionKey -> actionKey.startsWith(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
private cn.hutool.json.JSONObject readJson(File file) {
|
||||
try {
|
||||
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private cn.hutool.json.JSONObject readMcp(cn.hutool.json.JSONObject json, String id) {
|
||||
try {
|
||||
return json.getJSONObject(id);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private McpTransportConfig readParams(cn.hutool.json.JSONObject mcp) {
|
||||
Set<String> keys = mcp.keySet();
|
||||
int timeout = mcp.getInt("timeout", 30);
|
||||
|
||||
if (matchesKeys(keys, Set.of("command", "args", "env"), Set.of("timeout"))) {
|
||||
String command = mcp.getStr("command");
|
||||
Map<String, String> env = mcp.getBean("env", Map.class);
|
||||
java.util.List<String> args = mcp.getBeanList("args", String.class);
|
||||
if (command == null || env == null || args == null) {
|
||||
return null;
|
||||
}
|
||||
return new McpTransportConfig.Stdio(timeout, command, env, args);
|
||||
}
|
||||
if (matchesKeys(keys, Set.of("uri", "endpoint", "headers"), Set.of("timeout"))) {
|
||||
String uri = mcp.getStr("uri");
|
||||
String endpoint = mcp.getStr("endpoint");
|
||||
Map<String, String> headers = mcp.getBean("headers", Map.class);
|
||||
if (uri == null || endpoint == null || headers == null) {
|
||||
return null;
|
||||
}
|
||||
return new McpTransportConfig.Http(timeout, uri, endpoint, headers);
|
||||
}
|
||||
if (matchesKeys(keys, Set.of("url"), Set.of("timeout"))) {
|
||||
String url = mcp.getStr("url");
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
return new McpTransportConfig.Http(timeout, url, "", Map.of());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchesKeys(Set<String> actualKeys, Set<String> requiredKeys, Set<String> optionalKeys) {
|
||||
if (!actualKeys.containsAll(requiredKeys)) {
|
||||
return false;
|
||||
}
|
||||
Set<String> allowedKeys = new HashSet<>(requiredKeys);
|
||||
allowedKeys.addAll(optionalKeys);
|
||||
return allowedKeys.containsAll(actualKeys);
|
||||
}
|
||||
|
||||
private void checkAndReload(boolean trustCache) {
|
||||
HashMap<String, McpTransportConfig> changedMap = new HashMap<>();
|
||||
HashSet<String> existingMcpIdSet = new HashSet<>();
|
||||
|
||||
File[] files = loadFiles(root);
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
if (!normalFile(file)) {
|
||||
continue;
|
||||
}
|
||||
McpConfigFileRecord fileRecord = mcpConfigFileCache.get(file);
|
||||
boolean fileRecordExists = fileRecord != null;
|
||||
if (fileRecordExists && !fileChanged(file, fileRecord) && trustCache) {
|
||||
existingMcpIdSet.addAll(fileRecord.paramsCacheMap().keySet());
|
||||
continue;
|
||||
}
|
||||
|
||||
cn.hutool.json.JSONObject mcpConfigJson = readJson(file);
|
||||
if (mcpConfigJson == null) {
|
||||
if (fileRecordExists) {
|
||||
existingMcpIdSet.addAll(fileRecord.paramsCacheMap().keySet());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
McpConfigFileRecord newFileRecord = new McpConfigFileRecord(file.lastModified(), file.length(), new HashMap<>());
|
||||
for (String id : mcpConfigJson.keySet()) {
|
||||
cn.hutool.json.JSONObject mcp = readMcp(mcpConfigJson, id);
|
||||
if (mcp == null) {
|
||||
continue;
|
||||
}
|
||||
McpTransportConfig params = readParams(mcp);
|
||||
if (params == null) {
|
||||
continue;
|
||||
}
|
||||
existingMcpIdSet.add(id);
|
||||
newFileRecord.paramsCacheMap().put(id, params);
|
||||
if (fileRecordExists) {
|
||||
McpTransportConfig paramsCache = fileRecord.paramsCacheMap().get(id);
|
||||
if (paramsCache != null && paramsCache.equals(params)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changedMap.put(id, params);
|
||||
}
|
||||
mcpConfigFileCache.put(file, newFileRecord);
|
||||
}
|
||||
updateMcpClients(changedMap, existingMcpIdSet);
|
||||
}
|
||||
|
||||
private void updateMcpClients(HashMap<String, McpTransportConfig> changedMap, HashSet<String> existingMcpIdSet) {
|
||||
changedMap.forEach((clientId, config) -> {
|
||||
unregisterClient(clientId);
|
||||
registerMcpClient(clientId, config);
|
||||
});
|
||||
for (String clientId : mcpClientRegistry.listIds()) {
|
||||
if (clientId.equals(LocalRunnerClient.MCP_NAME_DESC) || clientId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)) {
|
||||
continue;
|
||||
}
|
||||
if (!existingMcpIdSet.contains(clientId)) {
|
||||
unregisterClient(clientId);
|
||||
}
|
||||
}
|
||||
existedMetaActions.keySet().removeIf(actionKey -> {
|
||||
String serverId = actionKey.split("::")[0];
|
||||
return !serverId.equals("local")
|
||||
&& !serverId.equals(LocalRunnerClient.MCP_NAME_DESC)
|
||||
&& !serverId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)
|
||||
&& !existingMcpIdSet.contains(serverId);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean fileChanged(File file, McpConfigFileRecord fileRecord) {
|
||||
return fileRecord.lastModified() != file.lastModified() || fileRecord.length() != file.length();
|
||||
}
|
||||
|
||||
private void loadAndRegisterMcpClientsFromFile(File file) {
|
||||
cn.hutool.json.JSONObject mcpConfigJson = readJson(file);
|
||||
if (mcpConfigJson == null) {
|
||||
return;
|
||||
}
|
||||
McpConfigFileRecord newFileRecord = new McpConfigFileRecord(file.lastModified(), file.length());
|
||||
for (String id : mcpConfigJson.keySet()) {
|
||||
cn.hutool.json.JSONObject mcp = readMcp(mcpConfigJson, id);
|
||||
if (mcp == null) {
|
||||
continue;
|
||||
}
|
||||
McpTransportConfig params = readParams(mcp);
|
||||
if (params == null) {
|
||||
continue;
|
||||
}
|
||||
registerMcpClient(id, params);
|
||||
newFileRecord.paramsCacheMap().put(id, params);
|
||||
}
|
||||
mcpConfigFileCache.put(file, newFileRecord);
|
||||
}
|
||||
|
||||
private File[] loadFiles(Path path) {
|
||||
if (!path.toFile().isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
return path.toFile().listFiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
watchSupport.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPolicyChanged(@NotNull ExecutionPolicy policy) {
|
||||
checkAndReload(false);
|
||||
}
|
||||
|
||||
private record McpConfigFileRecord(long lastModified, long length, Map<String, McpTransportConfig> paramsCacheMap) {
|
||||
private McpConfigFileRecord(long lastModified, long length) {
|
||||
this(lastModified, length, new HashMap<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Slf4j
|
||||
public class McpDescWatcher implements AutoCloseable {
|
||||
|
||||
private final Path root;
|
||||
private final McpMetaRegistry mcpMetaRegistry;
|
||||
private final DirectoryWatchSupport watchSupport;
|
||||
|
||||
public McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
|
||||
this.root = root;
|
||||
this.mcpMetaRegistry = mcpMetaRegistry;
|
||||
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, () -> mcpMetaRegistry.loadDirectory(root))
|
||||
.onCreate(this::handleUpsert)
|
||||
.onModify(this::handleUpsert)
|
||||
.onDelete(this::handleDelete)
|
||||
.onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
watchSupport.start();
|
||||
log.info("DescMcp 文件监听注册完毕");
|
||||
}
|
||||
|
||||
private void handleUpsert(Path thisDir, Path context) {
|
||||
if (context == null || Files.isDirectory(context) || !mcpMetaRegistry.isValidDescFile(context.getFileName().toString())) {
|
||||
return;
|
||||
}
|
||||
if (!mcpMetaRegistry.addOrUpdate(context)) {
|
||||
mcpMetaRegistry.remove(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDelete(Path thisDir, Path context) {
|
||||
if (context == null || !mcpMetaRegistry.isValidDescFile(context.getFileName().toString())) {
|
||||
return;
|
||||
}
|
||||
mcpMetaRegistry.remove(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
watchSupport.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
|
||||
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import javassist.NotFoundException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Slf4j
|
||||
public class McpMetaRegistry implements AutoCloseable {
|
||||
|
||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||
private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, MetaActionInfo> descInfoCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, MetaActionInfo> originalInfoCache = new ConcurrentHashMap<>();
|
||||
private final McpStatelessAsyncServer descServer;
|
||||
private final InProcessMcpTransport clientTransport;
|
||||
|
||||
public McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) {
|
||||
this.existedMetaActions = existedMetaActions;
|
||||
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
|
||||
this.clientTransport = pair.clientSide();
|
||||
McpSchema.ServerCapabilities serverCapabilities = McpSchema.ServerCapabilities.builder()
|
||||
.resources(true, true)
|
||||
.build();
|
||||
this.descServer = McpServer.async(pair.serverSide())
|
||||
.capabilities(serverCapabilities)
|
||||
.jsonMapper(McpJsonMapper.getDefault())
|
||||
.build();
|
||||
}
|
||||
|
||||
public McpTransportConfig.InProcess clientConfig(String serverName, int timeout) {
|
||||
return new McpTransportConfig.InProcess(timeout, clientTransport);
|
||||
}
|
||||
|
||||
public void loadDirectory(Path root) {
|
||||
File[] files = loadFiles(root);
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
addOrUpdate(file);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addOrUpdate(Path path) {
|
||||
return addOrUpdate(path.toFile());
|
||||
}
|
||||
|
||||
public boolean addOrUpdate(File file) {
|
||||
String name = file.getName();
|
||||
if (!isValidDescFile(name)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
MetaActionInfo info = JSON.parseObject(Files.readString(file.toPath(), StandardCharsets.UTF_8), MetaActionInfo.class);
|
||||
String uri = file.toPath().toUri().toString();
|
||||
descCache.put(uri, JSONObject.toJSONString(info));
|
||||
String actionKey = name.replace(".desc.json", "");
|
||||
descInfoCache.put(actionKey, copyMetaActionInfo(info));
|
||||
descServer.addResource(buildAsyncResourceSpecification(name, uri)).block();
|
||||
if (existedMetaActions.containsKey(actionKey)) {
|
||||
existedMetaActions.put(actionKey, mergeWithOriginal(actionKey, info));
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("desc.json 解析失败: {}", file.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(Path path) {
|
||||
String uri = path.toUri().toString();
|
||||
String actionKey = path.getFileName().toString().replace(".desc.json", "");
|
||||
descCache.remove(uri);
|
||||
descInfoCache.remove(actionKey);
|
||||
descServer.removeResource(uri).block();
|
||||
MetaActionInfo originalInfo = originalInfoCache.get(actionKey);
|
||||
if (originalInfo != null) {
|
||||
existedMetaActions.put(actionKey, copyMetaActionInfo(originalInfo));
|
||||
return;
|
||||
}
|
||||
MetaActionInfo info = existedMetaActions.get(actionKey);
|
||||
if (info != null) {
|
||||
existedMetaActions.put(actionKey, resetMetaActionInfo(info));
|
||||
}
|
||||
}
|
||||
|
||||
public void reconcile(Path root) {
|
||||
File[] files = loadFiles(root);
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> currentUris = ConcurrentHashMap.newKeySet();
|
||||
for (File file : files) {
|
||||
if (!isValidDescFile(file.getName())) {
|
||||
continue;
|
||||
}
|
||||
currentUris.add(file.toURI().toString());
|
||||
if (!addOrUpdate(file)) {
|
||||
remove(file.toPath());
|
||||
}
|
||||
}
|
||||
List<String> serverUris = descServer.listResources()
|
||||
.map(McpSchema.Resource::uri)
|
||||
.collectList()
|
||||
.block();
|
||||
if (serverUris == null) {
|
||||
log.error("无法获取 DescMcpServer 持有的资源列表");
|
||||
return;
|
||||
}
|
||||
for (String uri : serverUris) {
|
||||
if (!currentUris.contains(uri)) {
|
||||
remove(Path.of(java.net.URI.create(uri)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) {
|
||||
String actionKey = serverId + "::" + tool.name();
|
||||
MetaActionInfo baseInfo = buildToolMetaActionInfo(tool);
|
||||
originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo));
|
||||
MetaActionInfo override = descInfoCache.get(actionKey);
|
||||
return override == null ? baseInfo : mergeWithOriginal(actionKey, override);
|
||||
}
|
||||
|
||||
private McpStatelessServerFeatures.AsyncResourceSpecification buildAsyncResourceSpecification(String name, String uri) {
|
||||
McpSchema.Resource resource = McpSchema.Resource.builder()
|
||||
.name(name)
|
||||
.title(name)
|
||||
.description("Action descriptor for " + name)
|
||||
.mimeType("application/json")
|
||||
.uri(uri)
|
||||
.build();
|
||||
BiFunction<McpTransportContext, McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler = (context, request) -> {
|
||||
String result = descCache.get(request.uri());
|
||||
if (result == null) {
|
||||
return Mono.error(new NotFoundException("未找到 Resource: " + request.uri()));
|
||||
}
|
||||
return Mono.just(new McpSchema.ReadResourceResult(List.of(
|
||||
new McpSchema.TextResourceContents(request.uri(), "application/json", result)
|
||||
)));
|
||||
};
|
||||
return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler);
|
||||
}
|
||||
|
||||
public boolean isValidDescFile(String fileName) {
|
||||
return fileName.endsWith(".desc.json") && fileName.contains("::");
|
||||
}
|
||||
|
||||
private File[] loadFiles(Path root) {
|
||||
if (!Files.isDirectory(root)) {
|
||||
return null;
|
||||
}
|
||||
return root.toFile().listFiles();
|
||||
}
|
||||
|
||||
private MetaActionInfo resetMetaActionInfo(@NotNull MetaActionInfo info) {
|
||||
return new MetaActionInfo(
|
||||
false,
|
||||
info.getLauncher(),
|
||||
copyParams(info.getParams()),
|
||||
info.getDescription(),
|
||||
new LinkedHashSet<>(),
|
||||
new LinkedHashSet<>(),
|
||||
new LinkedHashSet<>(),
|
||||
false,
|
||||
copyResponseSchema(info.getResponseSchema())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
descServer.close();
|
||||
}
|
||||
|
||||
private MetaActionInfo buildToolMetaActionInfo(McpSchema.Tool tool) {
|
||||
Map<String, Object> outputSchema = tool.outputSchema();
|
||||
boolean io = false;
|
||||
Set<String> preActions = new LinkedHashSet<>();
|
||||
Set<String> postActions = new LinkedHashSet<>();
|
||||
boolean strictDependencies = false;
|
||||
Set<String> tags = new LinkedHashSet<>();
|
||||
Map<String, Object> meta = tool.meta();
|
||||
if (meta != null) {
|
||||
JSONObject metaJson = JSONObject.from(meta);
|
||||
io = Boolean.TRUE.equals(metaJson.getBoolean("io"));
|
||||
preActions = toOrderedSet(metaJson.getList("pre", String.class));
|
||||
postActions = toOrderedSet(metaJson.getList("post", String.class));
|
||||
strictDependencies = Boolean.TRUE.equals(metaJson.getBoolean("strict"));
|
||||
tags = toOrderedSet(metaJson.getList("tag", String.class));
|
||||
}
|
||||
return new MetaActionInfo(
|
||||
io,
|
||||
null,
|
||||
copyParams(tool.inputSchema().properties()),
|
||||
tool.description(),
|
||||
tags,
|
||||
preActions,
|
||||
postActions,
|
||||
strictDependencies,
|
||||
outputSchema == null ? JSONObject.of() : JSONObject.from(outputSchema)
|
||||
);
|
||||
}
|
||||
|
||||
private MetaActionInfo mergeWithOriginal(String actionKey, MetaActionInfo override) {
|
||||
MetaActionInfo original = originalInfoCache.get(actionKey);
|
||||
return override == null ? copyMetaActionInfo(original) : copyMetaActionInfo(override);
|
||||
}
|
||||
|
||||
private MetaActionInfo copyMetaActionInfo(MetaActionInfo source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return new MetaActionInfo(
|
||||
source.getIo(),
|
||||
source.getLauncher(),
|
||||
copyParams(source.getParams()),
|
||||
source.getDescription(),
|
||||
toOrderedSet(source.getTags()),
|
||||
toOrderedSet(source.getPreActions()),
|
||||
toOrderedSet(source.getPostActions()),
|
||||
source.getStrictDependencies(),
|
||||
copyResponseSchema(source.getResponseSchema())
|
||||
);
|
||||
}
|
||||
|
||||
private <T> LinkedHashSet<T> toOrderedSet(Collection<T> source) {
|
||||
return source == null ? new LinkedHashSet<>() : new LinkedHashSet<>(source);
|
||||
}
|
||||
|
||||
private Map<String, String> copyParams(Map<String, ?> params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> copied = new LinkedHashMap<>();
|
||||
params.forEach((key, value) -> copied.put(key, value == null ? null : String.valueOf(value)));
|
||||
return copied;
|
||||
}
|
||||
|
||||
private JSONObject copyResponseSchema(JSONObject responseSchema) {
|
||||
return responseSchema == null ? JSONObject.of() : JSONObject.from(responseSchema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess {
|
||||
|
||||
int timeout();
|
||||
|
||||
record Http(int timeout, String baseUri, String endpoint,
|
||||
Map<String, String> headers) implements McpTransportConfig {
|
||||
}
|
||||
|
||||
record Stdio(int timeout, String command, Map<String, String> env,
|
||||
List<String> args) implements McpTransportConfig {
|
||||
}
|
||||
|
||||
record InProcess(int timeout, InProcessMcpTransport clientTransport) implements McpTransportConfig {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class McpTransportFactory {
|
||||
|
||||
public McpClientTransport create(McpTransportConfig config) {
|
||||
return switch (config) {
|
||||
case McpTransportConfig.Stdio stdio -> {
|
||||
List<String> commands = new ArrayList<>();
|
||||
commands.add(stdio.command());
|
||||
commands.addAll(stdio.args());
|
||||
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(commands);
|
||||
Map<String, String> env = new HashMap<>(stdio.env());
|
||||
env.putAll(wrapped.getEnvironment());
|
||||
ServerParameters serverParameters = ServerParameters.builder(wrapped.getCommand())
|
||||
.args(wrapped.getArgs())
|
||||
.env(env)
|
||||
.build();
|
||||
yield new StdioClientTransport(serverParameters, McpJsonMapper.getDefault());
|
||||
}
|
||||
case McpTransportConfig.Http http -> {
|
||||
McpSyncHttpClientRequestCustomizer customizer = (builder, method, endpoint, body, context) -> http.headers().forEach(builder::setHeader);
|
||||
yield HttpClientSseClientTransport.builder(http.baseUri())
|
||||
.httpRequestCustomizer(customizer)
|
||||
.sseEndpoint(http.endpoint())
|
||||
.build();
|
||||
}
|
||||
case McpTransportConfig.InProcess inProcess -> inProcess.clientTransport();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package work.slhaf.partner.core.action.runner.policy
|
||||
|
||||
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException
|
||||
|
||||
private const val BWRAP_COMMAND = "bwrap"
|
||||
|
||||
object BwrapPolicyProvider : PolicyProvider(
|
||||
policyName = "bwrap"
|
||||
) {
|
||||
|
||||
init {
|
||||
requireBwrapAvailable()
|
||||
}
|
||||
|
||||
override fun prepare(
|
||||
policy: ExecutionPolicy,
|
||||
commands: List<String>
|
||||
): WrappedLaunchSpec {
|
||||
val (command, args) = splitCommands(commands)
|
||||
val wrappedArgs = buildList {
|
||||
add("--ro-bind")
|
||||
add("/")
|
||||
add("/")
|
||||
add("--proc")
|
||||
add("/proc")
|
||||
add("--dev")
|
||||
add("/dev")
|
||||
if (policy.net == ExecutionPolicy.Network.DISABLE) {
|
||||
add("--unshare-net")
|
||||
}
|
||||
if (!policy.workingDirectory.isNullOrBlank()) {
|
||||
add("--chdir")
|
||||
add(policy.workingDirectory)
|
||||
}
|
||||
policy.readOnlyPaths.forEach { path ->
|
||||
add("--ro-bind")
|
||||
add(path)
|
||||
add(path)
|
||||
}
|
||||
policy.writablePaths.forEach { path ->
|
||||
add("--bind")
|
||||
add(path)
|
||||
add(path)
|
||||
}
|
||||
add("--")
|
||||
add(command)
|
||||
addAll(args)
|
||||
}
|
||||
return WrappedLaunchSpec(
|
||||
command = BWRAP_COMMAND,
|
||||
args = wrappedArgs,
|
||||
workingDirectory = policy.workingDirectory,
|
||||
environment = resolveEnvironment(policy)
|
||||
)
|
||||
}
|
||||
|
||||
private fun requireBwrapAvailable() {
|
||||
val available = try {
|
||||
val process = ProcessBuilder(BWRAP_COMMAND, "--version")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val exitCode = process.waitFor()
|
||||
exitCode == 0
|
||||
} catch (e: Exception) {
|
||||
throw ActionInfrastructureStartupException(
|
||||
"Failed to detect executable command '$BWRAP_COMMAND'",
|
||||
"bwrap-policy-provider",
|
||||
null,
|
||||
BWRAP_COMMAND,
|
||||
e
|
||||
)
|
||||
}
|
||||
if (!available) {
|
||||
throw ActionInfrastructureStartupException(
|
||||
"Executable command '$BWRAP_COMMAND' is not available",
|
||||
"bwrap-policy-provider",
|
||||
null,
|
||||
BWRAP_COMMAND
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package work.slhaf.partner.core.action.runner.policy
|
||||
|
||||
object DirectPolicyProvider : PolicyProvider(
|
||||
policyName = "direct"
|
||||
) {
|
||||
|
||||
override fun prepare(
|
||||
policy: ExecutionPolicy,
|
||||
commands: List<String>
|
||||
): WrappedLaunchSpec {
|
||||
val (command, args) = splitCommands(commands)
|
||||
return WrappedLaunchSpec(
|
||||
command = command,
|
||||
args = args,
|
||||
workingDirectory = policy.workingDirectory,
|
||||
environment = resolveEnvironment(policy)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package work.slhaf.partner.core.action.runner.policy
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import work.slhaf.partner.framework.agent.config.Config
|
||||
import work.slhaf.partner.framework.agent.config.ConfigRegistration
|
||||
import work.slhaf.partner.framework.agent.config.Configurable
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
object ExecutionPolicyRegistry : Configurable, ConfigRegistration<ExecutionPolicy> {
|
||||
|
||||
private const val DEFAULT_PROVIDER = "direct"
|
||||
|
||||
private val policyProviders = ConcurrentHashMap<String, PolicyProvider>().apply {
|
||||
put(DEFAULT_PROVIDER, DirectPolicyProvider)
|
||||
}
|
||||
|
||||
private val listeners = CopyOnWriteArraySet<RunnerExecutionPolicyListener>()
|
||||
|
||||
init {
|
||||
register()
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private lateinit var currentPolicy: ExecutionPolicy
|
||||
|
||||
fun prepare(commands: List<String>): WrappedLaunchSpec {
|
||||
val policy = currentPolicy
|
||||
val provider = policyProviders[policy.provider]
|
||||
?: policyProviders[DEFAULT_PROVIDER]
|
||||
?: error("Default provider '${DEFAULT_PROVIDER}' is not registered")
|
||||
return provider.prepare(policy, commands)
|
||||
}
|
||||
fun updatePolicy(policy: ExecutionPolicy) {
|
||||
currentPolicy = policy
|
||||
listeners.forEach { it.onPolicyChanged(policy) }
|
||||
}
|
||||
|
||||
fun addListener(listener: RunnerExecutionPolicyListener) {
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
fun removeListener(listener: RunnerExecutionPolicyListener) {
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
fun registerPolicyProvider(policyProvider: PolicyProvider) {
|
||||
val name = policyProvider.policyName
|
||||
if (policyProviders.containsKey(name)) {
|
||||
return
|
||||
}
|
||||
policyProviders[name] = policyProvider
|
||||
}
|
||||
|
||||
override fun declare(): Map<Path, ConfigRegistration<out Config>> {
|
||||
return mapOf(Path.of("action", "runner_policy.json") to this)
|
||||
}
|
||||
|
||||
override fun type(): Class<ExecutionPolicy> {
|
||||
return ExecutionPolicy::class.java
|
||||
}
|
||||
|
||||
override fun init(
|
||||
config: ExecutionPolicy,
|
||||
json: JSONObject?
|
||||
) {
|
||||
this.currentPolicy = config
|
||||
}
|
||||
|
||||
override fun defaultConfig(): ExecutionPolicy {
|
||||
return ExecutionPolicy(
|
||||
provider = "direct",
|
||||
mode = ExecutionPolicy.Mode.DIRECT,
|
||||
net = ExecutionPolicy.Network.ENABLE,
|
||||
inheritEnv = true,
|
||||
env = emptyMap(),
|
||||
workingDirectory = null,
|
||||
readOnlyPaths = emptySet(),
|
||||
writablePaths = emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReload(
|
||||
config: ExecutionPolicy,
|
||||
json: JSONObject?
|
||||
) {
|
||||
this.currentPolicy = config
|
||||
}
|
||||
}
|
||||
|
||||
data class ExecutionPolicy(
|
||||
val mode: Mode,
|
||||
val provider: String,
|
||||
val net: Network,
|
||||
val inheritEnv: Boolean,
|
||||
val env: Map<String, String>,
|
||||
val workingDirectory: String?,
|
||||
val readOnlyPaths: Set<String>,
|
||||
val writablePaths: Set<String>,
|
||||
) : Config() {
|
||||
|
||||
enum class Mode {
|
||||
DIRECT,
|
||||
SANDBOX
|
||||
}
|
||||
|
||||
enum class Network {
|
||||
DISABLE,
|
||||
ENABLE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class WrappedLaunchSpec(
|
||||
val command: String,
|
||||
val args: List<String>,
|
||||
val workingDirectory: String? = null,
|
||||
val environment: Map<String, String> = emptyMap()
|
||||
)
|
||||
|
||||
abstract class PolicyProvider(
|
||||
val policyName: String
|
||||
) {
|
||||
|
||||
abstract fun prepare(
|
||||
policy: ExecutionPolicy,
|
||||
commands: List<String>
|
||||
): WrappedLaunchSpec
|
||||
|
||||
protected fun resolveEnvironment(policy: ExecutionPolicy): Map<String, String> {
|
||||
val result = LinkedHashMap<String, String>()
|
||||
if (policy.inheritEnv) {
|
||||
result.putAll(System.getenv())
|
||||
}
|
||||
result.putAll(policy.env)
|
||||
return result
|
||||
}
|
||||
|
||||
protected fun splitCommands(commands: List<String>): Pair<String, List<String>> {
|
||||
require(commands.isNotEmpty()) { "commands must not be empty" }
|
||||
return commands.first() to commands.drop(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface RunnerExecutionPolicyListener {
|
||||
fun onPolicyChanged(policy: ExecutionPolicy)
|
||||
|
||||
fun registerPolicyListener() {
|
||||
ExecutionPolicyRegistry.addListener(this)
|
||||
}
|
||||
|
||||
fun unregisterPolicyListener() {
|
||||
ExecutionPolicyRegistry.removeListener(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package work.slhaf.partner.core.action.runner.support;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.exception.ActionSerializationException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
@Slf4j
|
||||
public class ActionSerializer {
|
||||
|
||||
private final String tmpActionPath;
|
||||
private final String dynamicActionPath;
|
||||
|
||||
public ActionSerializer(String tmpActionPath, String dynamicActionPath) {
|
||||
this.tmpActionPath = tmpActionPath;
|
||||
this.dynamicActionPath = dynamicActionPath;
|
||||
}
|
||||
|
||||
public static String normalizeCodeType(String codeType) {
|
||||
if (codeType == null || codeType.isBlank()) {
|
||||
throw new IllegalArgumentException("codeType 不能为空");
|
||||
}
|
||||
return codeType.startsWith(".") ? codeType : "." + codeType;
|
||||
}
|
||||
|
||||
private static @NotNull Path createActionDir(String baseName, Path baseDir, String fileExt) {
|
||||
for (int i = 0; ; i++) {
|
||||
String dirName = i == 0 ? baseName : baseName + "(" + i + ")";
|
||||
Path candidate = baseDir.resolve(dirName);
|
||||
try {
|
||||
Files.createDirectory(candidate);
|
||||
return candidate;
|
||||
} catch (FileAlreadyExistsException ignored) {
|
||||
} catch (IOException e) {
|
||||
throw new ActionSerializationException(
|
||||
"Failed to create action directory: " + candidate.toAbsolutePath(),
|
||||
baseName,
|
||||
baseDir.toAbsolutePath().toString(),
|
||||
fileExt,
|
||||
"CREATE_DIRECTORY",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String buildTmpPath(String actionKey, String codeType) {
|
||||
return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString();
|
||||
}
|
||||
|
||||
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
|
||||
log.debug("行动程序临时序列化: {}", tempAction);
|
||||
Path path = Path.of(tempAction.getLocation());
|
||||
validateTmpLocation(path, codeType);
|
||||
File file = path.toFile();
|
||||
file.createNewFile();
|
||||
Files.writeString(path, code);
|
||||
log.debug("临时序列化完毕");
|
||||
}
|
||||
|
||||
private void validateTmpLocation(Path path, String codeType) throws IOException {
|
||||
String normalizedCodeType = normalizeCodeType(codeType);
|
||||
String fileName = path.getFileName().toString();
|
||||
if (!fileName.endsWith(normalizedCodeType)) {
|
||||
throw new IOException("临时文件路径与 codeType 不匹配: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
|
||||
log.debug("行动程序持久序列化: {}", metaActionInfo);
|
||||
val baseDir = Path.of(dynamicActionPath);
|
||||
|
||||
if (!Files.isDirectory(baseDir)) {
|
||||
throw new ActionSerializationException(
|
||||
"Action base directory is not available: " + baseDir.toAbsolutePath(),
|
||||
fileMetaData.getName(),
|
||||
baseDir.toAbsolutePath().toString(),
|
||||
fileMetaData.getExt(),
|
||||
"VALIDATE_BASE_DIR"
|
||||
);
|
||||
}
|
||||
|
||||
val actionDir = createActionDir(fileMetaData.getName(), baseDir, fileMetaData.getExt());
|
||||
val runTmp = actionDir.resolve("run." + fileMetaData.getExt() + ".tmp");
|
||||
val descTmp = actionDir.resolve("desc.json.tmp");
|
||||
val runFinal = actionDir.resolve("run." + fileMetaData.getExt());
|
||||
val descFinal = actionDir.resolve("desc.json");
|
||||
|
||||
try {
|
||||
Files.writeString(runTmp, fileMetaData.getContent());
|
||||
Files.writeString(descTmp, JSONObject.toJSONString(metaActionInfo));
|
||||
Files.move(runTmp, runFinal, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.move(descTmp, descFinal, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
safeDelete(runTmp);
|
||||
safeDelete(descTmp);
|
||||
safeDelete(runFinal);
|
||||
safeDelete(descFinal);
|
||||
safeDelete(actionDir);
|
||||
throw new ActionSerializationException(
|
||||
"Failed to persist action files",
|
||||
fileMetaData.getName(),
|
||||
baseDir.toAbsolutePath().toString(),
|
||||
fileMetaData.getExt(),
|
||||
"WRITE_FILES",
|
||||
e
|
||||
);
|
||||
}
|
||||
log.debug("持久序列化结束");
|
||||
}
|
||||
|
||||
private void safeDelete(Path path) {
|
||||
try {
|
||||
if (Files.exists(path)) {
|
||||
Files.delete(path);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package work.slhaf.partner.core.cognition;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
@Capability("cognition")
|
||||
public interface CognitionCapability {
|
||||
|
||||
void initiateTurn(String input, String target, String... skippedModules);
|
||||
|
||||
ContextWorkspace contextWorkspace();
|
||||
|
||||
List<Message> getChatMessages();
|
||||
|
||||
List<Message> snapshotChatMessages();
|
||||
|
||||
void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor);
|
||||
|
||||
void refreshRecentChatMessagesContext();
|
||||
|
||||
Element messageNotesElement();
|
||||
|
||||
Lock getMessageLock();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package work.slhaf.partner.core.cognition;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import kotlin.Unit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.common.base.Block;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@CapabilityCore(value = "cognition")
|
||||
public class CognitionCore implements StateSerializable {
|
||||
|
||||
private static final String RECENT_CHAT_MESSAGE_NOTES = """
|
||||
消息格式:
|
||||
- 所有消息统一写为“标记行 + 空行 + 正文”,比如:
|
||||
|
||||
[[AGENT]: self]: [NOT_REPLIED][COMPRESSED]:
|
||||
|
||||
正文内容
|
||||
|
||||
- 标记行一定包含身份标签,通常格式为 [[USER]: <userName>] 或 [[AGENT]: self]
|
||||
- 若身份标签提取失败,可能回退为 [[Unknown]: Unknown]
|
||||
- 若存在其他标签,则写为“身份标签: 状态标签串:”
|
||||
- 正文永远从空行后开始
|
||||
|
||||
标记含义:
|
||||
- [USER]: 外部用户来源
|
||||
- [AGENT]: 系统内部来源
|
||||
- [NOT_REPLIED]: 仅保留在历史中的未直接回复结果
|
||||
- [COMPRESSED]: 该消息正文经过压缩
|
||||
""";
|
||||
|
||||
private final ReentrantLock messageLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* 主模型的聊天记录
|
||||
*/
|
||||
private List<Message> chatMessages = new ArrayList<>();
|
||||
|
||||
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
|
||||
|
||||
public CognitionCore() {
|
||||
register();
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public ContextWorkspace contextWorkspace() {
|
||||
return contextWorkspace;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void initiateTurn(String input, String target, String... skippedModules) {
|
||||
PartnerRunningFlowContext primaryContext = PartnerRunningFlowContext.fromSelf(input);
|
||||
primaryContext.setTarget(target);
|
||||
if (skippedModules != null) {
|
||||
for (String skippedModule : skippedModules) {
|
||||
primaryContext.addSkippedModule(skippedModule);
|
||||
}
|
||||
}
|
||||
AgentRuntime.INSTANCE.submit(primaryContext);
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public List<Message> getChatMessages() {
|
||||
return chatMessages;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public List<Message> snapshotChatMessages() {
|
||||
messageLock.lock();
|
||||
try {
|
||||
return List.copyOf(chatMessages);
|
||||
} finally {
|
||||
messageLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor) {
|
||||
messageLock.lock();
|
||||
try {
|
||||
int safeSnapshotSize = Math.clamp(snapshotSize, 0, chatMessages.size());
|
||||
if (safeSnapshotSize == 0) {
|
||||
return;
|
||||
}
|
||||
int safeDivisor = Math.max(retainDivisor, 1);
|
||||
int retainCount = safeSnapshotSize / safeDivisor;
|
||||
int retainStart = Math.max(0, safeSnapshotSize - retainCount);
|
||||
|
||||
List<Message> rolled = new ArrayList<>(chatMessages.subList(retainStart, safeSnapshotSize));
|
||||
if (chatMessages.size() > safeSnapshotSize) {
|
||||
rolled.addAll(chatMessages.subList(safeSnapshotSize, chatMessages.size()));
|
||||
}
|
||||
chatMessages = rolled;
|
||||
} finally {
|
||||
messageLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Lock getMessageLock() {
|
||||
return messageLock;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void refreshRecentChatMessagesContext() {
|
||||
ContextBlock block = new ContextBlock(
|
||||
new BlockContent("recent_chat_messages", "communication_producer") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
document.appendChild(document.importNode(messageNotesElement(), true));
|
||||
Element chatMessagesElement = document.createElement("chat_messages");
|
||||
root.appendChild(chatMessagesElement);
|
||||
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
|
||||
messageElement.setAttribute("role", message.roleValue());
|
||||
messageElement.setTextContent(message.getContent());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
},
|
||||
Set.of(ContextBlock.FocusedDomain.COMMUNICATION),
|
||||
100,
|
||||
10,
|
||||
4
|
||||
);
|
||||
contextWorkspace.register(block);
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Element messageNotesElement() {
|
||||
return new Block("message_tag_notes") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
root.setTextContent(RECENT_CHAT_MESSAGE_NOTES);
|
||||
}
|
||||
}.encodeToXml();
|
||||
}
|
||||
|
||||
private List<Message> resolveRecentChatMessages() {
|
||||
int exclusiveEnd = Math.max(chatMessages.size() - 1, 0);
|
||||
if (exclusiveEnd == 0) {
|
||||
return List.of();
|
||||
}
|
||||
int start = Math.max(exclusiveEnd - 6, 0);
|
||||
return chatMessages.subList(start, exclusiveEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("core", "cognition.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
JSONArray messageArray = state.getJSONArray("chat_messages");
|
||||
|
||||
if (messageArray == null) {
|
||||
log.warn("chat_messages is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < messageArray.size(); i++) {
|
||||
JSONObject messageObject = messageArray.getJSONObject(i);
|
||||
if (messageObject == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String role = messageObject.getString("role");
|
||||
String content = messageObject.getString("content");
|
||||
if (role == null || content == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.chatMessages.add(new Message(Message.Character.fromValue(role), content));
|
||||
}
|
||||
|
||||
refreshRecentChatMessagesContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
|
||||
List<StateValue.Obj> convertedMessageList = chatMessages.stream().map(message -> {
|
||||
Map<String, StateValue> convertedMap = Map.of(
|
||||
"role", StateValue.str(message.roleValue()),
|
||||
"content", StateValue.str(message.getContent())
|
||||
);
|
||||
return StateValue.obj(convertedMap);
|
||||
}).toList();
|
||||
state.append("chat_messages", StateValue.arr(convertedMessageList));
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package work.slhaf.partner.core.cognition
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import work.slhaf.partner.common.base.Block
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter
|
||||
import work.slhaf.partner.framework.agent.log.TraceEvent
|
||||
import work.slhaf.partner.framework.agent.log.TraceRecorder
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.write
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ContextWorkspace {
|
||||
|
||||
private val tracePath: Path = ConfigCenter.paths.stateDir
|
||||
.resolve("trace")
|
||||
.resolve("context-workspace")
|
||||
.normalize()
|
||||
.toAbsolutePath()
|
||||
private val stateSet = mutableSetOf<ContextBlock>()
|
||||
private val lock = ReentrantReadWriteLock()
|
||||
|
||||
/**
|
||||
* 根据传入的 [ContextBlock.FocusedDomain] 列表,获取上下文块
|
||||
* @param domains 需要获取上下文的域列表,顺序将决定权重优先级,按照列表排序将具备线性权重分层,最终反映到 blockContent 列表的排序上
|
||||
*/
|
||||
fun resolve(domains: List<ContextBlock.FocusedDomain>): ResolvedContext = lock.write {
|
||||
if (domains.isEmpty()) {
|
||||
return@write ResolvedContext(emptyList())
|
||||
}
|
||||
val primaryDomain = domains.first()
|
||||
|
||||
val domainWeights = domains
|
||||
.distinct()
|
||||
.withIndex()
|
||||
.associate { (index, domain) -> domain to (domains.size - index) }
|
||||
|
||||
val activeBlocks = mutableListOf<ResolvedContextBlock>()
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val block = iterator.next()
|
||||
val fadedScore = block.applyTimeFade()
|
||||
if (fadedScore <= 0.0) {
|
||||
iterator.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
val matchedDomains = block.focusedOn.intersect(domainWeights.keys)
|
||||
if (matchedDomains.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
val exposure = if (primaryDomain in matchedDomains) {
|
||||
ContextBlock.Exposure.PRIMARY
|
||||
} else {
|
||||
ContextBlock.Exposure.SECONDARY
|
||||
}
|
||||
val activationScore = block.activate(exposure)
|
||||
if (activationScore <= 0.0) {
|
||||
iterator.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
activeBlocks += ResolvedContextBlock(
|
||||
block = block,
|
||||
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
|
||||
activationScore = activationScore,
|
||||
renderedBlock = block.render(exposure)
|
||||
)
|
||||
}
|
||||
|
||||
val blocks = activeBlocks
|
||||
.sortedWith(
|
||||
compareByDescending<ResolvedContextBlock> { it.domainWeight }
|
||||
.thenByDescending { it.activationScore }
|
||||
.thenBy { it.block.sourceKey.blockName }
|
||||
.thenBy { it.block.sourceKey.source }
|
||||
)
|
||||
.groupBy { it.block.sourceKey }
|
||||
.values
|
||||
.map { groupedBlocks ->
|
||||
if (groupedBlocks.size == 1) {
|
||||
renderResolvedBlock(groupedBlocks.first())
|
||||
} else {
|
||||
AggregatedBlockContent(groupedBlocks)
|
||||
}
|
||||
}
|
||||
ResolvedContext(blocks)
|
||||
}
|
||||
|
||||
private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent {
|
||||
return resolved.renderedBlock
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param contextBlock 注册的新上下文块
|
||||
*/
|
||||
fun register(contextBlock: ContextBlock) = lock.write {
|
||||
val removedBlocks = mutableListOf<ContextBlock>()
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val currentBlock = iterator.next()
|
||||
if (!currentBlock.sameWith(contextBlock)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentBlock.applyReplaceFade() <= 0.0) {
|
||||
iterator.remove()
|
||||
removedBlocks.add(currentBlock)
|
||||
}
|
||||
}
|
||||
stateSet += contextBlock
|
||||
recordRegister(contextBlock, removedBlocks)
|
||||
}
|
||||
|
||||
fun expire(blockName: String, source: String) = lock.write {
|
||||
val sourceKey = ContextBlock.SourceKey(blockName, source)
|
||||
val removedBlocks = mutableListOf<ContextBlock>()
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val block = iterator.next()
|
||||
if (block.sourceKey == sourceKey) {
|
||||
iterator.remove()
|
||||
removedBlocks.add(block)
|
||||
}
|
||||
}
|
||||
if (removedBlocks.isNotEmpty()) {
|
||||
recordExpire(sourceKey, removedBlocks)
|
||||
}
|
||||
}
|
||||
|
||||
private fun recordRegister(addedBlock: ContextBlock, removedBlocks: List<ContextBlock>) {
|
||||
val payload = JSONObject()
|
||||
payload["action"] = "register"
|
||||
payload["added"] = blockSnapshot(addedBlock)
|
||||
payload["removed"] = removedBlocks.map(::blockSnapshot)
|
||||
TraceRecorder.record(TraceEvent(tracePath, payload))
|
||||
}
|
||||
|
||||
private fun recordExpire(sourceKey: ContextBlock.SourceKey, removedBlocks: List<ContextBlock>) {
|
||||
val payload = JSONObject()
|
||||
payload["action"] = "expire"
|
||||
payload["blockName"] = sourceKey.blockName
|
||||
payload["source"] = sourceKey.source
|
||||
payload["removed"] = removedBlocks.map(::blockSnapshot)
|
||||
TraceRecorder.record(TraceEvent(tracePath, payload))
|
||||
}
|
||||
|
||||
private fun blockSnapshot(block: ContextBlock): JSONObject {
|
||||
val payload = JSONObject()
|
||||
payload["blockName"] = block.sourceKey.blockName
|
||||
payload["source"] = block.sourceKey.source
|
||||
payload["focusedOn"] = block.focusedOn.map { it.name }
|
||||
payload["fullRendered"] = block.blockContent.encodeToXmlString()
|
||||
payload["compactRendered"] = block.compactBlock.encodeToXmlString()
|
||||
payload["abstractRendered"] = block.abstractBlock.encodeToXmlString()
|
||||
return payload
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class ResolvedContextBlock(
|
||||
val block: ContextBlock,
|
||||
val domainWeight: Int,
|
||||
val activationScore: Double,
|
||||
val renderedBlock: BlockContent
|
||||
)
|
||||
|
||||
data class ContextBlock @JvmOverloads constructor(
|
||||
val blockContent: BlockContent,
|
||||
val compactBlock: BlockContent = blockContent,
|
||||
val abstractBlock: BlockContent = blockContent,
|
||||
/**
|
||||
* 该 block 集中在哪些域
|
||||
*/
|
||||
val focusedOn: Set<FocusedDomain>,
|
||||
|
||||
/**
|
||||
* 新的 [blockContent] 属性与其相同时,发生的衰退步长
|
||||
*/
|
||||
private val replaceFadeFactor: Double,
|
||||
|
||||
/**
|
||||
* 随时间发生的衰退步长,按照分钟定义
|
||||
*/
|
||||
private val timeFadeFactor: Double,
|
||||
|
||||
/**
|
||||
* 触发一次激活时,发生的强化步长
|
||||
*/
|
||||
private val activateFactor: Double
|
||||
) {
|
||||
internal enum class Exposure {
|
||||
PRIMARY,
|
||||
SECONDARY
|
||||
}
|
||||
|
||||
private enum class ProjectionLevel {
|
||||
ABSTRACT,
|
||||
COMPACT,
|
||||
FULL
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认活跃分数,降低至0时将在 [ContextWorkspace] 中移除该 block
|
||||
* 此外还参与到当同源 block 存在时的排序,按该分数升序排列,只影响同源 block 间的顺序
|
||||
*/
|
||||
private var activationScore = 100.0
|
||||
private var lastTouchedAt = Instant.now()
|
||||
|
||||
enum class FocusedDomain {
|
||||
ACTION,
|
||||
MEMORY,
|
||||
PERCEIVE,
|
||||
COGNITION,
|
||||
COMMUNICATION,
|
||||
}
|
||||
|
||||
internal val sourceKey: SourceKey
|
||||
get() = SourceKey(blockContent.blockName, blockContent.source)
|
||||
|
||||
fun applyTimeFade(): Double {
|
||||
refreshByElapsedTime()
|
||||
return activationScore
|
||||
}
|
||||
|
||||
fun applyReplaceFade(): Double {
|
||||
refreshByElapsedTime()
|
||||
activationScore = max(0.0, activationScore - replaceFadeFactor)
|
||||
return activationScore
|
||||
}
|
||||
|
||||
internal fun activate(exposure: Exposure): Double {
|
||||
refreshByElapsedTime()
|
||||
val currentLevel = currentProjectionLevel()
|
||||
val increasedScore = when (exposure) {
|
||||
Exposure.PRIMARY -> activationScore + when (currentLevel) {
|
||||
ProjectionLevel.FULL -> activateFactor
|
||||
ProjectionLevel.COMPACT -> activateFactor * 0.6
|
||||
ProjectionLevel.ABSTRACT -> activateFactor * 0.6
|
||||
}
|
||||
|
||||
Exposure.SECONDARY -> activationScore + when (currentLevel) {
|
||||
ProjectionLevel.COMPACT -> activateFactor * 0.2
|
||||
ProjectionLevel.ABSTRACT -> activateFactor * 0.1
|
||||
ProjectionLevel.FULL -> 0.0
|
||||
}
|
||||
}
|
||||
activationScore = min(activationCeiling(exposure, currentLevel), increasedScore)
|
||||
return activationScore
|
||||
}
|
||||
|
||||
private fun refreshByElapsedTime() {
|
||||
val now = Instant.now()
|
||||
val elapsedSeconds = Duration.between(lastTouchedAt, now).toMillis() / 1000.0
|
||||
activationScore = max(0.0, activationScore - elapsedSeconds * (timeFadeFactor / 60.0))
|
||||
lastTouchedAt = now
|
||||
}
|
||||
|
||||
fun sameWith(contextBlock: ContextBlock): Boolean {
|
||||
return this.sourceKey == contextBlock.sourceKey
|
||||
}
|
||||
|
||||
internal fun render(exposure: Exposure): BlockContent {
|
||||
return when (exposure) {
|
||||
Exposure.PRIMARY -> when (currentProjectionLevel()) {
|
||||
ProjectionLevel.FULL -> blockContent
|
||||
ProjectionLevel.COMPACT, ProjectionLevel.ABSTRACT -> compactBlock
|
||||
}
|
||||
|
||||
Exposure.SECONDARY -> when (currentProjectionLevel()) {
|
||||
ProjectionLevel.ABSTRACT -> abstractBlock
|
||||
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> compactBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun render(): BlockContent {
|
||||
return when (currentProjectionLevel()) {
|
||||
ProjectionLevel.ABSTRACT -> abstractBlock
|
||||
ProjectionLevel.COMPACT -> compactBlock
|
||||
ProjectionLevel.FULL -> blockContent
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentProjectionLevel(): ProjectionLevel {
|
||||
return when {
|
||||
activationScore < ABSTRACT_TO_COMPACT_THRESHOLD -> ProjectionLevel.ABSTRACT
|
||||
activationScore < COMPACT_TO_FULL_THRESHOLD -> ProjectionLevel.COMPACT
|
||||
else -> ProjectionLevel.FULL
|
||||
}
|
||||
}
|
||||
|
||||
private fun activationCeiling(exposure: Exposure, currentLevel: ProjectionLevel): Double {
|
||||
return when (exposure) {
|
||||
Exposure.PRIMARY -> when (currentLevel) {
|
||||
ProjectionLevel.ABSTRACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
|
||||
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
|
||||
}
|
||||
|
||||
Exposure.SECONDARY -> when (currentLevel) {
|
||||
ProjectionLevel.ABSTRACT -> ABSTRACT_TO_COMPACT_THRESHOLD - PROJECTION_EPSILON
|
||||
ProjectionLevel.COMPACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
|
||||
ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SourceKey(
|
||||
val blockName: String,
|
||||
val source: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MAX_ACTIVATION_SCORE = 100.0
|
||||
private const val ABSTRACT_TO_COMPACT_THRESHOLD = 30.0
|
||||
private const val COMPACT_TO_FULL_THRESHOLD = 70.0
|
||||
private const val PROJECTION_EPSILON = 0.000001
|
||||
}
|
||||
}
|
||||
|
||||
private class AggregatedBlockContent(
|
||||
private val groupedBlocks: List<ResolvedContextBlock>
|
||||
) : BlockContent(
|
||||
groupedBlocks.first().block.sourceKey.blockName,
|
||||
groupedBlocks.first().block.sourceKey.source,
|
||||
groupedBlocks.maxByOrNull { it.renderedBlock.urgency.ordinal }?.renderedBlock?.urgency ?: Urgency.NORMAL
|
||||
) {
|
||||
|
||||
override fun fillXml(document: Document, root: Element) {
|
||||
val snapshotIndex = groupedBlocks.withIndex()
|
||||
.maxWithOrNull(
|
||||
compareBy<IndexedValue<ResolvedContextBlock>> { it.value.activationScore }
|
||||
.thenBy { it.index }
|
||||
)?.index ?: 0
|
||||
|
||||
groupedBlocks.forEachIndexed { index, groupedBlock ->
|
||||
val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot"
|
||||
val wrapper = document.createElement(tagName)
|
||||
val renderedBlock = groupedBlock.renderedBlock
|
||||
val encoded = renderedBlock.encodeToXml()
|
||||
val attributes = encoded.attributes
|
||||
for (attributeIndex in 0 until attributes.length) {
|
||||
val attribute = attributes.item(attributeIndex)
|
||||
wrapper.setAttribute(attribute.nodeName, attribute.nodeValue)
|
||||
}
|
||||
root.appendChild(wrapper)
|
||||
|
||||
val childNodes = encoded.childNodes
|
||||
for (childIndex in 0 until childNodes.length) {
|
||||
wrapper.appendChild(document.importNode(childNodes.item(childIndex), true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BlockContent @JvmOverloads protected constructor(
|
||||
blockName: String,
|
||||
val source: String,
|
||||
val urgency: Urgency = Urgency.NORMAL
|
||||
) : Block(blockName) {
|
||||
|
||||
enum class Urgency {
|
||||
LOW,
|
||||
NORMAL,
|
||||
HIGH,
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
override fun appendRootAttributes(): Map<String, String> {
|
||||
return mapOf(
|
||||
"source" to source,
|
||||
"urgency" to urgency.name.lowercase(Locale.ROOT)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class CommunicationBlockContent @JvmOverloads constructor(
|
||||
blockName: String,
|
||||
source: String,
|
||||
urgency: Urgency = Urgency.NORMAL,
|
||||
val type: Projection = Projection.CONTEXT,
|
||||
) : BlockContent(
|
||||
blockName,
|
||||
source,
|
||||
urgency
|
||||
) {
|
||||
|
||||
enum class Projection {
|
||||
SUPPLY,
|
||||
CONTEXT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package work.slhaf.partner.core.cognition
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||
import java.io.StringWriter
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
data class ResolvedContext(
|
||||
val blocks: List<BlockContent>
|
||||
) {
|
||||
|
||||
fun encodeToMessage(): Message {
|
||||
val content = if (blocks.isEmpty()) {
|
||||
"<no_context></no_context>"
|
||||
} else {
|
||||
buildContextXml(blocks)
|
||||
}
|
||||
return Message(Message.Character.USER, content)
|
||||
}
|
||||
|
||||
private fun buildContextXml(blocks: List<BlockContent>): String {
|
||||
return try {
|
||||
val document = newDocument()
|
||||
val root = document.createElement("context")
|
||||
document.appendChild(root)
|
||||
|
||||
blocks.stream()
|
||||
.map(BlockContent::encodeToXml)
|
||||
.forEach { blockElement ->
|
||||
root.appendChild(document.importNode(blockElement, true))
|
||||
}
|
||||
|
||||
toXmlString(document)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("构建 context 区段失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newDocument(): Document {
|
||||
return DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.newDocument()
|
||||
}
|
||||
|
||||
private fun toXmlString(document: Document): String {
|
||||
val transformer = TransformerFactory.newInstance().newTransformer().apply {
|
||||
setOutputProperty(OutputKeys.INDENT, "yes")
|
||||
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
|
||||
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
|
||||
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
|
||||
}
|
||||
return StringWriter().use { writer ->
|
||||
transformer.transform(DOMSource(document), StreamResult(writer))
|
||||
writer.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package work.slhaf.partner.core.memory;
|
||||
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Capability(value = "memory")
|
||||
public interface MemoryCapability {
|
||||
|
||||
MemoryUnit getMemoryUnit(String unitId);
|
||||
|
||||
Result<MemorySlice> getMemorySlice(String unitId, String sliceId);
|
||||
|
||||
MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary);
|
||||
|
||||
Collection<MemoryUnit> listMemoryUnits();
|
||||
|
||||
void refreshMemorySession();
|
||||
|
||||
String getMemorySessionId();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package work.slhaf.partner.core.memory;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@CapabilityCore(value = "memory")
|
||||
@Slf4j
|
||||
public class MemoryCore implements StateSerializable {
|
||||
|
||||
private final Lock memoryLock = new ReentrantLock();
|
||||
private final ConcurrentHashMap<String, MemoryUnit> memoryUnits = new ConcurrentHashMap<>();
|
||||
|
||||
// 默认值一般只存在于智能体初次启动时
|
||||
private String memorySessionId = UUID.randomUUID().toString();
|
||||
|
||||
public MemoryCore() {
|
||||
register();
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
|
||||
memoryLock.lock();
|
||||
try {
|
||||
MemoryUnit unit = getMemoryUnit(memorySessionId);
|
||||
unit.updateTimestamp();
|
||||
|
||||
List<Message> conversationMessages = unit.getConversationMessages();
|
||||
int startIndex = conversationMessages.size();
|
||||
|
||||
MemorySlice memorySlice = new MemorySlice(
|
||||
startIndex,
|
||||
startIndex + chatMessages.size(),
|
||||
summary
|
||||
);
|
||||
|
||||
conversationMessages.addAll(chatMessages);
|
||||
|
||||
unit.getSlices().add(memorySlice);
|
||||
normalizeMemoryUnit(unit);
|
||||
return unit;
|
||||
} finally {
|
||||
memoryLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public MemoryUnit getMemoryUnit(String unitId) {
|
||||
MemoryUnit unit = memoryUnits.computeIfAbsent(unitId, id -> {
|
||||
MemoryUnit newUnit = new MemoryUnit(id);
|
||||
newUnit.register();
|
||||
return newUnit;
|
||||
});
|
||||
unit.load();
|
||||
return unit;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Result<MemorySlice> getMemorySlice(String unitId, String sliceId) {
|
||||
MemoryUnit memoryUnit = memoryUnits.get(unitId);
|
||||
if (memoryUnit == null) {
|
||||
return Result.failure(new MemoryLookupException(
|
||||
"Memory slice not found: " + unitId + ":" + sliceId,
|
||||
unitId + ":" + sliceId,
|
||||
"MEMORY_SLICE"
|
||||
));
|
||||
}
|
||||
for (MemorySlice slice : memoryUnit.getSlices()) {
|
||||
if (sliceId.equals(slice.getId())) {
|
||||
return Result.success(slice);
|
||||
}
|
||||
}
|
||||
return Result.failure(new MemoryLookupException(
|
||||
"Memory slice not found: " + unitId + ":" + sliceId,
|
||||
unitId + ":" + sliceId,
|
||||
"MEMORY_SLICE"
|
||||
));
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Collection<MemoryUnit> listMemoryUnits() {
|
||||
return new ArrayList<>(memoryUnits.values());
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void refreshMemorySession() {
|
||||
memorySessionId = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public String getMemorySessionId() {
|
||||
return memorySessionId;
|
||||
}
|
||||
|
||||
private void normalizeMemoryUnit(MemoryUnit memoryUnit) {
|
||||
if (memoryUnit.getTimestamp() == null || memoryUnit.getTimestamp() <= 0) {
|
||||
memoryUnit.updateTimestamp();
|
||||
}
|
||||
int maxEndExclusive = memoryUnit.getConversationMessages().size();
|
||||
List<MemorySlice> normalizedSlices = new ArrayList<>(memoryUnit.getSlices().size());
|
||||
for (MemorySlice slice : memoryUnit.getSlices()) {
|
||||
if (slice == null) {
|
||||
continue;
|
||||
}
|
||||
String sliceId = slice.getId();
|
||||
if (sliceId == null || sliceId.isBlank()) {
|
||||
sliceId = UUID.randomUUID().toString();
|
||||
}
|
||||
long sliceTimestamp = slice.getTimestamp() == null || slice.getTimestamp() <= 0
|
||||
? memoryUnit.getTimestamp()
|
||||
: slice.getTimestamp();
|
||||
int startIndex = slice.getStartIndex() == null || slice.getStartIndex() < 0
|
||||
? 0
|
||||
: Math.min(slice.getStartIndex(), maxEndExclusive);
|
||||
int endIndex = slice.getEndIndex() == null || slice.getEndIndex() < startIndex
|
||||
? maxEndExclusive
|
||||
: Math.min(slice.getEndIndex(), maxEndExclusive);
|
||||
normalizedSlices.add(MemorySlice.restore(
|
||||
sliceId,
|
||||
startIndex,
|
||||
endIndex,
|
||||
slice.getSummary(),
|
||||
sliceTimestamp
|
||||
));
|
||||
}
|
||||
memoryUnit.getSlices().clear();
|
||||
memoryUnit.getSlices().addAll(normalizedSlices);
|
||||
memoryUnit.getSlices().sort(Comparator.naturalOrder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("core", "memory.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
String memorySessionId = state.getString("memory_session_id");
|
||||
if (memorySessionId == null) {
|
||||
throw new IllegalStateException("Memory session id is missing");
|
||||
}
|
||||
JSONArray array = state.getJSONArray("memory_unit_uuid_set");
|
||||
if (array == null) {
|
||||
throw new IllegalStateException("Memory unit uuid set is missing");
|
||||
}
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
String unitUuid = array.getString(i);
|
||||
if (unitUuid == null) {
|
||||
throw new IllegalStateException("memory_unit_uuid_set is not a uuid array, index: " + i);
|
||||
}
|
||||
MemoryUnit memoryUnit = new MemoryUnit(unitUuid);
|
||||
memoryUnits.put(unitUuid, memoryUnit);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
state.append("memory_session_id", StateValue.str(memorySessionId));
|
||||
|
||||
List<StateValue.Str> unitOverview = memoryUnits.keySet().stream()
|
||||
.map(StateValue::str)
|
||||
.toList();
|
||||
state.append("memory_unit_uuid_set", StateValue.arr(unitOverview));
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package work.slhaf.partner.core.memory.pojo;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public class MemorySlice implements Comparable<MemorySlice> {
|
||||
|
||||
private final String id;
|
||||
private final Integer startIndex;
|
||||
private final Integer endIndex;
|
||||
private final String summary;
|
||||
private final Long timestamp;
|
||||
|
||||
public MemorySlice(Integer startIndex, Integer endIndex, String summary) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.startIndex = startIndex;
|
||||
this.endIndex = endIndex;
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
private MemorySlice(String id, Integer startIndex, Integer endIndex, String summary, Long timestamp) {
|
||||
this.id = id;
|
||||
this.startIndex = startIndex;
|
||||
this.endIndex = endIndex;
|
||||
this.summary = summary;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public static MemorySlice restore(String id, Integer startIndex, Integer endIndex, String summary, Long timestamp) {
|
||||
return new MemorySlice(id, startIndex, endIndex, summary, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MemorySlice memorySlice) {
|
||||
if (memorySlice.getTimestamp() > this.getTimestamp()) {
|
||||
return -1;
|
||||
} else if (memorySlice.getTimestamp() < this.timestamp) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package work.slhaf.partner.core.memory.pojo;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
public class MemoryUnit implements StateSerializable {
|
||||
|
||||
private final String id;
|
||||
private final List<Message> conversationMessages = new ArrayList<>();
|
||||
private Long timestamp = 0L;
|
||||
private final List<MemorySlice> slices = new ArrayList<>();
|
||||
|
||||
public MemoryUnit(String id) {
|
||||
this.id = id;
|
||||
this.register();
|
||||
}
|
||||
|
||||
public void updateTimestamp() {
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("core", "memory", "memory-unit" + id + ".json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
Long loadedTimestamp = state.getLong("update_timestamp");
|
||||
this.timestamp = loadedTimestamp != null ? loadedTimestamp : 0L;
|
||||
|
||||
this.conversationMessages.clear();
|
||||
this.slices.clear();
|
||||
|
||||
JSONArray messageArray = state.getJSONArray("conversation_messages");
|
||||
if (messageArray != null) {
|
||||
for (int i = 0; i < messageArray.size(); i++) {
|
||||
JSONObject messageObject = messageArray.getJSONObject(i);
|
||||
if (messageObject == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String role = messageObject.getString("role");
|
||||
String content = messageObject.getString("content");
|
||||
if (role == null || content == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Message message = new Message(Message.Character.fromValue(role), content);
|
||||
|
||||
this.conversationMessages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
var sliceArray = state.getJSONArray("memory_slices");
|
||||
if (sliceArray != null) {
|
||||
for (int i = 0; i < sliceArray.size(); i++) {
|
||||
JSONObject sliceObject = sliceArray.getJSONObject(i);
|
||||
if (sliceObject == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String sliceId = sliceObject.getString("id");
|
||||
Integer startIndex = sliceObject.getInteger("start_index");
|
||||
Integer endIndex = sliceObject.getInteger("end_index");
|
||||
String summary = sliceObject.getString("summary");
|
||||
Long createdTimestamp = sliceObject.getLong("created_timestamp");
|
||||
|
||||
if (sliceId == null || startIndex == null || endIndex == null || summary == null || createdTimestamp == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MemorySlice slice = MemorySlice.restore(sliceId, startIndex, endIndex, summary, createdTimestamp);
|
||||
|
||||
this.slices.add(slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
state.append("id", StateValue.str(id));
|
||||
state.append("update_timestamp", StateValue.num(timestamp));
|
||||
|
||||
List<StateValue.Obj> convertedMessageList = conversationMessages.stream().map(message -> {
|
||||
Map<String, StateValue> convertedMap = Map.of(
|
||||
"role", StateValue.str(message.roleValue()),
|
||||
"content", StateValue.str(message.getContent())
|
||||
);
|
||||
return StateValue.obj(convertedMap);
|
||||
}).toList();
|
||||
state.append("conversation_messages", StateValue.arr(convertedMessageList));
|
||||
|
||||
List<StateValue.Obj> convertedSliceList = slices.stream().map(slice -> {
|
||||
Map<String, StateValue> convertedMap = Map.of(
|
||||
"id", StateValue.str(slice.getId()),
|
||||
"start_index", StateValue.num(slice.getStartIndex()),
|
||||
"end_index", StateValue.num(slice.getEndIndex()),
|
||||
"summary", StateValue.str(slice.getSummary()),
|
||||
"created_timestamp", StateValue.num(slice.getTimestamp())
|
||||
);
|
||||
return StateValue.obj(convertedMap);
|
||||
}).toList();
|
||||
state.append("memory_slices", StateValue.arr(convertedSliceList));
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean autoLoadOnRegister() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package work.slhaf.partner.core.memory.pojo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SliceRef {
|
||||
private String unitId;
|
||||
private String sliceId;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package work.slhaf.partner.core.perceive;
|
||||
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Capability(value = "perceive")
|
||||
public interface PerceiveCapability {
|
||||
String refreshInteract();
|
||||
|
||||
Instant showLastInteract();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package work.slhaf.partner.core.perceive;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@CapabilityCore(value = "perceive")
|
||||
public class PerceiveCore implements StateSerializable {
|
||||
|
||||
private Instant lastInteractTime = Instant.ofEpochMilli(0);
|
||||
|
||||
public PerceiveCore() {
|
||||
register();
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public String refreshInteract() {
|
||||
String last = lastInteractTime.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
lastInteractTime = Instant.now();
|
||||
return last;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Instant showLastInteract() {
|
||||
return lastInteractTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("core", "perceive.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
this.lastInteractTime = Instant.ofEpochMilli(state.getLong("last_interact_time"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
state.append("last_interact_time", StateValue.num(lastInteractTime.toEpochMilli()));
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package work.slhaf.partner.module
|
||||
|
||||
import work.slhaf.partner.common.base.Block
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||
|
||||
abstract class TaskBlock @JvmOverloads constructor(
|
||||
blockName: String = "task_input"
|
||||
) : Block(blockName) {
|
||||
|
||||
fun encodeToMessage(): Message {
|
||||
return Message(Message.Character.USER, encodeToXmlString())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
interface BuiltinActionProvider {
|
||||
List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions();
|
||||
|
||||
String createActionKey(String actionName);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.exception.ActionLookupException;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
|
||||
|
||||
public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
|
||||
|
||||
@Getter
|
||||
private final Map<String, BuiltinActionDefinition> definitions = new LinkedHashMap<>();
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
definitions.clear();
|
||||
for (BuiltinActionDefinition definition : buildDefaultActionDefinitions()) {
|
||||
definitions.put(definition.actionKey(), definition);
|
||||
}
|
||||
actionCapability.registerMetaActions(exportMetaActionInfos());
|
||||
actionCapability.runnerClient().setBuiltinActionRegistry(this);
|
||||
}
|
||||
|
||||
protected List<BuiltinActionDefinition> buildDefaultActionDefinitions() {
|
||||
List<BuiltinActionDefinition> builtinActionDefinitions = new ArrayList<>();
|
||||
BuiltinActionProvider commandActionProvider = new BuiltinCommandActionProvider();
|
||||
BuiltinActionProvider capabilityActionProvider = new BuiltinCapabilityActionProvider();
|
||||
BuiltinActionProvider interventionActionProvider = new BuiltinInterventionActionProvider();
|
||||
BuiltinActionProvider dynamicActionProvider = new BuiltinDynamicActionProvider();
|
||||
|
||||
builtinActionDefinitions.addAll(commandActionProvider.provideBuiltinActions());
|
||||
builtinActionDefinitions.addAll(capabilityActionProvider.provideBuiltinActions());
|
||||
builtinActionDefinitions.addAll(interventionActionProvider.provideBuiltinActions());
|
||||
builtinActionDefinitions.addAll(dynamicActionProvider.provideBuiltinActions());
|
||||
|
||||
return builtinActionDefinitions;
|
||||
}
|
||||
|
||||
public void defineBuiltinAction(String name, MetaActionInfo metaActionInfo, Function<Map<String, Object>, String> invoker) {
|
||||
BuiltinActionDefinition definition = new BuiltinActionDefinition(BUILTIN_LOCATION + "::" + name, metaActionInfo, invoker);
|
||||
definitions.put(definition.actionKey(), definition);
|
||||
}
|
||||
|
||||
public String call(@NonNull String actionKey, @NonNull Map<String, Object> params) {
|
||||
BuiltinActionDefinition definition = definitions.get(actionKey);
|
||||
if (definition == null) {
|
||||
throw new ActionLookupException(
|
||||
"Builtin action definition not found: " + actionKey,
|
||||
actionKey,
|
||||
"BUILTIN_DEFINITION"
|
||||
);
|
||||
}
|
||||
String result = definition.invoker().apply(params);
|
||||
if (result == null) {
|
||||
return "null";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, MetaActionInfo> exportMetaActionInfos() {
|
||||
Map<String, MetaActionInfo> metaActions = new LinkedHashMap<>();
|
||||
definitions.forEach((key, value) -> metaActions.put(key, value.metaActionInfo()));
|
||||
return metaActions;
|
||||
}
|
||||
|
||||
public record BuiltinActionDefinition(
|
||||
String actionKey,
|
||||
MetaActionInfo metaActionInfo,
|
||||
Function<Map<String, Object>, String> invoker
|
||||
) {
|
||||
|
||||
static String requireString(Map<String, Object> params, String key) {
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("缺少参数: " + key);
|
||||
}
|
||||
if (!(value instanceof String s)) {
|
||||
throw new IllegalArgumentException("参数 " + key + " 必须为字符串");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static String optionalString(Map<String, Object> params, String key, String defaultValue) {
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (!(value instanceof String s)) {
|
||||
throw new IllegalArgumentException("参数 " + key + " 必须为字符串");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static Integer requireInt(Map<String, Object> params, String key) {
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("缺少参数: " + key);
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
if (value instanceof String string) {
|
||||
return Integer.parseInt(string);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
throw new IllegalArgumentException("参数 " + key + " 必须为整数");
|
||||
}
|
||||
|
||||
static Integer optionalInt(Map<String, Object> params, String key, Integer defaultValue) {
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Number n) {
|
||||
return n.intValue();
|
||||
}
|
||||
try {
|
||||
if (value instanceof String string) {
|
||||
return Integer.parseInt(string);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
throw new IllegalArgumentException("参数 " + key + " 必须为整数");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
|
||||
|
||||
@AgentComponent
|
||||
class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
|
||||
|
||||
private static final String CAPABILITY_LOCATION = BUILTIN_LOCATION + "::" + "capability";
|
||||
|
||||
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Agent Capability");
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
@InjectCapability
|
||||
private MemoryCapability memoryCapability;
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
return List.of(
|
||||
buildInitiateTurnDefinition(),
|
||||
buildMemoryRecallDefinition()
|
||||
);
|
||||
}
|
||||
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildMemoryRecallDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Memory");
|
||||
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"unit_id", "The id of the memory unit that contains the target memory slice.",
|
||||
"slice_id", "The id of the memory slice to recall into context."
|
||||
),
|
||||
"Recall the target memory slice into context using its unit_id and slice_id. " +
|
||||
"This action loads the slice's original conversation messages as a short-lived recalled memory context block.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"ok", "boolean, whether the target memory slice is found and recalled successfully",
|
||||
"message", "string, short execution result description"
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
String unitId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "unit_id");
|
||||
String sliceId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "slice_id");
|
||||
Result<MemorySlice> sliceResult = memoryCapability.getMemorySlice(unitId, sliceId);
|
||||
if (sliceResult.exceptionOrNull() != null) {
|
||||
return JSONObject.of(
|
||||
"ok", false,
|
||||
"message", sliceResult.exceptionOrNull().getLocalizedMessage()
|
||||
).toJSONString();
|
||||
}
|
||||
MemorySlice slice = sliceResult.getOrThrow();
|
||||
|
||||
MemoryUnit unit = memoryCapability.getMemoryUnit(unitId);
|
||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||
buildMemoryRecallFullBlock(unit, slice),
|
||||
Set.of(ContextBlock.FocusedDomain.MEMORY),
|
||||
60,
|
||||
16,
|
||||
28
|
||||
));
|
||||
|
||||
return JSONObject.of(
|
||||
"ok", true,
|
||||
"message", "Memory slice found and recalled into context"
|
||||
).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("memory_recall"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnit unit, MemorySlice slice) {
|
||||
return new BlockContent("memory_recall", "memory_capability") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
root.setAttribute("unit_id", unit.getId());
|
||||
root.setAttribute("slice_id", slice.getId());
|
||||
appendRepeatedElements(document, root, "message", unit.getConversationMessages().subList(slice.getStartIndex(), slice.getEndIndex()), (messageElement, message) -> {
|
||||
messageElement.setAttribute("role", message.getRole().name().toLowerCase(Locale.ROOT));
|
||||
messageElement.setTextContent(message.getContent());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于发起自对话的 Builtin MetaAction
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildInitiateTurnDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Agent Turn");
|
||||
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"input", "Input required to initiate an internal Agent Turn.",
|
||||
"target", "The people expected to reply to this internal Agent Turn."
|
||||
),
|
||||
"Create an internal Agent Turn to resolve a task.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"result", "turn initiate result"
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
String input = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "input");
|
||||
String target = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "target");
|
||||
cognitionCapability.initiateTurn(input, target);
|
||||
return "agent turn initiated";
|
||||
};
|
||||
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("initiate_turn"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createActionKey(String actionName) {
|
||||
return CAPABILITY_LOCATION + "::" + actionName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.Duration;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
|
||||
|
||||
class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
|
||||
private static final String COMMAND_LOCATION = BUILTIN_LOCATION + "::" + "command";
|
||||
private static final String COMMAND_ARG_PREFIX = "arg";
|
||||
private static final int DEFAULT_READ_LIMIT = 4096;
|
||||
private static final int SUMMARY_MAX_LINES = 5;
|
||||
private static final int SUMMARY_MAX_LENGTH = 2048;
|
||||
private static final Duration COMMAND_SESSION_TTL = Duration.ofMinutes(10);
|
||||
private static final int COMMAND_SESSION_TAIL_LIMIT = 64 * 1024;
|
||||
private static final Duration OUTPUT_FLUSH_INTERVAL = Duration.ofMillis(100);
|
||||
private static final Path COMMAND_SESSION_LOG_DIR = Path.of(System.getProperty("java.io.tmpdir"), "partner-command-sessions");
|
||||
|
||||
private final Set<String> basicTags = Set.of("Builtin MetaAction", "System Command Tool");
|
||||
|
||||
private final ConcurrentHashMap<String, CommandHandle> commandHandles = new ConcurrentHashMap<>();
|
||||
private final CommandExecutionService commandExecutionService = CommandExecutionService.INSTANCE;
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
return List.of(
|
||||
buildCommandExecuteDefinition(),
|
||||
buildCommandStartDefinition(),
|
||||
buildCommandInspectDefinition(),
|
||||
buildCommandReadDefinition(),
|
||||
buildCommandCancelDefinition(),
|
||||
buildCommandOverviewDefinition()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于直接执行的 Builtin MetaAction
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据,参数为常规命令列表,返回值为该命令的响应内容
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandExecuteDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Command Execution");
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("arg / argN", "Command arguments. Use arg for first argument, arg1/arg2... for remaining arguments."),
|
||||
"Execute any allowed system commands and get result instantly, the number of arguments is not limited.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("inspect")),
|
||||
false,
|
||||
JSONObject.of("result", "Command execution result text.")
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
List<String> commands = requireCommandArguments(params);
|
||||
CommandExecutionService.Result result = commandExecutionService.exec(wrapCommands(commands));
|
||||
return JSONObject.of("result", result.getTotal()).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("execute"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于启动后台命令的 Builtin MetaAction,后台将持续接收 stdout/stderr
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据,参数为命令列表及进程描述,返回值为进程句柄 id
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandStartDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Command Session");
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"desc", "Command session description.",
|
||||
"arg / argN", "Command arguments. Use arg for first argument, arg1/arg2... for remaining arguments."
|
||||
),
|
||||
"Start a background command session and return execution id.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("inspect")),
|
||||
false,
|
||||
JSONObject.of("executionId", "Command execution session id.")
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
cleanupExpiredHandles();
|
||||
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc");
|
||||
List<String> commands = requireCommandArguments(params);
|
||||
CommandExecutionService.CommandSession session = commandExecutionService.createSessionTask(wrapCommands(commands));
|
||||
String executionId = UUID.randomUUID().toString();
|
||||
Path stdoutLogPath = createSessionLogPath(executionId, "stdout.log");
|
||||
Path stderrLogPath = createSessionLogPath(executionId, "stderr.log");
|
||||
CommandHandle handle = new CommandHandle(
|
||||
executionId,
|
||||
desc,
|
||||
commands,
|
||||
Instant.now(),
|
||||
session.getProcess(),
|
||||
session.getStdoutBuffer(),
|
||||
session.getStderrBuffer(),
|
||||
new StringBuilder(),
|
||||
new StringBuilder(),
|
||||
stdoutLogPath,
|
||||
stderrLogPath,
|
||||
null,
|
||||
null
|
||||
);
|
||||
commandHandles.put(executionId, handle);
|
||||
startOutputFlusher(handle);
|
||||
monitorProcess(handle);
|
||||
return JSONObject.of("executionId", executionId).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("start"), info, invoker);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于返回指定后台 Builtin MetaAction 的摘要内容
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据,参数为进程 id,返回值为摘要内容 JSON
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandInspectDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Command Session");
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("id", "Command session id."),
|
||||
"Inspect a background command session.",
|
||||
tags,
|
||||
Set.of(createActionKey("overview")),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"executionId", "Command execution session id.",
|
||||
"desc", "Command session description.",
|
||||
"exitCode", "Process exit code. Null when the process is still running.",
|
||||
"stdoutSize", "Current stdout buffer size in characters.",
|
||||
"stderrSize", "Current stderr buffer size in characters.",
|
||||
"stdoutSummary", "Summary text for stdout output.",
|
||||
"stderrSummary", "Summary text for stderr output.",
|
||||
"startAt", "Command session start time.",
|
||||
"endAt", "Command session end time. Null when the process is still running."
|
||||
)
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
cleanupExpiredHandles();
|
||||
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
||||
flushHandleBuffers(handle);
|
||||
return JSONObject.of(
|
||||
"executionId", handle.executionId,
|
||||
"desc", handle.desc,
|
||||
"exitCode", handle.exitCode,
|
||||
"stdoutSize", streamLength(handle.stdoutLogPath, handle.stdoutSourceBuffer),
|
||||
"stderrSize", streamLength(handle.stderrLogPath, handle.stderrSourceBuffer),
|
||||
"stdoutSummary", summarizeBuffer(handle.stdoutBuffer),
|
||||
"stderrSummary", summarizeBuffer(handle.stderrBuffer),
|
||||
"startAt", handle.startAt,
|
||||
"endAt", handle.exitAt
|
||||
).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("inspect"), info, invoker);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于读取指定后台 Builtin MetaAction 的输出内容
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据,参数为进程 id 与读取流(stdout/stderr),返回值为读取内容 JSON
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandReadDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Command Session");
|
||||
tags.add("Command Read");
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"id", "Command execution session id.",
|
||||
"stream", "Target stream, stdout or stderr. Default stdout.",
|
||||
"offset", "Read start offset. Default 0.",
|
||||
"limit", "Read max length. Default 4096."
|
||||
),
|
||||
"Read output from a background command session.",
|
||||
tags,
|
||||
Set.of(createActionKey("overview")),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"executionId", "Command execution session id.",
|
||||
"desc", "Command session description.",
|
||||
"stream", "The stream that was read, stdout or stderr.",
|
||||
"content", "Content read from the selected stream in this call.",
|
||||
"contentTruncated", "Whether the content was truncated because of the read limit.",
|
||||
"offset", "Read start offset used in this call.",
|
||||
"nextOffset", "Suggested next offset for the following read.",
|
||||
"eof", "Whether the stream has reached end-of-file and the process has already exited."
|
||||
)
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
cleanupExpiredHandles();
|
||||
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
||||
flushHandleBuffers(handle);
|
||||
String stream = BuiltinActionRegistry.BuiltinActionDefinition.optionalString(params, "stream", "stdout");
|
||||
if (!"stdout".equals(stream) && !"stderr".equals(stream)) {
|
||||
throw new IllegalArgumentException("参数 stream 只能为 stdout 或 stderr");
|
||||
}
|
||||
int offset = BuiltinActionRegistry.BuiltinActionDefinition.optionalInt(params, "offset", 0);
|
||||
int limit = BuiltinActionRegistry.BuiltinActionDefinition.optionalInt(params, "limit", DEFAULT_READ_LIMIT);
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("参数 offset 必须大于等于 0");
|
||||
}
|
||||
if (limit <= 0) {
|
||||
throw new IllegalArgumentException("参数 limit 必须大于 0");
|
||||
}
|
||||
|
||||
Path logPath = "stderr".equals(stream) ? handle.stderrLogPath : handle.stdoutLogPath;
|
||||
StreamChunk chunk = readChunk(logPath, offset, limit);
|
||||
boolean eof = !handle.isRunning() && chunk.nextOffset >= chunk.totalLength;
|
||||
|
||||
return JSONObject.of(
|
||||
"executionId", handle.executionId,
|
||||
"desc", handle.desc,
|
||||
"stream", stream,
|
||||
"content", chunk.content,
|
||||
"contentTruncated", chunk.truncated,
|
||||
"offset", chunk.offset,
|
||||
"nextOffset", chunk.nextOffset,
|
||||
"eof", eof
|
||||
).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("read"), info, invoker);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于取消指定后台命令的 Builtin MetaAction
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据,参数为进程 id,返回值为是否成功取消
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandCancelDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Command Session");
|
||||
tags.add("Command Cancel");
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("id", "Command session id."),
|
||||
"Cancel a background command session.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("overview")),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"ok", "Whether the command has been cancelled.",
|
||||
"executionId", "Command execution session id."
|
||||
)
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
cleanupExpiredHandles();
|
||||
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
||||
flushHandleBuffers(handle);
|
||||
if (handle.process.isAlive()) {
|
||||
handle.process.destroy();
|
||||
waitProcessExit(handle.process, 200);
|
||||
if (handle.process.isAlive()) {
|
||||
handle.process.destroyForcibly();
|
||||
waitProcessExit(handle.process, 200);
|
||||
}
|
||||
}
|
||||
if (!handle.process.isAlive()) {
|
||||
try {
|
||||
handle.exitCode = handle.process.exitValue();
|
||||
} catch (IllegalThreadStateException ignored) {
|
||||
}
|
||||
if (handle.exitAt == null) {
|
||||
handle.exitAt = Instant.now();
|
||||
}
|
||||
}
|
||||
return JSONObject.of(
|
||||
"ok", !handle.process.isAlive(),
|
||||
"executionId", handle.executionId
|
||||
).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("cancel"), info, invoker);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于列出全量后台进程的 Builtin MetaAction
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据,无参数,返回值为后台进程集合 JSON
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandOverviewDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Command Session");
|
||||
tags.add("Command Overview");
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"List all background command sessions.",
|
||||
tags,
|
||||
Set.of(createActionKey("start")),
|
||||
Set.of(createActionKey("inspect"), createActionKey("read"), createActionKey("cancel")),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"commands", "Array of command session overview items.",
|
||||
"command.executionId", "Command execution session id for each overview item.",
|
||||
"command.desc", "Command session description for each overview item.",
|
||||
"command.exitCode", "Process exit code for each overview item. Null when still running."
|
||||
)
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
cleanupExpiredHandles();
|
||||
commandHandles.values().forEach(this::flushHandleBuffers);
|
||||
List<JSONObject> items = commandHandles.values().stream()
|
||||
.sorted(Comparator.comparing(handle -> handle.startAt))
|
||||
.map(handle -> JSONObject.of(
|
||||
"executionId", handle.executionId,
|
||||
"desc", handle.desc,
|
||||
"exitCode", handle.exitCode
|
||||
))
|
||||
.toList();
|
||||
return JSONObject.of("result", items).toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("overview"), info, invoker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createActionKey(String actionName) {
|
||||
return COMMAND_LOCATION + "::" + actionName;
|
||||
}
|
||||
|
||||
private void monitorProcess(CommandHandle handle) {
|
||||
Thread.startVirtualThread(() -> {
|
||||
try {
|
||||
handle.exitCode = handle.process.waitFor();
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
flushHandleBuffers(handle);
|
||||
handle.exitAt = Instant.now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startOutputFlusher(CommandHandle handle) {
|
||||
Thread.startVirtualThread(() -> {
|
||||
try {
|
||||
while (handle.process.isAlive()) {
|
||||
flushHandleBuffers(handle);
|
||||
Thread.sleep(OUTPUT_FLUSH_INTERVAL.toMillis());
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
flushHandleBuffers(handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void flushHandleBuffers(CommandHandle handle) {
|
||||
flushStreamBuffer(handle.stdoutSourceBuffer, handle.stdoutBuffer, handle.stdoutLogPath);
|
||||
flushStreamBuffer(handle.stderrSourceBuffer, handle.stderrBuffer, handle.stderrLogPath);
|
||||
}
|
||||
|
||||
private void flushStreamBuffer(StringBuilder sourceBuffer, StringBuilder tailBuffer, Path logPath) {
|
||||
String chunk;
|
||||
synchronized (sourceBuffer) {
|
||||
if (sourceBuffer.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
chunk = sourceBuffer.toString();
|
||||
sourceBuffer.setLength(0);
|
||||
}
|
||||
try {
|
||||
Files.writeString(logPath, chunk, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("写入命令输出日志失败: " + logPath, e);
|
||||
}
|
||||
synchronized (tailBuffer) {
|
||||
tailBuffer.append(chunk);
|
||||
trimTailBuffer(tailBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void trimTailBuffer(StringBuilder buffer) {
|
||||
if (buffer.length() <= COMMAND_SESSION_TAIL_LIMIT) {
|
||||
return;
|
||||
}
|
||||
buffer.delete(0, buffer.length() - COMMAND_SESSION_TAIL_LIMIT);
|
||||
}
|
||||
|
||||
private void cleanupExpiredHandles() {
|
||||
Instant now = Instant.now();
|
||||
Iterator<Map.Entry<String, CommandHandle>> iterator = commandHandles.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, CommandHandle> entry = iterator.next();
|
||||
CommandHandle handle = entry.getValue();
|
||||
if (!isExpired(handle, now)) {
|
||||
continue;
|
||||
}
|
||||
flushHandleBuffers(handle);
|
||||
cleanupLogFiles(handle);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExpired(CommandHandle handle, Instant now) {
|
||||
Instant exitTime = handle.exitAt;
|
||||
return exitTime != null && !exitTime.plus(COMMAND_SESSION_TTL).isAfter(now);
|
||||
}
|
||||
|
||||
private Path createSessionLogPath(String executionId, String fileName) {
|
||||
try {
|
||||
Files.createDirectories(COMMAND_SESSION_LOG_DIR);
|
||||
Path file = COMMAND_SESSION_LOG_DIR.resolve(executionId + "-" + fileName);
|
||||
Files.deleteIfExists(file);
|
||||
Files.createFile(file);
|
||||
return file;
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("创建命令会话日志文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private long streamLength(Path logPath, StringBuilder sourceBuffer) {
|
||||
long fileLength;
|
||||
try {
|
||||
fileLength = Files.exists(logPath) ? Files.size(logPath) : 0L;
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("读取命令会话日志长度失败", e);
|
||||
}
|
||||
synchronized (sourceBuffer) {
|
||||
return fileLength + sourceBuffer.length();
|
||||
}
|
||||
}
|
||||
|
||||
private StreamChunk readChunk(Path logPath, int offset, int limit) {
|
||||
if (!Files.exists(logPath)) {
|
||||
return new StreamChunk("", 0, 0, false, 0);
|
||||
}
|
||||
try (RandomAccessFile raf = new RandomAccessFile(logPath.toFile(), "r")) {
|
||||
int totalLength = (int) raf.length();
|
||||
int safeOffset = Math.min(offset, totalLength);
|
||||
int nextOffset = Math.min(safeOffset + limit, totalLength);
|
||||
byte[] bytes = new byte[nextOffset - safeOffset];
|
||||
raf.seek(safeOffset);
|
||||
raf.readFully(bytes);
|
||||
String content = new String(bytes, StandardCharsets.UTF_8);
|
||||
return new StreamChunk(content, safeOffset, nextOffset, nextOffset < totalLength, totalLength);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("读取命令会话日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupLogFiles(CommandHandle handle) {
|
||||
try {
|
||||
Files.deleteIfExists(handle.stdoutLogPath);
|
||||
Files.deleteIfExists(handle.stderrLogPath);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private WrappedLaunchSpec wrapCommands(List<String> commands) {
|
||||
return ExecutionPolicyRegistry.INSTANCE.prepare(commands);
|
||||
}
|
||||
|
||||
private CommandHandle requireHandle(String id) {
|
||||
CommandHandle handle = commandHandles.get(id);
|
||||
if (handle == null) {
|
||||
throw new IllegalArgumentException("未找到对应命令会话: " + id);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
private List<String> requireCommandArguments(Map<String, Object> params) {
|
||||
List<Map.Entry<String, Object>> entries = params.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().startsWith(COMMAND_ARG_PREFIX))
|
||||
.sorted(Comparator.comparingInt(entry -> commandArgIndex(entry.getKey())))
|
||||
.toList();
|
||||
if (entries.isEmpty()) {
|
||||
throw new IllegalArgumentException("缺少命令参数");
|
||||
}
|
||||
List<String> commands = new ArrayList<>();
|
||||
for (Map.Entry<String, Object> entry : entries) {
|
||||
commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, entry.getKey()));
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
private int commandArgIndex(String key) {
|
||||
String suffix = key.substring(COMMAND_ARG_PREFIX.length()).trim();
|
||||
if (suffix.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(suffix);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private String bufferSnapshot(StringBuilder buffer) {
|
||||
synchronized (buffer) {
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private String summarizeBuffer(StringBuilder buffer) {
|
||||
String snapshot = bufferSnapshot(buffer);
|
||||
if (snapshot.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
List<String> lines = snapshot.lines().toList();
|
||||
if (lines.size() <= SUMMARY_MAX_LINES * 2) {
|
||||
return trimSummary(snapshot);
|
||||
}
|
||||
|
||||
List<String> head = lines.subList(0, SUMMARY_MAX_LINES);
|
||||
List<String> tail = lines.subList(Math.max(lines.size() - SUMMARY_MAX_LINES, SUMMARY_MAX_LINES), lines.size());
|
||||
String summary = String.join("\n", head)
|
||||
+ "\n...\n"
|
||||
+ String.join("\n", tail);
|
||||
return trimSummary(summary);
|
||||
}
|
||||
|
||||
private String trimSummary(String content) {
|
||||
if (content.length() <= SUMMARY_MAX_LENGTH) {
|
||||
return content;
|
||||
}
|
||||
return content.substring(0, SUMMARY_MAX_LENGTH);
|
||||
}
|
||||
|
||||
private void waitProcessExit(Process process, long millis) {
|
||||
try {
|
||||
process.waitFor(millis, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private record StreamChunk(String content, int offset, int nextOffset, boolean truncated, int totalLength) {
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
private static class CommandHandle {
|
||||
private String executionId;
|
||||
private String desc;
|
||||
private List<String> commands;
|
||||
private Instant startAt;
|
||||
|
||||
private Process process;
|
||||
|
||||
private StringBuilder stdoutSourceBuffer;
|
||||
private StringBuilder stderrSourceBuffer;
|
||||
|
||||
/**
|
||||
* stdout/stderr 摘要 tail
|
||||
*/
|
||||
private StringBuilder stdoutBuffer;
|
||||
private StringBuilder stderrBuffer;
|
||||
|
||||
private Path stdoutLogPath;
|
||||
private Path stderrLogPath;
|
||||
|
||||
/**
|
||||
* 退出码:进程未结束时可为 null
|
||||
*/
|
||||
private volatile Integer exitCode;
|
||||
private volatile Instant exitAt;
|
||||
|
||||
boolean isRunning() {
|
||||
return exitCode == null && process.isAlive();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import kotlin.Unit;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
|
||||
|
||||
@AgentComponent
|
||||
class BuiltinDynamicActionProvider implements BuiltinActionProvider {
|
||||
private static final String ORIGIN_LOCATION = "origin";
|
||||
private static final long TEMP_ACTION_TTL_MILLIS = 30 * 60 * 1000L;
|
||||
private static final String DYNAMIC_LOCATION = BUILTIN_LOCATION + "::" + "dynamic";
|
||||
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Dynamic Generation");
|
||||
private final ConcurrentHashMap<String, TempDynamicActionRecord> tempDynamicActions = new ConcurrentHashMap<>();
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
@InjectModule
|
||||
private ActionScheduler actionScheduler;
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
return List.of(
|
||||
buildGenerateDynamicActionDefinition(),
|
||||
buildPersistDynamicActionDefinition()
|
||||
);
|
||||
}
|
||||
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildGenerateDynamicActionDefinition() {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"desc", "Human-readable description for the temporary action. Required because the generated action name is only a short id.",
|
||||
"code", "Dynamic action source code content.",
|
||||
"codeType", "Code extension, for example py/sh/js.",
|
||||
"launcher", "Interpreter or launcher used for ORIGIN execution.",
|
||||
"meta", "MetaActionInfo extra fields as JSON string. Available fields example: {\"io\":true,\"params\":{\"input\":\"user input\"},\"tags\":[\"dynamic\"],\"preActions\":[\"builtin::command::execute\"],\"postActions\":[\"builtin::dynamic::persist\"],\"strictDependencies\":false,\"responseSchema\":{\"result\":\"dynamic result\"}}"
|
||||
),
|
||||
"Generate a temporary ORIGIN action from source code and return a temporary actionKey.",
|
||||
basicTags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("persist")),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"ok", "Whether the dynamic action was generated successfully.",
|
||||
"actionKey", "Temporary ORIGIN actionKey."
|
||||
)
|
||||
);
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("generate"), info, params -> {
|
||||
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc").trim();
|
||||
if (desc.isEmpty()) {
|
||||
throw new IllegalArgumentException("参数 desc 不能为空");
|
||||
}
|
||||
String code = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "code");
|
||||
String codeType = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "codeType");
|
||||
String launcher = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "launcher");
|
||||
String metaJson = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "meta");
|
||||
|
||||
JSONObject meta = parseMeta(metaJson);
|
||||
MetaActionInfo metaActionInfo = buildMetaActionInfo(meta, launcher, desc);
|
||||
|
||||
String tempName = "dyn-" + shortUuid();
|
||||
String location = actionCapability.runnerClient().buildTmpPath(tempName, codeType);
|
||||
MetaAction tempAction = new MetaAction(
|
||||
tempName,
|
||||
metaActionInfo.getIo(),
|
||||
launcher,
|
||||
MetaAction.Type.ORIGIN,
|
||||
location
|
||||
);
|
||||
String actionKey = ORIGIN_LOCATION + "::" + location;
|
||||
try {
|
||||
actionCapability.runnerClient().tmpSerialize(tempAction, code, codeType);
|
||||
} catch (java.io.IOException e) {
|
||||
throw new IllegalStateException("临时动态行动序列化失败", e);
|
||||
}
|
||||
actionCapability.registerMetaActions(Map.of(actionKey, metaActionInfo));
|
||||
|
||||
ActionFileMetaData fileMetaData = buildActionFileMetaData(location, code, codeType);
|
||||
StateAction cleanupAction = buildCleanupAction(actionKey);
|
||||
tempDynamicActions.put(actionKey, new TempDynamicActionRecord(
|
||||
actionKey,
|
||||
location,
|
||||
cleanupAction.getUuid(),
|
||||
fileMetaData,
|
||||
metaActionInfo
|
||||
));
|
||||
actionScheduler.schedule(cleanupAction);
|
||||
return JSONObject.of(
|
||||
"ok", true,
|
||||
"actionKey", actionKey
|
||||
).toJSONString();
|
||||
});
|
||||
}
|
||||
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildPersistDynamicActionDefinition() {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("actionKey", "Temporary ORIGIN actionKey returned by generate."),
|
||||
"Persist a temporary ORIGIN action and cancel its cleanup task.",
|
||||
basicTags,
|
||||
Set.of(createActionKey("generate")),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"ok", "Whether the dynamic action was persisted successfully.",
|
||||
"actionKey", "Temporary ORIGIN actionKey."
|
||||
)
|
||||
);
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("persist"), info, params -> {
|
||||
String actionKey = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionKey");
|
||||
TempDynamicActionRecord record = tempDynamicActions.get(actionKey);
|
||||
if (record == null) {
|
||||
throw new IllegalArgumentException("未找到对应临时动态行动: " + actionKey);
|
||||
}
|
||||
actionCapability.runnerClient().persistSerialize(record.metaActionInfo(), record.fileMetaData());
|
||||
actionScheduler.cancel(record.cleanupActionId());
|
||||
removeTempDynamicAction(actionKey);
|
||||
return JSONObject.of(
|
||||
"ok", true,
|
||||
"actionKey", actionKey
|
||||
).toJSONString();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createActionKey(String actionName) {
|
||||
return DYNAMIC_LOCATION + "::" + actionName;
|
||||
}
|
||||
|
||||
private JSONObject parseMeta(String metaJson) {
|
||||
try {
|
||||
return JSON.parseObject(metaJson);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("参数 meta 必须为合法 JSON 字符串", e);
|
||||
}
|
||||
}
|
||||
|
||||
private MetaActionInfo buildMetaActionInfo(JSONObject meta, String launcher, String description) {
|
||||
return new MetaActionInfo(
|
||||
Boolean.TRUE.equals(meta.getBoolean("io")),
|
||||
launcher,
|
||||
copyStringMap(meta.getJSONObject("params")),
|
||||
description,
|
||||
toOrderedSet(meta.getJSONArray("tags")),
|
||||
toOrderedSet(meta.getJSONArray("preActions")),
|
||||
toOrderedSet(meta.getJSONArray("postActions")),
|
||||
Boolean.TRUE.equals(meta.getBoolean("strictDependencies")),
|
||||
copyJsonObject(meta.getJSONObject("responseSchema"))
|
||||
);
|
||||
}
|
||||
|
||||
private ActionFileMetaData buildActionFileMetaData(String location, String code, String codeType) {
|
||||
ActionFileMetaData fileMetaData = new ActionFileMetaData();
|
||||
fileMetaData.setContent(code);
|
||||
fileMetaData.setExt(normalizeCodeType(codeType));
|
||||
fileMetaData.setName(extractFileBaseName(location, fileMetaData.getExt()));
|
||||
return fileMetaData;
|
||||
}
|
||||
|
||||
private StateAction buildCleanupAction(String actionKey) {
|
||||
return new StateAction(
|
||||
"system",
|
||||
"dynamic-action-cleanup:" + actionKey,
|
||||
"清理临时动态行动",
|
||||
Schedulable.ScheduleType.ONCE,
|
||||
ZonedDateTime.now().plusSeconds(TEMP_ACTION_TTL_MILLIS / 1000).toString(),
|
||||
new StateAction.Trigger.Call(() -> {
|
||||
removeTempDynamicAction(actionKey);
|
||||
return Unit.INSTANCE;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void removeTempDynamicAction(String actionKey) {
|
||||
TempDynamicActionRecord record = tempDynamicActions.remove(actionKey);
|
||||
if (record == null) {
|
||||
return;
|
||||
}
|
||||
actionCapability.listAvailableMetaActions().remove(actionKey);
|
||||
deleteTempFileQuietly(record.location());
|
||||
}
|
||||
|
||||
private void deleteTempFileQuietly(String location) {
|
||||
try {
|
||||
Files.deleteIfExists(Path.of(location));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> copyStringMap(JSONObject jsonObject) {
|
||||
if (jsonObject == null) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
jsonObject.forEach((key, value) -> params.put(key, value == null ? "" : String.valueOf(value)));
|
||||
return params;
|
||||
}
|
||||
|
||||
private Set<String> toOrderedSet(com.alibaba.fastjson2.JSONArray jsonArray) {
|
||||
if (jsonArray == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return jsonArray.toJavaList(String.class).stream()
|
||||
.filter(item -> item != null && !item.isBlank())
|
||||
.collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private JSONObject copyJsonObject(JSONObject jsonObject) {
|
||||
return jsonObject == null ? JSONObject.of() : JSONObject.from(jsonObject);
|
||||
}
|
||||
|
||||
private String normalizeCodeType(String codeType) {
|
||||
return codeType.startsWith(".") ? codeType.substring(1) : codeType;
|
||||
}
|
||||
|
||||
private String extractFileBaseName(String location, String ext) {
|
||||
String fileName = Path.of(location).getFileName().toString();
|
||||
String suffix = "." + ext;
|
||||
if (fileName.endsWith(suffix)) {
|
||||
return fileName.substring(0, fileName.length() - suffix.length());
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private String shortUuid() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
}
|
||||
|
||||
private record TempDynamicActionRecord(
|
||||
String actionKey,
|
||||
String location,
|
||||
String cleanupActionId,
|
||||
ActionFileMetaData fileMetaData,
|
||||
MetaActionInfo metaActionInfo
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.entity.Action;
|
||||
import work.slhaf.partner.core.action.entity.ExecutableAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
|
||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
import work.slhaf.partner.core.cognition.*;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
|
||||
|
||||
@AgentComponent
|
||||
class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
|
||||
private static final String INTERVENTION_LOCATION = BUILTIN_LOCATION + "::" + "intervention";
|
||||
|
||||
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Action Intervention");
|
||||
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
return List.of(
|
||||
buildCreateInterventionDefinition(),
|
||||
buildShowAvailableMetaActionsDefinition(),
|
||||
buildShowIntervenableActionsDefinition(),
|
||||
buildAcquireInterventionDefinition(),
|
||||
buildResumeInterruptedActionDefinition()
|
||||
);
|
||||
}
|
||||
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildResumeInterruptedActionDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Agent Turn");
|
||||
tags.add("Action Management");
|
||||
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"actionId", "Uuid of the interrupted executable action to resume."
|
||||
),
|
||||
"Resume action that is interrupted.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"result", "Plain text resume result, describes whether it is succeed or fail reason."
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId");
|
||||
try {
|
||||
ExecutableAction executableAction = getExecutableAction(actionId);
|
||||
executableAction.resume();
|
||||
return "Resume succeed";
|
||||
} catch (Exception e) {
|
||||
return "Failed to resume action[" + actionId + "], reason: " + e.getLocalizedMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("resume_interrupted_action"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 尝试向指定用户请求干预,通过自对话通道
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildAcquireInterventionDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Agent Turn");
|
||||
tags.add("Action Management");
|
||||
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
true,
|
||||
null,
|
||||
Map.of(
|
||||
"actionId", "Uuid of the executing action that should enter intervention flow.",
|
||||
"actionInfo", "Readable summary of the current action, used to explain the intervention context to the target.",
|
||||
"demand", "What feedback, decision or operation is required from the target user.",
|
||||
"target", "Target user or channel identifier that should receive the intervention request.",
|
||||
"input", "Prompt content used to initiate the intervention turn toward the target.",
|
||||
"timeout", "Maximum wait time for the interruption result, in the unit expected by ExecutableAction.interrupt(timeout)."
|
||||
),
|
||||
"Try to acquire the target user to intervene this Action.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"result", "Plain text intervention status. It describes whether the target answered before timeout, or returns an error reason when the turn/interruption flow fails."
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId").trim();
|
||||
String actionInfo = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionInfo").trim();
|
||||
String demand = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "demand").trim();
|
||||
String target = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "target").trim();
|
||||
String input = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "input").trim();
|
||||
int timeout = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "timeout");
|
||||
|
||||
ContextWorkspace contextWorkspace = cognitionCapability.contextWorkspace();
|
||||
String blockName = "acquire_intervention-" + actionId;
|
||||
String source = "action_executor";
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
new CommunicationBlockContent(blockName, source, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "Partner needs some help.");
|
||||
appendTextElement(document, root, "action_id", actionId);
|
||||
appendTextElement(document, root, "action_info", actionInfo);
|
||||
appendTextElement(document, root, "demand", demand);
|
||||
}
|
||||
},
|
||||
Set.of(ContextBlock.FocusedDomain.COMMUNICATION),
|
||||
10,
|
||||
10,
|
||||
20
|
||||
));
|
||||
|
||||
ExecutableAction executableAction = null;
|
||||
try {
|
||||
executableAction = getExecutableAction(actionId);
|
||||
cognitionCapability.initiateTurn(input, target);
|
||||
boolean normal = executableAction.interrupt(timeout);
|
||||
return normal ? target + "not resumed execution in time" : target + "answered, looking for related answer in recent-chat-messages";
|
||||
} catch (Exception e) {
|
||||
return "Error happened while calling turn: " + e.getLocalizedMessage();
|
||||
} finally {
|
||||
contextWorkspace.expire(blockName, source);
|
||||
if (executableAction != null) {
|
||||
executableAction.resume();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("acquire_intervention"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
private ExecutableAction getExecutableAction(String actionId) {
|
||||
return actionCapability.listActions(Action.Status.EXECUTING, null)
|
||||
.stream()
|
||||
.filter(action -> action.getUuid().equals(actionId))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于展示当前已存在的可被干预的行动
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildShowIntervenableActionsDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Available Resource");
|
||||
tags.add("Agent State");
|
||||
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"List existing actions that can be intervened",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"actions", "Info list of actions that could be intervened.",
|
||||
"action.tendency", "Original tendency that the action is intended to resolve.",
|
||||
"action.description", "Description of this action.",
|
||||
"action.status", "Execution status of this action.",
|
||||
"action.uuid", "Unique uuid of each action."
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
Set<ExecutableAction> executableActions = actionCapability.listActions(null, null);
|
||||
JSONArray interventions = new JSONArray();
|
||||
for (ExecutableAction action : executableActions) {
|
||||
JSONObject item = interventions.addObject();
|
||||
item.put("tendency", action.getTendency());
|
||||
item.put("description", action.getDescription());
|
||||
item.put("status", action.getStatus().name().toLowerCase());
|
||||
item.put("uuid", action.getUuid());
|
||||
}
|
||||
return interventions.toJSONString();
|
||||
};
|
||||
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("list_intervenable_actions"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于展示当前可用的 MetaAction
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildShowAvailableMetaActionsDefinition() {
|
||||
Set<String> tags = new HashSet<>(basicTags);
|
||||
tags.add("Available Resource");
|
||||
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"List available MetaActions.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"meta_actions", "Available MetaAction info list.",
|
||||
"meta_action.actionKey", "MetaAction actionKey, describing action source and its unique calling id.",
|
||||
"meta_action.description", "MetaAction description.",
|
||||
"meta_action.tags", "MetaAction tag list as string.",
|
||||
"meta_action.params", "MetaAction parameter definition JSON string."
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
Map<String, MetaActionInfo> availableMetaActions = actionCapability.listAvailableMetaActions();
|
||||
JSONArray actions = new JSONArray();
|
||||
for (Map.Entry<String, MetaActionInfo> entry : availableMetaActions.entrySet()) {
|
||||
JSONObject item = actions.addObject();
|
||||
item.put("location", entry.getKey());
|
||||
|
||||
MetaActionInfo action = entry.getValue();
|
||||
item.put("description", action.getDescription());
|
||||
item.put("tags", Arrays.toString(action.getTags().toArray()));
|
||||
item.put("params", JSONObject.toJSONString(action.getParams()));
|
||||
}
|
||||
return actions.toJSONString();
|
||||
};
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("list_available_meta_actions"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于创建一个 Action Intervention,并作用于指定的 Executable Action
|
||||
*
|
||||
* @return 内建 MetaAction 定义数据
|
||||
*/
|
||||
private BuiltinActionRegistry.BuiltinActionDefinition buildCreateInterventionDefinition() {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"id", "The uuid of the Action to be intervened on.",
|
||||
"type", "Intervention type. Allowed values: APPEND, INSERT, DELETE, CANCEL.",
|
||||
"order", "Action chain order/stage to apply the intervention on.",
|
||||
"actions", "Comma-separated actionKey list to be inserted, appended, rebuilt or deleted. Example: \"builtin::command::execute, builtin::capability::show_memory_slices\""
|
||||
),
|
||||
"Used to create an Action Intervention and act on the specified Executable Action.",
|
||||
basicTags,
|
||||
Set.of(
|
||||
createActionKey("list_available_meta_actions"),
|
||||
createActionKey("list_intervenable_actions")
|
||||
),
|
||||
Set.of(),
|
||||
true,
|
||||
JSONObject.of(
|
||||
"ok", "Intervene status",
|
||||
"result", "Intervene result or failed message."
|
||||
)
|
||||
);
|
||||
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
String targetId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id").trim();
|
||||
if (targetId.isEmpty()) {
|
||||
throw new IllegalArgumentException("参数 id 不能为空");
|
||||
}
|
||||
|
||||
InterventionType type = requireInterventionType(params);
|
||||
Integer order = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "order");
|
||||
List<String> actions = requireActions(params, type);
|
||||
ExecutableAction target = requireTargetAction(targetId);
|
||||
Result<Void> validationResult = validateActionKeys(actions);
|
||||
AgentRuntimeException validationFailure = validationResult.onFailure(ExceptionReporterHandler.INSTANCE::report).exceptionOrNull();
|
||||
if (validationFailure != null) {
|
||||
return JSONObject.of("ok", false, "result", validationFailure.getLocalizedMessage()).toJSONString();
|
||||
}
|
||||
|
||||
MetaIntervention intervention = new MetaIntervention();
|
||||
intervention.setType(type);
|
||||
intervention.setOrder(order);
|
||||
intervention.setActions(actions);
|
||||
|
||||
actionCapability.handleInterventions(List.of(intervention), target);
|
||||
|
||||
ExecutableAction executableAction = getExecutableAction(targetId);
|
||||
executableAction.resume();
|
||||
|
||||
return JSONObject.of("ok", true).toJSONString();
|
||||
};
|
||||
|
||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||
createActionKey("create_intervention"),
|
||||
info,
|
||||
invoker
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createActionKey(String actionName) {
|
||||
return INTERVENTION_LOCATION + "::" + actionName;
|
||||
}
|
||||
|
||||
private InterventionType requireInterventionType(Map<String, Object> params) {
|
||||
String typeValue = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "type").trim();
|
||||
if (typeValue.isEmpty()) {
|
||||
throw new IllegalArgumentException("参数 type 不能为空");
|
||||
}
|
||||
try {
|
||||
return InterventionType.valueOf(typeValue.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("参数 type 非法: " + typeValue, e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> requireActions(Map<String, Object> params, InterventionType type) {
|
||||
Object value = params.get("actions");
|
||||
if (value == null) {
|
||||
if (type == InterventionType.CANCEL) {
|
||||
return List.of();
|
||||
}
|
||||
throw new IllegalArgumentException("缺少参数: actions");
|
||||
}
|
||||
|
||||
if (!(value instanceof String actionsValue)) {
|
||||
throw new IllegalArgumentException("参数 actions 必须为字符串");
|
||||
}
|
||||
|
||||
String trimmedActionsValue = actionsValue.trim();
|
||||
if (trimmedActionsValue.isEmpty()) {
|
||||
if (type == InterventionType.CANCEL) {
|
||||
return List.of();
|
||||
}
|
||||
throw new IllegalArgumentException("参数 actions 不能为空");
|
||||
}
|
||||
|
||||
List<String> actions = Arrays.stream(trimmedActionsValue.split(","))
|
||||
.map(String::trim)
|
||||
.filter(item -> !item.isEmpty())
|
||||
.toList();
|
||||
|
||||
if (type != InterventionType.CANCEL && actions.isEmpty()) {
|
||||
throw new IllegalArgumentException("参数 actions 不能为空");
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
private ExecutableAction requireTargetAction(String targetId) {
|
||||
return actionCapability.listActions(null, null)
|
||||
.stream()
|
||||
.filter(action -> targetId.equals(action.getUuid()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("未找到对应的 Action: " + targetId));
|
||||
}
|
||||
|
||||
private Result<Void> validateActionKeys(List<String> actions) {
|
||||
for (String actionKey : actions) {
|
||||
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
|
||||
AgentRuntimeException failure = metaActionResult.exceptionOrNull();
|
||||
if (failure != null) {
|
||||
return Result.failure(failure);
|
||||
}
|
||||
}
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package work.slhaf.partner.module.action.executor;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.TaskBlock;
|
||||
import work.slhaf.partner.module.action.executor.entity.CorrectorInput;
|
||||
import work.slhaf.partner.module.action.executor.entity.RecognizerResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 负责在行动链执行过程中判断当前进度是否异常,是否需要引入 corrector 介入。
|
||||
*/
|
||||
public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<CorrectorInput, Result<RecognizerResult>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责在行动链执行过程中识别当前执行是否出现异常,并判断是否需要引入 corrector 介入。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
|
||||
- 一条任务消息,其中包含:
|
||||
- executable_action_info:当前正在执行的行动信息,包括 executing_action_id、original_tendency、evaluation_passed_reason、description 与 from_who;
|
||||
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description 与 status。
|
||||
|
||||
你的任务:
|
||||
- 基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
|
||||
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移,或与最新语境不再一致的情况;
|
||||
- 若需要引入 corrector,则返回 needCorrection=true,并给出简洁明确的 reason;
|
||||
- 若当前仍可继续推进,则返回 needCorrection=false。
|
||||
|
||||
识别原则:
|
||||
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的判断必须围绕这条行动本身,而不是被无关历史带偏。
|
||||
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构与已知阶段状态。
|
||||
- original_tendency 表示这条行动链最初要解决的问题;若当前链路明显偏离这一倾向,应优先视为异常信号。
|
||||
- evaluation_passed_reason 与 description 表示该行动最初为何被判定为可推进;若当前状态已经与这些前提不再一致,应考虑触发纠正。
|
||||
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常或整体失衡的主要依据。
|
||||
- communication 域用于判断最新交流语境是否已发生明显变化,导致当前行动继续推进不再合适。
|
||||
- memory 域只在与当前行动明显相关时作为辅助参考使用。
|
||||
|
||||
应优先考虑需要纠正的情形:
|
||||
- 当前行动长期没有形成有效推进,只在重复相近步骤、相近动作或相近结论;
|
||||
- 当前执行显著偏离 original_tendency,开始围绕无关目标展开;
|
||||
- 当前行动所依赖的前提已被新的交流内容或上下文推翻;
|
||||
- 当前行动链反复遇到同类失败、阻塞或空转迹象,继续按原链推进意义不大;
|
||||
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛。
|
||||
|
||||
不应轻易触发纠正的情形:
|
||||
- 只是正常的多步推进;
|
||||
- 存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
|
||||
- 行动链整体仍与 original_tendency、evaluation_passed_reason 和当前语境保持一致;
|
||||
- 仅凭旧对话、低相关记忆或轻微波动,不足以判定当前行动异常。
|
||||
|
||||
关于输出:
|
||||
- needCorrection=true 表示当前应引入 corrector 介入。
|
||||
- needCorrection=false 表示当前行动仍可继续按既有链路推进。
|
||||
- reason 用于简洁说明判断依据;若 needCorrection=false,也应给出简短理由,说明为何当前仍可继续推进。
|
||||
- 不要输出结构之外的解释、说明或额外文本。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 RecognizerResult 对应结构输出。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<RecognizerResult> doExecute(CorrectorInput input) {
|
||||
List<Message> messages = List.of(
|
||||
resolveContextMessage(),
|
||||
resolveTaskMessage(input)
|
||||
);
|
||||
return formattedChat(messages, RecognizerResult.class);
|
||||
}
|
||||
|
||||
private Message resolveTaskMessage(CorrectorInput input) {
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendChildElement(document, root, "executable_action_info", block -> {
|
||||
appendTextElement(document, block, "executing_action_id", input.getActionId());
|
||||
appendTextElement(document, block, "original_tendency", input.getTendency());
|
||||
appendTextElement(document, block, "evaluation_passed_reason", input.getReason());
|
||||
appendTextElement(document, block, "description", input.getDescription());
|
||||
appendTextElement(document, block, "from_who", input.getSource());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
appendListElement(document, root, "current_action_chain_overview", "action_chain_stage", input.getActionChainOverview().entrySet(), (stageElement, stageData) -> {
|
||||
stageElement.setAttribute("stage_count", String.valueOf(stageData.getKey()));
|
||||
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
|
||||
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
|
||||
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
|
||||
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}.encodeToMessage();
|
||||
}
|
||||
|
||||
private Message resolveContextMessage() {
|
||||
return cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
)).encodeToMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "action_correction_recognizer";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package work.slhaf.partner.module.action.executor;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.TaskBlock;
|
||||
import work.slhaf.partner.module.action.executor.entity.CorrectorInput;
|
||||
import work.slhaf.partner.module.action.executor.entity.CorrectorResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 负责在单组行动执行后,根据行动意图与结果检查后续行动是否符合目的,必要时直接调整行动链,或发起自对话请求进行干预
|
||||
*/
|
||||
public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Result<CorrectorResult>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责在行动链执行过程中进行纠偏。你的任务不是重新评估是否要启动这条行动,而是在当前行动已经进入执行后,根据原始行动意图、当前链路结构与最新语境,判断是否需要调整后续行动链。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
|
||||
- 一条任务消息,其中包含:
|
||||
- executable_action_info:当前正在执行的行动信息,包括 executing_action_id、original_tendency、evaluation_passed_reason、description 与 from_who;
|
||||
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description 与 status。
|
||||
|
||||
你的任务:
|
||||
- 基于当前上下文、原始行动意图与当前行动链进展,判断后续行动是否仍然符合目的;
|
||||
- 若当前链路仍可继续推进,则不要随意干预;
|
||||
- 若当前链路已明显跑偏、缺少必要步骤、顺序不合理、存在冗余、已经不再适合继续,或需要引入新的动作单元,则输出干预方案;
|
||||
- correctionReason 用于简洁说明为何需要这些纠偏。
|
||||
|
||||
纠偏原则:
|
||||
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的纠偏必须围绕这条行动本身,而不是转向新的无关目标。
|
||||
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构与阶段状态。
|
||||
- original_tendency 是这条行动链最初要解决的问题;后续行动的调整必须仍然围绕它。
|
||||
- evaluation_passed_reason 与 description 表示该行动最初为何能够成立;若当前链路已经与这些前提不一致,可据此进行纠偏。
|
||||
- current_action_chain_overview 是判断后续链路是否缺步骤、顺序失衡、重复冗余或整体方向错误的主要依据。
|
||||
- communication 域用于判断最新交流语境是否已经变化,导致当前链路需要调整。
|
||||
- memory 域只在与当前行动明显相关时作为辅助参考使用。
|
||||
|
||||
何时应考虑干预:
|
||||
- 当前链路缺少继续推进所必需的动作;
|
||||
- 当前链路顺序明显不合理,继续执行会降低成功率或偏离目的;
|
||||
- 某些动作已经失效、重复、无意义,或不再适合当前状态;
|
||||
- 当前链路在某一阶段之后整体方向需要重建;
|
||||
- 当前行动已不应继续推进,应直接取消整条链路。
|
||||
|
||||
何时不应轻易干预:
|
||||
- 当前只是正常的多步推进;
|
||||
- 某一步刚完成,尚不足以说明后续链路错误;
|
||||
- 没有足够依据证明当前行动链存在明显问题;
|
||||
- 仅凭旧对话、低相关记忆或轻微波动,不足以支持修改行动链。
|
||||
|
||||
关于干预类型:
|
||||
- APPEND: 在指定 order 之后追加新的动作。
|
||||
- INSERT: 在指定 order 执行过程中即时插入并执行新的动作。
|
||||
- DELETE: 删除指定 order 上的指定动作。
|
||||
- CANCEL: 取消当前行动链后续执行。
|
||||
- REBUILD: 清空当前行动链与既有执行进度,并用新的规划内容整体重建行动链。
|
||||
|
||||
关于 intervention 列表:
|
||||
- 系统会按照 metaInterventionList 的顺序逐条应用干预;前面的干预可能改变后续可理解的 order 位置,因此你在生成后续 intervention 时,必须考虑前面 intervention 已经生效后的链路形态。
|
||||
- 不要把一组本应整体表达的修改拆成互相冲突、顺序不自洽的 intervention。
|
||||
- actions 中只能填写当前系统中真实存在、可用的 action_key,不要编造不存在的动作。
|
||||
- order 必须对应当前行动链中的合理阶段位置。
|
||||
|
||||
关于 REBUILD:
|
||||
- REBUILD 表示放弃当前既有链路,并重新给出新的整体规划。
|
||||
- 一旦本次纠偏结果中使用了 REBUILD,则 metaInterventionList 中所有 intervention 都必须是 REBUILD;不要将 REBUILD 与 APPEND、INSERT、DELETE、CANCEL 混用。
|
||||
- 使用 REBUILD 时,应把新的链路规划完整表达为一组按 order 分布的 REBUILD intervention,而不是只局部补几步。
|
||||
|
||||
其他约束:
|
||||
- 若无需干预,可返回空的 metaInterventionList,并给出简短的 correctionReason 说明当前为何可继续推进。
|
||||
- 不要输出结构之外的解释、说明或额外文本。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 CorrectorResult 对应结构输出。
|
||||
- metaInterventionList 中每一项都必须是明确、可执行、且与其他 intervention 顺序一致的干预单元。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<CorrectorResult> doExecute(CorrectorInput input) {
|
||||
List<Message> messages = List.of(
|
||||
resolveContextMessage(),
|
||||
resolveTaskMessage(input)
|
||||
);
|
||||
return formattedChat(messages, CorrectorResult.class);
|
||||
}
|
||||
|
||||
private Message resolveTaskMessage(CorrectorInput input) {
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendChildElement(document, root, "executable_action_info", block -> {
|
||||
appendTextElement(document, block, "executing_action_id", input.getActionId());
|
||||
appendTextElement(document, block, "original_tendency", input.getTendency());
|
||||
appendTextElement(document, block, "evaluation_passed_reason", input.getReason());
|
||||
appendTextElement(document, block, "description", input.getDescription());
|
||||
appendTextElement(document, block, "from_who", input.getSource());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
appendListElement(document, root, "current_action_chain_overview", "action_chain_stage", input.getActionChainOverview().entrySet(), (stageElement, stageData) -> {
|
||||
stageElement.setAttribute("stage_count", String.valueOf(stageData.getKey()));
|
||||
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
|
||||
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
|
||||
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
|
||||
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}.encodeToMessage();
|
||||
}
|
||||
|
||||
private Message resolveContextMessage() {
|
||||
return cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
)).encodeToMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "action_corrector";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
package work.slhaf.partner.module.action.executor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.factory.context.Shutdown;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.executor.entity.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
|
||||
private static final int MAX_EXTRACTOR_ATTEMPTS = 3;
|
||||
|
||||
private final AssemblyHelper assemblyHelper = new AssemblyHelper();
|
||||
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@InjectModule
|
||||
private ParamsExtractor paramsExtractor;
|
||||
@InjectModule
|
||||
private ActionCorrector actionCorrector;
|
||||
@InjectModule
|
||||
private ActionCorrectionRecognizer actionCorrectionRecognizer;
|
||||
|
||||
private ExecutingActionBlockManager blockManager;
|
||||
|
||||
private ExecutorService virtualExecutor;
|
||||
private ExecutorService platformExecutor;
|
||||
private RunnerClient runnerClient;
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
virtualExecutor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
platformExecutor = actionCapability.getExecutor(ActionCore.ExecutorType.PLATFORM);
|
||||
runnerClient = actionCapability.runnerClient();
|
||||
blockManager = new ExecutingActionBlockManager(cognitionCapability.contextWorkspace());
|
||||
|
||||
Set<ExecutableAction> recoveredActions = new HashSet<>();
|
||||
recoveredActions.addAll(actionCapability.listActions(Action.Status.EXECUTING, null));
|
||||
recoveredActions.addAll(actionCapability.listActions(Action.Status.INTERRUPTED, null).stream()
|
||||
.peek(executableAction -> executableAction.setStatus(Action.Status.EXECUTING))
|
||||
.collect(Collectors.toSet()));
|
||||
if (recoveredActions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
recoveredActions.forEach(this::execute);
|
||||
blockManager.emitActionRecoveredBlock(recoveredActions);
|
||||
}
|
||||
|
||||
@Shutdown
|
||||
public void shutdown() {
|
||||
closed.set(true);
|
||||
}
|
||||
|
||||
public void execute(Action action) {
|
||||
|
||||
Future<?> future = virtualExecutor.submit(actionExecutionRouter(action));
|
||||
|
||||
virtualExecutor.execute(() -> {
|
||||
try {
|
||||
future.get(action.getTimeoutMills(), TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
action.setStatus(Action.Status.FAILED);
|
||||
if (action instanceof ExecutableAction executableAction) {
|
||||
ensureExecutableResult(executableAction, true, "行动执行超时");
|
||||
}
|
||||
log.warn("Action timeout, uuid: {}", action.getUuid());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Runnable actionExecutionRouter(Action action) {
|
||||
return () -> {
|
||||
try {
|
||||
switch (action) {
|
||||
case ExecutableAction executableAction -> handleExecutableAction(executableAction);
|
||||
case StateAction stateAction -> handleStateAction(stateAction);
|
||||
default -> handleUnknownAction(action);
|
||||
}
|
||||
if (action instanceof ExecutableAction executableAction) {
|
||||
if (action.getStatus() == Action.Status.FAILED) {
|
||||
ensureExecutableResult(executableAction, true, null);
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
return;
|
||||
}
|
||||
action.setStatus(Action.Status.SUCCESS);
|
||||
ensureExecutableResult(executableAction, false, null);
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
return;
|
||||
}
|
||||
if (action.getStatus() == Action.Status.FAILED) {
|
||||
return;
|
||||
}
|
||||
action.setStatus(Action.Status.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
log.warn("Unexpected action execution failure, uuid: {}, description: {}, failure reason: {}", action.getUuid(), action.getDescription(), e.getLocalizedMessage());
|
||||
action.setStatus(Action.Status.FAILED);
|
||||
if (action instanceof ExecutableAction executableAction) {
|
||||
ensureExecutableResult(executableAction, true, e.getLocalizedMessage());
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void handleUnknownAction(Action action) {
|
||||
log.warn("unknown Action type: {}", action.getClass().getSimpleName());
|
||||
action.setStatus(Action.Status.FAILED);
|
||||
}
|
||||
|
||||
private void handleStateAction(StateAction stateAction) {
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
blockManager.emitStateActionTriggeredBlock(stateAction);
|
||||
stateAction.getTrigger().onTrigger();
|
||||
}
|
||||
|
||||
private void handleExecutableAction(ExecutableAction executableAction) {
|
||||
actionCapability.putAction(executableAction);
|
||||
|
||||
val actionChain = executableAction.getActionChain();
|
||||
val phaser = new Phaser();
|
||||
if (!prepareExecutableAction(executableAction, actionChain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockManager.emitActionLaunchedBlock(executableAction);
|
||||
|
||||
val stageCursor = initStageCursor(executableAction, actionChain);
|
||||
while (true) {
|
||||
val stageSelection = selectCurrentStage(executableAction, actionChain);
|
||||
if (stageSelection.shouldReturn()) {
|
||||
return;
|
||||
}
|
||||
if (stageSelection.shouldStop()) {
|
||||
break;
|
||||
}
|
||||
val stageExecution = runCurrentStage(executableAction, phaser, stageCursor, stageSelection.metaActions());
|
||||
if (stageExecution.closed()) {
|
||||
return;
|
||||
}
|
||||
if (!applyStageCorrectionAndAdvance(executableAction, stageCursor, stageExecution)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
finishExecutableAction(executableAction);
|
||||
}
|
||||
|
||||
private boolean prepareExecutableAction(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
val status = executableAction.getStatus();
|
||||
if (status != Action.Status.PREPARE && status != Action.Status.EXECUTING) {
|
||||
return false;
|
||||
}
|
||||
if (actionChain.isEmpty()) {
|
||||
executableAction.setStatus(Action.Status.FAILED);
|
||||
executableAction.setResult("行动链为空");
|
||||
return false;
|
||||
}
|
||||
normalizeExecutingStage(executableAction, actionChain);
|
||||
executableAction.setStatus(Action.Status.EXECUTING);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private StageCursor initStageCursor(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
|
||||
StageCursor stageCursor = new StageCursor(executableAction, actionChain);
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
stageCursor.init();
|
||||
}
|
||||
return stageCursor;
|
||||
}
|
||||
|
||||
private StageSelection selectCurrentStage(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
if (closed.get()) {
|
||||
return StageSelection.returnNow();
|
||||
}
|
||||
if (executableAction.getStatus() == Action.Status.FAILED) {
|
||||
return StageSelection.stop();
|
||||
}
|
||||
return StageSelection.continueWith(actionChain.get(executableAction.getExecutingStage()));
|
||||
}
|
||||
}
|
||||
|
||||
private StageExecution runCurrentStage(
|
||||
ExecutableAction executableAction,
|
||||
Phaser phaser,
|
||||
StageCursor stageCursor,
|
||||
List<MetaAction> metaActions
|
||||
) {
|
||||
val recognizerRecord = startRecognizerIfNeeded(executableAction, phaser);
|
||||
val listeningRecord = executeAndListening(metaActions, phaser, executableAction);
|
||||
phaser.awaitAdvance(listeningRecord.phase());
|
||||
// synchronized 同步防止 accepting 循环间、phase guard 判定后发生 stage 推进
|
||||
// 导致新行动的 phaser 投放阶段错乱无法阻塞的场景
|
||||
// 该 synchronized 将阶段推进与 accepting 监听 loop 捆绑为互斥的原子事件,避免了细粒度的 phaser 阶段竞态问题
|
||||
if (closed.get()) {
|
||||
return StageExecution.closed(recognizerRecord, metaActions);
|
||||
}
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
synchronized (listeningRecord.accepting()) {
|
||||
listeningRecord.accepting().set(false);
|
||||
// 立即尝试推进,本次推进中,如果前方仍有未执行 stage,将执行一次阶段推进
|
||||
stageCursor.requestAdvance();
|
||||
}
|
||||
}
|
||||
|
||||
blockManager.emitActionStageSettledBlock(executableAction);
|
||||
return StageExecution.completed(recognizerRecord, metaActions);
|
||||
}
|
||||
|
||||
private boolean applyStageCorrectionAndAdvance(
|
||||
ExecutableAction executableAction,
|
||||
StageCursor stageCursor,
|
||||
StageExecution stageExecution
|
||||
) {
|
||||
boolean hasFailedMetaAction = hasFailedMetaAction(stageExecution.metaActions());
|
||||
boolean shouldRunCorrector = hasFailedMetaAction;
|
||||
if (!shouldRunCorrector) {
|
||||
val recognizerResult = resolveRecognizerResult(stageExecution.recognizerRecord());
|
||||
shouldRunCorrector = recognizerResult != null && recognizerResult.isNeedCorrection();
|
||||
}
|
||||
if (shouldRunCorrector) {
|
||||
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction);
|
||||
actionCorrector.execute(correctorInput)
|
||||
.onSuccess(correctorResult -> {
|
||||
actionCapability.handleInterventions(correctorResult.getMetaInterventionList(), executableAction);
|
||||
blockManager.emitActionCorrectionBlock(
|
||||
executableAction,
|
||||
hasFailedMetaAction ? "has_failed_meta_action" : correctorResult.getCorrectionReason(),
|
||||
correctorResult.getMetaInterventionList()
|
||||
);
|
||||
});
|
||||
}
|
||||
// 第二次尝试进行阶段推进,本次负责补充上一次在不存在 stage时,但 corrector 执行期间发生了 actionChain 的插入事件
|
||||
// 如果第一次已经推进完毕,本次将会跳过
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
stageCursor.requestAdvance();
|
||||
return stageCursor.next();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishExecutableAction(ExecutableAction executableAction) {
|
||||
// 如果是 ScheduledActionData, 则重置 ActionData 内容,记录执行历史与最终结果
|
||||
if (executableAction instanceof SchedulableExecutableAction scheduledActionData) {
|
||||
scheduledActionData.recordAndReset();
|
||||
}
|
||||
}
|
||||
|
||||
private MetaActionsListeningRecord executeAndListening(List<MetaAction> metaActions, Phaser phaser, ExecutableAction executableAction) {
|
||||
AtomicBoolean accepting = new AtomicBoolean(true);
|
||||
AtomicInteger cursor = new AtomicInteger();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
val phase = phaser.register();
|
||||
platformExecutor.execute(() -> {
|
||||
boolean first = true;
|
||||
while (accepting.get()) {
|
||||
synchronized (accepting) {
|
||||
MetaAction next = null;
|
||||
synchronized (metaActions) {
|
||||
if (cursor.get() < metaActions.size()) {
|
||||
next = metaActions.get(cursor.getAndIncrement());
|
||||
}
|
||||
}
|
||||
if (next == null) {
|
||||
Thread.onSpinWait();
|
||||
continue;
|
||||
}
|
||||
if (phaser.getPhase() != phase) {
|
||||
metaActions.remove(next);
|
||||
log.warn("行动阶段已推进,丢弃该行动: {}", next);
|
||||
continue;
|
||||
}
|
||||
ExecutorService executor = next.getIo() ? virtualExecutor : platformExecutor;
|
||||
executor.execute(buildMataActionTask(next, phaser, executableAction));
|
||||
if (first) {
|
||||
phaser.arriveAndDeregister();
|
||||
latch.countDown();
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
// 确保执行一次,防止没来得及注册任务就已经结束
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
return new MetaActionsListeningRecord(accepting, phase);
|
||||
}
|
||||
|
||||
private Runnable buildMataActionTask(MetaAction metaAction, Phaser phaser, ExecutableAction executableAction) {
|
||||
phaser.register();
|
||||
return () -> {
|
||||
val actionKey = metaAction.getKey();
|
||||
try {
|
||||
executeMetaActionWithRetry(metaAction, executableAction);
|
||||
} catch (Exception e) {
|
||||
log.error("Action executing failed: {}", actionKey, e);
|
||||
} finally {
|
||||
phaser.arriveAndDeregister();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void executeMetaActionWithRetry(MetaAction metaAction, ExecutableAction actionData) {
|
||||
AtomicReference<String> failureReason = new AtomicReference<>("参数提取失败");
|
||||
int executingStage = actionData.getExecutingStage();
|
||||
boolean succeeded = false;
|
||||
for (int attempt = 1; attempt <= MAX_EXTRACTOR_ATTEMPTS; attempt++) {
|
||||
val result = metaAction.getResult();
|
||||
result.reset();
|
||||
metaAction.getParams().clear();
|
||||
|
||||
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription());
|
||||
AgentRuntimeException exception = extractorInputResult.exceptionOrNull();
|
||||
if (exception != null) {
|
||||
failureReason.set(exception.getMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
ExtractorInput extractorInput = extractorInputResult.getOrThrow();
|
||||
Result<ExtractorResult> extractorResultWrapped = paramsExtractor.execute(extractorInput).onFailure(exp -> failureReason.set(exp.getLocalizedMessage()));
|
||||
if (extractorResultWrapped.exceptionOrNull() != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractorResult extractorResult = extractorResultWrapped.getOrThrow();
|
||||
if (!extractorResult.isOk()) {
|
||||
failureReason.set(buildAttemptFailureReason("参数提取失败", null));
|
||||
continue;
|
||||
}
|
||||
metaAction.getParams().putAll(toMetaActionParams(extractorResult.getParams()));
|
||||
|
||||
try {
|
||||
runnerClient.submit(metaAction);
|
||||
} catch (Exception e) {
|
||||
failureReason.set(buildAttemptFailureReason("行动执行异常", e.getLocalizedMessage()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.getStatus() == MetaAction.Result.Status.SUCCESS) {
|
||||
succeeded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
failureReason.set(buildAttemptFailureReason("行动执行失败", result.getData()));
|
||||
}
|
||||
if (!succeeded) {
|
||||
metaAction.getResult().setStatus(MetaAction.Result.Status.FAILED);
|
||||
metaAction.getResult().setData(failureReason.get());
|
||||
}
|
||||
appendHistoryAction(actionData, executingStage, metaAction);
|
||||
}
|
||||
|
||||
private Map<String, Object> toMetaActionParams(List<ExtractorResult.ParamEntry> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Object> converted = new LinkedHashMap<>();
|
||||
for (ExtractorResult.ParamEntry entry : params) {
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
String name = entry.getName();
|
||||
if (name == null || name.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
converted.put(name, entry.getValue());
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
private void appendHistoryAction(ExecutableAction actionData, int executingStage, MetaAction metaAction) {
|
||||
HistoryAction historyAction = new HistoryAction(
|
||||
metaAction.getKey(),
|
||||
resolveHistoryDescription(metaAction.getKey()),
|
||||
metaAction.getResult().getData()
|
||||
);
|
||||
actionData.getHistory()
|
||||
.computeIfAbsent(executingStage, integer -> new ArrayList<>())
|
||||
.add(historyAction);
|
||||
}
|
||||
|
||||
private RecognizerTaskRecord startRecognizerIfNeeded(ExecutableAction executableAction, Phaser phaser) {
|
||||
if (!shouldRunCorrectionRecognizer(executableAction)) {
|
||||
return RecognizerTaskRecord.disabled();
|
||||
}
|
||||
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction);
|
||||
val task = buildRecognizerTask(recognizerInput, phaser);
|
||||
Future<RecognizerResult> future = virtualExecutor.submit(task);
|
||||
return new RecognizerTaskRecord(true, future);
|
||||
}
|
||||
|
||||
private Callable<RecognizerResult> buildRecognizerTask(CorrectorInput input, Phaser phaser) {
|
||||
phaser.register();
|
||||
return () -> {
|
||||
try {
|
||||
return actionCorrectionRecognizer.execute(input)
|
||||
.getOrDefault(new RecognizerResult());
|
||||
} finally {
|
||||
phaser.arriveAndDeregister();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private RecognizerResult resolveRecognizerResult(RecognizerTaskRecord record) {
|
||||
if (record == null || !record.enabled() || record.future() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!record.future().isDone()) {
|
||||
return null;
|
||||
}
|
||||
return record.future().get();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String buildAttemptFailureReason(String prefix, String detail) {
|
||||
if (detail == null || detail.isBlank()) {
|
||||
return prefix;
|
||||
}
|
||||
return prefix + ": " + detail;
|
||||
}
|
||||
|
||||
private boolean hasFailedMetaAction(List<MetaAction> metaActions) {
|
||||
return metaActions.stream().anyMatch(metaAction -> metaAction.getResult().getStatus() == MetaAction.Result.Status.FAILED);
|
||||
}
|
||||
|
||||
private boolean shouldRunCorrectionRecognizer(ExecutableAction executableAction) {
|
||||
val orderedStages = new ArrayList<>(executableAction.getActionChain().keySet());
|
||||
orderedStages.sort(Integer::compareTo);
|
||||
int totalStages = orderedStages.size();
|
||||
if (totalStages < 3) {
|
||||
return false;
|
||||
}
|
||||
int stageIndex = orderedStages.indexOf(executableAction.getExecutingStage());
|
||||
if (stageIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
if (stageIndex == totalStages - 1) {
|
||||
return true;
|
||||
}
|
||||
return stageIndex >= 2 && (stageIndex - 2) % 2 == 0;
|
||||
}
|
||||
|
||||
private void normalizeExecutingStage(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
|
||||
Integer firstStage = actionChain.keySet().stream()
|
||||
.min(Integer::compareTo)
|
||||
.orElse(null);
|
||||
if (firstStage == null) {
|
||||
return;
|
||||
}
|
||||
if (actionChain.containsKey(executableAction.getExecutingStage())) {
|
||||
return;
|
||||
}
|
||||
if (executableAction.getStatus() == Action.Status.EXECUTING) {
|
||||
resetExecutableActionForReplay(executableAction);
|
||||
}
|
||||
executableAction.setExecutingStage(firstStage);
|
||||
}
|
||||
|
||||
private void resetExecutableActionForReplay(ExecutableAction executableAction) {
|
||||
executableAction.getHistory().clear();
|
||||
executableAction.getActionChain().values().forEach(metaActions -> metaActions.forEach(metaAction -> {
|
||||
metaAction.getParams().clear();
|
||||
metaAction.getResult().reset();
|
||||
}));
|
||||
if (hasExecutableResult(executableAction)) {
|
||||
executableAction.setResult("");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureExecutableResult(ExecutableAction executableAction, boolean failed, String failureReason) {
|
||||
if (hasExecutableResult(executableAction)) {
|
||||
return;
|
||||
}
|
||||
executableAction.setResult(resolveExecutableResult(executableAction, failed, failureReason));
|
||||
}
|
||||
|
||||
private String resolveExecutableResult(ExecutableAction executableAction, boolean failed, String failureReason) {
|
||||
String extracted = extractLastMetaActionResult(executableAction);
|
||||
if (extracted != null && !extracted.isBlank()) {
|
||||
return extracted;
|
||||
}
|
||||
if (!failed) {
|
||||
return "行动执行成功";
|
||||
}
|
||||
if (failureReason != null && !failureReason.isBlank()) {
|
||||
return "行动执行失败: " + failureReason;
|
||||
}
|
||||
return "行动执行失败";
|
||||
}
|
||||
|
||||
private String extractLastMetaActionResult(ExecutableAction executableAction) {
|
||||
if (!executableAction.getHistory().isEmpty()) {
|
||||
Integer lastStage = executableAction.getHistory().keySet().stream()
|
||||
.max(Integer::compareTo)
|
||||
.orElse(null);
|
||||
if (lastStage != null) {
|
||||
List<HistoryAction> historyActions = executableAction.getHistory().get(lastStage);
|
||||
if (historyActions != null && !historyActions.isEmpty()) {
|
||||
String result = historyActions.getLast().result();
|
||||
if (result != null && !result.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!executableAction.getActionChain().isEmpty()) {
|
||||
Integer lastStage = executableAction.getActionChain().keySet().stream()
|
||||
.max(Integer::compareTo)
|
||||
.orElse(null);
|
||||
if (lastStage != null) {
|
||||
List<MetaAction> metaActions = executableAction.getActionChain().get(lastStage);
|
||||
if (metaActions != null && !metaActions.isEmpty()) {
|
||||
String result = metaActions.getLast().getResult().getData();
|
||||
if (result != null && !result.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasExecutableResult(ExecutableAction executableAction) {
|
||||
try {
|
||||
String result = executableAction.getResult();
|
||||
return result != null && !result.isBlank();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveHistoryDescription(String actionKey) {
|
||||
return actionCapability.loadMetaActionInfo(actionKey)
|
||||
.fold(
|
||||
metaActionInfo -> metaActionInfo.getDescription().isBlank() ? actionKey : metaActionInfo.getDescription(),
|
||||
exception -> actionKey
|
||||
);
|
||||
}
|
||||
|
||||
private enum StageSelectionType {
|
||||
CONTINUE,
|
||||
STOP,
|
||||
RETURN
|
||||
}
|
||||
|
||||
private record StageSelection(StageSelectionType type, List<MetaAction> metaActions) {
|
||||
private static StageSelection continueWith(List<MetaAction> metaActions) {
|
||||
return new StageSelection(StageSelectionType.CONTINUE, metaActions);
|
||||
}
|
||||
|
||||
private static StageSelection stop() {
|
||||
return new StageSelection(StageSelectionType.STOP, null);
|
||||
}
|
||||
|
||||
private static StageSelection returnNow() {
|
||||
return new StageSelection(StageSelectionType.RETURN, null);
|
||||
}
|
||||
|
||||
private boolean shouldStop() {
|
||||
return type == StageSelectionType.STOP;
|
||||
}
|
||||
|
||||
private boolean shouldReturn() {
|
||||
return type == StageSelectionType.RETURN;
|
||||
}
|
||||
}
|
||||
|
||||
private record StageExecution(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions, boolean closed) {
|
||||
private static StageExecution completed(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions) {
|
||||
return new StageExecution(recognizerRecord, metaActions, false);
|
||||
}
|
||||
|
||||
private static StageExecution closed(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions) {
|
||||
return new StageExecution(recognizerRecord, metaActions, true);
|
||||
}
|
||||
}
|
||||
|
||||
private record MetaActionsListeningRecord(AtomicBoolean accepting, int phase) {
|
||||
}
|
||||
|
||||
private record RecognizerTaskRecord(boolean enabled, Future<RecognizerResult> future) {
|
||||
private static RecognizerTaskRecord disabled() {
|
||||
return new RecognizerTaskRecord(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StageCursor {
|
||||
private final ExecutableAction executableAction;
|
||||
private final Map<Integer, List<MetaAction>> actionChain;
|
||||
|
||||
private int stageCount;
|
||||
private boolean executingStageUpdated;
|
||||
private boolean stageCountUpdated;
|
||||
|
||||
private StageCursor(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
|
||||
this.executableAction = executableAction;
|
||||
this.actionChain = actionChain;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
val orderList = new ArrayList<>(actionChain.keySet());
|
||||
orderList.sort(Integer::compareTo);
|
||||
stageCount = orderList.indexOf(executableAction.getExecutingStage());
|
||||
update();
|
||||
}
|
||||
|
||||
private void requestAdvance() {
|
||||
if (!stageCountUpdated) {
|
||||
stageCount++;
|
||||
stageCountUpdated = true;
|
||||
}
|
||||
if (stageCount < actionChain.size() && !executingStageUpdated) {
|
||||
update();
|
||||
executingStageUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
executingStageUpdated = false;
|
||||
stageCountUpdated = false;
|
||||
return stageCount < actionChain.size();
|
||||
}
|
||||
|
||||
private void update() {
|
||||
val orderList = new ArrayList<>(actionChain.keySet());
|
||||
orderList.sort(Integer::compareTo);
|
||||
executableAction.setExecutingStage(orderList.get(stageCount));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("InnerClassMayBeStatic")
|
||||
private class AssemblyHelper {
|
||||
private AssemblyHelper() {
|
||||
}
|
||||
|
||||
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) {
|
||||
return actionCapability.loadMetaActionInfo(actionKey).fold(
|
||||
metaActionInfo -> {
|
||||
ExtractorInput input = new ExtractorInput();
|
||||
input.setMetaActionInfo(metaActionInfo);
|
||||
input.setTargetActionId(uuid);
|
||||
input.setTargetActionDesc(description);
|
||||
return Result.success(input);
|
||||
},
|
||||
Result::failure
|
||||
);
|
||||
}
|
||||
|
||||
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) {
|
||||
Map<Integer, List<CorrectorInput.ActionChainItem>> overview = new LinkedHashMap<>();
|
||||
executableAction.getActionChain().forEach((stage, list) -> {
|
||||
List<CorrectorInput.ActionChainItem> overviewItems = list.stream()
|
||||
.map(metaAction -> new CorrectorInput.ActionChainItem(
|
||||
metaAction.getKey(),
|
||||
resolveHistoryDescription(metaAction.getKey()),
|
||||
metaAction.getResult().getStatus().name().toLowerCase(Locale.ROOT)
|
||||
))
|
||||
.toList();
|
||||
overview.put(stage, overviewItems);
|
||||
});
|
||||
return CorrectorInput.builder()
|
||||
.tendency(executableAction.getTendency())
|
||||
.source(executableAction.getSource())
|
||||
.reason(executableAction.getReason())
|
||||
.description(executableAction.getDescription())
|
||||
.actionId(executableAction.getUuid())
|
||||
.actionChainOverview(overview)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package work.slhaf.partner.module.action.executor;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
||||
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class ExecutingActionBlockManager {
|
||||
|
||||
private static final String SOURCE = "action_executor";
|
||||
|
||||
private final ContextWorkspace contextWorkspace;
|
||||
|
||||
ExecutingActionBlockManager(ContextWorkspace contextWorkspace) {
|
||||
this.contextWorkspace = contextWorkspace;
|
||||
}
|
||||
|
||||
void emitActionRecoveredBlock(Set<ExecutableAction> recoveredActions) {
|
||||
Set<ExecutableActionSnapshot> snapshots = recoveredActions.stream().map(ExecutableAction::snapshot).collect(Collectors.toSet());
|
||||
|
||||
String blockName = "actions_recovered";
|
||||
String emittedAt = emittedAt();
|
||||
String event = "actions_recovered";
|
||||
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
buildExecutingActionRecoveredFullBlock(snapshots, blockName, emittedAt, event),
|
||||
buildExecutingActionRecoveredCompactBlock(snapshots, blockName, emittedAt, event),
|
||||
buildExecutingActionRecoveredAbstractBlock(snapshots, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
100,
|
||||
12,
|
||||
1
|
||||
));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildExecutingActionRecoveredAbstractBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "abstract", recoveredExecutingActions.size() + " executing actions recovered.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildExecutingActionRecoveredCompactBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendListElement(document, root, "recovered_actions", "action", recoveredExecutingActions, (actionElement, action) -> {
|
||||
appendTextElement(document, actionElement, "description", action.getDescription());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildExecutingActionRecoveredFullBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendListElement(document, root, "recovered_actions", "action", recoveredExecutingActions, (actionElement, action) -> {
|
||||
appendTextElement(document, actionElement, "description", action.getDescription());
|
||||
appendTextElement(document, actionElement, "source", action.getSource());
|
||||
appendTextElement(document, actionElement, "executing_stage", action.getExecutingStage());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void emitStateActionTriggeredBlock(StateAction stateAction) {
|
||||
StateActionSnapshot snapshot = stateAction.snapshot();
|
||||
|
||||
String blockName = buildBlockName(stateAction.getUuid());
|
||||
String emittedAt = emittedAt();
|
||||
String event = "state_action_triggered";
|
||||
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
buildStateActionFullBlock(snapshot, blockName, emittedAt, event),
|
||||
buildStateActionCompactBlock(snapshot, blockName, emittedAt, event),
|
||||
buildStateActionAbstractBlock(snapshot, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
70,
|
||||
18,
|
||||
10
|
||||
));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildStateActionAbstractBlock(StateActionSnapshot snapshot, String blockName, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "description", snapshot.getDescription());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildStateActionCompactBlock(StateActionSnapshot snapshot, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "reason", snapshot.getReason());
|
||||
appendTextElement(document, root, "description", snapshot.getDescription());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildStateActionFullBlock(StateActionSnapshot snapshot, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "reason", snapshot.getReason());
|
||||
appendTextElement(document, root, "description", snapshot.getDescription());
|
||||
appendTextElement(document, root, "source", snapshot.getSource());
|
||||
appendTextElement(document, root, "schedule_type", snapshot.getScheduleType().name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void emitActionLaunchedBlock(ExecutableAction action) {
|
||||
ExecutableActionSnapshot snapshot = action.snapshot();
|
||||
|
||||
String blockName = buildBlockName(action.getUuid());
|
||||
String emittedAt = emittedAt();
|
||||
String event = "executable_action_launched";
|
||||
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
buildActionLaunchedFullBlock(snapshot, blockName, event, emittedAt),
|
||||
buildActionCompactBlock(snapshot, blockName, event, emittedAt),
|
||||
buildActionAbstractBlock(snapshot, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
28,
|
||||
6,
|
||||
18
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "description", snapshot.getDescription());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionCompactBlock(ExecutableActionSnapshot snapshot, String blockName, String event, String emittedAt) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "primary_action_chain_size", snapshot.getActionChainSize());
|
||||
appendTextElement(document, root, "reason", snapshot.getReason());
|
||||
appendTextElement(document, root, "description", snapshot.getDescription());
|
||||
|
||||
Schedulable.ScheduleType scheduleType = snapshot.getScheduleType();
|
||||
String scheduleContent = snapshot.getScheduleContent();
|
||||
|
||||
if (scheduleType != null && scheduleContent != null) {
|
||||
appendChildElement(document, root, "schedule_info", (element) -> {
|
||||
appendTextElement(document, element, "schedule_type", scheduleType.name().toLowerCase(Locale.ROOT));
|
||||
appendTextElement(document, element, "schedule_content", scheduleContent);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionLaunchedFullBlock(ExecutableActionSnapshot snapshot, String blockName, String event, String emittedAt) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "primary_action_chain_size", snapshot.getActionChainSize());
|
||||
appendTextElement(document, root, "reason", snapshot.getReason());
|
||||
appendTextElement(document, root, "description", snapshot.getDescription());
|
||||
appendTextElement(document, root, "source", snapshot.getSource());
|
||||
appendTextElement(document, root, "tendency", snapshot.getTendency());
|
||||
|
||||
Schedulable.ScheduleType scheduleType = snapshot.getScheduleType();
|
||||
String scheduleContent = snapshot.getScheduleContent();
|
||||
|
||||
if (scheduleType != null && scheduleContent != null) {
|
||||
appendChildElement(document, root, "schedule_info", (element) -> {
|
||||
appendTextElement(document, element, "schedule_type", scheduleType.name().toLowerCase(Locale.ROOT));
|
||||
appendTextElement(document, element, "schedule_content", scheduleContent);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void emitActionStageSettledBlock(ExecutableAction action) {
|
||||
ExecutableActionSnapshot snapshot = action.snapshot();
|
||||
|
||||
String blockName = buildBlockName(action.getUuid());
|
||||
String emittedAt = emittedAt();
|
||||
String event = "executable_action_stage_settled";
|
||||
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
buildActionStageFullBlock(snapshot, blockName, emittedAt, event),
|
||||
buildActionStageCompactBlock(snapshot, blockName, emittedAt, event),
|
||||
buildActionStageAbstractBlock(snapshot, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
55,
|
||||
10,
|
||||
12
|
||||
));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionStageAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
|
||||
int settledStage = snapshot.getExecutingStage();
|
||||
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
|
||||
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "abstract", history.size() + " meta actions are resolved in stage " + settledStage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionStageCompactBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
|
||||
int settledStage = snapshot.getExecutingStage();
|
||||
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
|
||||
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
|
||||
appendTextElement(document, root, "abstract", history.size() + " meta actions are resolved in stage " + settledStage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionStageFullBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
|
||||
int settledStage = snapshot.getExecutingStage();
|
||||
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
|
||||
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
|
||||
appendTextElement(document, root, "settled_action_chain_stage", settledStage);
|
||||
appendListElement(document,
|
||||
root,
|
||||
"settled_meta_actions",
|
||||
"meta_action",
|
||||
history.subList(0, Math.min(3, history.size())),
|
||||
(item, action) -> {
|
||||
String primaryResult = action.result();
|
||||
String result = primaryResult.length() > 160 ? primaryResult.substring(0, 160) : primaryResult;
|
||||
|
||||
appendTextElement(document, item, "action_key", action.actionKey());
|
||||
appendTextElement(document, item, "result", result);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void emitActionCorrectionBlock(ExecutableAction action, String reason, List<MetaIntervention> interventions) {
|
||||
ExecutableActionSnapshot snapshot = action.snapshot();
|
||||
|
||||
String blockName = buildBlockName(action.getUuid());
|
||||
String emittedAt = emittedAt();
|
||||
String event = "executable_action_correction_triggered";
|
||||
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
buildActionCorrectionFullBlock(snapshot, reason, interventions, blockName, emittedAt, event),
|
||||
buildActionCorrectionCompactBlock(snapshot, reason, interventions, blockName, emittedAt, event),
|
||||
buildActionCorrectionAbstractBlock(snapshot, interventions, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
22,
|
||||
5,
|
||||
22
|
||||
));
|
||||
}
|
||||
|
||||
private List<HistoryAction> resolveStageHistory(ExecutableActionSnapshot snapshot, int settledStage) {
|
||||
List<HistoryAction> history = snapshot.getHistory().get(settledStage);
|
||||
return history == null ? List.of() : history;
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionCorrectionAbstractBlock(ExecutableActionSnapshot snapshot, List<MetaIntervention> interventions, String blockName, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "abstract", interventions.size() + " interventions occurred after stage " + snapshot.getExecutingStage());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionCorrectionCompactBlock(ExecutableActionSnapshot snapshot, String reason, List<MetaIntervention> interventions, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
Set<Integer> affectedStage = interventions.stream().map(MetaIntervention::getOrder).collect(Collectors.toSet());
|
||||
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
|
||||
appendTextElement(document, root, "abstract", interventions.size() + " interventions occurred after stage " + snapshot.getExecutingStage() + ", stage: " + affectedStage + " are affected");
|
||||
appendTextElement(document, root, "correction_reason", reason);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionCorrectionFullBlock(ExecutableActionSnapshot snapshot, String reason, List<MetaIntervention> interventions, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
|
||||
appendTextElement(document, root, "correction_reason", reason);
|
||||
appendListElement(document,
|
||||
root,
|
||||
"applied_interventions",
|
||||
"intervention",
|
||||
interventions,
|
||||
(item, intervention) -> {
|
||||
appendTextElement(document, item, "type", intervention.getType().name().toLowerCase(Locale.ROOT));
|
||||
appendTextElement(document, item, "affected_stage", intervention.getOrder());
|
||||
appendTextElement(document, item, "applied_action_key_set", intervention.getActions());
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void emitActionFinishedBlock(ExecutableAction action) {
|
||||
ExecutableActionSnapshot snapshot = action.snapshot();
|
||||
|
||||
String blockName = buildBlockName(action.getUuid());
|
||||
String emittedAt = emittedAt();
|
||||
String event = "executable_action_finished";
|
||||
|
||||
contextWorkspace.register(new ContextBlock(
|
||||
buildActionFinishedFullBlock(snapshot, blockName, emittedAt, event),
|
||||
buildActionFinishedFullBlock(snapshot, blockName, emittedAt, event),
|
||||
buildActionFinishedAbstractBlock(snapshot, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
35,
|
||||
14,
|
||||
24
|
||||
));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionFinishedAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionFinishedFullBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
|
||||
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendEventElement(document, root, event);
|
||||
appendTextElement(document, root, "emitted_at", emittedAt);
|
||||
appendTextElement(document, root, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
|
||||
appendTextElement(document, root, "result", snapshot.getResult());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String emittedAt() {
|
||||
return ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
private String buildBlockName(String actionId) {
|
||||
return "executing_action-" + actionId;
|
||||
}
|
||||
|
||||
private static abstract class ActionBlockContent extends BlockContent {
|
||||
|
||||
private ActionBlockContent(@NotNull String blockName, @NotNull String source, @NotNull Urgency urgency) {
|
||||
super(blockName, source, urgency);
|
||||
}
|
||||
|
||||
private ActionBlockContent(@NotNull String blockName, @NotNull String source) {
|
||||
super(blockName, source);
|
||||
}
|
||||
|
||||
protected void appendEventElement(@NotNull Document document, @NotNull Element root, String event) {
|
||||
appendTextElement(document, root, "event", event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package work.slhaf.partner.module.action.executor;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.TaskBlock;
|
||||
import work.slhaf.partner.module.action.executor.entity.ExtractorInput;
|
||||
import work.slhaf.partner.module.action.executor.entity.ExtractorResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 负责依据输入内容进行行动单元的参数信息提取
|
||||
*/
|
||||
public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Result<ExtractorResult>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责为指定行动提取参数信息。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中可能包含当前行动相关状态、近期交流轨迹、以及活跃记忆切片;
|
||||
- 一条任务消息,其中包含:
|
||||
- target_action:本次参数提取所面向的目标行动,用于帮助你判断上下文中哪些内容与当前提取直接相关;
|
||||
- meta_action_info:该行动对应的说明,以及允许提取的参数列表。每个 <param name="..."> 节点的文本内容表示该参数的含义或期望内容。
|
||||
|
||||
你的任务:
|
||||
- 根据当前上下文与目标行动信息,提取本次行动所需的参数;
|
||||
- 只提取能够从当前输入、近期会话、相关行动历史或明确上下文中得到支持的参数;
|
||||
- 若当前信息不足以支持可靠提取,则返回 ok=false,而不是猜测或编造参数。
|
||||
|
||||
提取原则:
|
||||
- target_action 用于帮助你在上下文中定位当前面对的是哪一个行动,不要把无关行动历史混入当前参数提取。
|
||||
- action 域中的执行中行动块及其阶段历史,可作为理解当前行动推进位置、已知条件与已出现参数的参考。
|
||||
- communication 域主要用于理解用户最近表达的条件、补充、修正、确认或否定信息。
|
||||
- memory 域只在与当前目标行动明显相关时作为辅助参考使用。
|
||||
- params 中只能填写 meta_action_info.params 中声明过的参数名,不要编造不存在的参数。
|
||||
- 每个参数值都应尽量保持为简洁、明确、可直接使用的文本,不要输出冗长解释。
|
||||
- 若某个参数无法从现有信息中可靠确定,就不要填写它。
|
||||
|
||||
关于输出:
|
||||
- ok=true 表示你已经提取出了一组可用参数;不要求必须填满全部参数,但结果应足以支持后续执行继续推进。
|
||||
- ok=false 表示当前信息不足以形成可靠参数结果。
|
||||
- params 为参数项列表;每一项都必须包含:
|
||||
- name: 参数名
|
||||
- value: 参数值文本
|
||||
- 不要把 params 输出为对象、Map 或其他结构,只能输出 ParamEntry 列表。
|
||||
- 不要输出结构之外的解释、说明或额外文本。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 ExtractorResult 对应结构输出。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<ExtractorResult> doExecute(ExtractorInput input) {
|
||||
List<Message> messages = List.of(
|
||||
resolveContextMessage(),
|
||||
resolveTaskMessage(input)
|
||||
);
|
||||
return formattedChat(messages, ExtractorResult.class);
|
||||
}
|
||||
|
||||
private Message resolveTaskMessage(ExtractorInput input) {
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendChildElement(document, root, "target_action", block -> {
|
||||
appendTextElement(document, block, "uuid", input.getTargetActionId());
|
||||
appendTextElement(document, block, "description", input.getTargetActionDesc());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
appendChildElement(document, root, "meta_action_info", element -> {
|
||||
MetaActionInfo info = input.getMetaActionInfo();
|
||||
appendTextElement(document, element, "description", info.getDescription());
|
||||
appendListElement(document, element, "params", "param", info.getParams().entrySet(), (item, param) -> {
|
||||
item.setAttribute("name", param.getKey());
|
||||
item.setTextContent(param.getValue());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
}.encodeToMessage();
|
||||
}
|
||||
|
||||
private Message resolveContextMessage() {
|
||||
return cognitionCapability.contextWorkspace()
|
||||
.resolve(List.of(
|
||||
ContextBlock.FocusedDomain.ACTION,
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
))
|
||||
.encodeToMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "params_extractor";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class CorrectorInput {
|
||||
private String tendency;
|
||||
private String source;
|
||||
private String reason;
|
||||
private String description;
|
||||
private String actionId;
|
||||
|
||||
private Map<Integer, List<ActionChainItem>> actionChainOverview;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class ActionChainItem {
|
||||
private String actionKey;
|
||||
private String description;
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class CorrectorResult {
|
||||
private List<MetaIntervention> metaInterventionList;
|
||||
private String correctionReason;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
|
||||
@Data
|
||||
public class ExtractorInput {
|
||||
|
||||
/**
|
||||
* 目标行动数据的 uuid
|
||||
*/
|
||||
private String targetActionId;
|
||||
|
||||
/**
|
||||
* 目标行动的 description
|
||||
*/
|
||||
private String targetActionDesc;
|
||||
|
||||
/**
|
||||
* 目标 MetaActionInfo
|
||||
*/
|
||||
private MetaActionInfo metaActionInfo;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ExtractorResult {
|
||||
private boolean ok;
|
||||
private List<ParamEntry> params;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class ParamEntry {
|
||||
private String name;
|
||||
private String value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
public record HistoryAction(String actionKey, String description, String result) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RecognizerResult {
|
||||
private boolean needCorrection = false;
|
||||
private String reason;
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package work.slhaf.partner.module.action.planner;
|
||||
|
||||
import kotlin.Unit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.CommunicationBlockContent;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.executor.ActionExecutor;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.ActionEvaluator;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorResult;
|
||||
import work.slhaf.partner.module.action.planner.extractor.ActionExtractor;
|
||||
import work.slhaf.partner.module.action.planner.extractor.entity.ExtractorResult;
|
||||
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
import work.slhaf.partner.runtime.exception.ContextExceptionReporter;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 负责针对本次输入生成基础的行动计划
|
||||
*/
|
||||
@Slf4j
|
||||
public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
|
||||
|
||||
private static final String IMMEDIATE_WATCHER_CRON = "0/5 * * * * ?";
|
||||
private static final String BLOCK_SOURCE = "action_planner_pending";
|
||||
private static final String TENDENCIES_EVALUATING_BLOCK_NAME = "tendencies_in_evaluating";
|
||||
|
||||
private final ActionAssemblyHelper assemblyHelper = new ActionAssemblyHelper();
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
@InjectModule
|
||||
private ActionEvaluator actionEvaluator;
|
||||
@InjectModule
|
||||
private ActionExtractor actionExtractor;
|
||||
@InjectModule
|
||||
private ActionScheduler actionScheduler;
|
||||
@InjectModule
|
||||
private ActionExecutor actionExecutor;
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
this.setModuleName("action_planner");
|
||||
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
|
||||
String input = context.encodeInputsBlock().encodeToXmlString();
|
||||
Result<ExtractorResult> result = actionExtractor.execute(input)
|
||||
.onFailure(exp -> ExceptionReporterHandler.INSTANCE.report(exp, ContextExceptionReporter.REPORTER_NAME));
|
||||
if (result.exceptionOrNull() != null) {
|
||||
return;
|
||||
}
|
||||
ExtractorResult extractorResult = result.getOrThrow();
|
||||
List<String> tendencies = extractorResult.getTendencies();
|
||||
if (tendencies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
appendTendencyBlock(tendencies);
|
||||
evaluateTendency(context.getSource(), input, extractorResult);
|
||||
}
|
||||
|
||||
private void appendTendencyBlock(List<String> tendencies) {
|
||||
String datetime = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||
buildTendenciesEvaluatingFullBlock(tendencies),
|
||||
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
|
||||
buildTendenciesEvaluatingAbstractBlock(tendencies, datetime),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
60,
|
||||
18,
|
||||
4
|
||||
));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildTendenciesEvaluatingAbstractBlock(List<String> tendencies, String datetime) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "datetime", datetime);
|
||||
appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " related tendencies in evaluating.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildTendenciesEvaluatingCompactBlock(List<String> tendencies, String datetime) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
int size = tendencies.size();
|
||||
boolean num = size > 3;
|
||||
appendTextElement(document, root, "datetime", datetime);
|
||||
appendTextElement(document, root, "state", "Partner is considering whether to do these");
|
||||
appendTextElement(document, root, "tendencies_count", size);
|
||||
appendListElement(document, root, num ? "tendencies_truncated" : "tendencies", "tendency", num ? tendencies.subList(0, 3) : tendencies);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildTendenciesEvaluatingFullBlock(List<String> tendencies) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "Partner is considering whether to do these");
|
||||
appendListElement(document, root, "tendencies", "tendency", tendencies);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void evaluateTendency(String source, String input, ExtractorResult extractorResult) {
|
||||
executor.execute(() -> {
|
||||
EvaluatorInput evaluatorInput = assemblyHelper.buildEvaluatorInput(extractorResult);
|
||||
List<EvaluatorResult> evaluatorResults = actionEvaluator.execute(evaluatorInput); // 并发操作均为访问
|
||||
handleEvaluatorResults(evaluatorResults, source, input);
|
||||
|
||||
cognitionCapability.contextWorkspace().expire(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName());
|
||||
});
|
||||
}
|
||||
|
||||
private void handleEvaluatorResults(List<EvaluatorResult> evaluatorResults, String source, String input) {
|
||||
List<ExecutableAction> passedActions = new ArrayList<>();
|
||||
int approvedExecutableCount = 0;
|
||||
int pendingConfirmCount = 0;
|
||||
for (EvaluatorResult evaluatorResult : evaluatorResults) {
|
||||
expireResolvedPending(evaluatorResult);
|
||||
if (!evaluatorResult.isOk()) {
|
||||
continue;
|
||||
}
|
||||
ExecutableAction executableAction = assemblyHelper.buildActionData(evaluatorResult, source);
|
||||
if (executableAction == null) {
|
||||
continue;
|
||||
}
|
||||
passedActions.add(executableAction);
|
||||
if (evaluatorResult.isNeedConfirm()) {
|
||||
registerPendingContextBlock(executableAction, evaluatorResult, input);
|
||||
pendingConfirmCount++;
|
||||
continue;
|
||||
}
|
||||
executeOrSchedule(executableAction);
|
||||
approvedExecutableCount++;
|
||||
}
|
||||
if (passedActions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
createTurn(approvedExecutableCount, pendingConfirmCount, source, input);
|
||||
}
|
||||
|
||||
private void createTurn(int approvedExecutableCount, int pendingConfirmCount, String source, String input) {
|
||||
String turnInput = approvedExecutableCount + " actions are approved for execution, " +
|
||||
pendingConfirmCount + " actions are waiting for confirmation, " +
|
||||
"according to input: " + trimInput(input) + ". For more information, please refer to the context content or other tags in this input block.";
|
||||
cognitionCapability.initiateTurn(turnInput, source, getModuleName());
|
||||
}
|
||||
|
||||
private void expireResolvedPending(EvaluatorResult evaluatorResult) {
|
||||
EvaluatorResult.ResolvedPending resolvedPending = evaluatorResult.getResolvedPending();
|
||||
if (resolvedPending == null) {
|
||||
return;
|
||||
}
|
||||
if (resolvedPending.getBlockName() == null || resolvedPending.getSource() == null) {
|
||||
return;
|
||||
}
|
||||
cognitionCapability.contextWorkspace().expire(
|
||||
resolvedPending.getBlockName(),
|
||||
resolvedPending.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
private void registerPendingContextBlock(ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
|
||||
String blockName = buildPendingBlockName(executableAction);
|
||||
input = trimInput(input);
|
||||
ContextBlock block = new ContextBlock(
|
||||
buildPendingBlock(blockName, executableAction, evaluatorResult),
|
||||
buildPendingCompactBlock(blockName, executableAction, evaluatorResult, input),
|
||||
buildPendingAbstractBlock(blockName, executableAction, evaluatorResult, input),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
30,
|
||||
10,
|
||||
5
|
||||
);
|
||||
cognitionCapability.contextWorkspace().register(block);
|
||||
}
|
||||
|
||||
private String buildPendingBlockName(ExecutableAction executableAction) {
|
||||
return "pending_action-" + executableAction.getUuid();
|
||||
}
|
||||
|
||||
private BlockContent buildPendingBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult) {
|
||||
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "need_user_confirm");
|
||||
appendTextElement(document, root, "action_uuid", executableAction.getUuid());
|
||||
appendTextElement(document, root, "action_type", evaluatorResult.getType());
|
||||
appendTextElement(document, root, "tendency", executableAction.getTendency());
|
||||
appendTextElement(document, root, "reason", executableAction.getReason());
|
||||
appendTextElement(document, root, "description", executableAction.getDescription());
|
||||
appendTextElement(document, root, "source_user", executableAction.getSource());
|
||||
EvaluatorResult.ScheduleData scheduleData = evaluatorResult.getScheduleData();
|
||||
if (scheduleData != null) {
|
||||
appendTextElement(document, root, "schedule_type", scheduleData.getType());
|
||||
appendTextElement(document, root, "schedule_content", scheduleData.getContent());
|
||||
}
|
||||
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
|
||||
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Element chainRoot = document.createElement("primary_action_chain");
|
||||
root.appendChild(chainRoot);
|
||||
List<Integer> orders = new ArrayList<>(primaryActionChain.keySet());
|
||||
orders.sort(Integer::compareTo);
|
||||
for (Integer order : orders) {
|
||||
Element orderElement = document.createElement("step");
|
||||
orderElement.setAttribute("order", String.valueOf(order));
|
||||
chainRoot.appendChild(orderElement);
|
||||
appendRepeatedElements(
|
||||
document,
|
||||
orderElement,
|
||||
"action_key",
|
||||
primaryActionChain.getOrDefault(order, List.of())
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private BlockContent buildPendingCompactBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
|
||||
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "need_user_confirm");
|
||||
appendTextElement(document, root, "related_input", input);
|
||||
appendTextElement(document, root, "tendency", executableAction.getTendency());
|
||||
appendTextElement(document, root, "description", executableAction.getDescription());
|
||||
appendTextElement(document, root, "action_type", evaluatorResult.getType());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private BlockContent buildPendingAbstractBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
|
||||
return new BlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "need_user_confirm");
|
||||
appendTextElement(document, root, "related_input", input);
|
||||
appendTextElement(document, root, "pending_tendency", executableAction.getTendency());
|
||||
appendTextElement(document, root, "summary", "exists pending action waiting for confirmation");
|
||||
appendTextElement(document, root, "action_type", evaluatorResult.getType());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void executeOrSchedule(ExecutableAction executableAction) {
|
||||
switch (executableAction) {
|
||||
case ImmediateExecutableAction action -> executeImmediateWithWatcher(action);
|
||||
case SchedulableExecutableAction action -> actionScheduler.schedule(action);
|
||||
default -> log.error("unknown executable action type: {}", executableAction.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private void executeImmediateWithWatcher(ImmediateExecutableAction action) {
|
||||
actionExecutor.execute(action);
|
||||
|
||||
AtomicBoolean notified = new AtomicBoolean(false);
|
||||
final StateAction[] watcherRef = new StateAction[1];
|
||||
StateAction watcher = new StateAction(
|
||||
action.getSource(),
|
||||
"immediate-action-watcher:" + action.getUuid(),
|
||||
"轮询即时行动执行结果",
|
||||
Schedulable.ScheduleType.CYCLE,
|
||||
IMMEDIATE_WATCHER_CRON,
|
||||
new StateAction.Trigger.Call(() -> {
|
||||
Action.Status status = action.getStatus();
|
||||
if (status != Action.Status.SUCCESS && status != Action.Status.FAILED) {
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
if (watcherRef[0] != null) {
|
||||
watcherRef[0].setEnabled(false);
|
||||
}
|
||||
if (!notified.compareAndSet(false, true)) {
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
watcherSelfTalk(action);
|
||||
return Unit.INSTANCE;
|
||||
})
|
||||
);
|
||||
watcherRef[0] = watcher;
|
||||
actionScheduler.schedule(watcher);
|
||||
}
|
||||
|
||||
private void watcherSelfTalk(ImmediateExecutableAction action) {
|
||||
String result = action.getResult();
|
||||
String structuredSignal = String.format(
|
||||
"{event=immediate_action_finished,actionUuid=%s,tendency=%s,status=%s,source=%s,result=%s}",
|
||||
action.getUuid(),
|
||||
action.getTendency(),
|
||||
action.getStatus(),
|
||||
action.getSource(),
|
||||
result == null ? "" : result //将会在 ActionExecutor
|
||||
);
|
||||
try {
|
||||
cognitionCapability.initiateTurn(structuredSignal, action.getSource());
|
||||
} catch (Exception e) {
|
||||
log.warn("触发 immediate 行动完成自对话失败, actionUuid: {}", action.getUuid(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
private String trimInput(@NotNull String input) {
|
||||
input = input.trim();
|
||||
input = input.length() <= 100 ? input : input.substring(0, 100);
|
||||
return input;
|
||||
}
|
||||
|
||||
private final class ActionAssemblyHelper {
|
||||
private ActionAssemblyHelper() {
|
||||
}
|
||||
|
||||
private EvaluatorInput buildEvaluatorInput(ExtractorResult extractorResult) {
|
||||
EvaluatorInput input = new EvaluatorInput();
|
||||
input.setTendencies(extractorResult.getTendencies());
|
||||
return input;
|
||||
}
|
||||
|
||||
private ExecutableAction buildActionData(EvaluatorResult evaluatorResult, String userId) {
|
||||
Map<Integer, List<MetaAction>> actionChain = getActionChain(evaluatorResult);
|
||||
if (actionChain == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (evaluatorResult.getType()) {
|
||||
case PLANNING -> new SchedulableExecutableAction(
|
||||
evaluatorResult.getTendency(),
|
||||
actionChain,
|
||||
evaluatorResult.getReason(),
|
||||
evaluatorResult.getDescription(),
|
||||
userId,
|
||||
evaluatorResult.getScheduleData().getType(),
|
||||
evaluatorResult.getScheduleData().getContent()
|
||||
);
|
||||
case IMMEDIATE -> new ImmediateExecutableAction(
|
||||
evaluatorResult.getTendency(),
|
||||
actionChain,
|
||||
evaluatorResult.getReason(),
|
||||
evaluatorResult.getDescription(),
|
||||
userId
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private Map<Integer, List<MetaAction>> getActionChain(EvaluatorResult evaluatorResult) {
|
||||
Map<Integer, List<MetaAction>> actionChain = new HashMap<>();
|
||||
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
|
||||
if (!fixDependencies(primaryActionChain)) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<Integer, List<String>> entry : primaryActionChain.entrySet()) {
|
||||
List<MetaAction> metaActions = new ArrayList<>();
|
||||
for (String actionKey : entry.getValue()) {
|
||||
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
|
||||
AgentRuntimeException failure = metaActionResult.onSuccess(metaActions::add)
|
||||
.exceptionOrNull();
|
||||
if (failure != null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
actionChain.put(entry.getKey(), metaActions);
|
||||
}
|
||||
return actionChain;
|
||||
}
|
||||
|
||||
private boolean fixDependencies(Map<Integer, List<String>> primaryActionChain) {
|
||||
// 先将 primaryActionChain 的节点序号修正为从1开始依次增大
|
||||
fixOrder(primaryActionChain);
|
||||
List<Integer> fixedOrders = new ArrayList<>(primaryActionChain.keySet().stream().toList());
|
||||
AtomicBoolean fixed = new AtomicBoolean(false);
|
||||
do {
|
||||
Set<Integer> tempOrders = new HashSet<>();
|
||||
fixedOrders.sort(Integer::compareTo);
|
||||
for (Integer fixedOrder : fixedOrders) {
|
||||
int lastOrder = fixedOrder - 1;
|
||||
List<String> actionKeys = primaryActionChain.get(fixedOrder);
|
||||
for (String actionKey : actionKeys) {
|
||||
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
|
||||
|
||||
Result<MetaActionInfo> infoResult = actionCapability.loadMetaActionInfo(actionKey);
|
||||
if (infoResult.exceptionOrNull() != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MetaActionInfo metaActionInfo = infoResult.getOrThrow();
|
||||
Set<String> preActions = metaActionInfo.getPreActions();
|
||||
boolean preActionsExist = preActions.isEmpty();
|
||||
if (!preActionsExist) {
|
||||
continue;
|
||||
}
|
||||
if (!metaActionInfo.getStrictDependencies()) {
|
||||
continue;
|
||||
}
|
||||
if (checkDependenciesExist(lastOrder, preActions, primaryActionChain)) {
|
||||
continue;
|
||||
}
|
||||
// 如果存在前置依赖,则将其放置在当前order之前的位置,
|
||||
// 放置位置优先选择已存在的上一节点,如果不存在(行动链的头节点时)则需要向行动链新增order
|
||||
// 不需要检查行动链的当前节点的已存在 Action 是否为新 Action 的依赖项,因为这些 Action 实际来自 LLM
|
||||
// 的评估结果,并非作为依赖项存在
|
||||
fixed.set(true);
|
||||
List<String> actionsInChain = primaryActionChain.computeIfAbsent(lastOrder,
|
||||
list -> new ArrayList<>());
|
||||
preActions = new HashSet<>(preActions);
|
||||
actionsInChain.forEach(preActions::remove);
|
||||
actionsInChain.addAll(preActions);
|
||||
tempOrders.add(lastOrder);
|
||||
}
|
||||
}
|
||||
fixedOrders.clear();
|
||||
fixedOrders.addAll(tempOrders);
|
||||
} while (fixed.getAndSet(false));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void fixOrder(Map<Integer, List<String>> primaryActionChain) {
|
||||
Map<Integer, List<String>> tempChain = new HashMap<>(primaryActionChain);
|
||||
primaryActionChain.clear();
|
||||
int chainSize = tempChain.size();
|
||||
for (int i = 0; i < chainSize; i++) {
|
||||
primaryActionChain.put(i, tempChain.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkDependenciesExist(int lastOrder, Set<String> preActions,
|
||||
Map<Integer, List<String>> primaryActionChain) {
|
||||
if (!primaryActionChain.containsKey(lastOrder)) {
|
||||
return false;
|
||||
}
|
||||
List<String> existActions = primaryActionChain.get(lastOrder);
|
||||
//noinspection SlowListContainsAll
|
||||
return existActions.containsAll(preActions);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package work.slhaf.partner.module.action.planner.evaluator;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.ResolvedContext;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel {
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责评估单条行动倾向,并判断它是否值得进入后续行动链。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中可能包含当前活跃的行动相关状态、近期交流轨迹、认知相关上下文、以及当前活跃记忆切片;
|
||||
- 一组 <available_meta_actions>,表示当前可用的 MetaAction 候选;
|
||||
- 一条最新的 user message,其内容就是当前需要评估的单条 tendency。
|
||||
|
||||
你的任务:
|
||||
- 判断该 tendency 是否成立、是否值得推进;
|
||||
- 判断它更适合立即执行,还是先进入规划;
|
||||
- 判断是否需要先向用户确认;
|
||||
- 若可推进,则基于 available_meta_actions 生成 primaryActionChain;
|
||||
- 若当前 tendency 与某个待处理 pending 明确对应,且本轮已完成承接或推进,应正确填写 resolvedPending;
|
||||
- 若当前 tendency 明确包含可调度语义,再填写 scheduleData。
|
||||
|
||||
评估原则:
|
||||
- 结合上下文理解当前 tendency 与近期交流、当前行动状态、活跃记忆之间的关系。
|
||||
- 若 tendency 与已有正在执行、等待确认、或已明确覆盖的行动完全等价,通常不应重复建立新的行动链。
|
||||
- 若 tendency 是对已有待确认行动的确认、拒绝、补充条件、修改要求或继续推进,则应优先视为对原有行动状态的承接,而不是无关新任务。
|
||||
- 若 action 相关上下文中存在等待确认的 block,且当前 tendency 明显与其相关,则必须显式承接这一点,不要因为其已存在于上下文中而省略。
|
||||
- 只有在 tendency 明确具有可执行意义时,才返回 ok=true。
|
||||
- 若 tendency 更适合直接交流回应,或尚不足以形成行动推进,则返回 ok=false。
|
||||
- primaryActionChain 中只能使用 available_meta_actions 内出现的 action_key,不要编造不存在的动作。
|
||||
- 不要输出完整执行细节、自然语言计划正文或额外解释,只输出 EvaluatorResult 对应结构。
|
||||
|
||||
关于字段:
|
||||
- ok 表示该 tendency 是否值得进入后续行动推进。
|
||||
- needConfirm 表示在真正推进前是否必须先得到用户确认。
|
||||
- type:
|
||||
- IMMEDIATE: 可直接进入即时执行链;
|
||||
- PLANNING: 需要先形成或进入规划链,再决定后续执行。
|
||||
- primaryActionChain 表示按 order 分组的候选动作链,每个元素包含:
|
||||
- order: 执行顺序
|
||||
- actionKeys: 该顺序下的 action_key 列表
|
||||
- reason 用于说明你为何做出该判断,应简洁明确。
|
||||
- description 用于概括本次行动评估结果,应能帮助后续模块快速理解该 tendency 的推进方向。
|
||||
- scheduleData 仅在该 tendency 明确包含可调度语义时填写;否则留空。
|
||||
- scheduleData.type: 一次性计划或周期性计划
|
||||
- scheduleData.content: 符合 Quartz 标准的 Cron 表达式
|
||||
- resolvedPending 仅在你能明确判断当前 tendency 已承接某个 pending block 时填写;否则留空。
|
||||
- 当 ok=false 时,type、primaryActionChain、scheduleData、resolvedPending 通常应留空或保持无效默认值,不要强行填充。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 EvaluatorResult 对应结构输出。
|
||||
- 不要输出结构之外的解释、注释或额外文本。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对输入的行为倾向进行评估,并根据评估结果,对缓存做出调整
|
||||
*
|
||||
* @param data 评估输入内容,包含提取/命中缓存的行动倾向、近几条聊天记录,正在生效的记忆切片内容
|
||||
* @return 评估结果集合
|
||||
*/
|
||||
@Override
|
||||
protected List<EvaluatorResult> doExecute(EvaluatorInput data) {
|
||||
List<String> tendencies = data.getTendencies();
|
||||
CountDownLatch latch = new CountDownLatch(tendencies.size());
|
||||
List<EvaluatorResult> evaluatorResults = new ArrayList<>();
|
||||
|
||||
for (String tendency : tendencies) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
List<Message> messages = List.of(
|
||||
cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.ACTION,
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.COGNITION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
)).encodeToMessage(),
|
||||
availableMetaActionContext(),
|
||||
new Message(Message.Character.USER, tendency)
|
||||
);
|
||||
Result<EvaluatorResult> result = formattedChat(
|
||||
messages,
|
||||
EvaluatorResult.class
|
||||
);
|
||||
result.onSuccess(evaluatorResult -> {
|
||||
evaluatorResult.setTendency(tendency);
|
||||
synchronized (evaluatorResults) {
|
||||
evaluatorResults.add(evaluatorResult);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
return evaluatorResults;
|
||||
}
|
||||
|
||||
private Message availableMetaActionContext() {
|
||||
// TODO select and filter available MetaActions by tags and embedding
|
||||
BlockContent content = new BlockContent("available_meta_actions", "action_planner") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendRepeatedElements(
|
||||
document,
|
||||
root,
|
||||
"available_meta_action",
|
||||
actionCapability.listAvailableMetaActions().entrySet(),
|
||||
(block, value) -> {
|
||||
appendTextElement(document, root, "action_key", value.getKey());
|
||||
appendTextElement(document, root, "action_value", value.getValue().getDescription());
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
return new ResolvedContext(List.of(content)).encodeToMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "action_evaluator";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package work.slhaf.partner.module.action.planner.evaluator.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class EvaluatorBatchInput {
|
||||
private List<Message> recentMessages;
|
||||
private List<ActivatedMemorySlice> activatedSlices;
|
||||
private Map<String, String> availableActions;
|
||||
private String tendency;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package work.slhaf.partner.module.action.planner.evaluator.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class EvaluatorInput {
|
||||
private List<String> tendencies;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package work.slhaf.partner.module.action.planner.evaluator.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.core.action.entity.Schedulable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
public class EvaluatorResult {
|
||||
private boolean ok;
|
||||
private boolean needConfirm;
|
||||
private ResolvedPending resolvedPending;
|
||||
private ActionType type;
|
||||
private ScheduleData scheduleData;
|
||||
private List<ChainElement> primaryActionChain;
|
||||
private String tendency;
|
||||
private String reason;
|
||||
private String description;
|
||||
|
||||
public enum ActionType {
|
||||
IMMEDIATE, PLANNING
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ResolvedPending {
|
||||
private String blockName;
|
||||
private String source;
|
||||
}
|
||||
|
||||
public Map<Integer, List<String>> getPrimaryActionChainAsMap() {
|
||||
return primaryActionChain.stream().collect(Collectors.toMap(
|
||||
ChainElement::getOrder,
|
||||
ChainElement::getActionKeys,
|
||||
(oldValue, newValue) -> newValue,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ScheduleData {
|
||||
private String content;
|
||||
private Schedulable.ScheduleType type;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ChainElement {
|
||||
private Integer order;
|
||||
private List<String> actionKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package work.slhaf.partner.module.action.planner.extractor;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ModuleExecutionException;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.planner.extractor.entity.ExtractorResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责从当前输入中提取可能的行动倾向,供后续模块继续评估。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,主要包含 communication 域与 action 域内容;
|
||||
- 一条最新输入,作为本轮重点分析对象。
|
||||
|
||||
规则:
|
||||
- communication 域主要用于理解当前会话语境、主题延续与用户意图。
|
||||
- action 域主要用于判断相关行动是否已经在执行、等待确认,或已被覆盖。
|
||||
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤或工具细节。
|
||||
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息或条件变化,应避免重复提出。
|
||||
- 若 action 域中存在等待确认的 block,且当前输入与其相关,则必须提取出对应的行动倾向;不要因为其已存在于上下文中而省略。
|
||||
- 若当前输入没有明显行动倾向,可返回空结果。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 ExtractorResult 对应结构输出。
|
||||
- 不要输出结构之外的解释或额外文本。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<ExtractorResult> doExecute(String input) {
|
||||
List<Message> messages = List.of(
|
||||
cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.ACTION
|
||||
)).encodeToMessage(),
|
||||
new Message(Message.Character.USER, input)
|
||||
);
|
||||
try {
|
||||
return Result.success(formattedChat(messages, ExtractorResult.class).getOrThrow());
|
||||
} catch (AgentRuntimeException e) {
|
||||
return Result.failure(new ModuleExecutionException(
|
||||
"collecting action tendencies failed",
|
||||
this.getClass(),
|
||||
getModuleName()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "action_extractor";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package work.slhaf.partner.module.action.planner.extractor.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ExtractorInput {
|
||||
private String input;
|
||||
private List<Message> recentMessages;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package work.slhaf.partner.module.action.planner.extractor.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ExtractorResult {
|
||||
private List<String> tendencies = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
package work.slhaf.partner.module.action.scheduler
|
||||
|
||||
import com.cronutils.model.CronType
|
||||
import com.cronutils.model.definition.CronDefinition
|
||||
import com.cronutils.model.definition.CronDefinitionBuilder
|
||||
import com.cronutils.model.time.ExecutionTime
|
||||
import com.cronutils.parser.CronParser
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.slf4j.LoggerFactory
|
||||
import work.slhaf.partner.core.action.ActionCapability
|
||||
import work.slhaf.partner.core.action.entity.Action
|
||||
import work.slhaf.partner.core.action.entity.Schedulable
|
||||
import work.slhaf.partner.core.action.entity.SchedulableExecutableAction
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule
|
||||
import work.slhaf.partner.module.action.executor.ActionExecutor
|
||||
import java.io.Closeable
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
import java.util.stream.Collectors
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
|
||||
@InjectCapability
|
||||
private lateinit var actionCapability: ActionCapability
|
||||
|
||||
@InjectModule
|
||||
private lateinit var actionExecutor: ActionExecutor
|
||||
|
||||
private lateinit var timeWheel: TimeWheel
|
||||
private val runtimeSchedulables: MutableSet<Schedulable> =
|
||||
Collections.synchronizedSet(mutableSetOf())
|
||||
private val schedulerScope =
|
||||
CoroutineScope(Dispatchers.Default + SupervisorJob() + CoroutineName("ActionScheduler"))
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(ActionScheduler::class.java)
|
||||
}
|
||||
|
||||
@Init
|
||||
fun init() {
|
||||
fun loadScheduledActions() {
|
||||
val listScheduledActions: () -> Set<Schedulable> = {
|
||||
val persistedExecutable = actionCapability.listActions(null, null)
|
||||
.stream()
|
||||
.filter { it is SchedulableExecutableAction }
|
||||
.map { it as SchedulableExecutableAction }
|
||||
.collect(Collectors.toSet())
|
||||
val persisted: MutableSet<Schedulable> = mutableSetOf()
|
||||
persisted.addAll(persistedExecutable)
|
||||
synchronized(runtimeSchedulables) {
|
||||
persisted.addAll(runtimeSchedulables.filter { it.enabled })
|
||||
}
|
||||
persisted
|
||||
}
|
||||
val onTrigger: (Set<Schedulable>) -> Unit = { schedulableSet ->
|
||||
schedulableSet.filterIsInstance<Action>()
|
||||
.forEach { actionExecutor.execute(it) }
|
||||
}
|
||||
val doneCondition: (Schedulable) -> Boolean = { schedulable ->
|
||||
if (schedulable is Action) {
|
||||
schedulable.status == Action.Status.FAILED || schedulable.status == Action.Status.SUCCESS
|
||||
}
|
||||
true
|
||||
}
|
||||
timeWheel = TimeWheel(listScheduledActions, onTrigger, doneCondition)
|
||||
}
|
||||
loadScheduledActions()
|
||||
setupShutdownHook()
|
||||
}
|
||||
|
||||
private fun setupShutdownHook() {
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
timeWheel.close()
|
||||
schedulerScope.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
fun <T> schedule(schedulableAction: T) where T : Action, T : Schedulable = schedulerScope.launch {
|
||||
if (!schedulableAction.enabled) {
|
||||
return@launch
|
||||
}
|
||||
runtimeSchedulables.add(schedulableAction)
|
||||
log.debug("New data to schedule: {}", schedulableAction)
|
||||
timeWheel.schedule(schedulableAction)
|
||||
if (schedulableAction is SchedulableExecutableAction) {
|
||||
actionCapability.putAction(schedulableAction)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(actionId: String): Boolean {
|
||||
val runtimeMatches = synchronized(runtimeSchedulables) {
|
||||
runtimeSchedulables.filter { it.uuid == actionId }.toSet()
|
||||
}
|
||||
val persistedMatches = actionCapability.listActions(null, null)
|
||||
.asSequence()
|
||||
.filterIsInstance<SchedulableExecutableAction>()
|
||||
.filter { it.uuid == actionId }
|
||||
.toSet()
|
||||
|
||||
val matches = LinkedHashSet<Schedulable>()
|
||||
matches.addAll(runtimeMatches)
|
||||
matches.addAll(persistedMatches)
|
||||
matches.forEach { it.enabled = false }
|
||||
|
||||
val removedFromWheel = if (::timeWheel.isInitialized) {
|
||||
runBlocking { timeWheel.cancel(actionId) }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return matches.isNotEmpty() || removedFromWheel
|
||||
}
|
||||
|
||||
private class TimeWheel(
|
||||
val listSource: () -> Set<Schedulable>,
|
||||
val onTrigger: (toTrigger: Set<Schedulable>) -> Unit,
|
||||
val doneCondition: (schedulable: Schedulable) -> Boolean
|
||||
) : Closeable {
|
||||
private val schedulableGroupByHour = Array<MutableSet<Schedulable>>(24) { mutableSetOf() }
|
||||
private val wheel = Array<MutableSet<Schedulable>>(60 * 60) { mutableSetOf() }
|
||||
private var recordHour: Int = -1
|
||||
private var recordDay: Int = -1
|
||||
private val state = MutableStateFlow(WheelState.SLEEPING)
|
||||
private val wheelActionsLock = Mutex()
|
||||
private val timeWheelScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("TimeWheel"))
|
||||
private val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
|
||||
private val cronParser: CronParser = CronParser(cronDefinition)
|
||||
|
||||
init {
|
||||
// 启动时间轮
|
||||
wheel()
|
||||
}
|
||||
|
||||
suspend fun schedule(schedulableData: Schedulable) {
|
||||
checkThenExecute {
|
||||
val parseToZonedDateTime = parseToZonedDateTime(
|
||||
schedulableData.scheduleType,
|
||||
schedulableData.scheduleContent,
|
||||
it
|
||||
) ?: run {
|
||||
logFailedStatus(schedulableData)
|
||||
return@checkThenExecute
|
||||
}
|
||||
log.debug("Action next execution time: {}", parseToZonedDateTime)
|
||||
val hour = parseToZonedDateTime.hour
|
||||
schedulableGroupByHour[hour].add(schedulableData)
|
||||
log.debug("Action scheduled at {}", hour)
|
||||
if (it.hour == hour) {
|
||||
val wheelOffset = parseToZonedDateTime.minute * 60 + parseToZonedDateTime.second
|
||||
wheel[wheelOffset].add(schedulableData)
|
||||
state.value = WheelState.ACTIVE
|
||||
log.debug("Action scheduled at wheel offset {}", wheelOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancel(actionId: String): Boolean = wheelActionsLock.withLock {
|
||||
var found = false
|
||||
for (bucket in schedulableGroupByHour) {
|
||||
var bucketFound = false
|
||||
bucket.removeIf {
|
||||
if (it.uuid == actionId) {
|
||||
it.enabled = false
|
||||
bucketFound = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
found = found || bucketFound
|
||||
}
|
||||
for (bucket in wheel) {
|
||||
var bucketFound = false
|
||||
bucket.removeIf {
|
||||
if (it.uuid == actionId) {
|
||||
it.enabled = false
|
||||
bucketFound = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
found = found || bucketFound
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
private fun wheel() {
|
||||
data class WheelStepResult(
|
||||
val toTrigger: Set<Schedulable>?,
|
||||
val shouldBreak: Boolean
|
||||
)
|
||||
|
||||
fun collectToTrigger(tick: Int, previousTick: Int, triggerHour: Int): Set<Schedulable>? {
|
||||
if (tick > previousTick) {
|
||||
val toTrigger = mutableSetOf<Schedulable>()
|
||||
for (i in previousTick..tick) {
|
||||
val bucket = wheel[i]
|
||||
if (bucket.isNotEmpty()) {
|
||||
toTrigger.addAll(bucket.filter { it.enabled })
|
||||
val bucketUuids = bucket.asSequence().map { it.uuid }.toHashSet()
|
||||
schedulableGroupByHour[triggerHour].removeIf { it.uuid in bucketUuids }
|
||||
bucket.clear() // 避免重复触发
|
||||
}
|
||||
}
|
||||
return toTrigger
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun handleToTrigger(toTrigger: Set<Schedulable>) {
|
||||
timeWheelScope.launch {
|
||||
onTrigger(toTrigger)
|
||||
}
|
||||
for (schedulable in toTrigger) timeWheelScope.launch {
|
||||
if (schedulable.scheduleType == Schedulable.ScheduleType.ONCE) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
withTimeoutOrNull(schedulable.timeout) {
|
||||
while (!doneCondition(schedulable)) {
|
||||
delay(50.milliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
if (!schedulable.enabled) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
this@TimeWheel.schedule(schedulable)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun CoroutineScope.wheel(launchingTime: ZonedDateTime, primaryTickAdvanceTime: Long) {
|
||||
val launchingHour = launchingTime.hour
|
||||
var tick = launchingTime.minute * 60 + launchingTime.second
|
||||
// 让节拍器从“启动时刻的下一秒”开始(避免立即 step=0)
|
||||
var nextTickNanos = primaryTickAdvanceTime + 1_000_000_000L
|
||||
while (isActive) {
|
||||
// 1) 计算落后多少秒:至少 1(正常推进),也可能 >1(追赶)
|
||||
val now0 = System.nanoTime()
|
||||
val lagNanos = now0 - nextTickNanos
|
||||
val step = if (lagNanos < 0) 1 else (lagNanos / 1_000_000_000L).toInt() + 1
|
||||
val previousTick = tick
|
||||
tick = (tick + step).coerceAtMost(wheel.lastIndex)
|
||||
// 2) 推进节拍器:按“理论秒”前进 step 次
|
||||
nextTickNanos += step.toLong() * 1_000_000_000L
|
||||
val stepResult = run {
|
||||
var shouldBreak = false
|
||||
var toTrigger: Set<Schedulable>? = null
|
||||
checkThenExecute(false) {
|
||||
if (it.hour != launchingHour) {
|
||||
shouldBreak = true
|
||||
toTrigger = collectToTrigger(wheel.lastIndex, previousTick, launchingHour)
|
||||
log.debug(
|
||||
"Hour changed, previousTick: {}, tick: {}, toTriggerSize: {}",
|
||||
previousTick,
|
||||
tick,
|
||||
toTrigger?.size
|
||||
)
|
||||
return@checkThenExecute
|
||||
}
|
||||
toTrigger = collectToTrigger(tick, previousTick, launchingHour)
|
||||
if (tick >= wheel.lastIndex || schedulableGroupByHour[launchingHour].isEmpty()) {
|
||||
state.value = WheelState.SLEEPING
|
||||
shouldBreak = true
|
||||
}
|
||||
}
|
||||
WheelStepResult(toTrigger, shouldBreak)
|
||||
}
|
||||
stepResult.toTrigger?.let { handleToTrigger(it) }
|
||||
if (stepResult.shouldBreak) {
|
||||
log.debug("Wheel stopped at tick {}", tick)
|
||||
break
|
||||
}
|
||||
// 3) 精确睡到下一次理论 tick(用最新 nanoTime)
|
||||
val now1 = System.nanoTime()
|
||||
val sleepNanos = nextTickNanos - now1
|
||||
if (sleepNanos > 0) {
|
||||
delay((sleepNanos / 1_000_000L).milliseconds) // 毫秒级 delay 足够;剩余 nanos 不必忙等
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun wait(currentTime: ZonedDateTime) {
|
||||
val nextHour = currentTime.truncatedTo(ChronoUnit.HOURS).plusHours(1)
|
||||
val seconds = Duration.between(
|
||||
currentTime, nextHour
|
||||
).toMillis()
|
||||
// withTimeoutOrNull 内部已处理 seconds 小于 0 的情况
|
||||
log.debug("Start waiting {} ms at {}, target time: {}", seconds, currentTime, nextHour)
|
||||
withTimeoutOrNull(seconds.milliseconds) {
|
||||
state.first { it == WheelState.ACTIVE }
|
||||
}
|
||||
log.debug("Waiting ended at {}", ZonedDateTime.now())
|
||||
}
|
||||
timeWheelScope.launch {
|
||||
while (isActive) {
|
||||
// 判断是否该步入下一小时
|
||||
var shouldWait: Boolean? = null
|
||||
var currentTime: ZonedDateTime? = null
|
||||
var primaryTickAdvanceTime: Long? = null
|
||||
checkThenExecute {
|
||||
currentTime = it
|
||||
shouldWait = schedulableGroupByHour[it.hour].isEmpty()
|
||||
// 由于 wheel 的启动时间可能存在延迟,而时内推进由 nanoTime 保证不会漏发,
|
||||
// 正常的时序结束又由 tick 是否触顶、当前时是否存在额外任务触发,
|
||||
// 而启动时无触发保障,此时一并初始化 tick 推进时间,足以应对 check 与 wheel 间的这段时间间隔
|
||||
primaryTickAdvanceTime = System.nanoTime()
|
||||
}
|
||||
// 如果该时无任务则等待,插入事件可提前唤醒
|
||||
if (shouldWait!!) {
|
||||
// 计算距离下一小时的时间,等待
|
||||
currentTime?.let { wait(it) }
|
||||
continue
|
||||
}
|
||||
// 唤醒进行时间轮循环
|
||||
wheel(currentTime!!, primaryTickAdvanceTime!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkThenExecute(finallyToExecute: Boolean = true, then: (currentTime: ZonedDateTime) -> Unit) =
|
||||
wheelActionsLock.withLock {
|
||||
fun loadActions(
|
||||
source: Set<Schedulable>,
|
||||
now: ZonedDateTime,
|
||||
load: (latestExecutingTime: ZonedDateTime, schedulableData: Schedulable) -> Unit,
|
||||
repair: () -> Unit
|
||||
) {
|
||||
val runLoading = {
|
||||
for (schedulableData in source) {
|
||||
val nextExecutingTime =
|
||||
parseToZonedDateTime(
|
||||
schedulableData.scheduleType,
|
||||
schedulableData.scheduleContent,
|
||||
now
|
||||
) ?: run {
|
||||
logFailedStatus(schedulableData)
|
||||
continue
|
||||
}
|
||||
load(nextExecutingTime, schedulableData)
|
||||
}
|
||||
}
|
||||
repair()
|
||||
runLoading()
|
||||
}
|
||||
|
||||
fun loadHourActions(currentTime: ZonedDateTime) {
|
||||
val load: (ZonedDateTime, Schedulable) -> Unit =
|
||||
{ latestExecutionTime, schedulableData ->
|
||||
val secondsTime = latestExecutionTime.minute * 60 + latestExecutionTime.second
|
||||
wheel[secondsTime].add(schedulableData)
|
||||
log.debug("Action loaded to hour: {}", schedulableData)
|
||||
}
|
||||
val repair: () -> Unit = {
|
||||
for (set in wheel) {
|
||||
set.clear()
|
||||
}
|
||||
}
|
||||
loadActions(schedulableGroupByHour[currentTime.hour], currentTime, load, repair)
|
||||
}
|
||||
|
||||
fun loadDayActions(currentTime: ZonedDateTime) {
|
||||
val load: (ZonedDateTime, Schedulable) -> Unit =
|
||||
{ latestExecutingTime, schedulableData ->
|
||||
schedulableGroupByHour[latestExecutingTime.hour].add(schedulableData)
|
||||
log.debug("Action loaded to day: {}", schedulableData)
|
||||
}
|
||||
val repair: () -> Unit = {
|
||||
for (set in schedulableGroupByHour) {
|
||||
set.clear()
|
||||
}
|
||||
}
|
||||
loadActions(listSource(), currentTime, load, repair)
|
||||
}
|
||||
|
||||
fun refreshIfNeeded(now: ZonedDateTime) {
|
||||
val d = now.dayOfMonth
|
||||
val h = now.hour
|
||||
if (d != recordDay) {
|
||||
recordDay = d
|
||||
recordHour = h
|
||||
loadDayActions(now)
|
||||
loadHourActions(now)
|
||||
} else if (h != recordHour) {
|
||||
recordHour = h
|
||||
loadHourActions(now)
|
||||
}
|
||||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
if (finallyToExecute) {
|
||||
refreshIfNeeded(now)
|
||||
then(now)
|
||||
} else {
|
||||
then(now)
|
||||
refreshIfNeeded(now)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseToZonedDateTime(
|
||||
scheduleType: Schedulable.ScheduleType,
|
||||
scheduleContent: String,
|
||||
now: ZonedDateTime
|
||||
): ZonedDateTime? {
|
||||
return when (scheduleType) {
|
||||
Schedulable.ScheduleType.CYCLE
|
||||
-> {
|
||||
val cron = try {
|
||||
cronParser.parse(scheduleContent).validate()
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
val executionTime = ExecutionTime.forCron(cron)
|
||||
executionTime.nextExecution(now).getOrNull()
|
||||
}
|
||||
|
||||
Schedulable.ScheduleType.ONCE -> {
|
||||
val executionTime = try {
|
||||
ZonedDateTime.parse(scheduleContent)
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
if (executionTime.plusSeconds(1).isBefore(now) || executionTime.dayOfMonth != now.dayOfMonth)
|
||||
null
|
||||
else
|
||||
executionTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logFailedStatus(scheduleData: Schedulable) {
|
||||
log.warn(
|
||||
"行动未加载,scheduleType: {}, scheduleContent: {}",
|
||||
scheduleData.scheduleType,
|
||||
scheduleData.scheduleContent,
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
timeWheelScope.cancel()
|
||||
}
|
||||
|
||||
private enum class WheelState {
|
||||
ACTIVE,
|
||||
SLEEPING,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package work.slhaf.partner.module.communication;
|
||||
|
||||
public interface AfterRolling {
|
||||
void consume(RollingResult result);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package work.slhaf.partner.module.communication;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
public class AfterRollingRegistry extends AbstractAgentModule.Standalone {
|
||||
|
||||
private final CopyOnWriteArrayList<AfterRolling> consumers = new CopyOnWriteArrayList<>();
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
private ExecutorService executor;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
}
|
||||
|
||||
public void register(AfterRolling consumer) {
|
||||
if (consumers.contains(consumer)) {
|
||||
return;
|
||||
}
|
||||
consumers.add(consumer);
|
||||
}
|
||||
|
||||
public void trigger(RollingResult result) {
|
||||
if (consumers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
executor.execute(() -> {
|
||||
for (AfterRolling consumer : List.copyOf(consumers)) {
|
||||
try {
|
||||
consumer.consume(result);
|
||||
} catch (Exception e) {
|
||||
ExceptionReporterHandler.INSTANCE.report(new AgentRuntimeException("after-rolling consumer occurred exception: " + consumer.getClass().getSimpleName(), e));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package work.slhaf.partner.module.communication;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.*;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRunningFlowContext> implements ActivateModel {
|
||||
|
||||
private static final String INTERRUPTED_MARKER = " [response interrupted due to internal exception]";
|
||||
private static final String NO_REPLY_MARKER = "NO_REPLY";
|
||||
private static final String AGENT_MARKER = "[[AGENT]: self]";
|
||||
private static final String NOT_REPLIED_PREFIX = "[NOT_REPLIED]";
|
||||
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
|
||||
|
||||
你接下来收到的消息,将按照出现顺序,固定分为三个区段:
|
||||
1. system message 是 Head,用于说明整个输入结构与输出要求,即本条消息。
|
||||
2. <context> 区段承载系统中所有模块产生的上下文块。它表示 Partner 在当前时刻的系统状态投影,不应被理解为普通聊天历史;其中每个子块都带有独立来源,可作为理解当前状态和辅助决策的依据。
|
||||
3. <conversation> 区段是系统此刻保留的对话轨迹,用于帮助你理解当前交流延续、最近问答关系与最新输入所处的位置;最新的一条 user message 会使用 <input> 结构,其中 <inputs> 承载本轮按时间顺序排列的输入序列,每个 <input> 节点会带有相对首条输入的时间间隔属性;其他子标签是输入元信息与 type=SUPPLY 的补充块,补充块会按 blockName 分区。
|
||||
|
||||
你的任务:
|
||||
- 最新输入是当前交流的直接触发点。
|
||||
- <conversation> 主要用于理解对话延续关系。
|
||||
- <context> 主要用于理解 Partner 此刻的系统状态;其中明显相关的状态信号不应被当作普通历史材料忽略。
|
||||
- 若最新输入与已有上下文存在张力,应以最新输入为当前交流的直接依据,再结合 <conversation> 与 <context> 判断如何回应。
|
||||
- 你当前负责的是对外交流,不负责直接规划行动、修改系统状态,或伪造并不存在的执行结果。
|
||||
|
||||
输出契约:
|
||||
- 默认情况下,直接输出要发送给用户的最终回复正文,不要添加额外标签、解释或前后缀。
|
||||
- 若当前情境下不应直接向用户发出回复,但仍需要留下本轮交流结果供系统后续保留在交流轨迹中,则输出以 NO_REPLY 开头。
|
||||
- 使用 NO_REPLY 时,格式为:
|
||||
|
||||
NO_REPLY
|
||||
这里写本轮交流结果正文
|
||||
|
||||
- 以 NO_REPLY 开头的输出不会直接展示给用户;系统在写入交流轨迹时,会以单独的历史标记形式保留该结果。
|
||||
- 不要输出空字符串;若选择不直接回复用户,应使用 NO_REPLY 契约明确表达。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
log.info("CommunicationProducer 注册完毕...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String modelKey() {
|
||||
return "communication_producer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(PartnerRunningFlowContext runningFlowContext) {
|
||||
log.debug("Communicating with: {}", runningFlowContext.getSource());
|
||||
executeChat(runningFlowContext);
|
||||
}
|
||||
|
||||
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
|
||||
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
|
||||
this.streamChat(buildChatMessages(runningFlowContext), consumer)
|
||||
.onFailure(exception -> consumer.onDelta(INTERRUPTED_MARKER));
|
||||
updateChatMessages(runningFlowContext, consumer.collectResponse());
|
||||
cognitionCapability.refreshRecentChatMessagesContext();
|
||||
}
|
||||
|
||||
private List<Message> buildChatMessages(PartnerRunningFlowContext runningFlowContext) {
|
||||
ResolvedContext resolvedContext = cognitionCapability.contextWorkspace()
|
||||
.resolve(List.of(ContextBlock.FocusedDomain.COGNITION, ContextBlock.FocusedDomain.ACTION, ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.PERCEIVE));
|
||||
List<BlockContent> communicationBlocks = resolvedContext.getBlocks();
|
||||
List<Message> historyMessages = snapshotConversationMessages();
|
||||
List<Message> temp = new ArrayList<>(historyMessages.size() + 2);
|
||||
temp.add(buildContextMessage(communicationBlocks));
|
||||
temp.addAll(historyMessages);
|
||||
temp.add(buildInputMessage(runningFlowContext, communicationBlocks));
|
||||
return temp;
|
||||
}
|
||||
|
||||
private void updateChatMessages(PartnerRunningFlowContext runningFlowContext, String response) {
|
||||
cognitionCapability.getMessageLock().lock();
|
||||
try {
|
||||
List<Message> chatMessages = cognitionCapability.getChatMessages();
|
||||
chatMessages.removeIf(this::isStructuredUserMessage);
|
||||
Message primaryUserMessage = new Message(
|
||||
Message.Character.USER,
|
||||
formatConversationUserMessage(runningFlowContext)
|
||||
);
|
||||
chatMessages.add(primaryUserMessage);
|
||||
Message assistantMessage = new Message(
|
||||
Message.Character.ASSISTANT,
|
||||
normalizeAssistantHistoryMessage(response)
|
||||
);
|
||||
chatMessages.add(assistantMessage);
|
||||
} finally {
|
||||
cognitionCapability.getMessageLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeAssistantHistoryMessage(String response) {
|
||||
String trimmed = response == null ? "" : response.trim();
|
||||
if (trimmed.equals(NO_REPLY_MARKER)) {
|
||||
return formatMarkedHistoryMessage(AGENT_MARKER, NOT_REPLIED_PREFIX, "");
|
||||
}
|
||||
if (trimmed.startsWith(NO_REPLY_MARKER + "\n")) {
|
||||
return formatMarkedHistoryMessage(
|
||||
AGENT_MARKER,
|
||||
NOT_REPLIED_PREFIX,
|
||||
trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim()
|
||||
);
|
||||
}
|
||||
if (trimmed.startsWith(NO_REPLY_MARKER + "\r\n")) {
|
||||
return formatMarkedHistoryMessage(
|
||||
AGENT_MARKER,
|
||||
NOT_REPLIED_PREFIX,
|
||||
trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim()
|
||||
);
|
||||
}
|
||||
return formatMarkedHistoryMessage(AGENT_MARKER, "", trimmed);
|
||||
}
|
||||
|
||||
private List<Message> snapshotConversationMessages() {
|
||||
List<Message> snapshot = new ArrayList<>(cognitionCapability.snapshotChatMessages());
|
||||
snapshot.removeIf(this::isStructuredUserMessage);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private Message buildContextMessage(List<BlockContent> communicationBlocks) {
|
||||
List<BlockContent> contextBlocks = communicationBlocks.stream()
|
||||
.filter(this::belongsToContextSection)
|
||||
.toList();
|
||||
return new ResolvedContext(contextBlocks).encodeToMessage();
|
||||
}
|
||||
|
||||
private Message buildInputMessage(PartnerRunningFlowContext runningFlowContext, List<BlockContent> communicationBlocks) {
|
||||
return new Message(Message.Character.USER, buildInputXml(runningFlowContext, communicationBlocks));
|
||||
}
|
||||
|
||||
private String buildInputXml(PartnerRunningFlowContext runningFlowContext, List<BlockContent> communicationBlocks) {
|
||||
try {
|
||||
Document document = newDocument();
|
||||
Element root = document.createElement("input");
|
||||
document.appendChild(root);
|
||||
|
||||
document.appendChild(document.importNode(runningFlowContext.encodeInputsBlock().encodeToXml(), true));
|
||||
appendTextElement(document, root, "source", runningFlowContext.getSource());
|
||||
for (Map.Entry<String, String> entry : runningFlowContext.getAdditionalUserInfo().entrySet()) {
|
||||
appendTextElement(document, root, sanitizeTagName(entry.getKey()), entry.getValue());
|
||||
}
|
||||
appendSupplyBlocks(document, root, communicationBlocks);
|
||||
|
||||
return toXmlString(document);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("构建 input 区段失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStructuredUserMessage(Message message) {
|
||||
if (message.getRole() == Message.Character.ASSISTANT) {
|
||||
return false;
|
||||
}
|
||||
String content = message.getContent();
|
||||
String trimmed = content.trim();
|
||||
return trimmed.startsWith("<input>") || trimmed.startsWith("<context>") || trimmed.startsWith("<?xml");
|
||||
}
|
||||
|
||||
private boolean belongsToContextSection(BlockContent blockContent) {
|
||||
if (!(blockContent instanceof CommunicationBlockContent communicationBlockContent)) {
|
||||
return true;
|
||||
}
|
||||
return communicationBlockContent.getType() == CommunicationBlockContent.Projection.CONTEXT;
|
||||
}
|
||||
|
||||
private String formatConversationUserMessage(PartnerRunningFlowContext runningFlowContext) {
|
||||
return formatMarkedHistoryMessage("[" + runningFlowContext.getSource() + "]", "", runningFlowContext.formatInputsForHistory());
|
||||
}
|
||||
|
||||
private String formatMarkedHistoryMessage(String identityMarker, String statusMarkers, String body) {
|
||||
String markerLine = statusMarkers == null || statusMarkers.isBlank()
|
||||
? identityMarker
|
||||
: identityMarker + ": " + statusMarkers;
|
||||
if (body == null || body.isBlank()) {
|
||||
return markerLine + ":";
|
||||
}
|
||||
return markerLine + MARKER_BODY_SEPARATOR + body.trim();
|
||||
}
|
||||
|
||||
private Document newDocument() throws Exception {
|
||||
return DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.newDocument();
|
||||
}
|
||||
|
||||
private void appendTextElement(Document document, Element parent, String tagName, String value) {
|
||||
Element element = document.createElement(tagName);
|
||||
element.setTextContent(value == null ? "" : value);
|
||||
parent.appendChild(element);
|
||||
}
|
||||
|
||||
private void appendSupplyBlocks(Document document, Element inputRoot, List<BlockContent> contextBlocks) {
|
||||
Map<String, List<CommunicationBlockContent>> groupedBlocks = contextBlocks.stream()
|
||||
.filter(CommunicationBlockContent.class::isInstance)
|
||||
.map(CommunicationBlockContent.class::cast)
|
||||
.filter(block -> block.getType() == CommunicationBlockContent.Projection.SUPPLY)
|
||||
.collect(Collectors.groupingBy(
|
||||
block -> sanitizeTagName(block.getBlockName()),
|
||||
LinkedHashMap::new,
|
||||
Collectors.toList()
|
||||
));
|
||||
|
||||
for (Map.Entry<String, List<CommunicationBlockContent>> entry : groupedBlocks.entrySet()) {
|
||||
Element groupElement = document.createElement(entry.getKey());
|
||||
inputRoot.appendChild(groupElement);
|
||||
for (CommunicationBlockContent block : entry.getValue()) {
|
||||
Element blockElement = block.encodeToXml();
|
||||
groupElement.appendChild(document.importNode(blockElement, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeTagName(String rawTagName) {
|
||||
if (rawTagName == null || rawTagName.isBlank()) {
|
||||
return "meta";
|
||||
}
|
||||
String sanitized = rawTagName.replaceAll("[^A-Za-z0-9_.-]", "_");
|
||||
if (!Character.isLetter(sanitized.charAt(0)) && sanitized.charAt(0) != '_') {
|
||||
sanitized = "_" + sanitized;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private String toXmlString(Document document) throws Exception {
|
||||
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
|
||||
StringWriter writer = new StringWriter();
|
||||
transformer.transform(new DOMSource(document), new StreamResult(writer));
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package work.slhaf.partner.module.communication;
|
||||
|
||||
import kotlin.Unit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.action.entity.Schedulable;
|
||||
import work.slhaf.partner.core.action.entity.StateAction;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.core.perceive.PerceiveCapability;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
|
||||
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
|
||||
import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
|
||||
|
||||
private static final String AUTO_UPDATE_CRON = "0/10 * * * * ?";
|
||||
private static final long UPDATE_TRIGGER_INTERVAL = 60 * 60 * 1000;
|
||||
private static final int CONTEXT_RETAIN_DIVISOR = 6;
|
||||
private static final int DIALOG_ROLLING_TRIGGER_LIMIT = 36;
|
||||
|
||||
private final AtomicBoolean rolling = new AtomicBoolean(false);
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
@InjectCapability
|
||||
private MemoryCapability memoryCapability;
|
||||
@InjectCapability
|
||||
private PerceiveCapability perceiveCapability;
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
@InjectModule
|
||||
private MessageSummarizer messageSummarizer;
|
||||
@InjectModule
|
||||
private MessageCompressor messageCompressor;
|
||||
@InjectModule
|
||||
private ActionScheduler actionScheduler;
|
||||
@InjectModule
|
||||
private AfterRollingRegistry afterRollingRegistry;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
registerScheduledUpdater();
|
||||
}
|
||||
|
||||
private void registerScheduledUpdater() {
|
||||
StateAction stateAction = new StateAction(
|
||||
"system",
|
||||
"dialog-rolling-auto-update",
|
||||
"定时检查并触发对话滚动",
|
||||
Schedulable.ScheduleType.CYCLE,
|
||||
AUTO_UPDATE_CRON,
|
||||
new StateAction.Trigger.Call(() -> {
|
||||
tryAutoRolling();
|
||||
return Unit.INSTANCE;
|
||||
})
|
||||
);
|
||||
actionScheduler.schedule(stateAction);
|
||||
log.info("Dialog rolling has been registered into ActionScheduler, cron={}", AUTO_UPDATE_CRON);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
|
||||
if (cognitionCapability.getChatMessages().size() < DIALOG_ROLLING_TRIGGER_LIMIT) {
|
||||
return;
|
||||
}
|
||||
actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL).execute(() -> triggerRolling(false));
|
||||
}
|
||||
|
||||
private void tryAutoRolling() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
int chatCount = cognitionCapability.snapshotChatMessages().size();
|
||||
if (currentTime - perceiveCapability.showLastInteract().toEpochMilli() > UPDATE_TRIGGER_INTERVAL && chatCount > 1) {
|
||||
triggerRolling(true);
|
||||
log.debug("Dialog rolling: auto triggered");
|
||||
}
|
||||
}
|
||||
|
||||
private void triggerRolling(boolean refreshMemoryId) {
|
||||
if (!rolling.compareAndSet(false, true)) {
|
||||
log.debug("Dialog rolling: rolling is already executing");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<Message> fullChatSnapshot = cognitionCapability.snapshotChatMessages();
|
||||
if (fullChatSnapshot.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
List<Message> chatIncrement = resolveChatIncrement(fullChatSnapshot);
|
||||
if (chatIncrement.isEmpty()) {
|
||||
if (refreshMemoryId) {
|
||||
memoryCapability.refreshMemorySession();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), CONTEXT_RETAIN_DIVISOR);
|
||||
applyRolling(result);
|
||||
afterRollingRegistry.trigger(result);
|
||||
|
||||
if (refreshMemoryId) {
|
||||
memoryCapability.refreshMemorySession();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ExceptionReporterHandler.INSTANCE.report(new AgentRuntimeException("Dialog rolling failed", e));
|
||||
} finally {
|
||||
rolling.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
List<Message> resolveChatIncrement(List<Message> fullChatSnapshot) {
|
||||
String memoryId = memoryCapability.getMemorySessionId();
|
||||
if (memoryId.isBlank()) {
|
||||
return fullChatSnapshot;
|
||||
}
|
||||
MemoryUnit existingUnit = memoryCapability.getMemoryUnit(memoryId);
|
||||
if (existingUnit.getConversationMessages().isEmpty()) {
|
||||
return fullChatSnapshot;
|
||||
}
|
||||
List<Message> existingMessages = existingUnit.getConversationMessages();
|
||||
int maxOverlap = Math.min(existingMessages.size(), fullChatSnapshot.size());
|
||||
for (int overlap = maxOverlap; overlap > 0; overlap--) {
|
||||
List<Message> existingSuffix = existingMessages.subList(existingMessages.size() - overlap, existingMessages.size());
|
||||
List<Message> snapshotPrefix = fullChatSnapshot.subList(0, overlap);
|
||||
if (existingSuffix.equals(snapshotPrefix)) {
|
||||
return fullChatSnapshot.subList(overlap, fullChatSnapshot.size());
|
||||
}
|
||||
}
|
||||
return fullChatSnapshot;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
|
||||
messageCompressor.execute(chatSnapshot);
|
||||
Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
|
||||
String summary = summaryResult.fold(
|
||||
value -> value,
|
||||
exp -> "no summary, due to exception"
|
||||
);
|
||||
if (summary.isBlank()) {
|
||||
summary = "no summary, due to empty summarize result";
|
||||
}
|
||||
MemoryUnit memoryUnit = memoryCapability.updateMemoryUnit(chatSnapshot, summary);
|
||||
MemorySlice newSlice = memoryUnit.getSlices().getLast();
|
||||
return new RollingResult(memoryUnit, newSlice, List.copyOf(chatSnapshot), newSlice.getSummary(), rollingSize, retainDivisor);
|
||||
}
|
||||
|
||||
private void applyRolling(RollingResult result) {
|
||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||
buildDialogAbstractBlock(result.summary(), result.memoryUnit().getId(), result.memorySlice().getId()),
|
||||
Set.of(ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.COMMUNICATION),
|
||||
20,
|
||||
5,
|
||||
10
|
||||
));
|
||||
cognitionCapability.rollChatMessagesWithSnapshot(result.rollingSize(), result.retainDivisor());
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildDialogAbstractBlock(String summary, String unitId, String sliceId) {
|
||||
return new BlockContent("dialog_history", "dialog_rolling") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
root.setAttribute("related_memory_unit_id", unitId);
|
||||
root.setAttribute("related_memory_slice_id", sliceId);
|
||||
root.setAttribute("datetime", ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
appendTextElement(document, root, "summary", summary);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package work.slhaf.partner.module.communication
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentRuntime
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent.EventStatus
|
||||
import work.slhaf.partner.framework.agent.interaction.data.Reply
|
||||
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object ReplyDispatcher {
|
||||
|
||||
private const val AGGREGATE_WINDOW_MILLIS = 100L
|
||||
private const val NO_REPLY_MARKER = "NO_REPLY"
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val collectorChannel = Channel<ReplyChunk>(Channel.UNLIMITED)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
var pendingChunk: ReplyChunk? = null
|
||||
while (true) {
|
||||
val firstChunk = pendingChunk ?: collectorChannel.receiveCatching().getOrNull() ?: break
|
||||
pendingChunk = null
|
||||
val builder = StringBuilder(firstChunk.delta)
|
||||
|
||||
while (true) {
|
||||
val nextChunk = withTimeoutOrNull(AGGREGATE_WINDOW_MILLIS.milliseconds) {
|
||||
collectorChannel.receiveCatching()
|
||||
} ?: break
|
||||
|
||||
if (nextChunk.isClosed) {
|
||||
flush(builder.toString(), firstChunk.target)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val chunk = nextChunk.getOrNull() ?: break
|
||||
if (chunk.target == firstChunk.target) {
|
||||
builder.append(chunk.delta)
|
||||
} else {
|
||||
pendingChunk = chunk
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
flush(builder.toString(), firstChunk.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* flush 将推送至 AgentRuntime 的默认通道。
|
||||
*/
|
||||
private fun flush(content: String, target: String) {
|
||||
if (content.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val event = Reply(
|
||||
status = EventStatus.RUNNING,
|
||||
target = target,
|
||||
content = content,
|
||||
mode = Reply.ContentMode.APPEND,
|
||||
done = false
|
||||
)
|
||||
AgentRuntime.response(event)
|
||||
}
|
||||
|
||||
fun createConsumer(target: String): StreamChatMessageConsumer = ReplyConsumer(
|
||||
collectorChannel = collectorChannel,
|
||||
target = target,
|
||||
)
|
||||
|
||||
private data class ReplyChunk(
|
||||
val delta: String,
|
||||
val target: String,
|
||||
)
|
||||
|
||||
private class ReplyConsumer(
|
||||
private val collectorChannel: Channel<ReplyChunk>,
|
||||
private val target: String,
|
||||
) : StreamChatMessageConsumer() {
|
||||
|
||||
private enum class VisibilityState {
|
||||
UNDECIDED,
|
||||
NO_REPLY,
|
||||
VISIBLE
|
||||
}
|
||||
|
||||
private val rawResponse = StringBuilder()
|
||||
private val undecidedBuffer = StringBuilder()
|
||||
private var visibilityState = VisibilityState.UNDECIDED
|
||||
|
||||
override fun onDelta(delta: String) {
|
||||
rawResponse.append(delta)
|
||||
routeDelta(delta)
|
||||
}
|
||||
|
||||
override fun collectResponse(): String {
|
||||
finalizeUndecidedBuffer()
|
||||
return rawResponse.toString()
|
||||
}
|
||||
|
||||
private fun routeDelta(delta: String) {
|
||||
when (visibilityState) {
|
||||
VisibilityState.NO_REPLY -> return
|
||||
VisibilityState.VISIBLE -> flushVisible(delta)
|
||||
VisibilityState.UNDECIDED -> {
|
||||
undecidedBuffer.append(delta)
|
||||
resolveVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveVisibility() {
|
||||
val content = undecidedBuffer.toString()
|
||||
if (content.length <= NO_REPLY_MARKER.length) {
|
||||
if (NO_REPLY_MARKER.startsWith(content)) {
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
return
|
||||
}
|
||||
|
||||
if (!content.startsWith(NO_REPLY_MARKER)) {
|
||||
revealBufferedContent()
|
||||
return
|
||||
}
|
||||
|
||||
val suffixFirstChar = content[NO_REPLY_MARKER.length]
|
||||
if (suffixFirstChar == '\n' || suffixFirstChar == '\r') {
|
||||
visibilityState = VisibilityState.NO_REPLY
|
||||
undecidedBuffer.setLength(0)
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
}
|
||||
|
||||
private fun finalizeUndecidedBuffer() {
|
||||
if (visibilityState != VisibilityState.UNDECIDED) {
|
||||
return
|
||||
}
|
||||
if (undecidedBuffer.toString() == NO_REPLY_MARKER) {
|
||||
visibilityState = VisibilityState.NO_REPLY
|
||||
undecidedBuffer.setLength(0)
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
}
|
||||
|
||||
private fun revealBufferedContent() {
|
||||
visibilityState = VisibilityState.VISIBLE
|
||||
if (undecidedBuffer.isNotEmpty()) {
|
||||
flushVisible(undecidedBuffer.toString())
|
||||
undecidedBuffer.setLength(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushVisible(delta: String) {
|
||||
collectorChannel.trySend(ReplyChunk(delta, target)).isSuccess
|
||||
}
|
||||
|
||||
override fun consumeDelta(delta: String?) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package work.slhaf.partner.module.communication;
|
||||
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RollingResult(
|
||||
MemoryUnit memoryUnit,
|
||||
MemorySlice memorySlice,
|
||||
List<Message> incrementMessages,
|
||||
String summary,
|
||||
int rollingSize,
|
||||
int retainDivisor
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package work.slhaf.partner.module.communication.summarizer;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class MessageCompressor extends AbstractAgentModule.Sub<List<Message>, Void> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责对单条消息进行压缩改写。
|
||||
|
||||
目标:
|
||||
- 在不改变原意的前提下,压缩冗余表达,减少长度;
|
||||
- 保留消息中真正有价值的信息;
|
||||
- 让压缩结果仍然像原消息,而不是另一种文体的摘要。
|
||||
|
||||
核心要求:
|
||||
- 尽量保留原消息的视角、语气、态度、情绪与表达倾向,不要无故改写成中性、客观、旁白式总结。
|
||||
- 不要把第一人称改成第三人称;不要把直接表达改成“用户表示……”“其意思是……”这类转述,除非原消息本身就是这种口吻。
|
||||
- 若原消息包含明显的情绪、评价、犹豫、强调、否定、推进意图、反问、吐槽等内容,压缩后应尽量保留这些信息。
|
||||
- 压缩的重点是删除冗余、合并重复、收紧表达,不是改写说话风格。
|
||||
|
||||
格式要求:
|
||||
- 允许保留原消息中已有的 markdown、标题、项目符号、编号列表、引用、代码块、代码片段等结构。
|
||||
- 不要为了压缩而强行去除这些结构;若这些结构本身承载了信息层级或语义边界,应尽量保留。
|
||||
- 也不要为了“更整齐”主动新增原消息没有的标题、列表或代码块。
|
||||
- 原消息有结构时,优先继承其组织方式;原消息没有结构时,保持自然文本即可。
|
||||
|
||||
压缩策略:
|
||||
- 删除明显重复、空转、口头垫话、对主旨无帮助的展开。
|
||||
- 合并语义接近、重复推进的句子。
|
||||
- 保留真正影响理解的事实、判断、条件、限制、结论、态度和情绪。
|
||||
- 若原消息包含技术内容、代码、配置、接口、规则、步骤等,优先保留这些实质信息,不要只保留泛泛结论。
|
||||
- 若原消息本身已经很短或进一步压缩会损失重要语义,则可基本保持原样。
|
||||
|
||||
关于日志、代码及长文本片段:
|
||||
- 若原消息中包含日志、代码、配置、报错堆栈、命令输出等长片段,且内容较长、重复性强或并非全部都对理解当前消息同等重要,则可以进行截断。
|
||||
- 截断时应优先保留:
|
||||
- 与当前问题、判断、结论直接相关的部分;
|
||||
- 首尾中能体现上下文和结果的关键部分;
|
||||
- 报错、异常、返回值、状态变化、关键参数、关键命令、关键代码段。
|
||||
- 不要无说明地直接删去中间内容;若发生截断,必须显式标注。
|
||||
- 截断标注统一使用以下格式之一,并与原文风格保持尽量一致:
|
||||
- `...[中间内容已截断]...`
|
||||
- 代码或日志块内可使用:`// ...[中间内容已截断]...` 或 `# ...[中间内容已截断]...`
|
||||
- 截断后的内容仍应保持可读,且不能歪曲原始含义。
|
||||
- 若长片段本身就是当前消息的核心,且截断会损失关键语义,则不要截断。
|
||||
|
||||
禁止事项:
|
||||
- 不要补充原消息没有的新信息。
|
||||
- 不要替原消息做解释、分析、总结或评价。
|
||||
- 不要把技术表达改写得过于口语化,也不要把口语表达改写得过于书面化。
|
||||
- 不要输出“压缩后:”之类前缀,只直接输出压缩结果。
|
||||
|
||||
输出要求:
|
||||
- 只输出压缩后的消息正文。
|
||||
""";
|
||||
|
||||
private static final int COMPRESS_TRIGGER_LENGTH = 1200;
|
||||
private static final int FALLBACK_MAX_LENGTH = 900;
|
||||
private static final String FALLBACK_OMITTED_MARKER = "\n...[中间内容已裁剪]...\n";
|
||||
private static final String COMPRESSED_MARKER = "[COMPRESSED]";
|
||||
private static final String UNKNOWN_ROLE_MARKER = "[[Unknown]: Unknown]";
|
||||
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
|
||||
private static final Pattern ROLE_PREFIX_PATTERN = Pattern.compile("(\\[\\[(?:USER|AGENT)]:\\s*[^]]+])");
|
||||
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doExecute(List<Message> chatMessages) {
|
||||
List<Integer> targetIndexes = IntStream.range(0, chatMessages.size())
|
||||
.filter(index -> shouldCompress(chatMessages.get(index)))
|
||||
.boxed()
|
||||
.toList();
|
||||
CountDownLatch latch = new CountDownLatch(targetIndexes.size());
|
||||
for (Integer index : targetIndexes) {
|
||||
Message chatMessage = chatMessages.get(index);
|
||||
ParsedMessage parsedMessage = parseMessage(chatMessage.getContent());
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String summarized = summarizeOrFallback(parsedMessage.body());
|
||||
chatMessages.set(index, new Message(chatMessage.getRole(), rebuildMessage(parsedMessage, summarized)));
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean shouldCompress(Message chatMessage) {
|
||||
return parseMessage(chatMessage.getContent()).body().length() > COMPRESS_TRIGGER_LENGTH;
|
||||
}
|
||||
|
||||
private String summarizeOrFallback(String content) {
|
||||
String summarized = chat(List.of(new Message(Message.Character.USER, content))).fold(
|
||||
res -> res,
|
||||
exp -> null
|
||||
);
|
||||
if (isAcceptableSummary(summarized, content)) {
|
||||
return summarized.trim();
|
||||
}
|
||||
return truncateForFallback(content);
|
||||
}
|
||||
|
||||
private boolean isAcceptableSummary(String summarized, String originalContent) {
|
||||
if (summarized == null) {
|
||||
return false;
|
||||
}
|
||||
String normalized = summarized.trim();
|
||||
return !normalized.isEmpty() && normalized.length() < originalContent.length();
|
||||
}
|
||||
|
||||
private String truncateForFallback(String content) {
|
||||
if (content == null || content.length() <= FALLBACK_MAX_LENGTH) {
|
||||
return content;
|
||||
}
|
||||
int available = FALLBACK_MAX_LENGTH - FALLBACK_OMITTED_MARKER.length();
|
||||
int headBudget = available / 2;
|
||||
int tailBudget = available - headBudget;
|
||||
int headEnd = adjustHeadEnd(content, headBudget);
|
||||
int tailStart = adjustTailStart(content, content.length() - tailBudget);
|
||||
if (tailStart <= headEnd) {
|
||||
return content.substring(0, FALLBACK_MAX_LENGTH).stripTrailing();
|
||||
}
|
||||
return content.substring(0, headEnd).stripTrailing()
|
||||
+ FALLBACK_OMITTED_MARKER
|
||||
+ content.substring(tailStart).stripLeading();
|
||||
}
|
||||
|
||||
private int adjustHeadEnd(String content, int preferredEnd) {
|
||||
int safePreferredEnd = Math.clamp(preferredEnd, 0, content.length());
|
||||
int windowEnd = Math.min(content.length(), safePreferredEnd + 80);
|
||||
for (int i = safePreferredEnd; i < windowEnd; i++) {
|
||||
if (isBoundary(content.charAt(i))) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return safePreferredEnd;
|
||||
}
|
||||
|
||||
private int adjustTailStart(String content, int preferredStart) {
|
||||
int safePreferredStart = Math.clamp(preferredStart, 0, content.length());
|
||||
int windowStart = Math.max(0, safePreferredStart - 80);
|
||||
for (int i = safePreferredStart; i > windowStart; i--) {
|
||||
if (isBoundary(content.charAt(i - 1))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return safePreferredStart;
|
||||
}
|
||||
|
||||
private boolean isBoundary(char ch) {
|
||||
return ch == '\n'
|
||||
|| ch == '。'
|
||||
|| ch == '!'
|
||||
|| ch == '?'
|
||||
|| ch == ';'
|
||||
|| ch == ';'
|
||||
|| ch == '.';
|
||||
}
|
||||
|
||||
private ParsedMessage parseMessage(String content) {
|
||||
String source = content == null ? "" : content;
|
||||
int separatorIndex = source.indexOf(MARKER_BODY_SEPARATOR);
|
||||
String markerLine = separatorIndex >= 0 ? source.substring(0, separatorIndex).trim() : "";
|
||||
String remaining = separatorIndex >= 0 ? source.substring(separatorIndex + MARKER_BODY_SEPARATOR.length()).trim() : source.trim();
|
||||
String rolePrefix = null;
|
||||
String statusMarkers = "";
|
||||
|
||||
Matcher roleMatcher = ROLE_PREFIX_PATTERN.matcher(markerLine);
|
||||
if (roleMatcher.find()) {
|
||||
rolePrefix = roleMatcher.group(1);
|
||||
statusMarkers = markerLine.substring(roleMatcher.end()).trim();
|
||||
if (statusMarkers.startsWith(":")) {
|
||||
statusMarkers = statusMarkers.substring(1).trim();
|
||||
}
|
||||
if (statusMarkers.endsWith(":")) {
|
||||
statusMarkers = statusMarkers.substring(0, statusMarkers.length() - 1).trim();
|
||||
}
|
||||
}
|
||||
return new ParsedMessage(rolePrefix, statusMarkers, remaining);
|
||||
}
|
||||
|
||||
private String rebuildMessage(ParsedMessage parsedMessage, String compressedBody) {
|
||||
return buildMarkerHeader(parsedMessage.rolePrefix(), parsedMessage.statusMarkers())
|
||||
+ MARKER_BODY_SEPARATOR
|
||||
+ compressedBody;
|
||||
}
|
||||
|
||||
private String buildMarkerHeader(String rolePrefix, String statusMarkers) {
|
||||
String identityMarker = rolePrefix == null || rolePrefix.isBlank() ? UNKNOWN_ROLE_MARKER : rolePrefix;
|
||||
String normalizedStatusMarkers = statusMarkers == null ? "" : statusMarkers.trim();
|
||||
normalizedStatusMarkers = normalizedStatusMarkers.replace(COMPRESSED_MARKER, "").trim();
|
||||
normalizedStatusMarkers += COMPRESSED_MARKER;
|
||||
return identityMarker + ": " + normalizedStatusMarkers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "single_summarizer";
|
||||
}
|
||||
|
||||
private record ParsedMessage(String rolePrefix, String statusMarkers, String body) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package work.slhaf.partner.module.communication.summarizer;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.TaskBlock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MessageSummarizer extends AbstractAgentModule.Sub<List<Message>, Result<String>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责对一组已经发生过的聊天消息进行总结整理,生成一段可供后续系统使用的摘要结果。
|
||||
|
||||
你会收到一条结构化任务消息,其中:
|
||||
- <message_tag_notes> 说明聊天消息中可能出现的标签及其含义;
|
||||
- <chat_messages> 承载本次需要总结的消息列表,每条消息都带有 role 与正文内容。
|
||||
|
||||
你的任务:
|
||||
- 基于整组消息,提炼出一段紧凑、连贯、信息完整的摘要;
|
||||
- 摘要应尽量覆盖这组消息中的主要事实、结论、约束、推进情况、未决点、明显态度与情绪变化;
|
||||
- 若消息中包含技术讨论、配置、代码、报错、规则、方案比较、设计判断等内容,应优先保留这些对后续理解真正有帮助的信息。
|
||||
|
||||
摘要视角要求:
|
||||
- 摘要默认采用 AGENT 视角书写,即以“我”的立场整理这组对话,而不是使用外部旁观者口吻。
|
||||
- 对于来自 [AGENT] 或 assistant 的消息,可将其理解为我的表达、我的判断、我的推进、我的反思或我的内部反馈,并以“我”来概括。
|
||||
- 对于来自 [USER] 的消息,应明确保留其“用户”身份,不要模糊为无来源的陈述,也不要误写成“我”的观点。
|
||||
- 不要默认把整组消息改写成“用户近期……”“系统如何……”这类第三人称阶段报告,除非原消息本身就是这种汇报视角。
|
||||
- 若消息中出现 [NOT_REPLIED],表示这是一条我未直接发给用户、但保留在交流轨迹中的内部交流结果;必要时可在摘要中说明这是我内部保留的判断或反馈。
|
||||
|
||||
总结原则:
|
||||
- 重点提炼这组消息中真正影响后续理解和推进的信息,不要平均分配篇幅。
|
||||
- 合并重复表达、重复确认和多轮来回拉扯后的同类结论。
|
||||
- 若消息中形成了明确结论、决定、偏好、限制条件、行动推进或阶段性判断,应优先写出。
|
||||
- 若消息中仍存在未解决问题、待确认事项、分歧点或风险点,也应明确保留。
|
||||
- 若消息整体只是闲聊、感叹或状态表达,也应如实概括其主要情绪和交流走向,不要硬总结出不存在的任务结论。
|
||||
|
||||
关于技术内容:
|
||||
- 若消息中包含代码、日志、命令输出、配置片段等长内容,不要原样大段复写;
|
||||
- 应概括其中真正关键的信息,例如:关键报错、关键配置、关键判断、关键修改点、关键结果。
|
||||
- 只有在少量原文片段对后续理解不可替代时,才可保留必要短句。
|
||||
|
||||
输出要求:
|
||||
- 只输出一段摘要正文,不要添加标题、前缀、说明或额外标签。
|
||||
- 不要输出项目符号列表,除非原始内容极度结构化且不用列表会明显损失可读性。
|
||||
- 不要输出结构之外的解释、注释或额外文本。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<String> doExecute(List<Message> messages) {
|
||||
return chat(List.of(buildChatMessagesBlock(messages).encodeToMessage()));
|
||||
}
|
||||
|
||||
private @NotNull TaskBlock buildChatMessagesBlock(List<Message> messages) {
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
document.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
|
||||
appendListElement(document, root, "chat_messages", "message", messages, (element, message) -> {
|
||||
element.setAttribute("role", message.roleValue());
|
||||
element.setTextContent(message.getContent());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "multi_summarizer";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package work.slhaf.partner.module.memory.pojo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActivationProfile {
|
||||
private Float activationWeight;
|
||||
private Float diffusionWeight;
|
||||
private Float contextIndependenceWeight;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
final class DateMemoryIndex {
|
||||
|
||||
private final Map<LocalDate, CopyOnWriteArrayList<SliceRef>> dateIndex = new HashMap<>();
|
||||
|
||||
void record(SliceRef sliceRef, LocalDate date) {
|
||||
dateIndex.computeIfAbsent(date, key -> new CopyOnWriteArrayList<>()).addIfAbsent(sliceRef);
|
||||
}
|
||||
|
||||
List<SliceRef> find(LocalDate date) {
|
||||
List<SliceRef> refs = dateIndex.get(date);
|
||||
return refs == null ? null : new ArrayList<>(refs);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
dateIndex.clear();
|
||||
}
|
||||
|
||||
void restore(LocalDate date, CopyOnWriteArrayList<SliceRef> refs) {
|
||||
dateIndex.put(date, refs);
|
||||
}
|
||||
|
||||
Map<LocalDate, CopyOnWriteArrayList<SliceRef>> entries() {
|
||||
return dateIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
|
||||
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class MemoryRuntime extends AbstractAgentModule.Standalone implements StateSerializable {
|
||||
|
||||
@InjectCapability
|
||||
private MemoryCapability memoryCapability;
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
private final ReentrantLock runtimeLock = new ReentrantLock();
|
||||
private final TopicMemoryIndex topicIndex = new TopicMemoryIndex();
|
||||
private final DateMemoryIndex dateIndex = new DateMemoryIndex();
|
||||
private final TopicRecallCollector topicRecallCollector = new TopicRecallCollector(new TopicRecallScorer());
|
||||
private final MemoryRuntimeStateCodec stateCodec = new MemoryRuntimeStateCodec();
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
register();
|
||||
checkAndSetMemoryId();
|
||||
}
|
||||
|
||||
private void checkAndSetMemoryId() {
|
||||
if (cognitionCapability.getChatMessages().isEmpty()) {
|
||||
memoryCapability.refreshMemorySession();
|
||||
}
|
||||
}
|
||||
|
||||
public void recordMemory(MemoryUnit memoryUnit,
|
||||
String topicPath,
|
||||
List<String> relatedTopicPaths,
|
||||
ActivationProfile activationProfile) {
|
||||
MemorySlice memorySlice = memoryUnit.getSlices().getLast();
|
||||
SliceRef sliceRef = new SliceRef(memoryUnit.getId(), memorySlice.getId());
|
||||
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
List<String> normalizedRelatedTopicPaths = topicIndex.normalizeTopicPaths(relatedTopicPaths);
|
||||
dateIndex.record(sliceRef, date);
|
||||
if (topicPath != null && !topicPath.isBlank()) {
|
||||
topicIndex.recordBinding(
|
||||
topicPath,
|
||||
sliceRef,
|
||||
memorySlice.getTimestamp(),
|
||||
normalizedRelatedTopicPaths,
|
||||
activationProfile
|
||||
);
|
||||
}
|
||||
topicIndex.ensureTopicPaths(normalizedRelatedTopicPaths);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public List<ActivatedMemorySlice> queryActivatedMemoryByTopicPath(String topicPath) {
|
||||
return buildActivatedMemorySlices(findByTopicPath(topicPath));
|
||||
}
|
||||
|
||||
public List<ActivatedMemorySlice> queryActivatedMemoryByDate(LocalDate date) {
|
||||
return buildActivatedMemorySlices(findByDate(date));
|
||||
}
|
||||
|
||||
public String getTopicTree() {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
return topicIndex.getTopicTree();
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public String fixTopicPath(String topicPath) {
|
||||
String[] parts = topicPath.split("->");
|
||||
List<String> cleanedParts = new ArrayList<>();
|
||||
for (String part : parts) {
|
||||
String cleaned = part.replaceAll("\\[[^]]*]", "").trim();
|
||||
if (!cleaned.isEmpty()) {
|
||||
cleanedParts.add(cleaned);
|
||||
}
|
||||
}
|
||||
return String.join("->", cleanedParts);
|
||||
}
|
||||
|
||||
private List<SliceRef> findByTopicPath(String topicPath) {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
TopicMemoryIndex.TopicTreeNode topicNode = topicIndex.findTopicNode(topicPath);
|
||||
if (topicNode == null) {
|
||||
String normalizedPath = topicIndex.normalizeTopicPath(topicPath);
|
||||
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
|
||||
"Unexisted topic path: " + normalizedPath,
|
||||
normalizedPath,
|
||||
"TOPIC"
|
||||
));
|
||||
return List.of();
|
||||
}
|
||||
return topicRecallCollector.collect(topicIndex, topicNode);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private List<SliceRef> findByDate(LocalDate date) {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
List<SliceRef> refs = dateIndex.find(date);
|
||||
if (refs == null) {
|
||||
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
|
||||
"Unexisted date index: " + date,
|
||||
date.toString(),
|
||||
"DATE_INDEX"
|
||||
));
|
||||
return List.of();
|
||||
}
|
||||
return refs;
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ActivatedMemorySlice> buildActivatedMemorySlices(List<SliceRef> refs) {
|
||||
List<ActivatedMemorySlice> slices = new ArrayList<>();
|
||||
for (SliceRef ref : refs) {
|
||||
ActivatedMemorySlice slice = buildActivatedMemorySlice(ref);
|
||||
if (slice != null) {
|
||||
slices.add(slice);
|
||||
}
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
|
||||
private ActivatedMemorySlice buildActivatedMemorySlice(SliceRef ref) {
|
||||
MemoryUnit memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
|
||||
Result<MemorySlice> memorySliceResult = memoryCapability.getMemorySlice(ref.getUnitId(), ref.getSliceId());
|
||||
if (memoryUnit == null || memorySliceResult.exceptionOrNull() != null) {
|
||||
return null;
|
||||
}
|
||||
MemorySlice memorySlice = memorySliceResult.getOrThrow();
|
||||
List<Message> messages = sliceMessages(memoryUnit, memorySlice);
|
||||
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
||||
return ActivatedMemorySlice.builder()
|
||||
.unitId(ref.getUnitId())
|
||||
.sliceId(ref.getSliceId())
|
||||
.summary(memorySlice.getSummary())
|
||||
.timestamp(memorySlice.getTimestamp())
|
||||
.date(date)
|
||||
.messages(messages)
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<Message> sliceMessages(MemoryUnit memoryUnit, MemorySlice memorySlice) {
|
||||
List<Message> conversationMessages = memoryUnit.getConversationMessages();
|
||||
if (conversationMessages.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
int size = conversationMessages.size();
|
||||
int start = Math.clamp(memorySlice.getStartIndex(), 0, size);
|
||||
int end = Math.clamp(memorySlice.getEndIndex(), start, size);
|
||||
if (start >= end) {
|
||||
return List.of();
|
||||
}
|
||||
return new ArrayList<>(conversationMessages.subList(start, end));
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Long timestamp) {
|
||||
return Instant.ofEpochMilli(timestamp)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("module", "memory", "topic_based_memory.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
stateCodec.load(state, topicIndex, dateIndex);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
return stateCodec.convert(topicIndex, dateIndex);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user