213 Commits

Author SHA1 Message Date
f6afe21b43 Merge branch 'feature/ActionModule'
# Conflicts:
#	.gitignore
2026-02-09 21:22:26 +08:00
d381a97731 refactor(ActionScheduler): add debug log for hour-change trigger scan 2026-02-09 21:12:38 +08:00
940beb2587 test(ActionScheduler): add test 2026-02-09 21:05:08 +08:00
69d9f04f11 fix(ActionScheduler): stabilize wheel tick pacing and run trigger scan before hour/day refresh 2026-02-09 21:04:48 +08:00
e2bd9eb0af fix(ActionScheduler): enqueue same-hour actions in wheel and add scheduling debug logs 2026-02-09 21:19:40 +08:00
9ec03c4c95 fix(ActionScheduler): include previous tick in trigger scan and tighten next execution filtering 2026-02-09 21:01:22 +08:00
ecbbbc9954 refactor(ActionScheduler): include tick in wheel stop debug log 2026-02-09 20:56:45 +08:00
a5d26769e8 fix(ActionScheduler): skip trigger callback when tick has no actions 2026-02-09 20:54:35 +08:00
2db1bdf3e9 refactor(ActionScheduler): add debug log for action execution 2026-02-09 20:50:36 +08:00
656d6b65e3 refactor(ActionScheduler): add debug logs for wheel start/stop, wait window, and action loading 2026-02-09 20:41:01 +08:00
7c46f1d1ff fix(ActionScheduler): remove triggered hour actions by uuid to avoid removeAll mismatch 2026-02-09 20:03:24 +08:00
406b4250aa refactor(ActionScheduler): correct actions loading logic in hour/day updating 2026-02-09 20:03:10 +08:00
eab3d00fe8 refactor(ActionScheduler): remove useless delay in TimeWheel#wheel 2026-02-09 20:02:26 +08:00
d47e9fbf95 fix(ActionScheduler): initialize wheel tick baseline before launch to avoid check-to-wheel startup drift 2026-02-09 17:29:32 +08:00
4b77f26e7b refactor(ActionScheduler): capture current hour once and reuse it for
day/hour rollover checks
2026-02-09 16:37:46 +08:00
650f9b27a1 fix(ActionScheduler): use checkThenExecute current hour consistently and trigger wheel tasks outside lock 2026-02-09 15:56:12 +08:00
9f479c5f6f fix(ActionScheduler): unify time check/loading under checkThenExecute and guard wheel loop with launch-hour consistency 2026-02-09 14:57:13 +08:00
227c735667 fix(ActionScheduler): make TimeWheel load scheduled actions dynamically instead of using init snapshot 2026-02-09 00:13:36 +08:00
b05b665960 fix(ActionScheduler): reload day/hour action buckets on time changes via checkTimeAndLoad, and reorganize functions 2026-02-09 00:03:21 +08:00
882ec43f2b fix(ActionScheduler): make scheduling thread-safe with Mutex and cancel scheduler/time wheel scopes on shutdown 2026-02-08 23:07:03 +08:00
7cb565fd1b fix(ActionScheduler): use withTimeoutOrNull when waiting for ACTIVE state, to avoid exception leading to wheel stopped 2026-02-08 21:56:35 +08:00
84b96b6645 test(ActionScheduler): remove unused actionExecutor mock 2026-02-08 21:52:16 +08:00
2169376062 test(ActionScheduler): add unit test for ActionScheduler 2026-02-08 21:51:57 +08:00
9bff74c8c7 fix(ActionScheduler): remove second offset when loading hour actions 2026-02-08 21:51:30 +08:00
76c9c27532 refactor(MetaAction): make result a read-only property 2026-02-08 17:22:47 +08:00
8524ca6f9f refactor(ActionData): use action.result.reset() when clearing action chain state 2026-02-08 17:22:10 +08:00
7dd2104689 refactor(MetaAction): migrate to Kotlin data class, merge MetaActionType/ResultStatus into nested enums, and update runner/action usages 2026-02-08 17:15:58 +08:00
6ba5784a7f 将 .java 重命名为 .kt 2026-02-08 17:15:58 +08:00
cdea8d6322 refactor(ActionData): migrate to Kotlin sealed class and data classes, update planner/scheduler usage 2026-02-08 16:27:44 +08:00
8ca2b9998d 将 .java 重命名为 .kt 2026-02-08 16:27:44 +08:00
d098b28f31 refactor(ActionExecutorInput): migrate to Kotlin data class 2026-02-08 15:12:10 +08:00
98e4d4cf1b 将 .java 重命名为 .kt 2026-02-08 15:12:10 +08:00
70489e57f7 chore(pom): add kotlinx-coroutines-test dependency 2026-02-08 14:34:19 +08:00
a43c87006e refactor(ActionCore): replace existing action with same UUID before putAction 2026-02-08 13:29:18 +08:00
be43b7eec6 refactor(ActionScheduler): implement Kotlin time-wheel scheduling and requeue scheduled actions after execution 2026-02-08 13:24:56 +08:00
3bc2ce839a 将 .java 重命名为 .kt 2026-02-08 13:24:56 +08:00
fe5a366527 refactor(ActionExecutor): remove userId from ActionExecutorInput and use source 2026-02-08 11:29:36 +08:00
9f724cee5d chore(pom): remove test scope from coroutines dependency 2026-02-08 11:22:47 +08:00
ad58b83020 refactor(ActionExecutor): rename actions variable for clarity 2026-02-08 11:22:30 +08:00
c9b64fec2a chore(pom): add cron-utils dependency 2026-02-07 15:36:45 +08:00
0eb4765235 refactor(ActionExecutor): use HistoryAction record and track scheduled history reset 2026-02-07 15:35:34 +08:00
050c39cbc7 refactor(ActionExecutor): correct input actions' type in ActionExecutor 2026-02-06 23:38:13 +08:00
08100aea8a refactor(ActionCore): replace prepared action APIs with generic list/put 2026-02-06 21:38:54 +08:00
2cd0774834 refactor(ActionCapability): rename listAvailableActions to listAvailableMetaActions 2026-02-06 21:05:10 +08:00
12df938d85 refactor(ActionCore): simplify handleInterventions to use ActionData 2026-02-06 20:41:08 +08:00
277c0d437f chore(ActionCore): update comment 2026-02-06 20:36:50 +08:00
6b861f4b77 fix(ActionExecutor): correct logic in ActionExecutor when actionChain is empty, which will skip execution 2026-02-05 20:40:02 +08:00
d33b6617c1 fix(ActionExecutor): support removing phaserRecord correctly when exception occurred in ActionCorrector 2026-02-05 19:40:28 +08:00
a1dcf4a6fa test(ActionExecutor): test with action failure 2026-02-05 17:05:53 +08:00
9c38719514 test(ActionExecutor): test with additionalContext appending 2026-02-05 16:58:22 +08:00
33df0fa017 test(ActionExecutor): test with virtual thread pool support 2026-02-05 16:53:38 +08:00
08bda84471 refactor(ActionCore): Specifies the minimum platform thread pool size
Context:
 The ActionExecutor needs at least 2 platform thread to support async  action execution,
 otherwise the current ActionExecutor logic cannot be executed
2026-02-05 16:49:58 +08:00
76da3c29f8 fix(ActionExecutorTest): repair stub in test 2026-02-05 16:13:21 +08:00
558b589830 refactor(ActionInterventor): redefine DTOs in ActionInterventor to adapt the actual intervention logic 2026-02-05 15:48:58 +08:00
80d7c283c5 refactor(ActionExecutor): update ActionChain execution, support executing and advancing correctly 2026-02-04 00:29:42 +08:00
b0bb40c5f0 test(ActionExecutor): add unit test for ActionExecutor 2026-02-01 19:49:21 +08:00
eec8f71096 fix(action): correct return type of method runnerClient() in ActionCapability 2026-02-01 16:43:12 +08:00
fbd30d1a96 build(maven): import Mockito related dependencies 2026-02-01 14:56:47 +08:00
346f925b66 chore(ActionExecutor): update comment 2026-01-31 23:35:17 +08:00
04e8d9e531 feat(ActionExecutor): support executing interventions in ActionExecutor 2026-01-30 20:58:12 +08:00
63d1552de2 refactor(ActionInterventor): remove InterventionHandler and related data class
Context:
Since last commit, the logic of interventions has been moved into ActionCapability,
the InterventionHandler is not needed.
2026-01-30 20:52:36 +08:00
77eb9b92a4 refactor(ActionCorrector): move intervention logic from InterventionHandler into ActionCapability 2026-01-30 20:10:01 +08:00
a1b4743eeb feat(ActionCorrector): complete corrector's executing logic 2026-01-30 19:19:48 +08:00
0768cddd2d fix(ActivateModel): correct modelSettings 2026-01-30 16:50:45 +08:00
75145cc547 chore(ActionRepairer): correct name of AssemblyHelper 2026-01-30 16:30:10 +08:00
d1ca1cda7d feature(ActionExecutor): complete CorrectorInput 2026-01-28 23:11:45 +08:00
fac6609d6b refactor(ActionExecutor): remove useless method getHistoryActionResults 2026-01-28 23:00:53 +08:00
dce8825e58 refactor(ActionExecutor): update type of history field in ActionData 2026-01-28 21:16:51 +08:00
cd641ac8dd fix(ActionExecutor): correct phaser block logic in method execute 2026-01-28 15:38:13 +08:00
5ffdab9e4a refactor(ActionExecutor): rework staged execution and runner submit
Context:
This refactor drops unnecessary method abstractions and cleans the action execution flow.
Additionally, method 'run' is renamed to 'submit' in RunnerClient, which better reflects that execution results are held in MetaAction.
2026-01-25 19:38:53 +08:00
830503eee4 chore(ActionExecutor): update comments 2026-01-23 19:23:07 +08:00
96e74ec877 test(LocalRunnerClient): add test for method run in RunnerClient 2026-01-17 11:28:21 +08:00
420d51af15 fix(LocalRunnerClient): harden doRun branches and add tests 2026-01-16 23:28:46 +08:00
8ead306b7b fix(RunnerClient): correct RunnerResponse's visibility 2026-01-16 22:17:15 +08:00
c793851107 fix(LocalRunnerClient): support cleaning non-existing MCP Servers' tools while MCP configuration files changed in CommonMcp 2026-01-16 21:48:17 +08:00
fb5cabc747 fix(LocalRunnerClient): support read MetaActionInfo according to desc files when an MCP Client with described tools registered by CommonMcp 2026-01-16 21:28:45 +08:00
c5f6c4e0ae fix(LocalRunnerClient): recover desc watcher after root deletion and expand DescMcp tests 2026-01-14 19:57:24 +08:00
200c0f3f13 fix(LocalRunnerClient): guard against null tool meta and ignore non-protocol MCP 2026-01-14 16:10:33 +08:00
fdf398b86e fix(LocalRunnerClient): close old MCP client while a new client's name is duplicated with the old one 2026-01-13 23:22:54 +08:00
774e2b6cd5 fix(LocalRunnerClient): correct abnormal deleting condition in CommonMcp 2026-01-13 23:13:52 +08:00
837a4c92d1 fix(LocalRunnerClient): treat missing action dir as invalid path during DELETE in DynamicMcp
Context:
Action directories may already be removed when DELETE events are handled.
Return null from loadFiles to signal invalid paths and lock behavior with DynamicAction watch tests.
2026-01-12 21:46:34 +08:00
ddd999d47b fix(LocalRunnerClient): prevent WatchService event loss caused by concurrent consumers
Context:
Shared WatchService with multiple watch threads caused WatchKey events to be consumed by mismatched processors, leading to missed file events.
Use isolated WatchService per WatchContext to restore correct semantics.
2026-01-12 19:46:45 +08:00
9694a022c7 chore(gitignore): update gitignore 2026-01-12 19:35:41 +08:00
31968c7076 chore(gitignore): create AGENTS.md for codex, and add it to .gitignore 2026-01-12 14:25:20 +08:00
abec141e4e fix(LocalRunnerClient): correct path creating logic in RunnerClient and its implementations 2026-01-11 16:47:14 +08:00
cdb6ae9d01 fix(LocalRunnerClient): correct method loadFiles in LocalWatchEventProcessor 2026-01-11 16:30:44 +08:00
dd8d86d3c4 chore(LocalRunnerClient): add logs to LocalRunnerClient 2026-01-11 16:27:14 +08:00
99b42620d0 refactor(LocalRunnerClient): repair paths registering order and support creating directories automatically 2026-01-11 15:01:19 +08:00
70b8335d49 feat(LocalRunnerClient): support atomic persist serialization in LocalRunnerClient 2026-01-11 14:24:34 +08:00
8ca475beeb feat(LocalRunnerClient): support registering CommonMcp 2026-01-08 22:28:12 +08:00
4f36c0dd2d feat(LocalRunnerClient): support deleting MCP configurations in CommonMcp 2026-01-08 22:23:08 +08:00
00993bd763 feat(LocalRunnerClient): support creating MCP configurations in CommonMcp 2026-01-08 22:09:14 +08:00
a0bca668cb refactor(LocalRunnerClient): support update existedMetaActions in method registerMcpClient 2026-01-08 21:48:30 +08:00
c6118c41b0 refactor(LocalRunnerClient): support loading primary fileMcpCache when CommonMcp launched 2026-01-08 21:33:39 +08:00
872d21170a feat(LocalRunnerClient): support modify and overflow events on mcp configurations in CommonMcp
Context:
Due to single file cannot present all mcp configurations, loading all MCPs at once is required.
This is compatible in both modify and overflow events.
2026-01-08 21:16:28 +08:00
44ab6cfac8 feat(LocalRunnerClient): support registering MCP clients in CommonMcp 2026-01-05 23:06:17 +08:00
ec30ac1922 refactor(LocalRunnerClient): remove tool change consumer in registerMcpClient
Context:
ExistedMetaActions' updating logic is covered by implementations of LocalWatchEventProcessor.
2026-01-03 16:34:04 +08:00
74b6d0c653 chore(RunnerClient): fix RunnerClient error usages in implementations 2026-01-03 15:49:54 +08:00
de462866b2 feat(LocalRunnerClient): support registering DescMcpServer watch service 2026-01-02 21:41:47 +08:00
4ea8926363 feat(LocalRunnerClient): support repairing description data while OVERFLOW event happened in DescMcpServer 2026-01-02 21:29:18 +08:00
04c98c7856 fix(LocalRunnerClient): support deleting descCache while *.desc.json is not available in DescMcpServer 2026-01-02 18:18:15 +08:00
0757856187 feat(LocalRunnerClient): support deleting *.desc.json in DescMcpServer 2026-01-02 17:20:12 +08:00
19ec93f248 feat(LocalRunnerClient): create modify *.desc.json in DescMcpServer 2026-01-02 16:45:01 +08:00
5877b9e80d feat(LocalRunnerClient): support modify *.desc.json in DescMcpServer 2026-01-02 16:42:56 +08:00
5db0b5fad1 feat(LocalRunnerClient): support load *.desc.json when DescMcpServer launched 2026-01-02 15:55:29 +08:00
623a86daab chore(LocalRunnerClient): update mcp servers' comments 2026-01-02 15:52:44 +08:00
64f24d3fc3 chore(LocalRunnerClient): adjust mcp servers' comments location 2026-01-02 13:38:43 +08:00
3097efe453 feat(LocalRunnerClient): support register DynamicActionMcp watch service 2026-01-02 13:29:00 +08:00
b58eeffd2f feat(LocalRunnerClient): support overflow event in DynamicActionMcpServer 2026-01-01 23:32:21 +08:00
62cec79005 refactor(LocalRunnerClient): extract duplicated action adding logic 2026-01-01 22:39:32 +08:00
03a5935107 fix(LocalRunnerClient): support deleting event for action directories in DynamicActionMcp 2026-01-01 21:29:30 +08:00
0ecaec0545 fix(LocalRunnerClient): repair loading logic of action subdirectories 2026-01-01 20:28:19 +08:00
74f2c6c950 fix(LocalRunnerClient): support creating and registering new action in
method buildCreate in DynamicActionMcp
2026-01-01 00:32:34 +08:00
f35a467ebc fix(LocalRunnerClient): support registering subdirectories in LocalWatchServiceBuild 2025-12-31 23:15:27 +08:00
64b907707a refactor(LocalRunnerClient): introduce WatchContext and decouple build/processor state 2025-12-31 23:11:15 +08:00
a6e33edc7a refactor(LocalRunnerClient): support remove action temporarily while action is not usable 2025-12-31 16:27:34 +08:00
94ef79c67d feat(LocalRunnerClient): support program deletion for DynamicActionMcp 2025-12-31 13:41:35 +08:00
a222015abb feat(LocalRunnerClient): support program modify and unify action load protocol
Context:
The method buildModify reuses AsyncToolSpecification building logic in buildLoad.
This feature unifies local action directory protocol, and refactors related logic in buildLoad.
New action directory protocol defines the file names of program and description files.
2025-12-30 20:52:32 +08:00
1c562f0e7b refactor(LocalRunnerClient): update action keys building source in
DynamicActionMcp

 Context:
 Building action keys by subdirector's name keeps unique identity for each local action.
2025-12-30 16:43:39 +08:00
89535a6b1c feat(LocalRunnerClient): add initial support for loading local action tools from filesystem
Context:
This feature supports DynamicActionMcpServer.

During initialization, directories containing a program file and a
.meta.json description are scanned and registered as MCP tools.
Tool execution is handled asynchronously via boundedElastic to avoid blocking server threads.
2025-12-29 20:46:26 +08:00
6e90bc8d67 refactor(LocalRunnerClient): co-locate system execution result 2025-12-29 18:53:41 +08:00
0e741802d1 refactor(LocalRunnerClient): consolidate MCP client transport params
Context:
Group HTTP and STDIO transport parameter variants under a sealed internal transport parameter hierarchy.
2025-12-29 18:48:53 +08:00
db3435fccf refactor(LocalRunnerClient): co-locate watch service builder internals
Context:
Group WatchService build interfaces and registry implementation into a
single internal structure for better cohesion.
2025-12-29 18:40:20 +08:00
e3294ec302 refactor(LocalRunnerClient): move system execution methods into SystemExecHelper 2025-12-29 18:26:30 +08:00
bf99e01b51 feat(LocalRunnerClient): introduce LocalWatchServiceHelper and internal implementations
Context:
This change introduces an internal scaffold to organize WatchEventHandler building logic.
2025-12-29 17:45:39 +08:00
1bd23b20c4 refactor(LocalRunnerClient): introduce DescMcpServer
Context:
This refactor supports creating descriptional files for common MCP Tools.
2025-12-29 17:35:03 +08:00
442dd55686 refactor(LocalRunnerClient): rename LocalWatchServiceRegistry 2025-12-29 14:27:00 +08:00
abe5dd5251 chore(idea): update misc.xml 2025-12-26 21:28:10 +08:00
1f737c0e29 refactor(action): reorganize constants in action module 2025-12-26 21:28:02 +08:00
d41074c814 refactor(LocalRunnerClient): replace ActionWatchService with unified watch service builder.
Context:
ActionWatchService was used to support SCRIPT and PLUGIN type actions loading from local FileSystem, this refactor allows register different paths to watch.
2025-12-25 15:41:49 +08:00
621441601a feat(LocalRunnerClient): correct method signature 2025-12-25 10:20:55 +08:00
e00d77f076 feat(LocalRunnerClient): add shutdown logic for dynamicActionMcpServer 2025-12-25 10:12:38 +08:00
d614ac0b15 feat(LocalRunnerClient): support initializing in-process dynamic action MCP Server 2025-12-24 21:36:39 +08:00
592e2604d9 refactor(mcp): move InProcessMcpTransport into Partner-Common module
Context:
Action modules in Partner-Main and SandboxRunner module rely on in-process MCP transport to support dynamically action generating.
2025-12-24 19:34:04 +08:00
dcbd2c6569 build(maven): introduce common module 2025-12-24 19:21:53 +08:00
476acb0641 refactor(LocalRunnerClient): rename McpServerParams into McpClientTrasnportParams 2025-12-22 15:02:07 +08:00
88a14f36b2 refactor(runner): relocate InProcessMcpTransport to experimental and move local MCP client logic into LocalRunnerClient
Context:
Recent changes blurred the responsibility boundary between RunnerClient and LocalRunnerClient.
This refactor moves local MCP client–specific logic into LocalRunnerClient and isolates InProcessMcpTransport and related code under the experimental package.
RunnerClient only defines indispensable methods and attributes.
2025-12-22 14:56:23 +08:00
05d1fff125 refactor(RunnerClient): remove unused MCP type enum class 2025-12-21 23:03:25 +08:00
49a4c9eb01 docs(RunnerClient): add architecture-location comment on RunnerClient 2025-12-21 22:05:46 +08:00
9e76c3e7ad refactor(SandboxRunnerClient): align doRun visibility with superclass 2025-12-19 23:34:17 +08:00
9762739138 refactor(action): replace HashMap with ConcurrentHashMap for thread-safe MetaAction storage 2025-12-19 23:30:27 +08:00
1f5509c17d refactor(RunnerClient): redesign existedMetaActions update strategy
Context:
Resource-change events cannot reliably represent tool changes.
The previous approach attempted to externalize descriptive content into files, but the meta attribute of McpSchema.Tool can provide this information.
2025-12-19 23:22:36 +08:00
ed042cfffa fix(action): correct params type in related DTOs 2025-12-19 22:57:34 +08:00
128592e23c chore(MetaActionInfo): remove unused type attribute 2025-12-19 22:47:06 +08:00
5ba36ed3e8 feat(LocalRunnerClient): support executing MetaActions via MCP type 2025-12-19 22:29:03 +08:00
4dea948f82 refactor(MetaAction): separate key attribute into name and location
Context:
This change adapts MetaAction locating to support different MetaAction types,
including loading from the local filesystem and from MCP tools.
2025-12-19 21:35:39 +08:00
dc4074715e chore(MetaAction): remove unused order attribute 2025-12-19 20:53:01 +08:00
225802c1a8 refactor(MetaActionInfo): remove key attribute and update related logic
Context:
MetaActionInfo was previously located via its own key attribute.
This is now redundant, as ActionCore already uses the key of existedMetaActions
as the single source of truth.
2025-12-19 20:41:07 +08:00
e851e33b2e feat(RunnerClient): support MCP type-based dynamic client/server registration
This allows implementations of RunnerClient to dynamically register different types of MCP service, and also provides a shutdown hook to close client/server properly.
2025-12-18 22:25:32 +08:00
cb28a5b068 feat(RunnerClient): add InProcessMcpTransport to support in-process MCP communication
Context:
This allows RunnerClient implementations to host local MCP servers without spawning another process.
2025-12-18 21:48:35 +08:00
ad58567ada chore(deps): introduce mcp dependencies 2025-12-18 17:52:15 +08:00
0eee12d685 refactor(MetaActionInfo): remove outdated constructor
Context:
Previously, MetaActionInfo comes from the local filesystem changes.
But now MCP Servers already provide a method to get information of MetaActions.
The pre- or post-dependencies are still required, for some MCP Tools cannot just be executed without additional context.
2025-12-18 17:49:52 +08:00
1e6ff1b30c chore(ActionCore): update outdated comment 2025-12-18 17:49:52 +08:00
0413fc281d chore(MetaAction): update outdated comment 2025-12-17 22:18:43 +08:00
8a7681ae31 chore(LocalRunnerClient): remove a redundant comment 2025-12-17 20:02:28 +08:00
1947f25ed6 feat(LocalRunnerClient): support executing origin actions
Context:
Origin actions are generated by DynamicActionGenerator and may optionally be
persistently serialized. This feature adds the basic execution flow for origin
actions within LocalRunnerClient.

