Skip to content

Commit f0ee091

Browse files
feat(jmanus): support refresh agent client and config headers (alibaba#1631)
* 添加通过页面配置刷新模型连接能力,添加配置模型时配置header功能 * 优化代码格式 * 添加license * 补充空格 * 优化agent动态刷新 * 优化client刷新功能 * 修复错误提交 * 合并代码,优化泛型 * 补充一下注释 * 格式化代码 * 修复 DynamicModelEntity 中 allowChange 字段的默认值设置,更新 MapToStringConverter 增加空字符串检查 * 添加无权限操作提示 * 添加非页面操作模型失败提示 * 格式化代码 --------- Co-authored-by: rainerWJY <answeropensource@alibabacloud.com>
1 parent ce5e2a0 commit f0ee091

File tree

17 files changed

+640
-59
lines changed

17 files changed

+640
-59
lines changed

spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/agent/DynamicAgent.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,7 @@ private boolean executeWithRetry(int maxRetries) throws Exception {
190190
chatClient = llmService.getAgentChatClient();
191191
}
192192
else {
193-
chatClient = llmService.getDynamicChatClient(model.getBaseUrl(), model.getApiKey(),
194-
model.getModelName());
193+
chatClient = llmService.getDynamicChatClient(model);
195194
}
196195
// Use streaming response handler for better user experience and content
197196
// merging

spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/model/controller/ModelController.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ public ResponseEntity<ModelConfig> createModel(@RequestBody ModelConfig modelCon
5252
@PutMapping("/{id}")
5353
public ResponseEntity<ModelConfig> updateModel(@PathVariable("id") Long id, @RequestBody ModelConfig modelConfig) {
5454
modelConfig.setId(id);
55-
return ResponseEntity.ok(modelService.updateModel(modelConfig));
55+
try {
56+
return ResponseEntity.ok(modelService.updateModel(modelConfig));
57+
}
58+
catch (UnsupportedOperationException e) {
59+
return ResponseEntity.status(499).build();
60+
}
5661
}
5762

5863
@DeleteMapping("/{id}")
@@ -64,6 +69,9 @@ public ResponseEntity<Void> deleteModel(@PathVariable("id") String id) {
6469
catch (IllegalArgumentException e) {
6570
return ResponseEntity.badRequest().build();
6671
}
72+
catch (UnsupportedOperationException e) {
73+
return ResponseEntity.status(499).build();
74+
}
6775
}
6876

6977
@GetMapping("/types")

spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/model/entity/DynamicModelEntity.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import jakarta.persistence.*;
2121

2222
import java.util.List;
23+
import java.util.Map;
2324

2425
@Entity
2526
@Table(name = "dynamic_models")
@@ -35,6 +36,10 @@ public class DynamicModelEntity {
3536
@Column(nullable = false)
3637
private String apiKey;
3738

39+
@Convert(converter = MapToStringConverter.class)
40+
@Column(columnDefinition = "VARCHAR")
41+
private Map<String, String> headers;
42+
3843
@Column(nullable = false)
3944
private String modelName;
4045

@@ -44,6 +49,9 @@ public class DynamicModelEntity {
4449
@Column(nullable = false)
4550
private String type;
4651

52+
@Column(nullable = false, columnDefinition = "boolean default false")
53+
private boolean allowChange;
54+
4755
@OneToMany(mappedBy = "model")
4856
private List<DynamicAgentEntity> agents;
4957

@@ -111,9 +119,26 @@ public void setAgents(List<DynamicAgentEntity> agents) {
111119
this.agents = agents;
112120
}
113121

122+
public Map<String, String> getHeaders() {
123+
return headers;
124+
}
125+
126+
public void setHeaders(Map<String, String> headers) {
127+
this.headers = headers;
128+
}
129+
130+
public Boolean isAllowChange() {
131+
return allowChange;
132+
}
133+
134+
public void setAllowChange(Boolean allowChange) {
135+
this.allowChange = allowChange;
136+
}
137+
114138
public ModelConfig mapToModelConfig() {
115139
ModelConfig config = new ModelConfig();
116140
config.setId(this.getId());
141+
config.setHeaders(this.getHeaders());
117142
config.setBaseUrl(this.getBaseUrl());
118143
config.setApiKey(maskValue(this.getApiKey()));
119144
config.setModelName(this.getModelName());
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.model.entity;
17+
18+
import com.fasterxml.jackson.core.type.TypeReference;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import jakarta.persistence.AttributeConverter;
21+
import jakarta.persistence.Converter;
22+
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
/**
27+
* @author dahua
28+
* @date 2025/7/12 13:02
29+
*/
30+
@Converter
31+
public class MapToStringConverter implements AttributeConverter<Map<String, String>, String> {
32+
33+
private final ObjectMapper objectMapper = new ObjectMapper();
34+
35+
@Override
36+
public String convertToDatabaseColumn(Map<String, String> attribute) {
37+
try {
38+
return objectMapper.writeValueAsString(attribute);
39+
}
40+
catch (Exception e) {
41+
throw new IllegalArgumentException("Error converting map to string", e);
42+
}
43+
}
44+
45+
@Override
46+
public Map<String, String> convertToEntityAttribute(String dbData) {
47+
// 增加一个 null 或空字符串的检查
48+
if (dbData == null || dbData.isEmpty()) {
49+
// 返回一个空的 Map 或者 null,根据业务逻辑决定
50+
return new HashMap<>();
51+
}
52+
try {
53+
return objectMapper.readValue(dbData, new TypeReference<>() {
54+
});
55+
}
56+
catch (Exception e) {
57+
throw new IllegalArgumentException("Error converting string to map", e);
58+
}
59+
}
60+
61+
}

spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/model/model/vo/ModelConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.alibaba.cloud.ai.example.manus.dynamic.model.model.vo;
1717

18+
import java.util.Map;
19+
1820
/**
1921
* @author lizhenning
2022
* @date 2025/7/8
@@ -27,6 +29,8 @@ public class ModelConfig {
2729

2830
private String apiKey;
2931

32+
private Map<String, String> headers;
33+
3034
private String modelName;
3135

3236
private String modelDescription;
@@ -81,4 +85,12 @@ public void setType(String type) {
8185
this.type = type;
8286
}
8387

88+
public Map<String, String> getHeaders() {
89+
return headers;
90+
}
91+
92+
public void setHeaders(Map<String, String> headers) {
93+
this.headers = headers;
94+
}
95+
8496
}

spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/model/service/ModelDataInitialization.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,44 @@
1818
import com.alibaba.cloud.ai.example.manus.dynamic.model.entity.DynamicModelEntity;
1919
import com.alibaba.cloud.ai.example.manus.dynamic.model.model.enums.ModelType;
2020
import com.alibaba.cloud.ai.example.manus.dynamic.model.repository.DynamicModelRepository;
21+
import com.alibaba.cloud.ai.example.manus.event.JmanusEventPublisher;
22+
import com.alibaba.cloud.ai.example.manus.event.ModelChangeEvent;
23+
import com.alibaba.cloud.ai.example.manus.llm.LlmService;
2124
import jakarta.annotation.PostConstruct;
25+
import org.springframework.ai.openai.OpenAiChatModel;
26+
import org.springframework.ai.openai.OpenAiChatOptions;
27+
import org.springframework.beans.factory.annotation.Autowired;
2228
import org.springframework.beans.factory.annotation.Value;
2329
import org.springframework.stereotype.Service;
2430

31+
import java.util.Map;
32+
2533
/**
2634
* @author lizhenning
2735
* @date 2025/7/8
2836
*/
2937
@Service
3038
public class ModelDataInitialization implements IModelDataInitialization {
3139

40+
@Value("${spring.ai.openai.api-key}")
41+
private String openAiApiKey;
42+
3243
@Value("${spring.ai.openai.base-url}")
3344
private String baseUrl;
3445

35-
@Value("${spring.ai.openai.api-key}")
36-
private String apiKey;
37-
3846
@Value("${spring.ai.openai.chat.options.model}")
3947
private String model;
4048

49+
@Autowired
50+
private OpenAiChatModel openAiChatModel;
51+
52+
// 为了保障llmService先初始化
53+
@Autowired
54+
private LlmService llmService;
55+
56+
@Autowired
57+
private JmanusEventPublisher jmanusEventPublisher;
58+
4159
private final DynamicModelRepository repository;
4260

4361
public ModelDataInitialization(DynamicModelRepository repository) {
@@ -46,15 +64,27 @@ public ModelDataInitialization(DynamicModelRepository repository) {
4664

4765
@PostConstruct
4866
public void init() {
49-
if (repository.count() == 0) {
50-
DynamicModelEntity dynamicModelEntity = new DynamicModelEntity();
51-
dynamicModelEntity.setBaseUrl(baseUrl);
52-
dynamicModelEntity.setApiKey(apiKey);
53-
dynamicModelEntity.setModelName(model);
54-
dynamicModelEntity.setModelDescription("base model");
55-
dynamicModelEntity.setType(ModelType.GENERAL.name());
56-
repository.save(dynamicModelEntity);
67+
68+
OpenAiChatOptions defaultOptions = (OpenAiChatOptions) openAiChatModel.getDefaultOptions();
69+
// 保持固定id,每次启动配置的模型都将覆盖存储
70+
DynamicModelEntity dynamicModelEntity = new DynamicModelEntity();
71+
dynamicModelEntity.setBaseUrl(baseUrl);
72+
Map<String, String> httpHeaders = defaultOptions.getHttpHeaders();
73+
if (httpHeaders.isEmpty()) {
74+
httpHeaders = null;
5775
}
76+
dynamicModelEntity.setHeaders(httpHeaders);
77+
dynamicModelEntity.setApiKey(openAiApiKey);
78+
dynamicModelEntity.setModelName(model);
79+
dynamicModelEntity.setModelDescription("base model");
80+
dynamicModelEntity.setType(ModelType.GENERAL.name());
81+
DynamicModelEntity existingModel = repository.findByModelName(model);
82+
if (existingModel != null) {
83+
dynamicModelEntity.setId(existingModel.getId());
84+
}
85+
DynamicModelEntity save = repository.save(dynamicModelEntity);
86+
jmanusEventPublisher.publish(new ModelChangeEvent(save));
87+
5888
}
5989

6090
}

spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/model/service/ModelServiceImpl.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import com.alibaba.cloud.ai.example.manus.dynamic.model.entity.DynamicModelEntity;
2222
import com.alibaba.cloud.ai.example.manus.dynamic.model.model.vo.ModelConfig;
2323
import com.alibaba.cloud.ai.example.manus.dynamic.model.repository.DynamicModelRepository;
24+
import com.alibaba.cloud.ai.example.manus.event.JmanusEventPublisher;
25+
import com.alibaba.cloud.ai.example.manus.event.ModelChangeEvent;
2426
import org.slf4j.Logger;
2527
import org.slf4j.LoggerFactory;
2628
import org.springframework.beans.factory.annotation.Autowired;
@@ -38,6 +40,9 @@ public class ModelServiceImpl implements ModelService {
3840

3941
private final DynamicAgentRepository agentRepository;
4042

43+
@Autowired
44+
private JmanusEventPublisher publisher;
45+
4146
@Autowired
4247
public ModelServiceImpl(DynamicModelRepository repository, DynamicAgentRepository agentRepository) {
4348
this.repository = repository;
@@ -63,13 +68,14 @@ public ModelConfig createModel(ModelConfig config) {
6368
DynamicModelEntity existingModel = repository.findByModelName(config.getModelName());
6469
if (existingModel != null) {
6570
log.info("Found Model with same name: {}, updating Model", config.getModelName());
66-
config.setId(existingModel.getId());
67-
return updateModel(config);
71+
return updateModel(existingModel);
6872
}
6973

7074
DynamicModelEntity entity = new DynamicModelEntity();
75+
entity.setAllowChange(true);
7176
updateEntityFromConfig(entity, config);
7277
entity = repository.save(entity);
78+
publisher.publish(new ModelChangeEvent(entity));
7379
log.info("Successfully created new Model: {}", config.getModelName());
7480
return entity.mapToModelConfig();
7581
}
@@ -94,26 +100,44 @@ public ModelConfig updateModel(ModelConfig config) {
94100
DynamicModelEntity entity = repository.findById(config.getId())
95101
.orElseThrow(() -> new IllegalArgumentException("Model not found: " + config.getId()));
96102
updateEntityFromConfig(entity, config);
103+
return updateModel(entity);
104+
}
105+
106+
public ModelConfig updateModel(DynamicModelEntity entity) {
107+
// 如果不允许修改,则返回原有数据
108+
if (!entity.isAllowChange()) {
109+
throw new UnsupportedOperationException("Not supported yet.");
110+
}
97111
entity = repository.save(entity);
112+
publisher.publish(new ModelChangeEvent(entity));
98113
return entity.mapToModelConfig();
99114
}
100115

101116
@Override
102117
public void deleteModel(String id) {
103-
List<DynamicAgentEntity> allByModel = agentRepository
104-
.findAllByModel(new DynamicModelEntity(Long.parseLong(id)));
105-
if (allByModel != null && !allByModel.isEmpty()) {
106-
allByModel.forEach(dynamicAgentEntity -> dynamicAgentEntity.setModel(null));
107-
agentRepository.saveAll(allByModel);
118+
DynamicModelEntity entity = repository.findById(Long.parseLong(id))
119+
.orElseThrow(() -> new IllegalArgumentException("Model not found: " + id));
120+
// 如果不允许修改,则返回原有数据
121+
if (entity.isAllowChange()) {
122+
List<DynamicAgentEntity> allByModel = agentRepository
123+
.findAllByModel(new DynamicModelEntity(Long.parseLong(id)));
124+
if (allByModel != null && !allByModel.isEmpty()) {
125+
allByModel.forEach(dynamicAgentEntity -> dynamicAgentEntity.setModel(null));
126+
agentRepository.saveAll(allByModel);
127+
}
128+
repository.deleteById(Long.parseLong(id));
129+
}
130+
else {
131+
throw new UnsupportedOperationException("Not supported yet.");
108132
}
109-
repository.deleteById(Long.parseLong(id));
110133
}
111134

112135
private void updateEntityFromConfig(DynamicModelEntity entity, ModelConfig config) {
113136
if (StrUtil.isNotBlank(config.getApiKey()) && !config.getApiKey().contains("*")) {
114137
entity.setApiKey(config.getApiKey());
115138
}
116139
entity.setBaseUrl(config.getBaseUrl());
140+
entity.setHeaders(config.getHeaders());
117141
entity.setModelName(config.getModelName());
118142
entity.setModelDescription(config.getModelDescription());
119143
entity.setType(config.getType());
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.event;
17+
18+
/**
19+
* @author dahua
20+
* @time 2025/7/15
21+
* @desc jmanus事件接口
22+
*/
23+
public interface JmanusEvent {
24+
25+
}

0 commit comments

Comments
 (0)