Skip to content

Commit faa1537

Browse files
authored
feat(jmanus): feat jmanus support memory (alibaba#2089)
* 记忆功能支持 * 优化 * 添加内存支持 * 添加页面选择记忆能力,待优化前后端交互数据 * 去除无效依赖 * 排除冲突依赖 * 优化页面展示 * 优化记忆能力 * 添加h2数据库内存支持 * 国际化支持 * 添加注释 * 优化页面图标 * 优化页面样式 * 去除中文 * 格式化代码 * 文件尾添加空行 * 去除无效依赖 * 优化页面展示 * 调整页面样式,优化功能 * 格式化和添加协议 * 去除无效依赖 * 优化记忆内容 * 优化前端写法 * 优化参数写法 * 前端编译后提交 * 记忆id构建从前端改为后端,优化重复任务会多次存储记忆问题 * 重新打包前端页面 * 优化home页对话失败问题 * 重新编译前端页面 * 优化页面显示 * 优化页面样式 * 优化样式,去除无用代码 * merge and build * 格式化代码
1 parent fb6a9c7 commit faa1537

File tree

67 files changed

+2281
-224
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2281
-224
lines changed

pom.xml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
<maven-checkstyle-plugin.failOnViolation>true</maven-checkstyle-plugin.failOnViolation>
3838
<puppycrawl-tools-checkstyle.version>9.3</puppycrawl-tools-checkstyle.version>
3939

40-
4140
</properties>
4241

4342
<dependencyManagement>
@@ -105,6 +104,10 @@
105104
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>
106105
<groupId>org.springframework.ai</groupId>
107106
</exclusion>
107+
<exclusion>
108+
<artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
109+
<groupId>org.springframework.ai</groupId>
110+
</exclusion>
108111
</exclusions>
109112
</dependency>
110113
<dependency>
@@ -248,10 +251,11 @@
248251
<artifactId>h2</artifactId>
249252
<scope>runtime</scope>
250253
</dependency>
254+
251255
<!-- MySQL Database -->
252256
<dependency>
253-
<groupId>mysql</groupId>
254-
<artifactId>mysql-connector-java</artifactId>
257+
<groupId>com.mysql</groupId>
258+
<artifactId>mysql-connector-j</artifactId>
255259
<version>8.0.33</version>
256260
<scope>runtime</scope>
257261
</dependency>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.cloud.ai.example.manus.config;
17+
18+
import com.alibaba.cloud.ai.example.manus.dynamic.memory.repository.H2ChatMemoryRepository;
19+
import com.alibaba.cloud.ai.example.manus.dynamic.memory.repository.MysqlChatMemoryRepository;
20+
import com.alibaba.cloud.ai.example.manus.dynamic.memory.repository.PostgresChatMemoryRepository;
21+
import org.springframework.ai.chat.memory.ChatMemory;
22+
import org.springframework.ai.chat.memory.ChatMemoryRepository;
23+
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
24+
import org.springframework.beans.factory.annotation.Value;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.jdbc.core.JdbcTemplate;
28+
29+
/**
30+
* @author dahua
31+
* @time 2025/8/5
32+
* @desc memory config for manus
33+
*/
34+
@Configuration
35+
public class MemoryConfig {
36+
37+
// import memory auto configuration
38+
// jmanus only support memory for mysql and postgresql now
39+
@Value("${spring.ai.memory.mysql.enabled:false}")
40+
private boolean mysqlEnabled;
41+
42+
@Value("${spring.ai.memory.postgres.enabled:false}")
43+
private boolean postgresEnabled;
44+
45+
@Value("${spring.ai.memory.h2.enabled:false}")
46+
private boolean h2Enabled;
47+
48+
@Bean
49+
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
50+
return MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).build();
51+
}
52+
53+
@Bean
54+
public ChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate) {
55+
ChatMemoryRepository chatMemoryRepository = null;
56+
if (mysqlEnabled) {
57+
chatMemoryRepository = MysqlChatMemoryRepository.mysqlBuilder().jdbcTemplate(jdbcTemplate).build();
58+
}
59+
else if (postgresEnabled) {
60+
chatMemoryRepository = PostgresChatMemoryRepository.postgresBuilder().jdbcTemplate(jdbcTemplate).build();
61+
}
62+
else if (h2Enabled) {
63+
chatMemoryRepository = H2ChatMemoryRepository.h2Builder().jdbcTemplate(jdbcTemplate).build();
64+
}
65+
if (chatMemoryRepository == null) {
66+
throw new RuntimeException("Please enable mysql or postgres or h2 memory");
67+
}
68+
return chatMemoryRepository;
69+
}
70+
71+
}