Notes:
The current mapping between action files and their extensions is hardcoded. This should later be replaced with a configurable registry or loaded dynamically
during application startup.
2025-12-16 21:59:53 +08:00
488246525f chore(gitignore): exclude runtime data directory from version control 2025-12-16 21:39:11 +08:00
534dcd5ade fix(LocalRunnerClient): correctly capture stdout and stderr to avoid missing output in method exec 2025-12-16 21:30:51 +08:00
ad58c0cc7c refactor(LocalRunnerClient): allow injecting action watch path
Context:
The hardcoded action watch path made LocalRunnerClient difficult to test and
tightened it to a specific runtime layout. Injecting the watch path improves
testability and allows the runner to work in different runtime environments.
2025-12-16 21:02:29 +08:00
d546148d69 chore(test): organize experimental tests and test resources 2025-12-16 19:58:08 +08:00
bf2d5ac707 refactor(RunnerClient): restructure serialization and temp execution paths
Context:
Following the consolidation of action types into ORIGIN and MCP,
the serialization logic needs to be separated into dedicated methods.
These methods are invoked by DynamicActionGenerator.
2025-12-16 10:47:23 +08:00
628234f6e2 refactor(MetaActionType): redefine meta action types into MCP and ORIGIN
Context:
Previously, SCRIPT and PLUGIN were treated as separate action types,
but their semantics are already covered by MCP.
However, a generic execution path for locally generated actions is still
required, which is represented by ORIGIN.
2025-12-16 10:37:04 +08:00
4b852e0049 推进 ActionExecutor 下的‘行动生成与执行’部分
- 新增 RunnerClient 抽象类,并划分 SandboxRunnerClient、LocalRunnerClient两个子类(内容待完善)。前者负责对接 SandboxRunner 模块,后者直接使用本地作为执行环境(但不推荐)。
- 将 ActionWatchService 划为 LocalRunnerClient 的内部类,负责采用本地执行环境时,监听行动程序变化
- 完善 ActionRepairer 处的修复逻辑
- 调整 MetaAction 中路径获取逻辑

这提交方式真该调整一下了,这阶段推进容易攒太多,但又不好停手。或许阶段目标可以保留,但推进点应该可以细化🤔
2025-12-15 21:54:24 +08:00
6e3deced77 推进 ActionExecutor 下的 DynamicActionGenerator 子模块
- 完善了 DynamicActionGenerator 的大致逻辑,序列化逻辑待实现
- 补充了 PhaserRecord 中的阻塞逻辑,使用普通的线程sleep操作
- 调整了 MetaAction 中参数形式,由列表替换为 Map,便于执行时填写参数
- 完善了 DynamicActionGenerator 相关的数据类
2025-12-07 20:10:53 +08:00
6a351413a1 推进行动执行模块: 调整了 ActionExecutor 以支持行动链动态修复和参数提取; 完善了 ActionRepairer、ParamsExtractor 的主要逻辑; 完善了部分数据类的内容
- 在 ActionData 中新增 additionalContext 用于存储各个执行阶段临时修复生成的上下文,同样以执行阶段为键
- 调整 ActionExecutor 的输入参数,可传入用户标识,用于执行器调用 ActionRepairer 的修复过程
- 完善了 ActionExecutor 中行动单元的执行与修复逻辑,将支持正常状态推进执行、触发自对话时阻塞当前行动单元、所有修复方式失败时将整个行动数据标为 FAILED
- 完善了 ActionExecutor 中各个DTO的构建方法
- 完善了 ParamsExtractor 中的参数提取逻辑
- 在 PhaserRecord 中新增 interrupt 和 complete 方法,将用于后续行动单元的阻塞(ActionExecutor中)与恢复(InterventionHandler中)
- 完善了 ActionRepairer 中的修复逻辑,但自对话通道的暴露方式、DynamicActionGenerator 的具体逻辑待完善
2025-12-05 21:58:21 +08:00
ad973d4230 对 ActionExecutor 下子模块的功能分布、某些实体类进行了调整; 完善了 ActionExecutor 中的大致执行逻辑
- 梳理执行链路时发现 ActionRepairer 的能力明显超出可实现边界,故将其能力进行限定
- 新增 ActionCorrector 负责单组行动执行完毕后,根据意图和执行状况进行行动链修正
- 将 PhaserRecord 拆分为独立实体,未来将封装一部分流程控制逻辑
2025-12-02 22:35:53 +08:00
1d315a9b62 ActionExecutor 的执行流程规划完毕,具体逻辑待填充
- 调整了部分代码分布,移除了某些非必需的转发方法
- 新增几个 TODO 内容,后续工作已明确

这套调度方式看起来真的有些‘探索性质’了。实际上看起来有些像把 ReAct 的逻辑显式地进行了工程实现,不管是修复、依据状态选择行动单元生成还是阶段间针对行动单元的参数提取,在 ReAct Agent 中都是由一个智能体完成的。

但在这里,它要做的事情太多了,再加上 Partner 行动链的干预逻辑、幻觉参数又不可接受所以需要自对话或者用户干预,这些东西交给一个 ReAct 模块恐怕并不合适也不放心。所以这种显式模块划分应该更符合 Partner 行动模块的需求。

这点硬要说的话,应该还是在于‘ReAct 行为’并非 Partner 的全部吧。

不过谁知道呢,也许以后也会变,但这套至少现在看来是更能实现理想行为的
2025-12-01 19:25:21 +08:00
4e32129b31 优化行动链结构及相关组件、针对 ActionPlanner 相关组件做出调整
- 将existedMetaActions的实现由LinkedHashMap替换为HashMap,免去不必要的性能消耗
- 在 ActionCapability 中新增 listAvailableActions 方法用于获取当前存在的可用行动
- 将 ActionData 及相关类中的 LinkedHashMap 替换为普通Map,阶段并发将通过遍历key集合进行,而非针对原始行动链进行遍历
- 在 ActionPlanner 中完善行动链依赖修正逻辑,防止行动单元执行时的输入缺失
- 在 ActionEvaluator 中调整了 Prompt 构建方式
- 调整处理行动链相关代码,移除多余参数,简化方法签名
- 修正 EvaluatorResult 中行动链数据结构为Map,LLM将直接返回初始行动链,后续将加载行动数据并修复行动单元间的依赖关系
- 优化 InterventionHandler、ActionExecutor 等模块中对行动链Map的使用
2025-12-01 17:20:54 +08:00
3f59719e16 调整 MetaAction 的执行方式,将交给 ActionCapability、SandBoxRunnerClient 执行 2025-11-30 22:16:57 +08:00
c548cceec6 新增 SandboxRunner 项目子模块,该模块将在指定容器运行持久服务,与外部主进程通信,将用于后续执行JARSCRIPT两类行动类型 2025-11-30 18:41:42 +08:00
b3098310b4 完善了 ActionConfirmer 的遗漏逻辑 2025-11-30 15:16:57 +08:00
f48d559a7b 调整了 ActionInterventor 中数据构建方法的组织方式 2025-11-30 14:38:50 +08:00
14a57f0be6 推进行动干预模块,前置部分逻辑已基本完成
- 在`ActionData`中添加必要注释、新增`executingStage`字段表示当前执行阶段、移除了`WAITING`的状态类型
- 调整并修正了`ActionExecutor`中的`Phaser`阻塞逻辑
- 完善了`ActionInterventor`中`识别 -> 评估 -> 异步执行`的干预逻辑,并将干预结果以 Prompt 形式回写至流程上下文,作为主模块的已知内容
- 调整了干预模块内部的各个数据类的字段结构,适配干预流程
- 完善了`InterventionEvaluator`、`InterventionHandler`、`InterventionRecognizer`等必需的干预子模块
2025-11-29 20:56:29 +08:00
dff7b69b51 更新 README 2025-11-12 19:53:48 +08:00
d77ffd1db6 Merge remote-tracking branch 'origin/doc/architechture' into doc/architechture 2025-11-11 16:51:18 +08:00
264cdb09e5 推进行动干预模块; 接下来将进一步完善 InterventionHandler 的具体内容
- 调整相关目录为 interventor
-  调整了某些 ActionInterventor 的子模块用到的数据类结构
- 完善了 InterventionEvaluator 的具体逻辑
- 为 InterventionType 添加了注释,并新增了 CANCEL 干预类型
2025-11-11 16:11:09 +08:00
fea7f9c81f PerceiveSelector、PeiceiveUpdater 流程图制作完毕 2025-11-11 08:47:21 +08:00
a1520f117b 推进行动干预模块
- 完善了 ActionInterventor 中的具体逻辑以及不同情况下的prompt填充内容;
- 调整了 PreRunningModule 中的 getPromptDataMap 方法;
- 在 ActionCapability 中新增了检查 actionKey 是否存在的逻辑
2025-11-10 23:02:48 +08:00
ae5caf8475 更新 memory.md 2025-11-10 18:59:05 +08:00
980d9384d1 MemoryUpdater 流程图制作完毕 2025-11-08 17:33:05 +08:00
9ba0d1363a 创建了 action、memory、perceive 三类模块的流程文档; 完成了记忆模块中 MemorySelector 的流程图 2025-11-07 15:14:29 +08:00
f6d5cad5cd 更新 README 2025-11-07 13:51:30 +08:00
c3ca4145b8 推进行动干预模块
- 完善了大致的执行流程
- 明确并创建了评估与处理所需的数据类及干预类型
- 不同情况的Prompt处理结果、评估和处理的具体流程需要进一步完善
2025-11-06 22:07:27 +08:00
5419722c40 更新文档内容 2025-11-06 11:17:25 +08:00
31ebee3ded 制作了整体流程图 2025-11-06 11:14:37 +08:00
746fda1a5e 干预意图提取模块初步完成,Prompt 待制定; 在 ChatClient 中添加了默认的超时设定,超时时间后续可能需要调整。
另: 发现很多细节错误,比如“各个后置模块允许执行的条件”、“主模块出现异常时需要如何处理”、“模块Prompt的构建方式、采用格式不统一”等,需要后续进行修复或调整
2025-10-31 21:26:45 +08:00
ec4fbb7f19 行动干预足以抽离为新的前置模块,但仍属于‘行动’语义,大致框架已确立。后续实现时并发控制、各种干预的协调与触发时机需要注意。 2025-10-31 21:26:45 +08:00
f9c3cacfea 推进 ActionExecutor 相关的动态插拔式行动调度机制
- 移除先前构想的 SpecializedPartnerInputData 及相关类,无论是自反思、向用户求助还是用户主动干预,都应当通过语义识别来作用于对应行动事件,使用固定行动id的机制不足以支撑这种机制
- 在 ActionCore 中新增执行中行动的 phaser 管理逻辑
- 新增几个异常类,适用于行动数据加载的异常情况
- 新增 ActionIdentifier 负责行动干预意图的识别
-
2025-10-31 21:25:12 +08:00
e35e18f3b7 推进 ActionExecutor、确定动态插拔式行动调度的实现思路
- 在 ActionCore 中添加关闭hook,用于正确设置异常中断时执行中任务的状态
- 修正 actionPool 相关注释及用法
- 将 ActionData 中行动链字段调整为 LinkedHashMap 用于更好地支持分组并发及动态调度
- 重构 ActionExecutor 行动链执行逻辑,采用 Phaser 支持动态调度
- 扩展 InputData、Context 字段并调整 GateWay 格式化逻辑以适应特殊输入
2025-10-31 21:25:12 +08:00
83832d2060 推进 ActionExecutor、针对action core做出了一些调整
- 将 ActionWatchService 抽取为独立的类,使用构造参数传递所需内容
- ActionCore 中除了pendingAction外,将只维护一个行动池,通过用户键和STATUS区分类型
- 开始推进 ActionExecutor,但其中的同组并发、动态行动链、行动间参数对齐、参数重构等内容需要仔细考虑
2025-10-31 21:25:12 +08:00
4757425a15 推进 ActionDispatcher 模块、完善行动程序规范与加载逻辑
- 明确行动程序的存储形式与加载规则,分为执行程序和描述文件,前者负责逻辑,后者提供必要的描述性信息;
- 将 ActionInfo 重命名为 ActionData,更新相关接口和实现,增强代码一致性和可读性;
- 添加异常处理类以支持行动程序、描述信息的初始化和加载失败的场景;
- 实现行动程序目录的监控功能,支持行动程序的动态加载与管理;
- 明确了 ActionDispatcher 两个子模块的输入输出规范
2025-10-31 21:25:12 +08:00
21b3a0e846 开始推进 ActionDispatcher 模块
- ActionDispatcher 划分为 ActionScheduler 和 ActionExecutor 两个子模块,分别负责处理计划任务和即时任务
- 正式确定 Action 将以 ActionChain 的形式进行执行,也采用同组并发策略,按照 order 字段在 chain 中进行排序
- 调整了 ActionInfo 等类以适应当前的元行动类
- 对于行动能力的支持,或可考虑这几种方式: Agent自生成python脚本(必须经过验证,确认可执行且无风险)、MCP调用(需适配为Partner所支持的形式)、普通插件(在指定目录动态加载)
2025-10-31 21:25:12 +08:00
6bfa941c35 更新 README 2025-10-31 21:24:46 +08:00
456a7e04e8 更新 README 2025-10-24 17:29:55 +08:00
5864760f35 Action 模块语义缓存机制实现完毕,支持三种情况的语义缓存相关行为: 命中缓存且评估通过、命中缓存但评估未通过、未命中缓存但评估通过。将在评估过后步入主模块之前,进行异步更新操作(借助@AfterExecute注解,通过虚拟线程进入异步流程,在真正调用处使用平台线程加速计算) 2025-10-19 22:05:27 +08:00
aee6d879e9 推进 Action 模块语义缓存机制
- 完善缓存命中部分;
- 调整 ActionExtractor 以适配缓存逻辑
- 缓存更新大致框架待填充具体更新逻辑;
2025-10-18 21:56:50 +08:00
d1ea8dde79 推进 ActionExtractor 语义缓存机制: 移除了 VectorUtil,实现了 ollama、onnx runtime 两种向量客户端,通过 Agent 启动类暴露的后置启动任务加载并进行测试。 2025-10-17 11:20:11 +08:00
7094a8a68b 推进 ActionExtractor 语义缓存机制: 两种嵌入模型的连接方式测试完毕,在高性能主机上,可以通过ollama调用mxbai-embed-large这类模型,但放到4核8G香橙派3B就会出现推理时长过长,哪怕换成ONNX RUNTIME JAVA 也难以避免,但如果更换成 nomic-embed-text + ONNX RUNTIME JAVA ,仍能够拿到70左右ms的推理时长,远低于提取模型以及向量模型API的调用时长。预期可提供两种语义缓存所用的嵌入模型接入方式: 通过 http 调用 本地ollama接口; 指定 ONNX 格式的嵌入模型直接调用。 2025-10-16 23:04:41 +08:00
e78048f66d 推进 ActionExtractor: 新增语义向量计算工具;开始推进语义缓存相关;调整配置类格式 2025-10-16 15:39:38 +08:00
2f09c0cd71 推进 ActionExtractor: 完善大致逻辑,开始语义-行为缓存相关部分 2025-10-16 15:39:31 +08:00
8c43d6594f 推进 ActionPlanner: 新增行动确认机制,将与原‘提取-评估’流程并发执行; 将繁杂的装配逻辑封装在内部类ActionAssemblyHelper
# Conflicts:
#	Partner-Main/src/main/java/work/slhaf/partner/core/cache/CacheCapability.java
#	Partner-Main/src/main/java/work/slhaf/partner/core/memory/MemoryCore.java
#	Partner-Main/src/main/java/work/slhaf/partner/module/modules/memory/selector/MemorySelector.java
2025-10-16 15:39:16 +08:00
2d052442b1 推进 ActionPlanner: 添加行动短路机制,如果未提取到行动,则跳过评估子模块 2025-10-16 15:34:30 +08:00
84f7befb75 推进 ActionPlanner: 完成了 ActionPlanner 模块中的执行逻辑,同步调整了数据类中的字段。下一步将进行 ActionPlanner 子模块的开发。 2025-10-16 15:34:30 +08:00
85818556f8 将记忆模块的缓存逻辑迁移至 MemoryCore; 移除了 CacheCore,并将 CoordinatedManager 中原记忆模块与缓存模块中的逻辑迁移至现记忆模块中,确保语义正确 2025-10-16 15:22:19 +08:00
cb1a25e9d5 移除 ActiveData ,其逻辑回归至 CacheCore,下一步将对 CacheCore 及 CoordinateManager 中的 cognation 相关内容进行拆分 2025-10-16 11:40:55 +08:00
a10a149edb 开始推进行动模块(ActionModule); 针对框架与本体分别进行了一系列架构优化。
框架:
- 调整模块注册以及AgentRunningFlow的相关逻辑,以支持同组模块并发执行,将以@AgentModule注解中的order属性区分组间顺序先后及是否同组
- 针对@CoordinateManager注解新增了Core的自动注入处理,以便更好的协调不同Core的逻辑

本体: - 开始推进行动模块。将采取类似记忆模块的分层思路,分为ActionPlanner与ActionDispatcher两个主要模块,再各自细分子模块划分
- 将CognationCore从核心统筹的身份下降至与其他核心平级,同时将其中的序列化逻辑抽取至统一的PartnerCore父类,所有核心都将继承该类以获得序列化能力,不同core的内容将序列化至各自的memory文件
- 将SessionManager移除,相关逻辑迁移至CognationCore,统一序列化逻辑的同时又保证语义正确
- 将CognationCore中的某些缓存性质逻辑移动至CacheCore,确保语义正确
- 调整了目录结构以适应优化过的架构
2025-10-12 16:23:11 +08:00
41bf19f43e 将 .java 重命名为 .kt 2025-10-12 16:23:11 +08:00
941943f696 Partner 主体与框架适配完成! 完整逻辑已达到适配框架之前的完成度。发现并修复了不少问题,以及更新了README
框架:
- 由于`Gateway`的启动属于`Agent`启动流程的子线程,而主线程可能由于逻辑执行结束时机早于`Gateway`创建完成时机而报错,故引入`CountDownLatch`进行阻塞
- 在`AgentRunningModule`与`AgentRunningSubModule`中添加日志hook,记录模块执行的起始与截止时机
- 修复了`AgentUtil`中收集继承链时遗忘起始类的错误
- 在`CapabilityCheckFactory`中针对`CoordinateManager`无参构造方法的实现检验
- 在`CapabilityRegisterFactory`中添加了收集模块之外的CapabilityHolder的逻辑,与`@InjectCapability`的校验与注入逻辑保持一致
- 修复了‘生成模块启用配置时,多余局部变量导致无法执行流正确读取启用情况’的错误
- 在GlobalExceptionHandler中添加了对于未知异常的处理逻辑,确保不会导致程序异常终止
- 发现`ModuleProxyFactory`中使用`record`类型会导致`ByteBuddy`无法正确创建代理类,已修复,替换成普通类

本体:
- `ActiveData`由于`CognationCore`的引用,也需要实现序列化,已修复
- 修复了`MemorySelectExtractor`中由于匹配到的主题列表为空导致的空指针异常
- 将后置模块的trigger判定抽取到新的父类中,统一判断
- 修复了`WebSocketServer`如果存在过ws连接,关闭后短时间再次启动内仍提示端口占用的情况,设置允许端口重用
- 在`WebSocketGateway`新增了断开ws客户端连接的逻辑
2025-09-30 15:46:05 +08:00
a7d54349e4 进行 框架-主题 的适配测试,发现了一些问题并进行了修复
框架:
- 去除了 ActivateModel 中 modelKey() 方法的默认实现,对于特殊的 AgentModule 继承者(CoreModule)而言,直接获取注解信息不可行,如果保持,则需要另加判断逻辑。这是没有必要的
- 发现 Agent 启动流程中,由于 Gateway 的启动可能依赖配置文件的加载,故将 AgentConfigManager 与 AgentGateway 的指定替换为类型指定,在合适的时机通过反射进行实例化
- 在 AgentUtil 中新增了链式判断指定类的注解链上是否存在指定注解的方法,目前用于 CapabilityHolder 的持有实例判定
- 发现 CapabilityFactoryContext 中 cores、capabilities 未赋值导致空指针异常,已修复
- 将 AgentConfigManager 中的检验逻辑进行抽离,放到了 ConfigLoaderFactory 中,避免职责混淆
- 发现 CoreModule 的注解使用错误,`@Retention(RetentionPolicy.RUNTIME)`元注解可以使得注解在代码运行时能够被反射扫描
- 在 ModuleCheckFactory 中添加了对于 Module 与 SubModule 的注解、继承使用是否匹配的检验
- 发现对于一个类来说,无法直接通过一层反射获取到‘注解的注解’,故在 ModuleRegisterFactory 中针对 CoreModule 的注册做了特殊处理

主体:
- 发现一些类缺少必要注解,已修复
- 发现存在有些必要的类未公开化无参构造函数,已修复,并在框架部分增加校验逻辑

其他:
- 由于项目的启动流程与完整的配置文件密不可分,所以开始尝试编写启动说明,目前只写了开头
2025-09-21 23:29:45 +08:00
3c2ac32708 完成了本体与框架的适配工作,并修复了某些问题。需要进一步进行测试
- 修复了 CognationCapability 相关的注解使用错误
- 将前置模块中的 setAppendedPrompt 与 setActiveModule 方法抽取到 execute 模板方法中
- 完善了已有模块的适配工作, 并去除了不必要的单例配置
2025-09-18 16:03:59 +08:00
7f9d007f07 适配框架时发现工厂注册链上存在一些执行顺序上的错误,于是尝试修复问题,为Agent启动链添加了完整的注释,并做出了必要的修复与调整 2025-09-13 23:37:35 +08:00
c1018d6b54 进行 Partner 框架层的部分调整
- 新增 AgentSubModule 注解,用于标识子模块
- 新增 MetaSubModule 类,用于存储子模块元信息
- 支持子模块初始化和注入逻辑,不再使用单例模式为执行模块提供子模块服务
- 重构模块初始化流程,支持模块和子模块的初始化
- 优化模块注册流程,分别处理模块和子模块
2025-09-11 13:07:48 +08:00
47684c78e0 进行 Partner 本体对于框架的适配,以及框架层的部分调整
框架:

- 调整 ActivateModel 中模型初始化设置的 initHook 权重为-1(最优先)
- 为 AgentGateway 中 receive 操作提供模板方法,子类需实现发送逻辑并提供适配器
- 取消了 AgentInteractionAdapter 的单例配置
- 调整 RunningFlow 的异常处理,并在RunningFlowContext中提供错误码进行判断
- 调整模块基类
-

本体:
- 新增配置加载异常,继承自Agent启动异常
- 修改 GlobalExceptionData 获取逻辑
- 移除 MessageSender 等交互接口,适配框架的交互逻辑
- 异常处理已适配
- 配置加载逻辑已适配
- Gateway 已适配
- CoreModel 已适配
2025-09-09 20:42:28 +08:00
258 changed files with 11657 additions and 2657 deletions

11
.gitignore vendored
View File

@@ -36,8 +36,8 @@ build/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
/data/ /backup/data/
/config/ /backup/config/
/Partner-Core/src/main/java/src/test/java/memory/test.json /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/input1.json
/Partner-Core/src/main/java/src/test/java/memory/result/input2.json /Partner-Core/src/main/java/src/test/java/memory/result/input2.json
@@ -51,3 +51,10 @@ build/
/backup/ /backup/
/Partner-Main/src/test/java/text/test.json /Partner-Main/src/test/java/text/test.json
/CLAUDE.md /CLAUDE.md
/config/
/data/
/generated-classes/
/.idea/copilot.data.migration.ask2agent.xml
/Partner-Main/data/
/AGENTS.md
/.serena/

6
.idea/copilot.data.migration.agent.xml generated Normal file
View 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
View 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
View 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>

3
.idea/encodings.xml generated
View File

@@ -3,10 +3,13 @@
<component name="Encoding"> <component name="Encoding">
<file url="file://$PROJECT_DIR$/Partner-Api/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/Partner-Api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Api/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/Partner-Api/src/main/resources" charset="UTF-8" />
<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-Main/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/Partner-Main/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Main/src/main/java/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/Partner-Main/src/main/java/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Main/src/main/java/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/Partner-Main/src/main/java/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Main/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/Partner-Main/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/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/resources" 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/java" charset="UTF-8" />

6
.idea/kotlinc.xml generated Normal file
View 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>

29
.idea/misc.xml generated
View File

@@ -1,20 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager"> <component name="EntryPointsManager">
<list size="6"> <list size="15">
<item index="0" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" /> <item index="0" class="java.lang.String" itemvalue="lombok.Data" />
<item index="1" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" /> <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.CapabilityMethod" /> <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.capability.annotation.CapabilityMethod" /> <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.capability.annotation.CoordinateManager" /> <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.register.capability.annotation.Capability" /> <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.module.annotation.AfterExecute" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentModule" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
</list> </list>
<writeAnnotations>
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />
<writeAnnotation name="work.slhaf.partner.api.agent.factory.module.annotation.InjectModule" />
</writeAnnotations>
</component> </component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager"> <component name="MavenProjectsManager">
<option name="originalFiles"> <option name="originalFiles">
<list> <list>
<option value="$PROJECT_DIR$/pom.xml" /> <option value="$PROJECT_DIR$/pom.xml" />
<option value="$PROJECT_DIR$/PartnerExecutor/pom.xml" />
</list> </list>
</option> </option>
</component> </component>

View File

@@ -1,33 +0,0 @@
autoDetectedPackages:
- factory
- module
- work.slhaf
enableAutoDetect: true
entryDisplayConfig:
excludedPathPatterns: []
skipJsCss: true
funcDisplayConfig:
skipConstructors: false
skipFieldAccess: true
skipFieldChange: true
skipGetters: false
skipNonProjectPackages: false
skipPrivateMethods: false
skipSetters: false
ignoreSameClassCall: null
ignoreSamePackageCall: null
includedPackagePrefixes: null
includedParentClasses: null
maxColSize: 32
maxNumFirst: 12
maxNumFirstImportant: 1024
maxNumHash: 3
maxNumHashImportant: 256
maxObjectDepth: 4
maxStrSize: 4096
name: xcodemap-filter
openMainWindow: true
recordMode: manual
sourceDisplayConfig:
color: blue
startOnDebug: false

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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-Api</artifactId>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<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>

View File

@@ -1,5 +1,6 @@
package work.slhaf.partner.api.agent; package work.slhaf.partner.api.agent;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.agent.factory.AgentRegisterFactory; import work.slhaf.partner.api.agent.factory.AgentRegisterFactory;
import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager; import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager;
import work.slhaf.partner.api.agent.runtime.exception.AgentExceptionCallback; import work.slhaf.partner.api.agent.runtime.exception.AgentExceptionCallback;
@@ -9,20 +10,30 @@ import work.slhaf.partner.api.agent.runtime.interaction.AgentGateway;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
/**
* <h2>Agent 启动入口</h2>
* 详细启动流程请参阅{@link AgentRegisterFactory}
*/
@Slf4j
public final class Agent { public final class Agent {
public static AgentGatewayStep newAgent(Class<?> clazz) { public static AgentConfigManagerStep newAgent(Class<?> clazz) {
if (clazz == null) { if (clazz == null) {
throw new AgentLaunchFailedException("Agent class 和 interaction flow context 不能为 null"); throw new AgentLaunchFailedException("Agent class 和 interaction flow context 不能为 null");
} }
return new AgentApp(clazz); return new AgentApp(clazz);
} }
public interface AgentConfigManagerStep {
AgentGatewayStep setAgentConfigManager(Class<? extends AgentConfigManager> agentConfigManager);
}
public interface AgentGatewayStep { public interface AgentGatewayStep {
AgentStep setGateway(AgentGateway gateway); AgentStep setGateway(Class<? extends AgentGateway> gateway);
} }
public interface AgentStep { public interface AgentStep {
@@ -30,9 +41,7 @@ public final class Agent {
AgentStep addAfterLaunchRunners(Runnable... runners); AgentStep addAfterLaunchRunners(Runnable... runners);
AgentStep setAgentConfigManager(AgentConfigManager agentConfigManager); AgentStep setAgentExceptionCallback(Class<? extends AgentExceptionCallback> agentExceptionCallback);
AgentStep setAgentExceptionCallback(AgentExceptionCallback agentExceptionCallback);
AgentStep addScanPackage(String packageName); AgentStep addScanPackage(String packageName);
@@ -42,21 +51,26 @@ public final class Agent {
} }
public static class AgentApp implements AgentStep, AgentGatewayStep { public static class AgentApp implements AgentStep, AgentGatewayStep, AgentConfigManagerStep {
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
private final List<Runnable> beforeLaunchRunners = new ArrayList<>(); private final List<Runnable> beforeLaunchRunners = new ArrayList<>();
private final List<Runnable> afterLaunchRunners = new ArrayList<>(); private final List<Runnable> afterLaunchRunners = new ArrayList<>();
private AgentGateway gateway; private AgentGateway gateway;
private final Class<?> applicationClass; private final Class<?> applicationClass;
private Class<? extends AgentConfigManager> agentConfigManagerClass;
private Class<? extends AgentGateway> gatewayClass;
private Class<? extends AgentExceptionCallback> agentExceptionCallbackClass;
private final CountDownLatch latch = new CountDownLatch(1);
private AgentApp(Class<?> clazz) { private AgentApp(Class<?> clazz) {
this.applicationClass = clazz; this.applicationClass = clazz;
} }
@Override @Override
public AgentStep setGateway(AgentGateway gateway) { public AgentStep setGateway(Class<? extends AgentGateway> gateway) {
this.gateway = gateway; this.gatewayClass = gateway;
return this; return this;
} }
@@ -73,14 +87,14 @@ public final class Agent {
} }
@Override @Override
public AgentStep setAgentConfigManager(AgentConfigManager agentConfigManager) { public AgentGatewayStep setAgentConfigManager(Class<? extends AgentConfigManager> agentConfigManager) {
AgentConfigManager.setINSTANCE(agentConfigManager); this.agentConfigManagerClass = agentConfigManager;
return this; return this;
} }
@Override @Override
public AgentStep setAgentExceptionCallback(AgentExceptionCallback agentExceptionCallback) { public AgentStep setAgentExceptionCallback(Class<? extends AgentExceptionCallback> agentExceptionCallback) {
GlobalExceptionHandler.setExceptionCallback(agentExceptionCallback); agentExceptionCallbackClass = agentExceptionCallback;
return this; return this;
} }
@@ -98,10 +112,38 @@ public final class Agent {
@Override @Override
public void launch() { public void launch() {
launchRunners(beforeLaunchRunners); beforeLaunch();
AgentRegisterFactory.launch(applicationClass.getPackageName()); AgentRegisterFactory.launch(applicationClass.getPackageName());
executorService.execute(() -> gateway.launch()); afterLaunch();
}
private void afterLaunch() {
try {
this.gateway = gatewayClass.getDeclaredConstructor().newInstance();
executorService.execute(() -> {
gateway.launch();
latch.countDown();
log.info("Gateway 启动完毕: {}", gatewayClass.getSimpleName());
});
latch.await();
launchRunners(afterLaunchRunners); launchRunners(afterLaunchRunners);
log.info("后置任务启动完毕");
} catch (Exception e) {
throw new AgentLaunchFailedException("Agent 后置任务启动失败", e);
}
}
private void beforeLaunch() {
try {
AgentConfigManager.setINSTANCE(agentConfigManagerClass.getDeclaredConstructor().newInstance());
log.info("配置管理器设置完毕: {}",agentConfigManagerClass.getSimpleName());
GlobalExceptionHandler.setExceptionCallback(agentExceptionCallbackClass.getDeclaredConstructor().newInstance());
log.info("异常处理回调设置完毕: {}",agentExceptionCallbackClass.getSimpleName());
launchRunners(beforeLaunchRunners);
log.info("前置任务启动完毕");
} catch (Exception e) {
throw new AgentLaunchFailedException("Agent 前置任务启动失败", e);
}
} }
private void launchRunners(List<Runnable> runners) { private void launchRunners(List<Runnable> runners) {

View File

@@ -14,14 +14,22 @@ import work.slhaf.partner.api.agent.factory.module.ModuleInitHookExecuteFactory;
import work.slhaf.partner.api.agent.factory.module.ModuleProxyFactory; import work.slhaf.partner.api.agent.factory.module.ModuleProxyFactory;
import work.slhaf.partner.api.agent.factory.module.ModuleRegisterFactory; import work.slhaf.partner.api.agent.factory.module.ModuleRegisterFactory;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.agent.runtime.data.AgentContext;
import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager; import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager;
import work.slhaf.partner.api.agent.runtime.data.AgentContext;
import work.slhaf.partner.api.agent.runtime.interaction.flow.AgentRunningFlow;
import java.io.File; import java.io.File;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* <h2>Agent 注册工厂</h2>
*
* <p>
* 具体流程依次按照 {@link AgentRegisterFactory#launch(String)} 方法顺序执行,最终将执行模块列表对应实例交给 {@link AgentConfigManager} ,传递给 {@link AgentRunningFlow} 针对交互做出调用
* <p/>
*/
public class AgentRegisterFactory { public class AgentRegisterFactory {
private static final List<URL> urls = new ArrayList<>(); private static final List<URL> urls = new ArrayList<>();
@@ -35,20 +43,20 @@ public class AgentRegisterFactory {
//流程 //流程
//0. 加载配置 //0. 加载配置
new ConfigLoaderFactory().execute(registerContext); new ConfigLoaderFactory().execute(registerContext);
//1. 注册并检查Capability //1. 注册并检查Module
new CapabilityRegisterFactory().execute(registerContext);
new CapabilityCheckFactory().execute(registerContext);
//2. 注册并检查Module
new ModuleCheckFactory().execute(registerContext); new ModuleCheckFactory().execute(registerContext);
new ModuleRegisterFactory().execute(registerContext); new ModuleRegisterFactory().execute(registerContext);
//3. 为module通过动态代理添加PostHook逻辑并进行实例化 //2. 为module通过动态代理添加PostHook逻辑并进行实例化
new ModuleProxyFactory().execute(registerContext); new ModuleProxyFactory().execute(registerContext);
//3. 加载检查Capability层内容后进行能力层的内容注册
new CapabilityCheckFactory().execute(registerContext);
new CapabilityRegisterFactory().execute(registerContext);
//. 先一步注入Capability,避免因前hook逻辑存在针对能力的引用而报错 //. 先一步注入Capability,避免因前hook逻辑存在针对能力的引用而报错
new CapabilityInjectFactory().execute(registerContext); new CapabilityInjectFactory().execute(registerContext);
//. 执行模块PreHook逻辑 //. 执行模块PreHook逻辑
new ModuleInitHookExecuteFactory().execute(registerContext); new ModuleInitHookExecuteFactory().execute(registerContext);
List<MetaModule> moduleList = registerContext.getModuleFactoryContext().getModuleList(); List<MetaModule> moduleList = registerContext.getModuleFactoryContext().getAgentModuleList();
AgentConfigManager.INSTANCE.moduleEnabledStatusFilterAndRecord(moduleList); AgentConfigManager.INSTANCE.moduleEnabledStatusFilterAndRecord(moduleList);
BeanUtil.copyProperties(registerContext, AgentContext.INSTANCE); BeanUtil.copyProperties(registerContext, AgentContext.INSTANCE);

View File

@@ -1,24 +1,52 @@
package work.slhaf.partner.api.agent.factory.capability; package work.slhaf.partner.api.agent.factory.capability;
import cn.hutool.core.util.ClassUtil;
import org.reflections.Reflections; import org.reflections.Reflections;
import work.slhaf.partner.api.agent.factory.AgentBaseFactory; import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.capability.annotation.*; import work.slhaf.partner.api.agent.factory.capability.annotation.*;
import work.slhaf.partner.api.agent.factory.capability.exception.DuplicateCapabilityException; import work.slhaf.partner.api.agent.factory.capability.exception.*;
import work.slhaf.partner.api.agent.factory.capability.exception.UnMatchedCapabilityException;
import work.slhaf.partner.api.agent.factory.capability.exception.UnMatchedCapabilityMethodException;
import work.slhaf.partner.api.agent.factory.capability.exception.UnMatchedCoordinatedMethodException;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext; import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule;
import work.slhaf.partner.api.agent.util.AgentUtil; import work.slhaf.partner.api.agent.util.AgentUtil;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static work.slhaf.partner.api.agent.util.AgentUtil.isAssignableFromAnnotation;
import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature; import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature;
/** /**
* 执行<code>Capability</code>相关检查 * <h2>Agent启动流程 4</h2>
*
* <p>负责通过反射收集 {@link Capability} 和 {@link CapabilityCore} 注解所在类,并判断是否存在被错误忽略的方法</p>
*
* <ol>
* <li>
* <p>{@link CapabilityCheckFactory#loadCoresAndCapabilities()}</p>
* 通过反射收集 {@link Capability} 和 {@link CapabilityCore} 注解所在类为对应集合
* </li>
* <li>
* <p>{@link CapabilityCheckFactory#checkCountAndCapabilities()}</p>
* 检测 {@link Capability} 与 {@link CapabilityCore} 的数量、对应的能力是否相等。每一个core都将对应一个capability并通过value属性进行匹配
* </li>
* <li>
* <p>{@link CapabilityCheckFactory#checkCapabilityMethods()}</p>
* 检测在 {@link Capability} 与 {@link CapabilityCore} 中是否存在对方尚未实现/注册的方法
* </li>
* <li>
* <p>{@link CapabilityCheckFactory#checkCoordinatedMethods()}</p>
* 检查是否包含协调方法({@link ToCoordinated}),如果存在,则进一步检查在 {@link CoordinateManager} 所注类中是否有提供对应的实现
* </li>
* <li>
* <p>{@link CapabilityCheckFactory#checkInjectCapability()}</p>
* 检查 {@link InjectCapability} 注解是否只用在 {@link CapabilityHolder} 所标识类的字段上。{@link AgentModule} 与 {@link AgentSubModule} 已经被 {@link CapabilityHolder} 标注
* </li>
* </ol>
*
* <p>下一步流程请参阅{@link CapabilityRegisterFactory}</p>
*/ */
public class CapabilityCheckFactory extends AgentBaseFactory { public class CapabilityCheckFactory extends AgentBaseFactory {
@@ -37,19 +65,42 @@ public class CapabilityCheckFactory extends AgentBaseFactory {
@Override @Override
protected void run() { protected void run() {
loadCoresAndCapabilities();
checkCountAndCapabilities(); checkCountAndCapabilities();
checkCapabilityMethods(); checkCapabilityMethods();
checkCoordinatedMethods(); checkCoordinatedMethods();
checkCoordinatedManager();
checkInjectCapability(); checkInjectCapability();
} }
private void checkCoordinatedManager() {
reflections.getTypesAnnotatedWith(CoordinateManager.class)
.stream()
.filter(ClassUtil::isNormalClass)
.forEach(managerClass -> {
try {
if (!managerClass.getDeclaredConstructor().canAccess(null)) {
throw new CapabilityCheckFailedException("CoordinateManager 所注类的无参构造方法未公开!");
}
} catch (NoSuchMethodException e) {
throw new CapabilityCheckFailedException("CoordinateManager 所注类缺少无参构造方法!");
}
});
}
private void loadCoresAndCapabilities() {
cores.addAll(reflections.getTypesAnnotatedWith(CapabilityCore.class));
capabilities.addAll(reflections.getTypesAnnotatedWith(Capability.class));
}
/** /**
* 检查<code>@InjectCapability</code>注解是否只用在<code>@CapabilityHolder</code>所标识类的字段上 * 检查<code>@InjectCapability</code>注解是否只用在<code>@CapabilityHolder</code>所标识类的字段上
*/ */
private void checkInjectCapability() { private void checkInjectCapability() {
reflections.getFieldsAnnotatedWith(InjectCapability.class).forEach(field -> { reflections.getFieldsAnnotatedWith(InjectCapability.class).forEach(field -> {
if (!field.getDeclaringClass().isAssignableFrom(CapabilityHolder.class)) { Class<?> declaringClass = field.getDeclaringClass();
throw new UnMatchedCapabilityException("InjectCapability 注解只能用于 CapabilityHolder 注解所在类"); if (!isAssignableFromAnnotation(declaringClass, CapabilityHolder.class)) {
throw new UnMatchedCapabilityException("InjectCapability 注解只能用于 CapabilityHolder 注解所在类,检查该类是否使用了@CapabilityHolder注解或者受其标注的注解或父类: " + declaringClass);
} }
}); });
} }

View File

@@ -8,6 +8,9 @@ import work.slhaf.partner.api.agent.factory.capability.annotation.ToCoordinated;
import work.slhaf.partner.api.agent.factory.capability.exception.ProxySetFailedExceptionCapability; import work.slhaf.partner.api.agent.factory.capability.exception.ProxySetFailedExceptionCapability;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext; import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext;
import work.slhaf.partner.api.agent.factory.module.ModuleInitHookExecuteFactory;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
@@ -18,9 +21,23 @@ import java.util.function.Function;
import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature; import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature;
/** /**
* 负责执行<code>Capability</code>的注入逻辑 * <h2>Agent启动流程 6</h2>
*/ *
public class CapabilityInjectFactory extends AgentBaseFactory { * <p>负责执行 {@link Capability} 的注入逻辑。</p>
*
* <p>实现方式:</p>
* <ol>
* <li>通过动态代理,为 {@link AgentModule} 与 {@link AgentSubModule} 中待注入的
* <b>能力接口</b> 类型(即 {@link Capability} 标注的接口类)生成代理对象。
* </li>
* <li>在代理对象内部,根据调用方法的签名确定路由,将调用转发至对应的具体函数。
* </li>
* <li>通过此机制,实现了 {@link Capability} 单一语义层面上普通方法与协调方法的统一入口。
* </li>
* </ol>
*
* <p>下一步流程请参阅 {@link ModuleInitHookExecuteFactory}</p>
*/public class CapabilityInjectFactory extends AgentBaseFactory {
private Reflections reflections; private Reflections reflections;
private HashMap<String, Function<Object[], Object>> coordinatedMethodsRouterTable; private HashMap<String, Function<Object[], Object>> coordinatedMethodsRouterTable;

View File

@@ -1,5 +1,6 @@
package work.slhaf.partner.api.agent.factory.capability; package work.slhaf.partner.api.agent.factory.capability;
import cn.hutool.core.util.ClassUtil;
import org.reflections.Reflections; import org.reflections.Reflections;
import work.slhaf.partner.api.agent.factory.AgentBaseFactory; import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.capability.annotation.*; import work.slhaf.partner.api.agent.factory.capability.annotation.*;
@@ -8,28 +9,61 @@ import work.slhaf.partner.api.agent.factory.capability.exception.CoreInstancesCr
import work.slhaf.partner.api.agent.factory.capability.exception.DuplicateMethodException; import work.slhaf.partner.api.agent.factory.capability.exception.DuplicateMethodException;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext; import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.hutool.core.util.ClassUtil.isNormalClass;
import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature; import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature;
/** /**
* 负责获取<code>@Capability</code>和<code>@CapabilityCore</code>标识的类,并生成函数路由表、设置<code>Core</code>实例用于后续注入 * <h2>Agent启动流程 5</h2>
*
* <p>
* 负责收集注解 {@link Capability} 和 {@link CapabilityCore} 标识的类并生成函数路由表、创建core、capability实例以及放入instanceMap供后续进行注入操作
* </p>
*
* <ol>
* <li>
* <p>{@link CapabilityRegisterFactory#setCoreInstances()}</p>
* 通过反射调用无参构造函数创建core实例并将实例放入instanceMap供后续使用
* </li>
* <li>
* <p>{@link CapabilityRegisterFactory#generateRouterTable()}</p>
* 生成函数路由表:
* <ul>
* <li>
* <p>{@link CapabilityRegisterFactory#generateMethodsRouterTable()}</p>
* 生成普通方法对应的函数路由表
* </li>
* <li>
* <p>{@link CapabilityRegisterFactory#generateCoordinatedMethodsRouterTable()}</p>
* 生成协调方法对应的函数路由表
* </li>
* </ul>
* </li>
* <li>
* 函数路由表生成完毕、core实例创建完毕之后将交由下一工厂完成能力(Capability)注入操作,注入到 {@link AgentModule} 与 {@link AgentSubModule} 对应的实例中
* </li>
* </ol>
*
* <p>下一步流程请参阅{@link CapabilityInjectFactory}</p>
*/ */
public final class CapabilityRegisterFactory extends AgentBaseFactory { public class CapabilityRegisterFactory extends AgentBaseFactory {
private Reflections reflections; private Reflections reflections;
private HashMap<String, Function<Object[], Object>> methodsRouterTable; private HashMap<String, Function<Object[], Object>> methodsRouterTable;
private HashMap<String, Function<Object[], Object>> coordinatedMethodsRouterTable; private HashMap<String, Function<Object[], Object>> coordinatedMethodsRouterTable;
private HashMap<Class<?>, Object> capabilityCoreInstances; private HashMap<Class<?>, Object> coreInstances;
private HashMap<Class<?>, Object> capabilityHolderInstances; private HashMap<Class<?>, Object> capabilityHolderInstances;
private Set<Class<?>> cores; private Set<Class<?>> cores;
private Set<Class<?>> capabilities; private Set<Class<?>> capabilities;
@@ -40,35 +74,35 @@ public final class CapabilityRegisterFactory extends AgentBaseFactory {
reflections = context.getReflections(); reflections = context.getReflections();
methodsRouterTable = factoryContext.getMethodsRouterTable(); methodsRouterTable = factoryContext.getMethodsRouterTable();
coordinatedMethodsRouterTable = factoryContext.getCoordinatedMethodsRouterTable(); coordinatedMethodsRouterTable = factoryContext.getCoordinatedMethodsRouterTable();
capabilityCoreInstances = factoryContext.getCapabilityCoreInstances(); coreInstances = factoryContext.getCapabilityCoreInstances();
cores = factoryContext.getCores(); cores = factoryContext.getCores();
capabilities = factoryContext.getCapabilities(); capabilities = factoryContext.getCapabilities();
capabilityHolderInstances = factoryContext.getCapabilityHolderInstances(); capabilityHolderInstances = factoryContext.getCapabilityHolderInstances();
} }
@Override @Override
protected void run() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { protected void run() {
setCapabilityCoreInstances(); setCapabilityHolderInstances();
setAnnotatedClasses(); setCoreInstances();
generateRouterTable(); generateRouterTable();
} }
/** private void setCapabilityHolderInstances() {
* 设置<code>CapabilityCore</code>、<code>Capability</code>注解标识类 Set<Class<?>> collect = reflections.getTypesAnnotatedWith(CapabilityHolder.class).stream()
*/ .filter(ClassUtil::isNormalClass)
private void setAnnotatedClasses() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { .filter(clazz -> !capabilityHolderInstances.containsKey(clazz))
cores.addAll(reflections.getTypesAnnotatedWith(CapabilityCore.class)); .collect(Collectors.toSet());
capabilities.addAll(reflections.getTypesAnnotatedWith(Capability.class)); for (Class<?> clazz : collect) {
setCapabilityHolderInstances(); try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
if (constructor.canAccess(null)) {
throw new CapabilityFactoryExecuteFailedException("缺少无参构造方法的类: " + clazz);
} }
Object o = constructor.newInstance();
private void setCapabilityHolderInstances() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
for (Class<?> clazz : reflections.getTypesAnnotatedWith(CapabilityHolder.class)) {
if (!isNormalClass(clazz)){
continue;
}
Object o = clazz.getDeclaredConstructor().newInstance();
capabilityHolderInstances.put(clazz, o); capabilityHolderInstances.put(clazz, o);
} catch (Exception e) {
throw new CapabilityFactoryExecuteFailedException("创建代理对象失败: " + clazz, e);
}
} }
} }
@@ -116,7 +150,7 @@ public final class CapabilityRegisterFactory extends AgentBaseFactory {
for (Class<?> c : reflections.getTypesAnnotatedWith(CoordinateManager.class)) { for (Class<?> c : reflections.getTypesAnnotatedWith(CoordinateManager.class)) {
Constructor<?> constructor = c.getDeclaredConstructor(); Constructor<?> constructor = c.getDeclaredConstructor();
Object instance = constructor.newInstance(); Object instance = constructor.newInstance();
setCores(instance, c);
Arrays.stream(c.getMethods()) Arrays.stream(c.getMethods())
.filter(method -> method.isAnnotationPresent(Coordinated.class)) .filter(method -> method.isAnnotationPresent(Coordinated.class))
.forEach(method -> { .forEach(method -> {
@@ -127,18 +161,26 @@ public final class CapabilityRegisterFactory extends AgentBaseFactory {
return map; return map;
} }
private void setCores(Object cmInstance, Class<?> cmClazz) throws IllegalAccessException {
for (Field field : cmClazz.getFields()) {
if (field.getType().isAnnotationPresent(CapabilityCore.class)) {
field.setAccessible(true);
field.set(cmInstance, coreInstances.get(field.getType()));
}
}
}
/** /**
* 生成普通方法对应的函数路由表 * 扫描`@Capability`与`@CapabilityMethod`注解的类与方法
* 将`capabilityValue.methodSignature`作为key,函数对象为通过反射拿到的core实例对应的方法
*/ */
private void generateMethodsRouterTable() { private void generateMethodsRouterTable() {
//扫描`@Capability`与`@CapabilityMethod`注解的类与方法
//将`capabilityValue.methodSignature`作为key,函数对象为通过反射拿到的core实例对应的方法
cores.forEach(core -> Arrays.stream(core.getMethods()) cores.forEach(core -> Arrays.stream(core.getMethods())
.filter(method -> method.isAnnotationPresent(CapabilityMethod.class)) .filter(method -> method.isAnnotationPresent(CapabilityMethod.class))
.forEach(method -> { .forEach(method -> {
Function<Object[], Object> function = args -> { Function<Object[], Object> function = args -> {
try { try {
return method.invoke(capabilityCoreInstances.get(core), args); return method.invoke(coreInstances.get(core), args);
} catch (IllegalAccessException | InvocationTargetException e) { } catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -154,12 +196,12 @@ public final class CapabilityRegisterFactory extends AgentBaseFactory {
/** /**
* 反射获取<code>CapabilityCore</code>实例 * 反射获取<code>CapabilityCore</code>实例
*/ */
private void setCapabilityCoreInstances() { private void setCoreInstances() {
try { try {
for (Class<?> core : cores) { for (Class<?> core : cores) {
Constructor<?> constructor = core.getDeclaredConstructor(); Constructor<?> constructor = core.getDeclaredConstructor();
constructor.setAccessible(true); constructor.setAccessible(true);
capabilityCoreInstances.put(core, constructor.newInstance()); coreInstances.put(core, constructor.newInstance());
} }
} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | } catch (InvocationTargetException | NoSuchMethodException | InstantiationException |
IllegalAccessException e) { IllegalAccessException e) {

View File

@@ -5,6 +5,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/**
* Core的协调类该注解的实现类中如果存在任何{@link CapabilityCore}实例的引用,都将被自动注入
*/
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface CoordinateManager { public @interface CoordinateManager {

View File

@@ -1,16 +1,31 @@
package work.slhaf.partner.api.agent.factory.config; package work.slhaf.partner.api.agent.factory.config;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.agent.factory.AgentBaseFactory; import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.config.exception.ConfigNotExistException;
import work.slhaf.partner.api.agent.factory.config.exception.PromptNotExistException;
import work.slhaf.partner.api.agent.factory.config.pojo.ModelConfig; import work.slhaf.partner.api.agent.factory.config.pojo.ModelConfig;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.ConfigFactoryContext; import work.slhaf.partner.api.agent.factory.context.ConfigFactoryContext;
import work.slhaf.partner.api.agent.factory.module.ModuleCheckFactory;
import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager; import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager;
import work.slhaf.partner.api.agent.runtime.config.DefaultAgentConfigManager; import work.slhaf.partner.api.agent.runtime.config.FileAgentConfigManager;
import work.slhaf.partner.api.chat.pojo.Message; import work.slhaf.partner.api.chat.pojo.Message;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/**
* <h2>Agent启动流程 0</h2>
* <p>
* 通过指定的 {@link AgentConfigManager} 或者默认的 {@link FileAgentConfigManager} 加载配置文件
* <p/>
*
* <p>下一步流程请参阅{@link ModuleCheckFactory}</p>
*/
@Slf4j
public class ConfigLoaderFactory extends AgentBaseFactory { public class ConfigLoaderFactory extends AgentBaseFactory {
private AgentConfigManager agentConfigManager; private AgentConfigManager agentConfigManager;
@@ -24,7 +39,7 @@ public class ConfigLoaderFactory extends AgentBaseFactory {
modelPromptMap = factoryContext.getModelPromptMap(); modelPromptMap = factoryContext.getModelPromptMap();
if (AgentConfigManager.INSTANCE == null) { if (AgentConfigManager.INSTANCE == null) {
AgentConfigManager.setINSTANCE(new DefaultAgentConfigManager()); AgentConfigManager.setINSTANCE(new FileAgentConfigManager());
} }
agentConfigManager = AgentConfigManager.INSTANCE; agentConfigManager = AgentConfigManager.INSTANCE;
@@ -33,9 +48,30 @@ public class ConfigLoaderFactory extends AgentBaseFactory {
@Override @Override
protected void run() { protected void run() {
agentConfigManager.load(); agentConfigManager.load();
agentConfigManager.check();
modelConfigMap.putAll(agentConfigManager.getModelConfigMap()); modelConfigMap.putAll(agentConfigManager.getModelConfigMap());
modelPromptMap.putAll(agentConfigManager.getModelPromptMap()); modelPromptMap.putAll(agentConfigManager.getModelPromptMap());
check();
} }
/**
* 对模型Config与Prompt分别进行检验,除了都必须包含default外还需要确保数量、key一致毕竟是模型配置与提示词
*/
private void check() {
log.info("执行config与prompt检测...");
if (!modelConfigMap.containsKey("default")) {
throw new ConfigNotExistException("缺少默认配置! 需确保存在一个模型配置的key为`default`");
}
if (!modelPromptMap.containsKey("basic")) {
throw new PromptNotExistException("缺少基础Prompt! 需要确保存在key为basic的Prompt文件它将与其他Prompt共同作用于模块节点。");
}
Set<String> configKeySet = new HashSet<>(modelConfigMap.keySet());
configKeySet.remove("default");
Set<String> promptKeySet = new HashSet<>(modelPromptMap.keySet());
promptKeySet.remove("basic");
if (!promptKeySet.containsAll(configKeySet)) {
log.warn("存在未被提示词包含的模型配置,该配置将无法生效!");
}
//检查提示词数量与`ActivateModel`的实现数量是否一致
log.info("检测完毕.");
}
} }

View File

@@ -3,6 +3,7 @@ package work.slhaf.partner.api.agent.factory.context;
import lombok.Data; import lombok.Data;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
@@ -12,6 +13,6 @@ public class CapabilityFactoryContext {
private final HashMap<String, Function<Object[], Object>> coordinatedMethodsRouterTable = new HashMap<>(); private final HashMap<String, Function<Object[], Object>> coordinatedMethodsRouterTable = new HashMap<>();
private final HashMap<Class<?>, Object> capabilityCoreInstances = new HashMap<>(); private final HashMap<Class<?>, Object> capabilityCoreInstances = new HashMap<>();
private final HashMap<Class<?>, Object> capabilityHolderInstances = new HashMap<>(); private final HashMap<Class<?>, Object> capabilityHolderInstances = new HashMap<>();
private Set<Class<?>> cores; private Set<Class<?>> cores = new HashSet<>();
private Set<Class<?>> capabilities; private Set<Class<?>> capabilities = new HashSet<>();
} }

View File

@@ -2,11 +2,13 @@ package work.slhaf.partner.api.agent.factory.context;
import lombok.Data; import lombok.Data;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaSubModule;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Data @Data
public class ModuleFactoryContext { public class ModuleFactoryContext {
private List<MetaModule> moduleList = new ArrayList<>(); private List<MetaModule> agentModuleList = new ArrayList<>();
private List<MetaSubModule> agentSubModuleList = new ArrayList<>();
} }

View File

@@ -6,8 +6,8 @@ import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute; import work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule; import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule;
import work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute; import work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute;
import work.slhaf.partner.api.agent.factory.module.annotation.Init;
import work.slhaf.partner.api.agent.factory.module.exception.ModuleCheckException; import work.slhaf.partner.api.agent.factory.module.exception.ModuleCheckException;
import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager; import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.ActivateModel; import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.ActivateModel;
@@ -19,6 +19,32 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static work.slhaf.partner.api.agent.util.AgentUtil.getMethodAnnotationTypeSet;
/**
* <h2>Agent启动流程 1</h2>
*
* <p>
* 检查模块部分抽象类与注解、接口的使用方式
* </p>
*
* <ol>
* <li>
* <p>{@link ModuleCheckFactory#annotationAbstractCheck(Set, Class)}</p>
* 所有添加了 {@link AgentModule} 注解的类都将作为Agent的执行模块为规范模块入口都必须实现抽象类: {@link AgentRunningModule}; {@link AgentSubModule} 注解所在类则必须实现 {@link AgentRunningSubModule}
* </li>
* <li>
* <p>{@link ModuleCheckFactory#moduleConstructorsCheck(Set)}</p>
* 所有 {@link AgentModule} 与 {@link AgentSubModule} 注解所在类都必须具备空参构造方法,初始化逻辑可放在 @Init 注解所处方法中,将在 Capability 与 subModules 注入后才会执行
* </li>
* <li>
* <p>{@link ModuleCheckFactory#activateModelImplCheck()}</p>
* 检查实现了 {@link ActivateModel} 的模块数量、名称与prompt是否一致
* </li>
* </ol>
*
* <p>下一步流程请参阅{@link ModuleRegisterFactory}</p>
*/
public class ModuleCheckFactory extends AgentBaseFactory { public class ModuleCheckFactory extends AgentBaseFactory {
private Reflections reflections; private Reflections reflections;
@@ -30,15 +56,43 @@ public class ModuleCheckFactory extends AgentBaseFactory {
@Override @Override
protected void run() { protected void run() {
Set<Class<?>> types = reflections.getTypesAnnotatedWith(AgentModule.class); AnnotatedModules annotatedModules = getAnnotatedModules();
//检查注解AgentModule所在类是否继承了AgentInteractionModule ExtendedModules extendedModules = getExtendedModules();
agentModuleAnnotationCheck(types); checkIfClassCorresponds(annotatedModules, extendedModules);
//检查注解AgentModule或AgentSubModule所在类是否继承了对应的抽象类
annotationAbstractCheck(annotatedModules.moduleTypes(), AgentRunningModule.class);
annotationAbstractCheck(annotatedModules.subModuleTypes(), AgentRunningSubModule.class);
//检查AgentModule是否具备无参构造方法 //检查AgentModule是否具备无参构造方法
moduleConstructorsCheck(types); moduleConstructorsCheck(annotatedModules.moduleTypes());
//检查hook注解所在方法是否位于AgentInteractionModule子类/AgentInteractionSubModule子类/ActivateModel子类 moduleConstructorsCheck(annotatedModules.subModuleTypes());
hookLocationCheck();
//检查实现了ActivateModel的模块数量、名称与prompt是否一致 //检查实现了ActivateModel的模块数量、名称与prompt是否一致
activateModelImplCheck(); activateModelImplCheck();
//检查hook注解所在位置是否正确
hookLocationCheck();
}
private ExtendedModules getExtendedModules() {
Set<Class<?>> moduleTypes = reflections.getSubTypesOf(AgentRunningModule.class)
.stream()
.filter(ClassUtil::isNormalClass)
.collect(Collectors.toSet());
Set<Class<?>> subModuleTypes = reflections.getSubTypesOf(AgentRunningSubModule.class)
.stream()
.filter(ClassUtil::isNormalClass)
.collect(Collectors.toSet());
return new ExtendedModules(moduleTypes, subModuleTypes);
}
private AnnotatedModules getAnnotatedModules() {
Set<Class<?>> moduleTypes = reflections.getTypesAnnotatedWith(AgentModule.class)
.stream()
.filter(ClassUtil::isNormalClass)
.collect(Collectors.toSet());
Set<Class<?>> subModuleTypes = reflections.getTypesAnnotatedWith(AgentSubModule.class)
.stream()
.filter(ClassUtil::isNormalClass)
.collect(Collectors.toSet());
return new AnnotatedModules(moduleTypes, subModuleTypes);
} }
private void moduleConstructorsCheck(Set<Class<?>> types) { private void moduleConstructorsCheck(Set<Class<?>> types) {
@@ -76,24 +130,10 @@ public class ModuleCheckFactory extends AgentBaseFactory {
preHookLocationCheck(); preHookLocationCheck();
//检查@Init注解 //检查@Init注解
initHookLocationCheck(); initHookLocationCheck();
//检查@AgentModule注解是否只位于普通类上
agentModuleLocationCheck();
}
private void agentModuleLocationCheck() {
Set<Class<?>> types = reflections.getTypesAnnotatedWith(AgentModule.class);
for (Class<?> type : types) {
if (!ClassUtil.isNormalClass(type)) {
throw new ModuleCheckException("AgentModule 注解仅能位于普通类上! 异常类信息: " + type.getSimpleName());
}
}
} }
private void initHookLocationCheck() { private void initHookLocationCheck() {
Set<Method> methods = reflections.getMethodsAnnotatedWith(Init.class); Set<Class<?>> types = getMethodAnnotationTypeSet(AgentModule.class, reflections);
Set<Class<?>> types = methods.stream()
.map(Method::getDeclaringClass)
.collect(Collectors.toSet());
checkLocation(types); checkLocation(types);
} }
@@ -129,15 +169,59 @@ public class ModuleCheckFactory extends AgentBaseFactory {
} }
} }
private void agentModuleAnnotationCheck(Set<Class<?>> types) { private void annotationAbstractCheck(Set<Class<?>> types, Class<?> clazz) {
for (Class<?> type : types) { for (Class<?> type : types) {
if (type.isAnnotation()) { if (type.isAnnotation()) {
continue; continue;
} }
if (AgentRunningModule.class.isAssignableFrom(type) && ClassUtil.isNormalClass(type)) { if (clazz.isAssignableFrom(type) && ClassUtil.isNormalClass(type)) {
continue; continue;
} }
throw new ModuleCheckException("存在未继承AgentInteractionModule.class的AgentModule实现: " + type.getSimpleName()); throw new ModuleCheckException("存在未继承AgentInteractionModule.class的AgentModule实现: " + type.getSimpleName());
} }
} }
private void checkIfClassCorresponds(AnnotatedModules annotatedModules, ExtendedModules extendedModules) {
// 检查是否有被@AgentModule注解但没有继承AgentRunningModule的类
checkSets(annotatedModules.moduleTypes(), extendedModules.moduleTypes(),
"存在被@AgentModule注解但未继承AgentRunningModule的类");
// 检查是否有继承AgentRunningModule但没有被@AgentModule注解的类
checkSets(extendedModules.moduleTypes(), annotatedModules.moduleTypes(),
"存在继承AgentRunningModule但未被@AgentModule注解的类");
// 检查是否有被@AgentSubModule注解但没有继承AgentRunningSubModule的类
checkSets(annotatedModules.subModuleTypes(), extendedModules.subModuleTypes(),
"存在被@AgentSubModule注解但未继承AgentRunningSubModule的类");
// 检查是否有继承AgentRunningSubModule但没有被@AgentSubModule注解的类
checkSets(extendedModules.subModuleTypes(), annotatedModules.subModuleTypes(),
"存在继承AgentRunningSubModule但未被@AgentSubModule注解的类");
}
/**
* 检查源集合中是否有不在目标集合中的元素
* @param source 源集合
* @param target 目标集合
* @param errorMessage 错误信息前缀
*/
private void checkSets(Set<Class<?>> source, Set<Class<?>> target, String errorMessage) {
// 只有在需要时才创建HashSet以节省内存
if (!target.containsAll(source)) {
// 使用流式处理找出差异部分,避免创建完整的中间集合
String classNames = source.stream()
.filter(clazz -> !target.contains(clazz))
.map(Class::getSimpleName)
.limit(10) // 限制显示数量,避免信息泄露
.collect(Collectors.joining(", ", "[", "]"));
throw new ModuleCheckException(errorMessage + ": " + classNames);
}
}
private record AnnotatedModules(Set<Class<?>> moduleTypes, Set<Class<?>> subModuleTypes) {
}
private record ExtendedModules(Set<Class<?>> moduleTypes, Set<Class<?>> subModuleTypes) {
}
} }

View File

@@ -1,13 +1,19 @@
package work.slhaf.partner.api.agent.factory.module; package work.slhaf.partner.api.agent.factory.module;
import work.slhaf.partner.api.agent.factory.AgentBaseFactory; import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.AgentRegisterFactory;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.ModuleFactoryContext; import work.slhaf.partner.api.agent.factory.context.ModuleFactoryContext;
import work.slhaf.partner.api.agent.factory.module.annotation.Init; import work.slhaf.partner.api.agent.factory.module.annotation.Init;
import work.slhaf.partner.api.agent.factory.module.exception.ModuleInitHookExecuteFailedException; import work.slhaf.partner.api.agent.factory.module.exception.ModuleInitHookExecuteFailedException;
import work.slhaf.partner.api.agent.factory.module.pojo.BaseMetaModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaMethod; import work.slhaf.partner.api.agent.factory.module.pojo.MetaMethod;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaSubModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule; import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningSubModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.Module;
import work.slhaf.partner.api.agent.util.AgentUtil;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.Arrays; import java.util.Arrays;
@@ -20,39 +26,61 @@ import static work.slhaf.partner.api.agent.util.AgentUtil.collectExtendedClasses
import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature; import static work.slhaf.partner.api.agent.util.AgentUtil.methodSignature;
/** /**
* 负责执行前hook逻辑 * <h2>Agent启动流程 7</h2>
*
* <p>负责执行初始化hook逻辑即 {@link Init} 注解所在方法</p>
*
* <ol>
* <li>
* <p>{@link ModuleInitHookExecuteFactory#collectInitHookMethods(Class, Class)}</p>
* 分别遍历前置模块拿到的模块列表({@link ModuleInitHookExecuteFactory#moduleList}, {@link ModuleInitHookExecuteFactory#subModuleList}),通过 {@link AgentUtil#collectExtendedClasses(Class, Class)} 收集到当前模块类的继承链上的所有类后,收集其所有带有 {@link Init} 注解的方法
* </li>
* <li>
* <p>{@link ModuleInitHookExecuteFactory#proceedInitMethods(BaseMetaModule, List)}</p>
* 收集好初始化方法后,将通过反射执行该方法,所用实例即为前置模块中收集到的执行模块与子模块的 {@link MetaModule} 与 {@link MetaSubModule} 内容
* </li>
* </ol>
*
* <p>Agent启动流程到此进行完毕。整个工厂执行链中均为针对 {@link AgentRegisterContext} 进行的操作,在 {@link AgentRegisterFactory} 中,将进行最终处理以及将必要内容进行传递。</p>
*/ */
public class ModuleInitHookExecuteFactory extends AgentBaseFactory { public class ModuleInitHookExecuteFactory extends AgentBaseFactory {
private List<MetaModule> moduleList; private List<MetaModule> moduleList;
private List<MetaSubModule> subModuleList;
@Override @Override
protected void setVariables(AgentRegisterContext context) { protected void setVariables(AgentRegisterContext context) {
ModuleFactoryContext factoryContext = context.getModuleFactoryContext(); ModuleFactoryContext factoryContext = context.getModuleFactoryContext();
moduleList = factoryContext.getModuleList(); moduleList = factoryContext.getAgentModuleList();
subModuleList = factoryContext.getAgentSubModuleList();
} }
@Override @Override
protected void run() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { protected void run() {
//遍历模块列表,并向上查找@Init注解 //遍历模块列表,并向上查找@Init注解
for (MetaSubModule metaSubModule : subModuleList) {
List<MetaMethod> initHookMethods = collectInitHookMethods(metaSubModule.getClazz(),AgentRunningModule.class);
proceedInitMethods(metaSubModule, initHookMethods);
}
for (MetaModule metaModule : moduleList) { for (MetaModule metaModule : moduleList) {
List<MetaMethod> initHookMethods = collectInitHookMethods(metaModule.getClazz()); List<MetaMethod> initHookMethods = collectInitHookMethods(metaModule.getClazz(), AgentRunningSubModule.class);
proceedInitMethods(metaModule, initHookMethods); proceedInitMethods(metaModule, initHookMethods);
} }
} }
private void proceedInitMethods(MetaModule metaModule, List<MetaMethod> initHookMethods) { private void proceedInitMethods(BaseMetaModule metaModule, List<MetaMethod> initHookMethods) {
for (MetaMethod metaMethod : initHookMethods) { for (MetaMethod metaMethod : initHookMethods) {
try { try {
metaMethod.getMethod().invoke(metaModule.getInstance()); metaMethod.getMethod().invoke(metaModule.getInstance());
} catch (IllegalAccessException | InvocationTargetException e) { } catch (IllegalAccessException | InvocationTargetException e) {
throw new ModuleInitHookExecuteFailedException("模块的init hook方法执行失败! 模块: " + metaModule.getName() + " 方法签名: " + methodSignature(metaMethod.getMethod()), e); throw new ModuleInitHookExecuteFailedException("模块的init hook方法执行失败! 模块: " + metaModule.getClazz().getSimpleName() + " 方法签名: " + methodSignature(metaMethod.getMethod()), e);
} }
} }
} }
private List<MetaMethod> collectInitHookMethods(Class<?> clazz) { private List<MetaMethod> collectInitHookMethods(Class<?> clazz, Class<? extends Module> target) {
Set<Class<?>> classes = collectExtendedClasses(clazz, AgentRunningModule.class); Set<Class<?>> classes = collectExtendedClasses(clazz, target);
return classes.stream() return classes.stream()
.map(Class::getDeclaredMethods) .map(Class::getDeclaredMethods)
.flatMap(Arrays::stream) .flatMap(Arrays::stream)

View File

@@ -1,21 +1,29 @@
package work.slhaf.partner.api.agent.factory.module; package work.slhaf.partner.api.agent.factory.module;
import lombok.Getter;
import net.bytebuddy.ByteBuddy; import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.*; import net.bytebuddy.implementation.bind.annotation.*;
import net.bytebuddy.matcher.ElementMatchers; import net.bytebuddy.matcher.ElementMatchers;
import work.slhaf.partner.api.agent.factory.AgentBaseFactory; import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.capability.CapabilityCheckFactory;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.CapabilityFactoryContext;
import work.slhaf.partner.api.agent.factory.context.ModuleFactoryContext; import work.slhaf.partner.api.agent.factory.context.ModuleFactoryContext;
import work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute; import work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute;
import work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute; import work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute;
import work.slhaf.partner.api.agent.factory.module.annotation.InjectModule;
import work.slhaf.partner.api.agent.factory.module.exception.ModuleInstanceGenerateFailedException; import work.slhaf.partner.api.agent.factory.module.exception.ModuleInstanceGenerateFailedException;
import work.slhaf.partner.api.agent.factory.module.exception.ModuleProxyGenerateFailedException; import work.slhaf.partner.api.agent.factory.module.exception.ModuleProxyGenerateFailedException;
import work.slhaf.partner.api.agent.factory.module.exception.ProxiedModuleRunningException;
import work.slhaf.partner.api.agent.factory.module.pojo.BaseMetaModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaMethod; import work.slhaf.partner.api.agent.factory.module.pojo.MetaMethod;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaSubModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule; import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningSubModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.Module;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.*; import java.util.*;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@@ -24,50 +32,124 @@ import java.util.stream.Collectors;
import static work.slhaf.partner.api.agent.util.AgentUtil.collectExtendedClasses; import static work.slhaf.partner.api.agent.util.AgentUtil.collectExtendedClasses;
/** /**
* 通过扫描注解<code>@BeforeExecute</code>获取到各个模块的后hook逻辑并通过动态代理添加到执行逻辑之后 * <h2>Agent启动流程 3</h2>
*
* <p>
* 扫描前置模块各个hook注解生成代理对象放入对应的list中并按照类型为键放入 {@link ModuleProxyFactory#capabilityHolderInstances} 中供后续完成能力(capability)注入
* <p/>
*
* <ol>
*
* <li>
* <p>{@link ModuleProxyFactory#createProxiedInstances()}</p>
* 根据moduleList中的类型信息向上查找继承链获取所有hook方法收集为{@link MethodsListRecord}然后通过ByteBuddy根据收集到的preHook与postHook生成代理对象放入对应的 {@link MetaModule} 对象以及 instanceMap 中
* </li>
* <li>
* <p>{@link ModuleProxyFactory#injectSubModule()}</p>
* 通过反射将子模块实例注入到执行模块中带有注解 {@link InjectModule} 的字段
* </li>
* </ol>
*
* <p>下一步流程请参阅{@link CapabilityCheckFactory}</p>
*/ */
public class ModuleProxyFactory extends AgentBaseFactory { public class ModuleProxyFactory extends AgentBaseFactory {
private List<MetaModule> moduleList; private List<MetaModule> moduleList;
private List<MetaSubModule> subModuleList;
private HashMap<Class<?>, Object> capabilityHolderInstances;
private final HashMap<Class<?>, Object> subModuleInstances = new HashMap<>();
private final HashMap<Class<?>, Object> moduleInstances = new HashMap<>();
@Override @Override
protected void setVariables(AgentRegisterContext context) { protected void setVariables(AgentRegisterContext context) {
ModuleFactoryContext factoryContext = context.getModuleFactoryContext(); ModuleFactoryContext factoryContext = context.getModuleFactoryContext();
moduleList = factoryContext.getModuleList(); CapabilityFactoryContext capabilityFactoryContext = context.getCapabilityFactoryContext();
moduleList = factoryContext.getAgentModuleList();
subModuleList = factoryContext.getAgentSubModuleList();
capabilityHolderInstances = capabilityFactoryContext.getCapabilityHolderInstances();
} }
@Override @Override
protected void run() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { protected void run() {
generateInstances(); createProxiedInstances();
setHookProxy(); injectSubModule();
} }
private void setHookProxy() { private void injectSubModule() {
for (MetaModule module : moduleList) { for (MetaModule module : moduleList) {
//因为实际上ByteBuddy生成的是module.getClazz()的子类所以应当使用getDeclaredFields()获取字段
Arrays.stream(module.getClazz().getDeclaredFields())
.filter(field -> field.isAnnotationPresent(InjectModule.class))
.forEach(field -> {
try {
field.setAccessible(true);
field.set(
moduleInstances.get(module.getClazz()),
subModuleInstances.get(field.getType())
);
} catch (IllegalAccessException e) {
throw new ModuleInstanceGenerateFailedException("模块实例注入失败", e);
}
});
}
}
private void createProxiedInstances() {
generateModuleProxy(moduleList, AgentRunningModule.class);
generateModuleProxy(subModuleList, AgentRunningSubModule.class);
updateInstanceMap(moduleInstances, moduleList);
updateInstanceMap(subModuleInstances, subModuleList);
updateCapabilityHolderInstances();
}
private void updateCapabilityHolderInstances() {
capabilityHolderInstances.putAll(moduleInstances);
capabilityHolderInstances.putAll(subModuleInstances);
}
private void updateInstanceMap(HashMap<Class<?>, Object> instanceMap, List<? extends BaseMetaModule> list) {
for (BaseMetaModule baseMetaModule : list) {
instanceMap.put(baseMetaModule.getClazz(), baseMetaModule.getInstance());
}
}
private void generateModuleProxy(List<? extends BaseMetaModule> list, Class<? extends Module> overrideSource) {
for (BaseMetaModule module : list) {
Class<?> clazz = module.getClazz(); Class<?> clazz = module.getClazz();
try { try {
MethodsListRecord record = collectHookMethods(clazz); MethodsListRecord record = collectHookMethods(clazz);
//生成实例 //生成实例
generateProxiedInstances(record, module); generateProxiedInstances(record, module, overrideSource);
} catch (Exception e) { } catch (Exception e) {
throw new ModuleProxyGenerateFailedException("创建代理对象失败: " + clazz.getSimpleName(), e); throw new ModuleProxyGenerateFailedException("创建代理对象失败: " + clazz.getSimpleName(), e);
} }
} }
} }
private void generateProxiedInstances(MethodsListRecord record, MetaModule metaModule) { private void generateProxiedInstances(MethodsListRecord record, BaseMetaModule module, Class<? extends Module> overrideSource) {
try { try {
Class<? extends AgentRunningModule> clazz = metaModule.getClazz(); Class<? extends Module> clazz = module.getClazz();
Class<? extends AgentRunningModule> proxyClass = new ByteBuddy() Class<? extends Module> proxyClass = new ByteBuddy()
.subclass(clazz) .subclass(clazz)
.method(ElementMatchers.isOverriddenFrom(AgentRunningModule.class)) .method(ElementMatchers.isOverriddenFrom(overrideSource))
.intercept(MethodDelegation.to(new ModuleProxyInterceptor(record.post, record.pre))) .intercept(MethodDelegation.to(new ModuleProxyInterceptor(record.post, record.pre)))
.make() .make()
.load(ModuleProxyFactory.class.getClassLoader()) .load(ModuleProxyFactory.class.getClassLoader())
.getLoaded(); .getLoaded();
metaModule.setInstance(proxyClass.getConstructor().newInstance());
// new ByteBuddy()
// .subclass(clazz)
// .method(ElementMatchers.isOverriddenFrom(overrideSource))
// .intercept(MethodDelegation.to(new ModuleProxyInterceptor(record.post, record.pre)))
//
// .make()
// .saveIn(new File("./generated-classes"));
module.setInstance(proxyClass.getConstructor().newInstance());
} catch (Exception e) { } catch (Exception e) {
throw new ModuleProxyGenerateFailedException("模块Hook代理生成失败! 代理失败的模块名: " + metaModule.getClazz().getSimpleName(), e); throw new ModuleProxyGenerateFailedException("模块Hook代理生成失败! 代理失败的模块名: " + module.getClazz().getSimpleName(), e);
} }
} }
@@ -76,8 +158,8 @@ public class ModuleProxyFactory extends AgentBaseFactory {
List<MetaMethod> pre = new ArrayList<>(); List<MetaMethod> pre = new ArrayList<>();
//获取该类本身的hook逻辑 //获取该类本身的hook逻辑
collectHookMethods(post, pre, clazz); collectHookMethods(post, pre, clazz);
//获取它所继承、实现的抽象类或接口, 以AgentInteractionModule、ActiveModel为终点 //获取它所继承、实现的抽象类或接口, 以Module为终点收集继承链上所有父类和接口
Set<Class<?>> classes = collectExtendedClasses(clazz, AgentRunningModule.class); Set<Class<?>> classes = collectExtendedClasses(clazz, Module.class);
//获取这些类中的hook逻辑 //获取这些类中的hook逻辑
collectHookMethods(post, pre, classes); collectHookMethods(post, pre, classes);
return new MethodsListRecord(post, pre); return new MethodsListRecord(post, pre);
@@ -118,7 +200,7 @@ public class ModuleProxyFactory extends AgentBaseFactory {
private void collectHookMethods(List<MetaMethod> post, List<MetaMethod> pre, Class<?> clazz) { private void collectHookMethods(List<MetaMethod> post, List<MetaMethod> pre, Class<?> clazz) {
Method[] methods = clazz.getMethods(); Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) { for (Method method : methods) {
if (method.isAnnotationPresent(BeforeExecute.class)) { if (method.isAnnotationPresent(BeforeExecute.class)) {
MetaMethod metaMethod = new MetaMethod(); MetaMethod metaMethod = new MetaMethod();
@@ -134,30 +216,38 @@ public class ModuleProxyFactory extends AgentBaseFactory {
} }
} }
private void generateInstances() { @Getter
for (MetaModule metaModule : moduleList) { @SuppressWarnings("ClassCanBeRecord")
public static class ModuleProxyInterceptor {
private final List<MetaMethod> postHookMethods;
private final List<MetaMethod> preHookMethods;
public ModuleProxyInterceptor(List<MetaMethod> postHookMethods, List<MetaMethod> preHookMethods) {
this.postHookMethods = postHookMethods;
this.preHookMethods = preHookMethods;
}
@RuntimeType
public Object intercept(@Origin Method method, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper, @This Object proxy) throws Exception {
executeHookMethods(preHookMethods, proxy);
Object res = zuper.call();
executeHookMethods(postHookMethods, proxy);
return res;
}
private void executeHookMethods(List<MetaMethod> hookMethods, Object proxy) {
for (MetaMethod metaMethod : hookMethods) {
Method m = metaMethod.getMethod();
try { try {
Class<? extends AgentRunningModule> clazz = metaModule.getClazz(); m.setAccessible(true);
AgentRunningModule instance = clazz.getConstructor().newInstance(); m.invoke(proxy);
metaModule.setInstance(instance);
} catch (Exception e) { } catch (Exception e) {
throw new ModuleInstanceGenerateFailedException("模块实例构造失败:" + e.getMessage()); throw new ProxiedModuleRunningException("hook方法执行异常: " + m.getDeclaringClass() + "#" + m.getName(), e);
} }
} }
} }
private record ModuleProxyInterceptor(List<MetaMethod> postHookMethods, List<MetaMethod> preHookMethods) {
@RuntimeType
public Object intercept(@Origin Method method, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper, @This Object proxy) throws Exception {
for (MetaMethod metaMethod : preHookMethods) {
metaMethod.getMethod().invoke(proxy);
}
Object res = zuper.call();
for (MetaMethod metaMethod : postHookMethods) {
metaMethod.getMethod().invoke(proxy);
}
return res;
}
} }
record MethodsListRecord(List<MetaMethod> post, List<MetaMethod> pre) { record MethodsListRecord(List<MetaMethod> post, List<MetaMethod> pre) {

View File

@@ -6,31 +6,71 @@ import work.slhaf.partner.api.agent.factory.AgentBaseFactory;
import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext; import work.slhaf.partner.api.agent.factory.context.AgentRegisterContext;
import work.slhaf.partner.api.agent.factory.context.ModuleFactoryContext; import work.slhaf.partner.api.agent.factory.context.ModuleFactoryContext;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule; import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule;
import work.slhaf.partner.api.agent.factory.module.annotation.CoreModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaSubModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule; import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningSubModule;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* 负责扫描<code>@Module</code>注解获取模块实例 * <h2>Agent启动流程 2</h2>
*
* <p>
* 负责收集 {@link AgentModule} 与 {@link AgentSubModule} 注解所在类的信息,供后续工厂完成动态代理、模块与能力注入
* <p/>
*
* <ol>
* <li>
* <p>{@link ModuleRegisterFactory#setModuleList()}</p>
* 扫描 {@link AgentModule} 注解,获取执行模块信息: 类型、模块名称({@link AgentModule#name()}),执行顺序。并按照注解的 {@link AgentModule#order()} 字段进行排序
* </li>
* <li>
* <p>{@link ModuleRegisterFactory#setSubModuleList()}</p>
* 扫描 {@link AgentSubModule} 注册,获取子模块类型信息
* </li>
* <li>
* 两种模块都将存入各自的list中供后续模块完成注册与注入
* </li>
* </ol>
*
* <p>下一步流程请参阅{@link ModuleProxyFactory}</p>
*/ */
public class ModuleRegisterFactory extends AgentBaseFactory { public class ModuleRegisterFactory extends AgentBaseFactory {
private Reflections reflections; private Reflections reflections;
private List<MetaModule> moduleList; private List<MetaModule> moduleList;
private List<MetaSubModule> subModuleList;
@Override @Override
protected void setVariables(AgentRegisterContext context) { protected void setVariables(AgentRegisterContext context) {
ModuleFactoryContext factoryContext = context.getModuleFactoryContext(); ModuleFactoryContext factoryContext = context.getModuleFactoryContext();
reflections = context.getReflections(); reflections = context.getReflections();
moduleList = factoryContext.getModuleList(); moduleList = factoryContext.getAgentModuleList();
subModuleList = factoryContext.getAgentSubModuleList();
} }
@Override @Override
protected void run() { protected void run() {
setModuleList(); setModuleList();
setSubModuleList();
}
private void setSubModuleList() {
Set<Class<?>> subModules = reflections.getTypesAnnotatedWith(AgentSubModule.class);
for (Class<?> subModule : subModules) {
if (!ClassUtil.isNormalClass(subModule)) {
continue;
}
Class<? extends AgentRunningSubModule> clazz = subModule.asSubclass(AgentRunningSubModule.class);
MetaSubModule metaSubModule = new MetaSubModule();
metaSubModule.setClazz(clazz);
subModuleList.add(metaSubModule);
}
} }
private void setModuleList() { private void setModuleList() {
@@ -41,13 +81,24 @@ public class ModuleRegisterFactory extends AgentBaseFactory {
continue; continue;
} }
Class<? extends AgentRunningModule> clazz = module.asSubclass(AgentRunningModule.class); Class<? extends AgentRunningModule> clazz = module.asSubclass(AgentRunningModule.class);
AgentModule agentModule = clazz.getAnnotation(AgentModule.class); MetaModule metaModule = getMetaModule(clazz);
MetaModule metaModule = new MetaModule();
metaModule.setName(agentModule.name());
metaModule.setOrder(agentModule.order());
metaModule.setClazz(clazz);
moduleList.add(metaModule); moduleList.add(metaModule);
} }
moduleList.sort(Comparator.comparing(MetaModule::getOrder)); moduleList.sort(Comparator.comparing(MetaModule::getOrder));
} }
private static MetaModule getMetaModule(Class<? extends AgentRunningModule> clazz) {
MetaModule metaModule = new MetaModule();
AgentModule agentModule;
if (clazz.isAnnotationPresent(CoreModule.class)){
agentModule = CoreModule.class.getAnnotation(AgentModule.class);
}else{
agentModule = clazz.getAnnotation(AgentModule.class);
}
metaModule.setName(agentModule.name());
metaModule.setOrder(agentModule.order());
metaModule.setClazz(clazz);
return metaModule;
}
} }

View File

@@ -1,14 +1,17 @@
package work.slhaf.partner.api.agent.factory.module.annotation; package work.slhaf.partner.api.agent.factory.module.annotation;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityHolder;
import java.lang.annotation.*; import java.lang.annotation.*;
/** /**
* 用于注解执行模块 * 用于注解执行模块
*/ */
@Inherited
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@CapabilityHolder
@Inherited
public @interface AgentModule { public @interface AgentModule {
/** /**

View File

@@ -0,0 +1,14 @@
package work.slhaf.partner.api.agent.factory.module.annotation;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityHolder;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@CapabilityHolder
public @interface AgentSubModule {
}

View File

@@ -1,5 +1,9 @@
package work.slhaf.partner.api.agent.factory.module.annotation; package work.slhaf.partner.api.agent.factory.module.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@AgentModule(name = "core",order = 5) @AgentModule(name = "core",order = 5)
public @interface CoreModule { public @interface CoreModule {
} }

View File

@@ -0,0 +1,11 @@
package work.slhaf.partner.api.agent.factory.module.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectModule {
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.api.agent.factory.module.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class ProxiedModuleRunningException extends AgentRuntimeException {
public ProxiedModuleRunningException(String message) {
super(message);
}
public ProxiedModuleRunningException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.partner.api.agent.factory.module.pojo;
import lombok.Data;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.Module;
@Data
public abstract class BaseMetaModule <C extends Module> {
private Class<? extends C> clazz;
private C instance;
}

View File

@@ -1,13 +1,13 @@
package work.slhaf.partner.api.agent.factory.module.pojo; package work.slhaf.partner.api.agent.factory.module.pojo;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule; import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule;
@EqualsAndHashCode(callSuper = true)
@Data @Data
public class MetaModule { public class MetaModule extends BaseMetaModule<AgentRunningModule>{
private String name; private String name;
private int order; private int order;
private Class<? extends AgentRunningModule> clazz;
private AgentRunningModule instance;
private boolean enabled = true; private boolean enabled = true;
} }

View File

@@ -0,0 +1,10 @@
package work.slhaf.partner.api.agent.factory.module.pojo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningSubModule;
@EqualsAndHashCode(callSuper = true)
@Data
public class MetaSubModule extends BaseMetaModule<AgentRunningSubModule>{
}

View File

@@ -3,30 +3,27 @@ package work.slhaf.partner.api.agent.runtime.config;
import lombok.Data; import lombok.Data;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.agent.factory.config.exception.ConfigNotExistException;
import work.slhaf.partner.api.agent.factory.config.exception.ConfigUpdateFailedException; import work.slhaf.partner.api.agent.factory.config.exception.ConfigUpdateFailedException;
import work.slhaf.partner.api.agent.factory.config.exception.PromptNotExistException; import work.slhaf.partner.api.agent.factory.config.exception.PromptNotExistException;
import work.slhaf.partner.api.agent.factory.config.pojo.ModelConfig; import work.slhaf.partner.api.agent.factory.config.pojo.ModelConfig;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.chat.pojo.Message; import work.slhaf.partner.api.chat.pojo.Message;
import java.util.HashMap; import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j @Slf4j
@Data @Data
public abstract class AgentConfigManager { public abstract class AgentConfigManager {
@Setter @Setter
public static AgentConfigManager INSTANCE; public static AgentConfigManager INSTANCE = new FileAgentConfigManager();
private static final String DEFAULT_KEY = "default"; private static final String DEFAULT_KEY = "default";
protected HashMap<String, ModelConfig> modelConfigMap; protected HashMap<String, ModelConfig> modelConfigMap;
protected HashMap<String, List<Message>> modelPromptMap; protected HashMap<String, List<Message>> modelPromptMap;
protected HashMap<String, Boolean> moduleEnabledStatus; protected HashMap<String, Boolean> moduleEnabledStatus;
protected List<MetaModule> moduleList; protected Map<Integer, List<MetaModule>> moduleOrderedMap = new LinkedHashMap<>();
protected Map<String, MetaModule> moduleMap = new HashMap<>();
public void load() { public void load() {
modelConfigMap = loadModelConfig(); modelConfigMap = loadModelConfig();
@@ -41,11 +38,24 @@ public abstract class AgentConfigManager {
protected abstract void dumpModuleEnabledStatus(); protected abstract void dumpModuleEnabledStatus();
protected abstract HashMap<String, Boolean> loadModuleEnabledStatusMap(); protected abstract HashMap<String, Boolean> loadModuleEnabledStatusMap(List<MetaModule> moduleList);
public void moduleEnabledStatusFilterAndRecord(List<MetaModule> moduleList) { public void moduleEnabledStatusFilterAndRecord(List<MetaModule> moduleList) {
this.moduleList = moduleList; updateModuleMap(moduleList);
this.moduleEnabledStatus = loadModuleEnabledStatusMap(); updateModuleEnabledStatus(moduleList);
}
private void updateModuleMap(List<MetaModule> moduleList) {
//在ModuleRegisterFactory已进行过排序操作
for (MetaModule module : moduleList) {
int k = module.getOrder();
moduleOrderedMap.computeIfAbsent(k, order -> new ArrayList<>()).add(module);
moduleMap.put(module.getName(), module);
}
}
private void updateModuleEnabledStatus(List<MetaModule> moduleList) {
this.moduleEnabledStatus = loadModuleEnabledStatusMap(moduleList);
boolean unmatch = false; boolean unmatch = false;
for (MetaModule metaModule : moduleList) { for (MetaModule metaModule : moduleList) {
@@ -62,27 +72,6 @@ public abstract class AgentConfigManager {
} }
} }
/**
* 对模型Config与Prompt分别进行检验,除了都必须包含default外还需要确保数量、key一致毕竟是模型配置与提示词
*/
public void check() {
log.info("[AgentConfigManager]: 执行config与prompt检测...");
if (!modelConfigMap.containsKey("default")) {
throw new ConfigNotExistException("缺少默认配置! 需确保存在一个模型配置的key为`default`");
}
if (!modelPromptMap.containsKey("basic")) {
throw new PromptNotExistException("缺少基础Prompt! 需要确保存在key为basic的Prompt文件它将与其他Prompt共同作用于模块节点。");
}
Set<String> configKeySet = new HashSet<>(modelConfigMap.keySet());
configKeySet.remove("default");
Set<String> promptKeySet = new HashSet<>(modelPromptMap.keySet());
promptKeySet.remove("basic");
if (!promptKeySet.containsAll(configKeySet)) {
log.warn("存在未被提示词包含的模型配置,该配置将无法生效!");
}
log.info("[AgentConfigManager]: 检测完毕.");
}
public List<Message> loadModelPrompt(String modelKey) { public List<Message> loadModelPrompt(String modelKey) {
if (!modelPromptMap.containsKey(modelKey)) { if (!modelPromptMap.containsKey(modelKey)) {
throw new PromptNotExistException("不存在的modelPrompt: " + modelKey); throw new PromptNotExistException("不存在的modelPrompt: " + modelKey);
@@ -108,12 +97,7 @@ public abstract class AgentConfigManager {
} }
moduleEnabledStatus.put(key, status); moduleEnabledStatus.put(key, status);
dumpModuleEnabledStatus(); dumpModuleEnabledStatus();
for (MetaModule metaModule : moduleList) { moduleMap.get(key).setEnabled(status);
if (metaModule.getName().equals(key)) {
metaModule.setEnabled(status);
break;
}
}
} }
} }

View File

@@ -22,12 +22,12 @@ import java.util.List;
* 将从当前运行目录的config文件夹下创建并读取配置 * 将从当前运行目录的config文件夹下创建并读取配置
*/ */
@Slf4j @Slf4j
public class DefaultAgentConfigManager extends AgentConfigManager { public class FileAgentConfigManager extends AgentConfigManager {
private static final String CONFIG_DIR = "./config/"; protected static final String CONFIG_DIR = "./config/";
private static final String MODEL_CONFIG_DIR = "./config/model/"; protected static final String MODEL_CONFIG_DIR = "./config/model/";
private static final String PROMPT_CONFIG_DIR = "./config/prompt/"; protected static final String PROMPT_CONFIG_DIR = "./config/prompt/";
private static final String MODULE_ENABLED_STATUS_CONFIG_FILE = CONFIG_DIR + "module_enabled_status.json"; protected static final String MODULE_ENABLED_STATUS_CONFIG_FILE = CONFIG_DIR + "module_enabled_status.json";
@Override @Override
@@ -74,10 +74,10 @@ public class DefaultAgentConfigManager extends AgentConfigManager {
} }
@Override @Override
protected HashMap<String, Boolean> loadModuleEnabledStatusMap() { protected HashMap<String, Boolean> loadModuleEnabledStatusMap(List<MetaModule> moduleList) {
File file = new File(MODULE_ENABLED_STATUS_CONFIG_FILE); File file = new File(MODULE_ENABLED_STATUS_CONFIG_FILE);
try { try {
HashMap<String, Boolean> moduleEnabledStatus = new HashMap<>(); moduleEnabledStatus = new HashMap<>();
if (!file.exists()) { if (!file.exists()) {
file.createNewFile(); file.createNewFile();
for (MetaModule module : moduleList) { for (MetaModule module : moduleList) {

View File

@@ -0,0 +1,11 @@
package work.slhaf.partner.api.agent.runtime.exception;
public class AgentRunningFailedException extends AgentRuntimeException{
public AgentRunningFailedException(String message) {
super(message);
}
public AgentRunningFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,23 +1,36 @@
package work.slhaf.partner.api.agent.runtime.exception; package work.slhaf.partner.api.agent.runtime.exception;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
public static GlobalExceptionHandler INSTANCE = new GlobalExceptionHandler(); public static GlobalExceptionHandler INSTANCE = new GlobalExceptionHandler();
private AgentExceptionCallback exceptionCallback = new DefaultAgentExceptionCallback(); private AgentExceptionCallback exceptionCallback = new LogAgentExceptionCallback();
public void handle(Throwable e) { public boolean handle(Throwable e) {
boolean exit;
switch (e.getClass().getSimpleName()) { Throwable cause = e.getCause();
case "AgentRuntimeException": switch (cause) {
exceptionCallback.onRuntimeException((AgentRuntimeException) e); case AgentRunningFailedException arfe -> {
break; exit = true;
case "AgentLaunchFailedException": exceptionCallback.onRuntimeException((AgentRuntimeException) cause);
exceptionCallback.onFailedException((AgentLaunchFailedException) e);
break;
default:
throw new RuntimeException("未经处理的异常!", e);
} }
case AgentRuntimeException are -> {
exit = false;
exceptionCallback.onRuntimeException((AgentRuntimeException) cause);
}
case AgentLaunchFailedException alfe -> {
exit = true;
exceptionCallback.onFailedException((AgentLaunchFailedException) cause);
}
default -> {
exit = true;
log.error("意外异常: ", cause);
}
}
return exit;
} }
public static void setExceptionCallback(AgentExceptionCallback callback) { public static void setExceptionCallback(AgentExceptionCallback callback) {

View File

@@ -3,7 +3,7 @@ package work.slhaf.partner.api.agent.runtime.exception;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class DefaultAgentExceptionCallback implements AgentExceptionCallback { public class LogAgentExceptionCallback implements AgentExceptionCallback {
@Override @Override
public void onRuntimeException(AgentRuntimeException e) { public void onRuntimeException(AgentRuntimeException e) {

View File

@@ -4,9 +4,23 @@ import work.slhaf.partner.api.agent.runtime.interaction.data.AgentInputData;
import work.slhaf.partner.api.agent.runtime.interaction.data.AgentOutputData; import work.slhaf.partner.api.agent.runtime.interaction.data.AgentOutputData;
import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext; import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext;
public interface AgentGateway { public interface AgentGateway <I extends AgentInputData, O extends AgentOutputData, C extends RunningFlowContext>{
void launch(); void launch();
<I extends AgentInputData, O extends AgentOutputData, C extends RunningFlowContext> AgentInteractionAdapter<I, O, C> adapter(); default void receive(I inputData){
C finalInputData = adapter().parseInputData(inputData);
C outputContext = adapter().call(finalInputData);
O outputData = adapter().parseOutputData(outputContext);
send(outputData);
}
void send(O outputData);
/**
* 通过adapter提供的receive、send方法进行与客户端的交互行为
*
* @return adapter实例
*/
AgentInteractionAdapter<I, O, C> adapter();
} }

View File

@@ -8,34 +8,19 @@ import work.slhaf.partner.api.agent.runtime.interaction.flow.AgentRunningFlow;
import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext; import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext;
import java.util.List; import java.util.List;
import java.util.Map;
public abstract class AgentInteractionAdapter<I extends AgentInputData, O extends AgentOutputData, C extends RunningFlowContext> { public abstract class AgentInteractionAdapter<I extends AgentInputData, O extends AgentOutputData, C extends RunningFlowContext> {
private static AgentInteractionAdapter<?,?,?> INSTANCE;
protected AgentRunningFlow<C> agentRunningFlow = new AgentRunningFlow<>(); protected AgentRunningFlow<C> agentRunningFlow = new AgentRunningFlow<>();
protected List<MetaModule> moduleList = AgentConfigManager.INSTANCE.getModuleList(); protected Map<Integer, List<MetaModule>> moduleOrderedMap = AgentConfigManager.INSTANCE.getModuleOrderedMap();
public void receive(I inputData) { public C call(C finalInputData){
C finalInputData = parseInputData(inputData); return agentRunningFlow.launch(moduleOrderedMap, finalInputData);
C outputContext = agentRunningFlow.launch(moduleList, finalInputData);
O outputData = parseOutputData(outputContext);
send(outputData);
} }
protected abstract O parseOutputData(C outputContext); protected abstract O parseOutputData(C outputContext);
protected abstract C parseInputData(I inputData); protected abstract C parseInputData(I inputData);
public abstract void send(O outputData);
public static <I extends AgentInputData, O extends AgentOutputData, C extends RunningFlowContext> AgentInteractionAdapter<I, O, C> getInstance() {
@SuppressWarnings("unchecked")
AgentInteractionAdapter<I, O, C> instance = (AgentInteractionAdapter<I, O, C>) INSTANCE;
return instance;
}
public static <I extends AgentInputData, O extends AgentOutputData, C extends RunningFlowContext> void setInstance(AgentInteractionAdapter<I, O, C> instance) {
INSTANCE = instance;
}
} }

View File

@@ -7,7 +7,7 @@ import lombok.EqualsAndHashCode;
@Data @Data
public abstract class AgentOutputData extends InteractionData{ public abstract class AgentOutputData extends InteractionData{
private int code; protected int code;
public static class StatusCode { public static class StatusCode {
public static final int SUCCESS = 1; public static final int SUCCESS = 1;

View File

@@ -1,24 +1,48 @@
package work.slhaf.partner.api.agent.runtime.interaction.flow; package work.slhaf.partner.api.agent.runtime.interaction.flow;
import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule; import work.slhaf.partner.api.agent.factory.module.pojo.MetaModule;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
import work.slhaf.partner.api.agent.runtime.exception.GlobalExceptionHandler; import work.slhaf.partner.api.agent.runtime.exception.GlobalExceptionHandler;
import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext; import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** /**
* Agent执行流程 * Agent执行流程
*/ */
public class AgentRunningFlow<C extends RunningFlowContext> { public class AgentRunningFlow<C extends RunningFlowContext> {
public C launch(List<MetaModule> moduleList, C interactionContext){ public C launch(Map<Integer, List<MetaModule>> modules, C interactionContext) {
try { try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
//流程执行启动 //流程执行启动
for (MetaModule metaModule : moduleList) { for (Map.Entry<Integer, List<MetaModule>> entry : modules.entrySet()) {
metaModule.getInstance().execute(interactionContext); List<Future<?>> futures = new ArrayList<>();
List<MetaModule> moduleList = entry.getValue();
for (MetaModule module : moduleList) {
Future<?> future = executor.submit(() -> {
module.getInstance().execute(interactionContext);
});
futures.add(future);
} }
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) { } catch (Exception e) {
GlobalExceptionHandler.INSTANCE.handle(e); boolean exit = GlobalExceptionHandler.INSTANCE.handle(e);
if (exit) throw new AgentRuntimeException("Agent执行出错!", e);
interactionContext.getErrMsg().add(e.getLocalizedMessage());
}
}
}
interactionContext.setOk(1);
} catch (Exception e) {
interactionContext.setOk(0);
interactionContext.getErrMsg().add(e.getLocalizedMessage());
} }
return interactionContext; return interactionContext;
} }

View File

@@ -17,13 +17,13 @@ public interface ActivateModel {
AgentConfigManager AGENT_CONFIG_MANAGER = AgentConfigManager.INSTANCE; AgentConfigManager AGENT_CONFIG_MANAGER = AgentConfigManager.INSTANCE;
@Init @Init(order = -1)
default void modelSettings() { default void modelSettings() {
Model model = new Model(); Model model = new Model();
ModelConfig modelConfig = AgentConfigManager.INSTANCE.loadModelConfig(modelKey()); ModelConfig modelConfig = AgentConfigManager.INSTANCE.loadModelConfig(modelKey());
model.setBaseMessages(withBasicPrompt() ? loadSpecificPromptAndBasicPrompt(modelKey()) : loadSpecificPrompt(modelKey())); model.setBaseMessages(withBasicPrompt() ? loadSpecificPromptAndBasicPrompt(modelKey()) : loadSpecificPrompt(modelKey()));
model.setChatClient(new ChatClient(modelConfig.getBaseUrl(), modelConfig.getApikey(), modelConfig.getModel())); model.setChatClient(new ChatClient(modelConfig.getBaseUrl(), modelConfig.getApikey(), modelConfig.getModel()));
((Module) this).setModel(model); setModel(model);
} }
default void updateModelSettings(ChatClient newChatClient) { default void updateModelSettings(ChatClient newChatClient) {
@@ -87,6 +87,9 @@ public interface ActivateModel {
((Module) this).setModel(model); ((Module) this).setModel(model);
} }
/**
* 对应调用的模型配置名称
*/
String modelKey(); String modelKey();
boolean withBasicPrompt(); boolean withBasicPrompt();

View File

@@ -1,10 +1,36 @@
package work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts; package work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute;
import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
import work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute;
import work.slhaf.partner.api.agent.factory.module.annotation.CoreModule;
import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext; import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.RunningFlowContext;
/** /**
* 流程执行模块基类 * 流程执行模块基类
*/ */
public abstract class AgentRunningModule extends Module { @Slf4j
public abstract void execute(RunningFlowContext context); public abstract class AgentRunningModule<C extends RunningFlowContext> extends Module {
public abstract void execute(C context);
@BeforeExecute
private void beforeLog() {
log.debug("[{}] 模块执行开始...", getModuleName());
}
@AfterExecute
private void afterLog() {
log.debug("[{}] 模块执行结束...", getModuleName());
}
private String getModuleName(){
if (this.getClass().isAnnotationPresent(AgentModule.class)) {
return this.getClass().getAnnotation(AgentModule.class).name();
} else if (this.getClass().isAnnotationPresent(CoreModule.class)) {
return CoreModule.class.getAnnotation(AgentModule.class).name();
}else {
return "Unknown Module";
}
}
} }

View File

@@ -1,13 +1,35 @@
package work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts; package work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts;
/** import lombok.extern.slf4j.Slf4j;
* 流程子模块基类 import work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute;
* @param <I> 输入类型 import work.slhaf.partner.api.agent.factory.module.annotation.AgentModule;
* @param <O> 输出类型 import work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute;
*/ import work.slhaf.partner.api.agent.factory.module.annotation.CoreModule;
@Slf4j
public abstract class AgentRunningSubModule<I, O> extends Module { public abstract class AgentRunningSubModule<I, O> extends Module {
public abstract O execute(I data); public abstract O execute(I data);
@BeforeExecute
private void beforeLog() {
log.debug("[{}] 模块执行开始...", getModuleName());
}
@AfterExecute
private void afterLog() {
log.debug("[{}] 模块执行结束...", getModuleName());
}
private String getModuleName(){
if (this.getClass().isAnnotationPresent(AgentModule.class)) {
return this.getClass().getAnnotation(AgentModule.class).name();
} else if (this.getClass().isAnnotationPresent(CoreModule.class)) {
return CoreModule.class.getAnnotation(AgentModule.class).name();
}else {
return "Unknown Module";
}
}
} }

View File

@@ -2,13 +2,11 @@ package work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityHolder;
import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.Model; import work.slhaf.partner.api.agent.runtime.interaction.flow.entity.Model;
/** /**
* 模块基类 * 模块基类
*/ */
@CapabilityHolder
public abstract class Module { public abstract class Module {
@Getter @Getter

View File

@@ -1,11 +1,18 @@
package work.slhaf.partner.api.agent.runtime.interaction.flow.entity; package work.slhaf.partner.api.agent.runtime.interaction.flow.entity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import work.slhaf.partner.api.common.entity.PersistableObject;
import java.util.ArrayList;
import java.util.List;
/** /**
* 流程上下文 * 流程上下文
*/ */
@EqualsAndHashCode(callSuper = true)
@Data @Data
public abstract class RunningFlowContext { public abstract class RunningFlowContext extends PersistableObject {
protected int ok;
protected List<String> errMsg = new ArrayList<>();
} }

View File

@@ -1,11 +1,37 @@
package work.slhaf.partner.api.agent.util; package work.slhaf.partner.api.agent.util;
import org.reflections.Reflections;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
public final class AgentUtil { public final class AgentUtil {
public static boolean isAssignableFromAnnotation(Class<?> clazz,Class<? extends Annotation> targetAnnotation){
Set<Class<?>> visited = new HashSet<>();
return isAssignableFromAnnotation(clazz,targetAnnotation,visited);
}
private static boolean isAssignableFromAnnotation(Class<?> clazz,Class<? extends Annotation> targetAnnotation,Set<Class<?>> visited){
if (!visited.add(clazz)){
return false;
}
if (clazz.isAnnotationPresent(targetAnnotation)){
return true;
}
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
boolean ok = isAssignableFromAnnotation(annotation.annotationType(),targetAnnotation,visited);
if (ok){
return true;
}
}
return false;
}
public static String methodSignature(Method method) { public static String methodSignature(Method method) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("("); sb.append("(");
@@ -23,6 +49,7 @@ public final class AgentUtil {
public static Set<Class<?>> collectExtendedClasses(Class<?> clazz, Class<?> targetClass) { public static Set<Class<?>> collectExtendedClasses(Class<?> clazz, Class<?> targetClass) {
Set<Class<?>> classes = new HashSet<>(); Set<Class<?>> classes = new HashSet<>();
collectExtendedClasses(classes, clazz, targetClass); collectExtendedClasses(classes, clazz, targetClass);
classes.add(clazz);
return classes; return classes;
} }
@@ -36,10 +63,18 @@ public final class AgentUtil {
collectInterfaces(clazz, classes); collectInterfaces(clazz, classes);
} }
public static Set<Class<?>> getMethodAnnotationTypeSet(Class<? extends Annotation> clazz, Reflections reflections){
Set<Method> methods = reflections.getMethodsAnnotatedWith(clazz);
return methods.stream()
.map(Method::getDeclaringClass)
.collect(Collectors.toSet());
}
private static void collectInterfaces(Class<?> clazz, Set<Class<?>> classes) { private static void collectInterfaces(Class<?> clazz, Set<Class<?>> classes) {
for (Class<?> type : clazz.getInterfaces()) { for (Class<?> type : clazz.getInterfaces()) {
if (classes.add(type)) { if (classes.add(type)) {
collectInterfaces(type, classes); collectInterfaces(type, classes);
} }
} }
}} }
}

View File

@@ -1,10 +1,12 @@
package work.slhaf.partner.api.chat; package work.slhaf.partner.api.chat;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.chat.constant.ChatConstant; import work.slhaf.partner.api.chat.constant.ChatConstant;
import work.slhaf.partner.api.chat.pojo.ChatBody; import work.slhaf.partner.api.chat.pojo.ChatBody;
import work.slhaf.partner.api.chat.pojo.ChatResponse; import work.slhaf.partner.api.chat.pojo.ChatResponse;
@@ -13,6 +15,7 @@ import work.slhaf.partner.api.chat.pojo.PrimaryChatResponse;
import java.util.List; import java.util.List;
@Slf4j
@Data @Data
@NoArgsConstructor @NoArgsConstructor
public class ChatClient { public class ChatClient {
@@ -34,6 +37,8 @@ public class ChatClient {
public ChatResponse runChat(List<Message> messages) { public ChatResponse runChat(List<Message> messages) {
HttpRequest request = HttpRequest.post(url); HttpRequest request = HttpRequest.post(url);
request.setConnectionTimeout(2000);
request.setReadTimeout(15000);
request.header("Content-Type", "application/json"); request.header("Content-Type", "application/json");
request.header("Authorization", "Bearer " + apikey); request.header("Authorization", "Bearer " + apikey);
@@ -53,17 +58,26 @@ public class ChatClient {
.build(); .build();
} }
HttpResponse response = request.body(JSONUtil.toJsonStr(body)).execute();
ChatResponse finalResponse; ChatResponse finalResponse;
try {
HttpResponse response = request.body(JSONUtil.toJsonStr(body)).execute();
PrimaryChatResponse primaryChatResponse = JSONUtil.toBean(response.body(), PrimaryChatResponse.class); PrimaryChatResponse primaryChatResponse = JSONUtil.toBean(response.body(), PrimaryChatResponse.class);
finalResponse = ChatResponse.builder() finalResponse = ChatResponse.builder()
.type(ChatConstant.Response.SUCCESS) .status(ChatConstant.ResponseStatus.SUCCESS)
.message(primaryChatResponse.getChoices().get(0).getMessage().getContent()) .message(primaryChatResponse.getChoices().get(0).getMessage().getContent())
.usageBean(primaryChatResponse.getUsage()) .usageBean(primaryChatResponse.getUsage())
.build(); .build();
response.close(); response.close();
} catch (IORuntimeException e) {
log.error("请求超时", e);
finalResponse = ChatResponse.builder()
.message("连接超时")
.status(ChatConstant.ResponseStatus.FAILED)
.usageBean(null)
.build();
}
return finalResponse; return finalResponse;
} }

View File

@@ -8,8 +8,7 @@ public class ChatConstant {
public static final String ASSISTANT = "assistant"; public static final String ASSISTANT = "assistant";
} }
public static class Response { public enum ResponseStatus {
public static final String SUCCESS = "success"; SUCCESS, FAILED
public static final String ERROR = "error";
} }
} }

View File

@@ -4,13 +4,14 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import work.slhaf.partner.api.chat.constant.ChatConstant;
@Data @Data
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class ChatResponse { public class ChatResponse {
private String type; private ChatConstant.ResponseStatus status;
private String message; private String message;
private PrimaryChatResponse.UsageBean usageBean; private PrimaryChatResponse.UsageBean usageBean;
} }

View File

@@ -6,11 +6,12 @@ import net.bytebuddy.matcher.ElementMatchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule; import work.slhaf.partner.api.agent.runtime.interaction.flow.abstracts.AgentRunningModule;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
public class ModuleProxyTest { public class ModuleProxyTest {
@Test @Test
public void test() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { public void test() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Class<? extends AgentRunningModule> clazz = new ByteBuddy().subclass(MyAgentRunningModule.class) Class<? extends AgentRunningModule> clazz = new ByteBuddy().subclass(MyAgentRunningModule.class)
.method(ElementMatchers.isOverriddenFrom(AgentRunningModule.class)) .method(ElementMatchers.isOverriddenFrom(AgentRunningModule.class))
.intercept(MethodDelegation.to( .intercept(MethodDelegation.to(

28
Partner-Common/pom.xml Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>

View File

@@ -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 record Pair(InProcessMcpTransport clientSide, InProcessMcpTransport serverSide) {
}
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();
}
/* ======================================================
* other boilerplate
* ====================================================== */
@Override
public void close() {
McpClientTransport.super.close();
}
@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();
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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>

View File

@@ -22,6 +22,64 @@
<artifactId>Partner-Api</artifactId> <artifactId>Partner-Api</artifactId>
<version>0.5.0</version> <version>0.5.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.10.2</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-test</artifactId>
<version>1.10.2</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> </dependencies>
<properties> <properties>
@@ -30,4 +88,36 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </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.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -1,14 +0,0 @@
package work.slhaf;
import work.slhaf.partner.Agent;
import java.io.IOException;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws IOException {
Agent.initialize();
Scanner scanner = new Scanner(System.in);
while (!scanner.nextLine().equals("exit")) ;
}
}

View File

@@ -1,75 +0,0 @@
package work.slhaf.partner;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.common.config.Config;
import work.slhaf.partner.common.monitor.DebugMonitor;
import work.slhaf.partner.core.InteractionHub;
import work.slhaf.partner.core.interaction.agent_interface.InputReceiver;
import work.slhaf.partner.core.interaction.agent_interface.TaskCallback;
import work.slhaf.partner.core.interaction.data.InteractionInputData;
import work.slhaf.partner.core.interaction.data.InteractionOutputData;
import work.slhaf.partner.gateway.AgentWebSocketServer;
import work.slhaf.partner.gateway.MessageSender;
import java.io.IOException;
import java.time.LocalDateTime;
@Data
@Slf4j
public class Agent implements TaskCallback, InputReceiver {
private static volatile Agent agent;
private InteractionHub interactionHub;
private MessageSender messageSender;
public static void initialize() throws IOException {
if (agent == null) {
synchronized (Agent.class) {
if (agent == null) {
//加载配置
Config config = Config.getConfig();
agent = new Agent();
agent.setInteractionHub(InteractionHub.initialize());
agent.registerTaskCallback();
AgentWebSocketServer server = new AgentWebSocketServer(config.getWebSocketConfig().getPort(), agent);
server.launch();
agent.setMessageSender(server);
log.info("Agent 加载完毕..");
//启动监测线程
DebugMonitor.initialize();
}
}
}
}
public static Agent getInstance() throws IOException {
initialize();
return agent;
}
/**
* 接收用户输入,包装为标准输入数据类
*/
public void receiveInput(InteractionInputData inputData) throws IOException, ClassNotFoundException {
inputData.setLocalDateTime(LocalDateTime.now());
interactionHub.call(inputData);
}
/**
* 向用户返回输出内容
*/
public void sendToUser(String userInfo, String output) {
messageSender.sendMessage(new InteractionOutputData(output, userInfo));
}
@Override
public void onTaskFinished(String userInfo, String output) {
sendToUser(userInfo, output);
}
private void registerTaskCallback() {
interactionHub.setCallback(this);
}
}

View File

@@ -0,0 +1,18 @@
package work.slhaf.partner;
import work.slhaf.partner.api.agent.Agent;
import work.slhaf.partner.common.config.PartnerAgentConfigManager;
import work.slhaf.partner.common.vector.VectorClient;
import work.slhaf.partner.runtime.exception.PartnerExceptionCallback;
import work.slhaf.partner.runtime.interaction.WebSocketGateway;
public class Main {
public static void main(String[] args) {
Agent.newAgent(Main.class)
.setAgentConfigManager(PartnerAgentConfigManager.class)
.setGateway(WebSocketGateway.class)
.setAgentExceptionCallback(PartnerExceptionCallback.class)
.addAfterLaunchRunners(VectorClient::load)
.launch();
}
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.partner.common;
public final class Constant {
public static final class Path {
public static final String DATA = "data";
public static final String MEMORY_DATA = DATA + "/memory";
}
}

View File

@@ -1,138 +1,24 @@
package work.slhaf.partner.common.config; package work.slhaf.partner.common.config;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSONArray;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import work.slhaf.partner.module.modules.core.CoreModel;
import work.slhaf.partner.module.modules.memory.selector.MemorySelector;
import work.slhaf.partner.module.modules.memory.updater.MemoryUpdater;
import work.slhaf.partner.module.modules.process.PostprocessExecutor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Scanner;
@Data @Data
@Slf4j
public class Config { public class Config {
private static final String CONFIG_FILE_PATH = "./config/config.json";
private static final String LOG_FILE_PATH = "./data/log";
private static Config config;
private String agentId; private String agentId;
// private String basicCharacter;
private WebSocketConfig webSocketConfig; private WebSocketConfig webSocketConfig;
private VectorConfig vectorConfig;
private List<ModuleConfig> moduleConfigList; @Data
public static class VectorConfig {
private Config() { private int type;
private String ollamaEmbeddingUrl;
private String ollamaEmbeddingModel;
private String tokenizerPath;
private String embeddingModelPath;
} }
public static Config getConfig() throws IOException { @Data
if (config == null) { public static class WebSocketConfig {
File file = new File(CONFIG_FILE_PATH); private int port;
if (file.exists()) {
config = JSONUtil.readJSONObject(file, StandardCharsets.UTF_8).toBean(Config.class);
} else {
config = new Config();
Scanner scanner = new Scanner(System.in);
System.out.print("输入智能体名称: ");
config.setAgentId(scanner.nextLine());
System.out.println("(注意! 设定角色之后修改主配置文件将不会影响现有记忆除非同时更换agentId)");
System.out.println("\r\n--------模型配置--------\r\n");
generateModelConfig(scanner);
System.out.println("\r\n--------服务配置--------\r\n");
generateWsSocketConfig(scanner);
System.out.println("\r\n--------模块链配置--------\r\n");
generatePipelineConfig();
boolean launchOrNot = getLaunchOrNot(scanner);
//保存配置文件
String str = JSONUtil.toJsonPrettyStr(config);
FileUtils.writeStringToFile(file, str, StandardCharsets.UTF_8);
log.info("配置已保存");
if (!launchOrNot) {
System.exit(0);
} }
} }
config.generateCommonDirs();
}
return config;
}
private void generateCommonDirs() throws IOException {
Files.createDirectories(Paths.get(LOG_FILE_PATH));
}
private static boolean getLaunchOrNot(Scanner scanner) {
System.out.print("是否直接启动Partner?(y/n): ");
String input;
while (true) {
input = scanner.nextLine();
if (input.equals("y")) {
return true;
} else if (input.equals("n")) {
return false;
} else {
System.out.println("请输入y或n");
}
}
}
private static void generatePipelineConfig() {
List<ModuleConfig> moduleConfigList = List.of(
new ModuleConfig(MemorySelector.class.getName(), ModuleConfig.Constant.INTERNAL, null),
new ModuleConfig(CoreModel.class.getName(), ModuleConfig.Constant.INTERNAL, null),
new ModuleConfig(PostprocessExecutor.class.getName(),ModuleConfig.Constant.INTERNAL,null),
new ModuleConfig(MemoryUpdater.class.getName(), ModuleConfig.Constant.INTERNAL, null)
);
config.setModuleConfigList(moduleConfigList);
}
private static void generateWsSocketConfig(Scanner scanner) {
System.out.print("WebSocket port: ");
WebSocketConfig wsConfig = new WebSocketConfig();
wsConfig.setPort(scanner.nextInt());
config.setWebSocketConfig(wsConfig);
}
private static void generateModelConfig(Scanner scanner) throws IOException {
System.out.println("配置LLM APi:");
System.out.println("经测试, 目前只建议选择Qwen3: qwen-plus-latest或qwen-max-latest");
System.out.print("base_url: ");
String baseUrl = scanner.nextLine();
System.out.print("apikey: ");
String apikey = scanner.nextLine();
System.out.print("model: ");
String model = scanner.nextLine();
ModelConfig modelConfig = new ModelConfig();
modelConfig.setBaseUrl(baseUrl);
modelConfig.setApikey(apikey);
modelConfig.setModel(model);
InputStream stream = Config.class.getClassLoader().getResourceAsStream("modules/default_activated_model.json");
String content = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
stream.close();
for (String s : JSONArray.parseArray(content, String.class)) {
modelConfig.generateConfig(s);
}
}
}

View File

@@ -1,40 +0,0 @@
package work.slhaf.partner.common.config;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
@Data
public class ModelConfig {
private static final String MODEL_CONFIG_DIR_PATH = "./config/model/";
private static final HashMap<String, ModelConfig> modelConfigMap = new HashMap<>();
private String apikey;
private String baseUrl;
private String model;
public void generateConfig(String filename) throws IOException {
String str = JSONUtil.toJsonPrettyStr(this);
File file = new File(MODEL_CONFIG_DIR_PATH + filename + ".json");
FileUtils.writeStringToFile(file, str, StandardCharsets.UTF_8);
}
public static ModelConfig load(String modelKey) {
if (!modelConfigMap.containsKey(modelKey)) {
modelConfigMap.put(modelKey,loadConfig(modelKey));
}
return modelConfigMap.get(modelKey);
}
private static ModelConfig loadConfig(String modelKey) {
File file = new File(MODEL_CONFIG_DIR_PATH+modelKey+".json");
return JSONUtil.readJSONObject(file,StandardCharsets.UTF_8).toBean(ModelConfig.class);
}
}

View File

@@ -1,17 +0,0 @@
package work.slhaf.partner.common.config;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ModuleConfig {
private String className;
private String type;
private String path;
public static class Constant {
public static final String INTERNAL = "internal";
public static final String EXTERNAL = "external";
}
}

View File

@@ -0,0 +1,41 @@
package work.slhaf.partner.common.config;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import work.slhaf.partner.api.agent.factory.config.exception.ConfigNotExistException;
import work.slhaf.partner.api.agent.runtime.config.FileAgentConfigManager;
import work.slhaf.partner.common.exception.ConfigLoadFailedException;
import java.io.File;
import java.nio.charset.StandardCharsets;
@EqualsAndHashCode(callSuper = true)
@Data
public final class PartnerAgentConfigManager extends FileAgentConfigManager {
private static final String COMMON_CONFIG_FILE = CONFIG_DIR + "common_config.json";
private Config config;
@Override
public void load() {
loadWebSocketConfig();
super.load();
}
private void loadWebSocketConfig() {
File file = new File(COMMON_CONFIG_FILE);
if (!file.exists()) {
throw new ConfigNotExistException("Partner Config Not Exist: " + COMMON_CONFIG_FILE);
}
config = JSONUtil.readJSONObject(file, StandardCharsets.UTF_8).toBean(Config.class);
if (config == null || config.getAgentId() == null) {
throw new ConfigLoadFailedException("Partner Config Load Failed: " + COMMON_CONFIG_FILE);
}
int port = config.getWebSocketConfig().getPort();
if (port <= 0 || port > 65535) {
throw new ConfigLoadFailedException("Invalid Websocket port: " + port);
}
}
}

View File

@@ -1,8 +0,0 @@
package work.slhaf.partner.common.config;
import lombok.Data;
@Data
public class WebSocketConfig {
private Integer port;
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.common.exception;
import work.slhaf.partner.api.agent.factory.config.exception.ConfigFactoryInitFailedException;
public class ConfigLoadFailedException extends ConfigFactoryInitFailedException {
public ConfigLoadFailedException(String message, Throwable cause) {
super(message, cause);
}
public ConfigLoadFailedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.common.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentLaunchFailedException;
public class ServiceLoadFailedException extends AgentLaunchFailedException {
public ServiceLoadFailedException(String message, Throwable cause) {
super(message, cause);
}
public ServiceLoadFailedException(String message) {
super(message);
}
}

View File

@@ -1,43 +0,0 @@
package work.slhaf.partner.common.exception_handler;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.common.exception_handler.pojo.GlobalException;
import work.slhaf.partner.common.exception_handler.pojo.GlobalExceptionData;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
public class GlobalExceptionHandler {
private static final String EXCEPTION_STATIC_PATH = "./data/exception_snapshot/";
public static void writeExceptionState(GlobalException exception) {
GlobalExceptionData exceptionData = exception.getData();
Path filePath = Paths.get(EXCEPTION_STATIC_PATH, exceptionData.getExceptionTime() + ".dat");
try {
Files.createDirectories(Path.of(EXCEPTION_STATIC_PATH));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()));
oos.writeObject(exceptionData);
oos.close();
log.warn("[GlobalExceptionHandler] 捕获异常, 已保存到: {}", filePath);
} catch (IOException e) {
log.error("[GlobalExceptionHandler] 捕获异常, 保存失败: ", e);
}
}
public static GlobalExceptionData readExceptionState(String filePath) {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
GlobalExceptionData exceptionData = (GlobalExceptionData) ois.readObject();
ois.close();
log.info("[GlobalExceptionHandler] 已从: {} 读取异常快照", filePath);
return exceptionData;
} catch (IOException | ClassNotFoundException e) {
log.error("[GlobalExceptionHandler] 读取异常, 读取失败: ", e);
return null;
}
}
}

View File

@@ -1,30 +0,0 @@
package work.slhaf.partner.common.exception_handler.pojo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.core.cognation.cognation.CognationCore;
import work.slhaf.partner.core.interaction.data.context.InteractionContext;
import work.slhaf.partner.core.session.SessionManager;
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class GlobalException extends RuntimeException {
private GlobalExceptionData data;
public GlobalException(String message) {
super(message);
try {
this.data = new GlobalExceptionData();
this.data.setExceptionTime(System.currentTimeMillis());
this.data.setSessionManager(SessionManager.getInstance());
this.data.setCognationCore(CognationCore.getInstance());
this.data.setContext(InteractionContext.getInstance());
} catch (Exception e) {
log.error("[GlobalException] 捕获异常, 获取数据失败");
}
}
}

View File

@@ -1,26 +0,0 @@
package work.slhaf.partner.common.exception_handler.pojo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import work.slhaf.partner.api.common.entity.PersistableObject;
import work.slhaf.partner.core.cognation.cognation.CognationCore;
import work.slhaf.partner.core.interaction.data.context.InteractionContext;
import work.slhaf.partner.core.session.SessionManager;
import java.io.Serial;
import java.util.HashMap;
@EqualsAndHashCode(callSuper = true)
@Data
public class GlobalExceptionData extends PersistableObject {
@Serial
private static final long serialVersionUID = 1L;
private String exceptionMessage;
protected HashMap<String, InteractionContext> context;
protected SessionManager sessionManager;
protected CognationCore cognationCore;
protected Long exceptionTime;
}

View File

@@ -1,14 +1,9 @@
package work.slhaf.partner.common.thread; package work.slhaf.partner.common.thread;
import lombok.Getter; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Getter
public class InteractionThreadPoolExecutor { public class InteractionThreadPoolExecutor {
private static InteractionThreadPoolExecutor interactionThreadPoolExecutor; private static InteractionThreadPoolExecutor interactionThreadPoolExecutor;
@@ -33,9 +28,29 @@ public class InteractionThreadPoolExecutor {
public <T> void invokeAll(List<Callable<T>> tasks) { public <T> void invokeAll(List<Callable<T>> tasks) {
try { try {
executorService.invokeAll(tasks); List<Future<T>> futures = executorService.invokeAll(tasks);
for (Future<T> future : futures) {
future.get();
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
}
}
public <T> List<T> invokeAllAndReturn(List<Callable<T>> tasks) {
try {
List<Future<T>> futures = executorService.invokeAll(tasks);
List<T> results = new ArrayList<>();
for (Future<T> future : futures) {
results.add(future.get());
}
return results;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
} }
} }

View File

@@ -0,0 +1,14 @@
package work.slhaf.partner.common.util;
public class PathUtil {
public static 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();
}
}

View File

@@ -1,7 +1,7 @@
package work.slhaf.partner.common.util; package work.slhaf.partner.common.util;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import work.slhaf.partner.Agent; import work.slhaf.partner.api.agent.Agent;
import work.slhaf.partner.api.chat.pojo.Message; import work.slhaf.partner.api.chat.pojo.Message;
import java.io.InputStream; import java.io.InputStream;

View File

@@ -0,0 +1,48 @@
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.VectorClientExecuteException;
import java.util.Map;
@Slf4j
public class OllamaVectorClient extends VectorClient {
private String ollamaEmbeddingUrl;
private 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 VectorClientExecuteException("嵌入模型执行出错");
String resStr = response.body();
EmbeddingModelResponse embeddingResponse = JSONObject.parseObject(resStr, EmbeddingModelResponse.class);
return embeddingResponse.getEmbeddings()[0];
} catch (Exception e) {
throw new VectorClientExecuteException("嵌入模型执行出错", 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;
}
}

View File

@@ -0,0 +1,82 @@
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.VectorClientExecuteException;
import work.slhaf.partner.common.vector.exception.VectorClientLoadFailedException;
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 VectorClientLoadFailedException("加载ONNX模型失败", e);
}
}
private void loadTokenizer() {
try {
tokenizer = HuggingFaceTokenizer.newInstance(Path.of(tokenizerPath));
} catch (Exception e) {
throw new VectorClientLoadFailedException("加载Tokenizer失败", 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 VectorClientExecuteException("嵌入模型执行出错", e);
}
}
}

View File

@@ -0,0 +1,88 @@
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.api.agent.runtime.config.AgentConfigManager;
import work.slhaf.partner.common.config.Config.VectorConfig;
import work.slhaf.partner.common.config.PartnerAgentConfigManager;
import work.slhaf.partner.common.exception.ServiceLoadFailedException;
import work.slhaf.partner.common.vector.exception.VectorClientExecuteException;
import work.slhaf.partner.common.vector.exception.VectorClientLoadFailedException;
@Slf4j
public abstract class VectorClient {
public static boolean status;
public static VectorClient INSTANCE;
public static void load() {
PartnerAgentConfigManager configManager = (PartnerAgentConfigManager) AgentConfigManager.INSTANCE;
VectorConfig vectorConfig = configManager.getConfig().getVectorConfig();
int type = vectorConfig.getType();
try {
switch (type) {
case 0:
status = false;
break;
case 1:
status = true;
INSTANCE = new OllamaVectorClient(vectorConfig.getOllamaEmbeddingUrl(),
vectorConfig.getOllamaEmbeddingModel());
break;
case 2:
status = true;
INSTANCE = new OnnxVectorClient(vectorConfig.getTokenizerPath(),
vectorConfig.getEmbeddingModelPath());
break;
default:
throw new ServiceLoadFailedException(
"加载向量客户端失败! type: 0 -> 不启用语义缓存; type: 1 -> ollama; type: 2 -> ONNX RUNTIME");
}
log.info("向量客户端加载完毕");
} catch (VectorClientLoadFailedException | VectorClientExecuteException exception) {
status = false;
log.error("向量客户端加载失败", exception);
}
}
public float[] compute(String input) {
if (!status) {
return null;
}
return doCompute(input);
}
protected abstract float[] doCompute(String input);
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.max(0.05, Math.min(alpha, 0.5));
// 3⃣ 按比例混合旧向量与新向量
INDArray updated = primary.mul(1 - alpha).add(latest.mul(alpha));
// 4⃣ 归一化结果(保持方向空间一致)
updated = updated.div(updated.norm2Number());
return updated.toFloatVector();
}
}
}

View File

@@ -0,0 +1,15 @@
package work.slhaf.partner.common.vector.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class VectorClientExecuteException extends AgentRuntimeException {
public VectorClientExecuteException(String message) {
super(message);
}
public VectorClientExecuteException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,15 @@
package work.slhaf.partner.common.vector.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class VectorClientLoadFailedException extends AgentRuntimeException {
public VectorClientLoadFailedException(String message) {
super(message);
}
public VectorClientLoadFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,56 @@
package work.slhaf.partner.core;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager;
import work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated;
import work.slhaf.partner.api.chat.constant.ChatConstant;
import work.slhaf.partner.core.cognation.CognationCore;
import work.slhaf.partner.core.memory.MemoryCore;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import static work.slhaf.partner.common.util.ExtractUtil.extractUserId;
@Data
@Slf4j
@CoordinateManager
public class CoordinatedManager implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
//在框架将自动注入core,详见CapabilityRegistryFactory
private CognationCore cognationCore;
private MemoryCore memoryCore;
private boolean isCacheSingleUser() {
return memoryCore.getUserDialogMap().size() <= 1;
}
@Coordinated(capability = "cognation")
public boolean isSingleUser() {
return isCacheSingleUser() && isChatMessagesSingleUser();
}
private boolean isChatMessagesSingleUser() {
Set<String> userIdSet = new HashSet<>();
cognationCore.getChatMessages().forEach(m -> {
if (m.getRole().equals(ChatConstant.Character.ASSISTANT)) {
return;
}
String userId = extractUserId(m.getContent());
if (userId == null || userId.isEmpty()) {
return;
}
userIdSet.add(userId);
});
return userIdSet.size() <= 1;
}
}

View File

@@ -1,56 +0,0 @@
package work.slhaf.partner.core;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.common.exception_handler.GlobalExceptionHandler;
import work.slhaf.partner.common.exception_handler.pojo.GlobalException;
import work.slhaf.partner.core.interaction.agent_interface.TaskCallback;
import work.slhaf.partner.core.interaction.data.InteractionInputData;
import work.slhaf.partner.core.interaction.data.context.InteractionContext;
import work.slhaf.partner.core.interaction.module.InteractionFlow;
import work.slhaf.partner.core.interaction.module.InteractionModulesLoader;
import work.slhaf.partner.module.modules.process.PreprocessExecutor;
import java.io.IOException;
import java.util.List;
@Data
@Slf4j
public class InteractionHub {
private static volatile InteractionHub interactionHub;
@ToString.Exclude
private TaskCallback callback;
private List<InteractionFlow> interactionModules;
public static InteractionHub initialize() throws IOException {
if (interactionHub == null) {
synchronized (InteractionHub.class) {
if (interactionHub == null) {
interactionHub = new InteractionHub();
//加载模块
interactionHub.setInteractionModules(InteractionModulesLoader.getInstance().registerInteractionModules());
log.info("InteractionHub注册完毕...");
}
}
}
return interactionHub;
}
public void call(InteractionInputData inputData) throws IOException, ClassNotFoundException {
InteractionContext interactionContext = PreprocessExecutor.getInstance().execute(inputData);
try {
for (InteractionFlow interactionModule : interactionModules) {
interactionModule.execute(interactionContext);
}
} catch (GlobalException e) {
GlobalExceptionHandler.writeExceptionState(e);
interactionContext.getCoreResponse().put("text", "[ERROR] " + e.getMessage());
} finally {
callback.onTaskFinished(interactionContext.getUserInfo(), interactionContext.getCoreResponse().getString("text"));
interactionContext.clearUp();
}
}
}

View File

@@ -0,0 +1,94 @@
package work.slhaf.partner.core;
import cn.hutool.core.bean.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import work.slhaf.partner.api.agent.runtime.config.AgentConfigManager;
import work.slhaf.partner.api.common.entity.PersistableObject;
import work.slhaf.partner.common.config.PartnerAgentConfigManager;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import static work.slhaf.partner.common.Constant.Path.MEMORY_DATA;
@Slf4j
public abstract class PartnerCore<T extends PartnerCore<T>> extends PersistableObject {
private final String id = ((PartnerAgentConfigManager) AgentConfigManager.INSTANCE).getConfig().getAgentId();
public PartnerCore() throws IOException, ClassNotFoundException {
createStorageDirectory();
Path filePath = getFilePath(id);
if (Files.exists(filePath)) {
T deserialize = deserialize();
setupData(deserialize, (T) this);
} else {
FileUtils.createParentDirectories(filePath.toFile().getParentFile());
this.serialize();
}
setupHook(this);
log.info("[{}] 注册完毕", getCoreKey());
}
private void setupHook(PartnerCore<T> temp) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
temp.serialize();
log.info("[{}] 已保存", getCoreKey());
} catch (IOException e) {
log.error("[{}] 保存失败: ", getCoreKey(), e);
}
}));
}
private void setupData(T source, T current) {
BeanUtil.copyProperties(source, current);
}
public void serialize() throws IOException {
//先写入到临时文件,如果正常写入则覆盖原文件
Path filePath = getFilePath(id + "-temp");
Files.createDirectories(Path.of(MEMORY_DATA));
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()));
oos.writeObject(this);
oos.close();
Path path = getFilePath(id);
Files.move(filePath, path, StandardCopyOption.REPLACE_EXISTING);
log.info("[{}] 已保存到: {}", getCoreKey(), path);
} catch (IOException e) {
Files.delete(filePath);
log.error("[{}] 序列化保存失败: {}", getCoreKey(), e.getMessage());
}
}
private T deserialize() throws IOException, ClassNotFoundException {
Path filePath = getFilePath(id);
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filePath.toFile()))) {
T graph = (T) ois.readObject();
log.info("[{}] 已从文件加载: {}", getCoreKey(), filePath);
return graph;
}
}
private Path getFilePath(String s) {
return Paths.get(MEMORY_DATA, s + "-" + getCoreKey() + ".memory");
}
private void createStorageDirectory() {
try {
Files.createDirectories(Paths.get(MEMORY_DATA));
} catch (IOException e) {
log.error("[{}]创建存储目录失败: {}", getCoreKey(), e.getMessage());
}
}
protected abstract String getCoreKey();
}

View File

@@ -0,0 +1,59 @@
package work.slhaf.partner.core.action;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.api.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.core.action.entity.ActionData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.PhaserRecord;
import work.slhaf.partner.core.action.entity.cache.CacheAdjustData;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.module.modules.action.interventor.entity.MetaIntervention;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Phaser;
@Capability(value = "action")
public interface ActionCapability {
void putAction(@NonNull ActionData actionData);
Set<ActionData> listActions(@Nullable ActionData.ActionStatus actionStatus, @Nullable String source);
List<ActionData> popPendingAction(String userId);
List<ActionData> listPendingAction(String userId);
void putPendingActions(String userId, ActionData actionData);
List<String> selectTendencyCache(String input);
void updateTendencyCache(CacheAdjustData data);
ExecutorService getExecutor(ActionCore.ExecutorType type);
PhaserRecord putPhaserRecord(Phaser phaser, ActionData actionData);
void removePhaserRecord(Phaser phaser);
List<PhaserRecord> listPhaserRecords();
PhaserRecord getPhaserRecord(String tendency, String source);
MetaAction loadMetaAction(@NonNull String actionKey);
MetaActionInfo loadMetaActionInfo(@NonNull String actionKey);
Map<String, MetaActionInfo> listAvailableMetaActions();
boolean checkExists(String... actionKeys);
RunnerClient runnerClient();
void handleInterventions(List<MetaIntervention> interventions, ActionData data);
}

View File

@@ -0,0 +1,446 @@
package work.slhaf.partner.core.action;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.common.vector.VectorClient;
import work.slhaf.partner.core.PartnerCore;
import work.slhaf.partner.core.action.entity.ActionData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.PhaserRecord;
import work.slhaf.partner.core.action.entity.cache.ActionCacheData;
import work.slhaf.partner.core.action.entity.cache.CacheAdjustData;
import work.slhaf.partner.core.action.entity.cache.CacheAdjustMetaData;
import work.slhaf.partner.core.action.exception.ActionDataNotFoundException;
import work.slhaf.partner.core.action.exception.MetaActionNotFoundException;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.action.runner.SandboxRunnerClient;
import work.slhaf.partner.module.modules.action.interventor.entity.InterventionType;
import work.slhaf.partner.module.modules.action.interventor.entity.MetaIntervention;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@SuppressWarnings("FieldMayBeFinal")
@CapabilityCore(value = "action")
@Slf4j
public class ActionCore extends PartnerCore<ActionCore> {
/**
* 持久行动池
*/
private CopyOnWriteArraySet<ActionData> actionPool = new CopyOnWriteArraySet<>();
/**
* 待确认任务以userId区分不同用户因为需要跨请求确认
*/
private HashMap<String, List<ActionData>> pendingActions = new HashMap<>();
/**
* 语义缓存与行为倾向映射
*/
private List<ActionCacheData> actionCache = new ArrayList<>();
private final Lock cacheLock = new ReentrantLock();
// 由于当前的执行器逻辑实现,平台线程池大小不得小于 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 final List<PhaserRecord> phaserRecords = new ArrayList<>();
private RunnerClient runnerClient;
public ActionCore() throws IOException, ClassNotFoundException {
// TODO 通过 AgentConfigManager指定采用何种 runnerClient
runnerClient = new SandboxRunnerClient(existedMetaActions, virtualExecutor);
setupShutdownHook();
}
private void setupShutdownHook() {
// 将执行中的行动状态置为失败
val executingActionSet = listActions(ActionData.ActionStatus.EXECUTING, null);
for (ActionData actionData : executingActionSet) {
actionData.setStatus(ActionData.ActionStatus.FAILED);
actionData.setResult("由于系统中断而失败");
}
}
@CapabilityMethod
public void putAction(@NonNull ActionData actionData) {
actionPool.removeIf(data -> data.getUuid().equals(actionData.getUuid())); // 用来应对 ScheduledActionData 的重新排列
actionPool.add(actionData);
}
@CapabilityMethod
public Set<ActionData> listActions(@Nullable ActionData.ActionStatus actionStatus, @Nullable String source) {
return actionPool.stream()
.filter(actionData -> actionStatus == null || actionData.getStatus().equals(actionStatus))
.filter(actionData -> source == null || actionData.getSource().equals(source))
.collect(Collectors.toSet());
}
@CapabilityMethod
public synchronized void putPendingActions(String userId, ActionData actionData) {
pendingActions.computeIfAbsent(userId, k -> {
List<ActionData> temp = new ArrayList<>();
temp.add(actionData);
return temp;
});
}
@CapabilityMethod
public synchronized List<ActionData> popPendingAction(String userId) {
List<ActionData> infos = pendingActions.get(userId);
pendingActions.remove(userId);
return infos;
}
@CapabilityMethod
public List<ActionData> listPendingAction(String userId) {
return pendingActions.get(userId);
}
/**
* 计算输入内容的语义向量,根据与{@link ActionCacheData#getInputVector()}的相似度挑取缓存,后续将根据评估结果来更新计数
*
* @param input 本次输入内容
* @return 命中的行为倾向集合
*/
@CapabilityMethod
public List<String> selectTendencyCache(String input) {
if (!VectorClient.status) {
return null;
}
VectorClient vectorClient = VectorClient.INSTANCE;
// 计算本次输入的向量
float[] vector = vectorClient.compute(input);
if (vector == null)
return null;
// 与现有缓存比对,将匹配到的收集并返回
return actionCache.parallelStream()
.filter(ActionCacheData::isActivated)
.filter(data -> {
double compared = vectorClient.compare(vector, data.getInputVector());
return compared > data.getThreshold();
})
.map(ActionCacheData::getTendency)
.collect(Collectors.toList());
}
@CapabilityMethod
public void updateTendencyCache(CacheAdjustData data) {
VectorClient vectorClient = VectorClient.INSTANCE;
List<CacheAdjustMetaData> list = data.getMetaDataList();
String input = data.getInput();
float[] inputVector = vectorClient.compute(input);
List<CacheAdjustMetaData> matchAndPassed = new ArrayList<>();
List<CacheAdjustMetaData> matchNotPassed = new ArrayList<>();
List<CacheAdjustMetaData> notMatchPassed = new ArrayList<>();
for (CacheAdjustMetaData metaData : list) {
if (metaData.isHit() && metaData.isPassed()) {
matchAndPassed.add(metaData);
} else if (metaData.isHit()) {
matchNotPassed.add(metaData);
} else if (!metaData.isPassed()) {
notMatchPassed.add(metaData);
}
}
platformExecutor.execute(() -> adjustMatchAndPassed(matchAndPassed, inputVector, input, vectorClient));
platformExecutor.execute(() -> adjustMatchNotPassed(matchNotPassed, vectorClient));
platformExecutor.execute(() -> adjustNotMatchPassed(notMatchPassed, inputVector, input, vectorClient));
}
@CapabilityMethod
public ExecutorService getExecutor(ExecutorType type) {
return switch (type) {
case VIRTUAL -> virtualExecutor;
case PLATFORM -> platformExecutor;
};
}
@CapabilityMethod
public Map<String, MetaActionInfo> listAvailableActions() {
return existedMetaActions;
}
@CapabilityMethod
public synchronized PhaserRecord putPhaserRecord(Phaser phaser, ActionData actionData) {
PhaserRecord record = new PhaserRecord(phaser, actionData);
phaserRecords.add(record);
return record;
}
@CapabilityMethod
public synchronized void removePhaserRecord(Phaser phaser) {
PhaserRecord remove = null;
for (PhaserRecord record : phaserRecords) {
if (record.phaser().equals(phaser)) {
remove = record;
}
}
if (remove != null) {
phaserRecords.remove(remove);
}
}
@CapabilityMethod
public PhaserRecord getPhaserRecord(String tendency, String source) {
for (PhaserRecord record : phaserRecords) {
ActionData data = record.actionData();
if (data.getTendency().equals(tendency) && data.getSource().equals(source)) {
return record;
}
}
throw new ActionDataNotFoundException("未找到对应的 Phaser 记录: tendency=" + tendency + ", source=" + source);
}
@CapabilityMethod
public MetaAction loadMetaAction(@NonNull String actionKey) {
MetaActionInfo metaActionInfo = existedMetaActions.get(actionKey);
if (metaActionInfo == null) {
throw new MetaActionNotFoundException("未找到对应的行动程序信息" + actionKey);
}
String[] split = actionKey.split("::");
if (split.length < 2) {
throw new MetaActionNotFoundException("未找到对应的行动程序,原因: 传入的 actionKey(" + actionKey + ") 存在异常");
}
return new MetaAction(
split[1],
metaActionInfo.isIo(),
MetaAction.Type.MCP,
split[0]
);
}
@CapabilityMethod
public List<PhaserRecord> listPhaserRecords() {
return phaserRecords;
}
@CapabilityMethod
public MetaActionInfo loadMetaActionInfo(@NonNull String actionKey) {
MetaActionInfo info = existedMetaActions.get(actionKey);
if (info == null) {
throw new MetaActionNotFoundException("未找到对应的行动程序描述信息: " + actionKey);
}
return 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, ActionData actionData) {
// 加载数据
if (actionData == null) {
return;
}
// 加锁确保同步
synchronized (actionData.getStatus()) {
applyInterventions(interventions, actionData);
}
}
private void applyInterventions(List<MetaIntervention> interventions, ActionData actionData) {
boolean rebuildCleanTag = false;
interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder));
for (MetaIntervention intervention : interventions) {
List<MetaAction> actions = intervention.getActions()
.stream()
.map(this::loadMetaAction)
.toList();
switch (intervention.getType()) {
case InterventionType.APPEND -> handleAppend(actionData, intervention.getOrder(), actions);
case InterventionType.INSERT -> handleInsert(actionData, intervention.getOrder(), actions);
case InterventionType.DELETE -> handleDelete(actionData, intervention.getOrder(), actions);
case InterventionType.CANCEL -> handleCancel(actionData);
case InterventionType.REBUILD -> {
if (!rebuildCleanTag) {
cleanActionData(actionData);
rebuildCleanTag = true;
}
handleRebuild(actionData, intervention.getOrder(), actions);
}
}
}
}
/**
* 在未进入执行阶段的行动单元组新增新的行动
*/
private void handleAppend(ActionData actionData, int order, List<MetaAction> actions) {
if (order <= actionData.getExecutingStage())
return;
actionData.getActionChain().put(order, actions);
}
/**
* 在未进入执行阶段和正处于行动阶段的行动单元组插入新的行动
*/
private void handleInsert(ActionData actionData, int order, List<MetaAction> actions) {
if (order < actionData.getExecutingStage())
return;
actionData.getActionChain().computeIfAbsent(order, k -> new ArrayList<>()).addAll(actions);
}
private void handleDelete(ActionData actionData, int order, List<MetaAction> actions) {
if (order <= actionData.getExecutingStage())
return;
Map<Integer, List<MetaAction>> actionChain = actionData.getActionChain();
if (actionChain.containsKey(order)) {
actionChain.get(order).removeAll(actions);
if (actionChain.get(order).isEmpty()) {
actionChain.remove(order);
}
}
}
private void handleCancel(ActionData actionData) {
actionData.setStatus(ActionData.ActionStatus.FAILED);
actionData.setResult("行动取消");
}
private void handleRebuild(ActionData actionData, int order, List<MetaAction> actions) {
Map<Integer, List<MetaAction>> actionChain = actionData.getActionChain();
actionChain.put(order, actions);
}
private void cleanActionData(ActionData actionData) {
actionData.getActionChain().clear();
actionData.setExecutingStage(0);
actionData.setStatus(ActionData.ActionStatus.PREPARE);
actionData.getHistory().clear();
}
/**
* 命中缓存且评估通过时
*
* @param matchAndPassed 该类型的带调整缓存信息列表
* @param inputVector 本次输入内容的语义向量
* @param vectorClient 向量客户端
*/
private void adjustMatchAndPassed(List<CacheAdjustMetaData> matchAndPassed, float[] inputVector, String input,
VectorClient vectorClient) {
matchAndPassed.forEach(adjustData -> {
// 获取原始缓存条目
String tendency = adjustData.getTendency();
ActionCacheData primaryCacheData = selectCacheData(tendency);
if (primaryCacheData == null) {
return;
}
primaryCacheData.updateAfterMatchAndPassed(inputVector, vectorClient, input);
});
}
/**
* 针对命中缓存、但评估未通过的条目与输入进行处理
*
* @param matchNotPassed 该类型的带调整缓存信息列表
* @param vectorClient 向量客户端
*/
private void adjustMatchNotPassed(List<CacheAdjustMetaData> matchNotPassed, VectorClient vectorClient) {
List<ActionCacheData> toRemove = new ArrayList<>();
matchNotPassed.forEach(adjustData -> {
// 获取原始缓存条目
String tendency = adjustData.getTendency();
ActionCacheData primaryCacheData = selectCacheData(tendency);
if (primaryCacheData == null) {
return;
}
boolean remove = primaryCacheData.updateAfterMatchNotPassed(vectorClient);
if (remove) {
toRemove.add(primaryCacheData);
}
});
cacheLock.lock();
actionCache.removeAll(toRemove);
cacheLock.unlock();
}
/**
* 针对未命中但评估通过的缓存做出调整:
* <ol>
* <h3>如果存在缓存条目</h3>
* <li>
* 若已生效,但此时未匹配到则说明尚未生效或者阈值、向量{@link ActionCacheData#getInputVector()}存在问题,调低阈值,同时带权移动平均
* </li>
* <li>
* 若未生效,则只增加计数并带权移动平均
* </li>
* </ol>
* 如果不存在缓存条目,则新增并填充字段
*
* @param notMatchPassed 该类型的带调整缓存信息列表
* @param inputVector 本次输入内容的语义向量
* @param input 本次输入内容
* @param vectorClient 向量客户端
*/
private void adjustNotMatchPassed(List<CacheAdjustMetaData> notMatchPassed, float[] inputVector, String input,
VectorClient vectorClient) {
notMatchPassed.forEach(adjustData -> {
// 获取原始缓存条目
String tendency = adjustData.getTendency();
ActionCacheData primaryCacheData = selectCacheData(tendency);
float[] tendencyVector = vectorClient.compute(tendency);
if (primaryCacheData == null) {
actionCache.add(new ActionCacheData(tendency, tendencyVector, inputVector, input));
return;
}
primaryCacheData.updateAfterNotMatchPassed(input, inputVector, tendencyVector, vectorClient);
});
}
private ActionCacheData selectCacheData(String tendency) {
for (ActionCacheData actionCacheData : actionCache) {
if (actionCacheData.getTendency().equals(tendency)) {
return actionCacheData;
}
}
log.warn("[{}] 未找到行为倾向[{}]对应的缓存条目,可能是代码逻辑存在错误", getCoreKey(), tendency);
return null;
}
@Override
protected String getCoreKey() {
return "action-core";
}
public enum ExecutorType {
VIRTUAL, PLATFORM
}
}

View File

@@ -0,0 +1,143 @@
package work.slhaf.partner.core.action.entity
import work.slhaf.partner.module.modules.action.dispatcher.executor.entity.HistoryAction
import java.time.ZonedDateTime
import java.util.*
/**
* 行动模块传递的行动数据包含行动uuid、倾向、状态、行动链、结果、发起原因、行动描述等信息。
*/
sealed class ActionData {
/**
* 行动ID
*/
val uuid: String = UUID.randomUUID().toString()
/**
* 行动倾向
*/
abstract val tendency: String
/**
* 行动状态
*/
var status: ActionStatus = ActionStatus.PREPARE
/**
* 行动链
*/
abstract val actionChain: MutableMap<Int, MutableList<MetaAction>>
/**
* 行动阶段(当前阶段)
*/
var executingStage: Int = 0
/**
* 行动结果
*/
lateinit var result: String
val history: MutableMap<Int, MutableList<HistoryAction>> = mutableMapOf()
/**
* 修复上下文
*/
val additionalContext: MutableMap<Int, MutableList<String>> = mutableMapOf()
/**
* 行动原因
*/
abstract val reason: String
/**
* 行动描述
*/
abstract val description: String
/**
* 行动来源
*/
abstract val source: String
enum class ActionStatus {
/**
* 执行成功
*/
SUCCESS,
/**
* 执行失败
*/
FAILED,
/**
* 执行中
*/
EXECUTING,
/**
* 暂时中断
*/
INTERRUPTED,
/**
* 预备执行
*/
PREPARE
}
}
/**
* 计划行动数据类,继承自{@link ActionData},扩展了属性{@link ScheduledActionData#type}和{@link ScheduledActionData#scheduleContent},用于标识计划类型(单次还是周期性任务)和计划内容
*/
data class ScheduledActionData(
override val tendency: String,
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
override val reason: String,
override val description: String,
override val source: String,
val scheduleType: ScheduleType,
val scheduleContent: String,
) : ActionData() {
val scheduleHistories = ArrayList<ScheduleHistory>()
fun recordAndReset() {
val newHistory = ScheduleHistory(ZonedDateTime.now(), result, history.toMap())
scheduleHistories.add(newHistory)
additionalContext.clear()
executingStage = 0
for (entry in actionChain) {
for (action in entry.value) {
action.params.clear()
action.result.reset()
}
}
status = ActionStatus.PREPARE
}
enum class ScheduleType {
CYCLE,
ONCE
}
data class ScheduleHistory(
val endTime: ZonedDateTime,
val result: String,
val history: Map<Int, List<HistoryAction>>
)
}
/**
* 即时行动数据类
*/
data class ImmediateActionData(
override val tendency: String,
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
override val reason: String,
override val description: String,
override val source: String,
) : ActionData()

View File

@@ -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;
}

View File

@@ -0,0 +1,15 @@
package work.slhaf.partner.core.action.entity;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import java.util.List;
@Data
public class GeneratedData {
private List<String> dependencies;
private String code;
private String codeType;
private boolean serialize;
private JSONObject responseSchema;
}

View File

@@ -0,0 +1,5 @@
package work.slhaf.partner.core.action.entity;
public class McpData {
}

View File

@@ -0,0 +1,73 @@
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、ORIGIN 两种,前者对应读取到的 MCP Tool、后者对应生成的临时行动程序
*/
val type: Type,
/**
* 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id;
* 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串
*/
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
}
}

View File

@@ -0,0 +1,25 @@
package work.slhaf.partner.core.action.entity;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class MetaActionInfo {
private boolean io;
private Map<String, Object> params;
private String description;
private List<String> tags;
private List<String> preActions;
private List<String> postActions;
/**
* 是否严格依赖前置行动的成功执行若为true且前置行动失败则不执行该行动后置任务多为触发式。默认即执行。
*/
private boolean strictDependencies;
private JSONObject responseSchema;
}

View File

@@ -0,0 +1,33 @@
package work.slhaf.partner.core.action.entity;
import work.slhaf.partner.core.action.entity.ActionData.ActionStatus;
import java.util.concurrent.Phaser;
public record PhaserRecord(Phaser phaser, ActionData actionData) {
public void fail() {
actionData.setStatus(ActionStatus.FAILED);
}
/**
* 负责将 ActionData 的状态设置为 INTERRUPTED
* 同时循环检查进行阻塞
*/
public void interrupt() {
actionData.setStatus(ActionStatus.INTERRUPTED);
while (actionData().getStatus() == ActionStatus.INTERRUPTED) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
}
}
/**
* 将状态重新设置为 EXECUTING ,恢复 interrupt 阻塞状态
*/
public void complete() {
actionData().setStatus(ActionStatus.EXECUTING);
}
}

View File

@@ -0,0 +1,181 @@
package work.slhaf.partner.core.action.entity.cache;
import lombok.Data;
import work.slhaf.partner.common.vector.VectorClient;
import java.util.ArrayList;
import java.util.List;
@Data
public class ActionCacheData {
private boolean activated = false;
private int inputMatchCount = 1;
private float[] inputVector;
private float[] tendencyVector;
private String tendency;
private double threshold = 0.75;
private List<String> validSamples = new ArrayList<>();
private int failedCount = 0;
private Type type = Type.PRIMARY;
public ActionCacheData(String tendency, float[] tendencyVector, float[] inputVector, String input) {
this.tendency = tendency;
this.inputVector = inputVector;
this.tendencyVector = tendencyVector;
this.validSamples.add(input);
}
/**
* 命中缓存且评估通过时,根据输入内容的语义向量与现有的输入语义向量进行带权移动平均,以相似度为权重,同时降低失败计数,为零时置为上一级缓存类型{@link ActionCacheData.Type}
*
* @param inputVector 本次输入内容对应的语义向量
* @param vectorClient 向量客户端
* @param input 本次输入内容
*/
public synchronized void updateAfterMatchAndPassed(float[] inputVector, VectorClient vectorClient, String input) {
updateInputVector(inputVector, vectorClient);
addValidSample(input);
reduceFailedCount();
updateType();
addInputMatchCount();
}
private void updateType() {
if (this.failedCount == 0) {
this.type = switch (type) {
case PRIMARY, REBUILD_V1 -> ActionCacheData.Type.PRIMARY;
case REBUILD_V2 -> ActionCacheData.Type.REBUILD_V1;
case REBUILD_V3 -> ActionCacheData.Type.REBUILD_V2;
};
}
}
private void reduceFailedCount() {
this.failedCount = Math.max(this.failedCount - 1, 0);
}
private void addValidSample(String input) {
if (this.validSamples.size() == 12) {
this.validSamples.removeFirst();
}
this.validSamples.add(input);
}
private void updateInputVector(float[] inputVector, VectorClient vectorClient) {
this.inputVector = vectorClient.weightedAverage(inputVector, this.inputVector);
}
/**
* 针对命中缓存、但评估未通过的条目与输入进行处理: 增加失败计数(必要时重建并更新类型等级)、调高阈值(0.02),由于缓存匹配但评估未通过,所以不进行带权移动平均
*
* @param vectorClient 向量客户端
* @return 是否需要删除(已在REBUILD_V3状态且达到最大误判次数的)
*/
public synchronized boolean updateAfterMatchNotPassed(VectorClient vectorClient) {
adjustThreshold();
addFailedCount();
if (this.failedCount < 3) {
return false;
}
if (this.type == Type.REBUILD_V3) {
return true;
}
rebuildAndSwitchType(vectorClient);
return false;
}
private void rebuildAndSwitchType(VectorClient vectorClient) {
this.type = switch (this.type) {
case PRIMARY -> {
//样本顺序反转后,以全部样本重建
this.validSamples = this.validSamples.reversed();
rebuildWithSamples(vectorClient);
yield Type.REBUILD_V1;
}
case REBUILD_V1 -> {
//截取后一半样本,反转后以此重建
List<String> temp = this.validSamples.subList(this.validSamples.size() / 2, this.validSamples.size());
this.validSamples = temp.reversed();
rebuildWithSamples(vectorClient);
yield Type.REBUILD_V2;
}
case REBUILD_V2 -> {
//截取后四分之一样本,反转后以此重建
List<String> temp = this.validSamples.subList(this.validSamples.size() / 4, this.validSamples.size());
this.validSamples = temp.reversed();
rebuildWithSamples(vectorClient);
yield Type.REBUILD_V3;
}
case REBUILD_V3 -> null;
};
//阈值减0.05,防止重建后一直升高
this.threshold = Math.max(this.threshold - 0.05, 0.75);
this.failedCount = 0;
}
private void rebuildWithSamples(VectorClient vectorClient) {
for (int i = 0; i < this.validSamples.size(); i++) {
String sample = this.validSamples.get(i);
if (i == 0) {
this.inputVector = vectorClient.compute(sample);
} else {
float[] newSampleVector = vectorClient.compute(sample);
this.inputVector = vectorClient.weightedAverage(this.inputVector, newSampleVector);
}
}
}
private void addFailedCount() {
this.failedCount = Math.min(this.failedCount + 1, 3);
}
private void adjustThreshold() {
double newThreshold = this.threshold + 0.03;
this.threshold = Math.min(newThreshold, 0.95);
}
/**
* 针对未命中但评估通过的已存在缓存做出调整:
* <ol>
* <li>
* 若已生效,但此时未匹配到则说明阈值或者向量{@link ActionCacheData#getInputVector()}存在问题,调低阈值,同时带权移动平均
* </li>
* <li>
* 若未生效,则只增加计数并带权移动平均
* </li>
* </ol>
*
* @param input 本次输入内容
* @param inputVector 本次输入内容对应的语义向量
* @param tendencyVector 本次倾向对应的语义向量
* @param vectorClient 向量客户端
*/
public synchronized void updateAfterNotMatchPassed(String input, float[] inputVector, float[] tendencyVector, VectorClient vectorClient) {
if (this.activated) {
reduceThreshold();
this.inputVector = vectorClient.weightedAverage(inputVector, this.inputVector);
} else {
addValidSample(input);
this.tendencyVector = vectorClient.weightedAverage(tendencyVector, this.tendencyVector);
addInputMatchCount();
}
}
private void reduceThreshold() {
double newThreshold = this.threshold - 0.02;
this.threshold = Math.max(newThreshold, 0.75);
}
private void addInputMatchCount() {
this.inputMatchCount += 1;
if (inputMatchCount >= 6) {
this.activated = true;
}
}
public enum Type {
PRIMARY, REBUILD_V1, REBUILD_V2, REBUILD_V3
}
}

View File

@@ -0,0 +1,11 @@
package work.slhaf.partner.core.action.entity.cache;
import lombok.Data;
import java.util.List;
@Data
public class CacheAdjustData {
private String input;
private List<CacheAdjustMetaData> metaDataList;
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.partner.core.action.entity.cache;
import lombok.Data;
@Data
public class CacheAdjustMetaData {
private String tendency;
private boolean passed;
private boolean hit;
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class ActionDataNotFoundException extends AgentRuntimeException {
public ActionDataNotFoundException(String message) {
super(message);
}
public ActionDataNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentLaunchFailedException;
public class ActionInitFailedException extends AgentLaunchFailedException {
public ActionInitFailedException(String message, Throwable cause) {
super(message, cause);
}
public ActionInitFailedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class ActionLoadFailedException extends AgentRuntimeException {
public ActionLoadFailedException(String message) {
super(message);
}
public ActionLoadFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class ActionSerializeFailedException extends AgentRuntimeException {
public ActionSerializeFailedException(String message) {
super(message);
}
public ActionSerializeFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException;
public class MetaActionNotFoundException extends AgentRuntimeException {
public MetaActionNotFoundException(String message) {
super(message);
}
public MetaActionNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,107 @@
package work.slhaf.partner.core.action.runner;
import com.alibaba.fastjson2.JSONObject;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
import lombok.Data;
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.MetaAction.Result;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInitFailedException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import static work.slhaf.partner.common.Constant.Path.DATA;
import static work.slhaf.partner.common.util.PathUtil.buildPathStr;
/**
* 执行客户端抽象类
* <br/>
* 只负责暴露序列化、执行等相应接口,具体逻辑交给下游实现
* <br/>
* 默认存在两类实现,{@link LocalRunnerClient} 和 {@link SandboxRunnerClient}
* <ol>
* LocalRunnerClient:
* <li>
* 对应本地运行环境,可在本地启动 MCP 客户端将 RunnerClient 暴露的能力接口转发至本地 MCP Client 并执行
* </li>
* SandboxRunnerClient:
* <li>
* 对应沙盒运行环境,该 Client 仅作为沙盒环境的客户端,不持有额外能力,仅保持远端连接已存在行动的内容更新
* </li>
* </ol>
*/
@Slf4j
public abstract class RunnerClient {
protected final String ACTION_PATH;
protected final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
protected final ExecutorService executor;
//TODO 仍可提供内部 MCP但调用方式需要结合 AgentContext来获取否则生命周期不合
protected McpStatelessAsyncServer innerMcpServer;
/**
* ActionCore 将注入虚拟线程池
*/
public RunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @Nullable String baseActionPath) {
this.existedMetaActions = existedMetaActions;
this.executor = executor;
baseActionPath = baseActionPath == null ? DATA : baseActionPath;
this.ACTION_PATH = buildPathStr(baseActionPath, "action");
createPath(ACTION_PATH);
}
/**
* 执行行动程序
*/
public void submit(MetaAction metaAction) {
// 获取已存在行动列表
Result result = metaAction.getResult();
if (!result.getStatus().equals(Result.Status.WAITING)) {
return;
}
RunnerResponse response = doRun(metaAction);
result.setData(response.getData());
result.setStatus(response.isOk() ? Result.Status.SUCCESS : Result.Status.FAILED);
}
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 void createPath(String pathStr) {
val path = Path.of(pathStr);
try {
Files.createDirectory(path);
} catch (IOException e) {
if (!Files.exists(path)) {
throw new ActionInitFailedException("目录创建失败: " + pathStr, e);
}
}
}
/**
* 列出执行环境下的系统依赖情况
*/
public abstract JSONObject listSysDependencies();
@Data
public static class RunnerResponse {
private boolean ok;
private String data;
}
}

View File

@@ -0,0 +1,57 @@
package work.slhaf.partner.core.action.runner;
import com.alibaba.fastjson2.JSONObject;
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 null;
}
@Override
public JSONObject listSysDependencies() {
return 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'");
}
}

View File

@@ -0,0 +1,28 @@
package work.slhaf.partner.core.cognation;
import work.slhaf.partner.api.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.api.agent.factory.capability.annotation.ToCoordinated;
import work.slhaf.partner.api.chat.pojo.Message;
import work.slhaf.partner.api.chat.pojo.MetaMessage;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.locks.Lock;
@Capability("cognation")
public interface CognationCapability {
List<Message> getChatMessages();
void cleanMessage(List<Message> messages);
Lock getMessageLock();
void addMetaMessage(String userId, MetaMessage metaMessage);
List<Message> unpackAndClear(String userId);
void refreshMemoryId();
void resetLastUpdatedTime();
long getLastUpdatedTime();
HashMap<String,List<MetaMessage>> getSingleMetaMessageMap();
String getCurrentMemoryId();
@ToCoordinated
boolean isSingleUser();
}

View File

@@ -0,0 +1,116 @@
package work.slhaf.partner.core.cognation;
import com.alibaba.fastjson2.JSONObject;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.api.chat.pojo.Message;
import work.slhaf.partner.api.chat.pojo.MetaMessage;
import work.slhaf.partner.core.PartnerCore;
import java.io.IOException;
import java.io.Serial;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@EqualsAndHashCode(callSuper = true)
@Slf4j
@CapabilityCore(value = "cognation")
@Getter
@Setter
public class CognationCore extends PartnerCore<CognationCore> {
@Serial
private static final long serialVersionUID = 1L;
private final ReentrantLock messageLock = new ReentrantLock();
/**
* 主模型的聊天记录
*/
private List<Message> chatMessages = new ArrayList<>();
private HashMap<String /*startUserId*/, List<MetaMessage>> singleMetaMessageMap = new HashMap<>();
private String currentMemoryId;
private long lastUpdatedTime;
public CognationCore() throws IOException, ClassNotFoundException {
}
@CapabilityMethod
public List<Message> getChatMessages() {
return chatMessages;
}
@CapabilityMethod
public long getLastUpdatedTime(){
return lastUpdatedTime;
}
@CapabilityMethod
public HashMap<String,List<MetaMessage>> getSingleMetaMessageMap(){
return singleMetaMessageMap;
}
@CapabilityMethod
public String getCurrentMemoryId(){
return currentMemoryId;
}
@CapabilityMethod
public void cleanMessage(List<Message> messages) {
messageLock.lock();
this.getChatMessages().removeAll(messages);
messageLock.unlock();
}
@CapabilityMethod
public Lock getMessageLock() {
return messageLock;
}
@CapabilityMethod
public void addMetaMessage(String userId, MetaMessage metaMessage) {
log.debug("[{}] 当前会话历史: {}", getCoreKey(), JSONObject.toJSONString(singleMetaMessageMap));
if (singleMetaMessageMap.containsKey(userId)) {
singleMetaMessageMap.get(userId).add(metaMessage);
} else {
singleMetaMessageMap.put(userId, new java.util.ArrayList<>());
singleMetaMessageMap.get(userId).add(metaMessage);
}
log.debug("[{}] 会话历史更新: {}", getCoreKey(), JSONObject.toJSONString(singleMetaMessageMap));
}
@CapabilityMethod
public List<Message> unpackAndClear(String userId) {
List<Message> messages = new ArrayList<>();
for (MetaMessage metaMessage : singleMetaMessageMap.get(userId)) {
messages.add(metaMessage.getUserMessage());
messages.add(metaMessage.getAssistantMessage());
}
singleMetaMessageMap.remove(userId);
return messages;
}
@CapabilityMethod
public void refreshMemoryId() {
currentMemoryId = UUID.randomUUID().toString();
}
@CapabilityMethod
public void resetLastUpdatedTime() {
lastUpdatedTime = System.currentTimeMillis();
}
@Override
protected String getCoreKey() {
return "cognation-core";
}
}

Some files were not shown because too many files have changed in this diff Show More