src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/agent/service/AgentService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public interface AgentService {
3939
* Create and return a usable BaseAgent object, similar to the
4040
* createPlanningCoordinator method in PlanningFactory
4141
* @param name Agent name
42-
* @param planId Plan ID, used to identify the plan the agent belongs to
42+
* @param currentPlanId Plan ID, used to identify the plan the agent belongs to
4343
* @return Created BaseAgent object
4444
*/
4545
BaseAgent createDynamicBaseAgent(String name, String currentPlanId, String rootPlanId,
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.cloud.ai.example.manus.dynamic.memory.advisor;
17+
18+
import org.springframework.ai.chat.client.ChatClientMessageAggregator;
19+
import org.springframework.ai.chat.client.ChatClientRequest;
20+
import org.springframework.ai.chat.client.ChatClientResponse;
21+
import org.springframework.ai.chat.client.advisor.api.*;
22+
import org.springframework.ai.chat.memory.ChatMemory;
23+
import org.springframework.ai.chat.messages.Message;
24+
import org.springframework.ai.chat.messages.UserMessage;
25+
import org.springframework.util.Assert;
26+
import reactor.core.publisher.Flux;
27+
import reactor.core.publisher.Mono;
28+
import reactor.core.scheduler.Scheduler;
29+
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
33+
/**
34+
* 2025/8/7 auth: dahua
35+
*/
36+
public class CustomMessageChatMemoryAdvisor implements BaseChatMemoryAdvisor {
37+
38+
private final ChatMemory chatMemory;
39+
40+
private final String defaultConversationId;
41+
42+
private final int order;
43+
44+
private final Scheduler scheduler;
45+
46+
private String userRequest;
47+
48+
private AdvisorType advisorType;
49+
50+
private CustomMessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int order,
51+
Scheduler scheduler, String userRequest, AdvisorType advisorType) {
52+
Assert.notNull(chatMemory, "chatMemory cannot be null");
53+
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
54+
Assert.notNull(scheduler, "scheduler cannot be null");
55+
this.chatMemory = chatMemory;
56+
this.defaultConversationId = defaultConversationId;
57+
this.order = order;
58+
this.scheduler = scheduler;
59+
this.userRequest = userRequest;
60+
this.advisorType = advisorType;
61+
}
62+
63+
@Override
64+
public int getOrder() {
65+
return this.order;
66+
}
67+
68+
@Override
69+
public Scheduler getScheduler() {
70+
return this.scheduler;
71+
}
72+
73+
@Override
74+
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
75+
String conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId);
76+
77+
// 1. Retrieve the chat memory for the current conversation.
78+
List<Message> memoryMessages = this.chatMemory.get(conversationId);
79+
80+
// 2. Advise the request messages list.
81+
List<Message> processedMessages = new ArrayList<>(memoryMessages);
82+
processedMessages.addAll(chatClientRequest.prompt().getInstructions());
83+
84+
// 3. Create a new request with the advised messages.
85+
ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
86+
.prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build())
87+
.build();
88+
89+
if (this.advisorType == AdvisorType.AFTER) {
90+
return processedChatClientRequest;
91+
}
92+
93+
// 4. Add the new user message to the conversation memory.
94+
UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();
95+
96+
// 5. The user's question needs to be re-placed here because the default message
97+
// body contains template information,
98+
// which can be misleading when displayed on the front end.
99+
// Re-placement of the user's actual question does not affect the model's ability
100+
// to process new requests after loading memory and can also reduce token
101+
// consumption in the model.
102+
UserMessage customMessage = UserMessage.builder()
103+
.text(userRequest)
104+
.media(userMessage.getMedia())
105+
.metadata(userMessage.getMetadata())
106+
.build();
107+
this.chatMemory.add(conversationId, customMessage);
108+
109+
return processedChatClientRequest;
110+
}
111+
112+
@Override
113+
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
114+
if (this.advisorType == AdvisorType.BEFORE) {
115+
return chatClientResponse;
116+
}
117+
List<Message> assistantMessages = new ArrayList<>();
118+
if (chatClientResponse.chatResponse() != null) {
119+
assistantMessages = chatClientResponse.chatResponse()
120+
.getResults()
121+
.stream()
122+
.map(g -> (Message) g.getOutput())
123+
.toList();
124+
}
125+
this.chatMemory.add(this.getConversationId(chatClientResponse.context(), this.defaultConversationId),
126+
assistantMessages);
127+
return chatClientResponse;
128+
}
129+
130+
@Override
131+
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
132+
StreamAdvisorChain streamAdvisorChain) {
133+
// Get the scheduler from BaseAdvisor
134+
Scheduler scheduler = this.getScheduler();
135+
136+
// Process the request with the before method
137+
return Mono.just(chatClientRequest)
138+
.publishOn(scheduler)
139+
.map(request -> this.before(request, streamAdvisorChain))
140+
.flatMapMany(streamAdvisorChain::nextStream)
141+
.transform(flux -> new ChatClientMessageAggregator().aggregateChatClientResponse(flux,
142+
response -> this.after(response, streamAdvisorChain)));
143+
}
144+
145+
public static CustomMessageChatMemoryAdvisor.Builder builder(ChatMemory chatMemory, String userRequest,
146+
AdvisorType advisorType) {
147+
return new CustomMessageChatMemoryAdvisor.Builder(chatMemory, userRequest, advisorType);
148+
}
149+
150+
public static final class Builder {
151+
152+
private String conversationId = ChatMemory.DEFAULT_CONVERSATION_ID;
153+
154+
private int order = Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER;
155+
156+
private Scheduler scheduler = BaseAdvisor.DEFAULT_SCHEDULER;
157+
158+
private ChatMemory chatMemory;
159+
160+
private String userRequest;
161+
162+
private AdvisorType advisorType;
163+
164+
private Builder(ChatMemory chatMemory, String userRequest, AdvisorType advisorType) {
165+
this.chatMemory = chatMemory;
166+
this.userRequest = userRequest;
167+
this.advisorType = advisorType;
168+
}
169+
170+
/**
171+
* Set the conversation id.
172+
* @param conversationId the conversation id
173+
* @return the builder
174+
*/
175+
public CustomMessageChatMemoryAdvisor.Builder conversationId(String conversationId) {
176+
this.conversationId = conversationId;
177+
return this;
178+
}
179+
180+
/**
181+
* Set the order.
182+
* @param order the order
183+
* @return the builder
184+
*/
185+
public CustomMessageChatMemoryAdvisor.Builder order(int order) {
186+
this.order = order;
187+
return this;
188+
}
189+
190+
public CustomMessageChatMemoryAdvisor.Builder scheduler(Scheduler scheduler) {
191+
this.scheduler = scheduler;
192+
return this;
193+
}
194+
195+
/**
196+
* Build the advisor.
197+
* @return the advisor
198+
*/
199+
public CustomMessageChatMemoryAdvisor build() {
200+
return new CustomMessageChatMemoryAdvisor(this.chatMemory, this.conversationId, this.order, this.scheduler,
201+
this.userRequest, this.advisorType);
202+
}
203+
204+
}
205+
206+
public enum AdvisorType {
207+
208+
BEFORE, AFTER, ALL
209+
210+
}
211+
212+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.cloud.ai.example.manus.dynamic.memory.controller;
17+
18+
import com.alibaba.cloud.ai.example.manus.dynamic.memory.entity.MemoryEntity;
19+
import com.alibaba.cloud.ai.example.manus.dynamic.memory.service.MemoryService;
20+
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.web.bind.annotation.*;
23+
24+
import java.util.List;
25+
26+
/**
27+
* @author dahua
28+
* @time 2025/8/5
29+
* @desc memory controller
30+
*/
31+
@RestController
32+
@RequestMapping("/api/memories")
33+
@CrossOrigin(origins = "*") // Add cross-origin support
34+
public class MemoryController {
35+
36+
@Autowired
37+
private MemoryService memoryService;
38+
39+
@GetMapping
40+
public ResponseEntity<List<MemoryEntity>> getAllModels() {
41+
return ResponseEntity.ok(memoryService.getMemories());
42+
}
43+
44+
@GetMapping("/single")
45+
public ResponseEntity<MemoryEntity> singleMemory(String memoryId) {
46+
return ResponseEntity.ok(memoryService.singleMemory(memoryId));
47+
}
48+
49+
@PostMapping("/update")
50+
public ResponseEntity<MemoryEntity> updateMemory(@RequestBody MemoryEntity memoryEntity) {
51+
return ResponseEntity.ok(memoryService.updateMemory(memoryEntity));
52+
}
53+
54+
@GetMapping("/delete")
55+
public ResponseEntity<Void> deleteMemory(String memoryId) {
56+
memoryService.deleteMemory(memoryId);
57+
return ResponseEntity.ok().build();
58+
}
59+
60+
}

0 commit comments

Comments
 (0)