Commit 22801802ac24cae2e2d5c7aecf8821952f7e1ce4

Authored by YunaiV
1 parent c8485a1d

【代码重构】AI:使用 OpenAiApi 接入星火

yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
1 package cn.iocoder.yudao.framework.ai.core.model.xinghuo; 1 package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
2 2
3 -import cn.hutool.core.collection.ListUtil;  
4 -import cn.hutool.core.lang.Assert; 3 +import lombok.RequiredArgsConstructor;
5 import lombok.extern.slf4j.Slf4j; 4 import lombok.extern.slf4j.Slf4j;
6 -import org.springframework.ai.chat.metadata.ChatGenerationMetadata;  
7 import org.springframework.ai.chat.model.ChatModel; 5 import org.springframework.ai.chat.model.ChatModel;
8 import org.springframework.ai.chat.model.ChatResponse; 6 import org.springframework.ai.chat.model.ChatResponse;
9 -import org.springframework.ai.chat.model.Generation;  
10 import org.springframework.ai.chat.prompt.ChatOptions; 7 import org.springframework.ai.chat.prompt.ChatOptions;
11 import org.springframework.ai.chat.prompt.Prompt; 8 import org.springframework.ai.chat.prompt.Prompt;
12 -import org.springframework.ai.model.ModelOptionsUtils;  
13 -import org.springframework.ai.openai.OpenAiChatOptions;  
14 -import org.springframework.ai.openai.api.OpenAiApi;  
15 -import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata;  
16 -import org.springframework.ai.retry.RetryUtils;  
17 -import org.springframework.http.ResponseEntity;  
18 -import org.springframework.retry.support.RetryTemplate; 9 +import org.springframework.ai.openai.OpenAiChatModel;
19 import reactor.core.publisher.Flux; 10 import reactor.core.publisher.Flux;
20 11
21 -import java.util.HashMap;  
22 -import java.util.List;  
23 -import java.util.Map;  
24 -  
25 -import static cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions.MODEL_DEFAULT;  
26 -  
27 /** 12 /**
28 * 讯飞星火 {@link ChatModel} 实现类 13 * 讯飞星火 {@link ChatModel} 实现类
29 * 14 *
30 * @author fansili 15 * @author fansili
31 */ 16 */
32 @Slf4j 17 @Slf4j
  18 +@RequiredArgsConstructor
33 public class XingHuoChatModel implements ChatModel { 19 public class XingHuoChatModel implements ChatModel {
34 20
35 - private static final String BASE_URL = "https://spark-api-open.xf-yun.com"; 21 + public static final String BASE_URL = "https://spark-api-open.xf-yun.com";
36 22
37 - private final XingHuoChatOptions defaultOptions;  
38 - private final RetryTemplate retryTemplate; 23 + public static final String MODEL_DEFAULT = "generalv3.5";
39 24
40 /** 25 /**
41 - * 星火兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本  
42 - *  
43 - * 不过要注意,星火没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 26 + * 兼容 OpenAI 接口,进行复用
44 */ 27 */
45 - private final OpenAiApi openAiApi;  
46 -  
47 - public XingHuoChatModel(String apiKey, String secretKey) {  
48 - this(apiKey, secretKey,  
49 - XingHuoChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build());  
50 - }  
51 -  
52 - public XingHuoChatModel(String apiKey, String secretKey, XingHuoChatOptions options) {  
53 - this(apiKey, secretKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);  
54 - }  
55 -  
56 - public XingHuoChatModel(String apiKey, String secretKey, XingHuoChatOptions options, RetryTemplate retryTemplate) {  
57 - Assert.notEmpty(apiKey, "apiKey 不能为空");  
58 - Assert.notEmpty(secretKey, "secretKey 不能为空");  
59 - Assert.notNull(options, "options 不能为空");  
60 - Assert.notNull(retryTemplate, "retryTemplate 不能为空");  
61 - this.openAiApi = new OpenAiApi(BASE_URL, apiKey + ":" + secretKey);  
62 - this.defaultOptions = options;  
63 - this.retryTemplate = retryTemplate;  
64 - } 28 + private final OpenAiChatModel openAiChatModel;
65 29
66 @Override 30 @Override
67 public ChatResponse call(Prompt prompt) { 31 public ChatResponse call(Prompt prompt) {
68 - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false);  
69 - return this.retryTemplate.execute(ctx -> {  
70 - // 1.1 发起调用  
71 - ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = openAiApi.chatCompletionEntity(request);  
72 - // 1.2 校验结果  
73 - OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();  
74 - if (chatCompletion == null) {  
75 - log.warn("No chat completion returned for prompt: {}", prompt);  
76 - return new ChatResponse(ListUtil.of());  
77 - }  
78 - List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();  
79 - if (choices == null) {  
80 - log.warn("No choices returned for prompt: {}", prompt);  
81 - return new ChatResponse(ListUtil.of());  
82 - }  
83 -  
84 - // 2. 转换 ChatResponse 返回  
85 - List<Generation> generations = choices.stream().map(choice -> {  
86 - Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice));  
87 - if (choice.finishReason() != null) {  
88 - generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));  
89 - }  
90 - return generation;  
91 - }).toList();  
92 - return new ChatResponse(generations,  
93 - OpenAiChatResponseMetadata.from(completionEntity.getBody()));  
94 - });  
95 - }  
96 -  
97 - private Map<String, Object> toMap(String id, OpenAiApi.ChatCompletion.Choice choice) {  
98 - Map<String, Object> map = new HashMap<>();  
99 - OpenAiApi.ChatCompletionMessage message = choice.message();  
100 - if (message.role() != null) {  
101 - map.put("role", message.role().name());  
102 - }  
103 - if (choice.finishReason() != null) {  
104 - map.put("finishReason", choice.finishReason().name());  
105 - }  
106 - map.put("id", id);  
107 - return map; 32 + return openAiChatModel.call(prompt);
108 } 33 }
109 34
110 @Override 35 @Override
111 public Flux<ChatResponse> stream(Prompt prompt) { 36 public Flux<ChatResponse> stream(Prompt prompt) {
112 - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true);  
113 - return this.retryTemplate.execute(ctx -> {  
114 - // 1. 发起调用  
115 - Flux<OpenAiApi.ChatCompletionChunk> response = this.openAiApi.chatCompletionStream(request);  
116 - return response.map(chatCompletion -> {  
117 - String id = chatCompletion.id();  
118 - // 2. 转换 ChatResponse 返回  
119 - List<Generation> generations = chatCompletion.choices().stream().map(choice -> {  
120 - String finish = (choice.finishReason() != null ? choice.finishReason().name() : "");  
121 - Generation generation = new Generation(choice.delta().content(),  
122 - Map.of("id", id, "role", choice.delta().role().name(), "finishReason", finish));  
123 - if (choice.finishReason() != null) {  
124 - generation = generation.withGenerationMetadata(  
125 - ChatGenerationMetadata.from(choice.finishReason().name(), null));  
126 - }  
127 - return generation;  
128 - }).toList();  
129 - return new ChatResponse(generations);  
130 - });  
131 - });  
132 - }  
133 -  
134 - OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {  
135 - // 1. 构建 ChatCompletionMessage 对象  
136 - List<OpenAiApi.ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m ->  
137 - new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList();  
138 - OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream);  
139 -  
140 - // 2.1 补充 prompt 内置的 options  
141 - if (prompt.getOptions() != null) {  
142 - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {  
143 - OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,  
144 - ChatOptions.class, OpenAiChatOptions.class);  
145 - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class);  
146 - } else {  
147 - throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "  
148 - + prompt.getOptions().getClass().getSimpleName());  
149 - }  
150 - }  
151 - // 2.2 补充默认 options  
152 - if (this.defaultOptions != null) {  
153 - request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class);  
154 - }  
155 - return request; 37 + return openAiChatModel.stream(prompt);
156 } 38 }
157 39
158 @Override 40 @Override
159 public ChatOptions getDefaultOptions() { 41 public ChatOptions getDefaultOptions() {
160 - return XingHuoChatOptions.fromOptions(defaultOptions); 42 + return openAiChatModel.getDefaultOptions();
161 } 43 }
162 44
163 } 45 }
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java
@@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 import org.springframework.ai.chat.messages.UserMessage; 8 import org.springframework.ai.chat.messages.UserMessage;
9 import org.springframework.ai.chat.model.ChatResponse; 9 import org.springframework.ai.chat.model.ChatResponse;
10 import org.springframework.ai.chat.prompt.Prompt; 10 import org.springframework.ai.chat.prompt.Prompt;
  11 +import org.springframework.ai.openai.OpenAiChatModel;
  12 +import org.springframework.ai.openai.OpenAiChatOptions;
  13 +import org.springframework.ai.openai.api.OpenAiApi;
11 import reactor.core.publisher.Flux; 14 import reactor.core.publisher.Flux;
12 15
13 import java.util.ArrayList; 16 import java.util.ArrayList;
@@ -20,9 +23,18 @@ import java.util.List; @@ -20,9 +23,18 @@ import java.util.List;
20 */ 23 */
21 public class XingHuoChatModelTests { 24 public class XingHuoChatModelTests {
22 25
23 - private final XingHuoChatModel chatModel = new XingHuoChatModel(  
24 - "cb6415c19d6162cda07b47316fcb0416",  
25 - "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"); 26 + private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
  27 + .openAiApi(OpenAiApi.builder()
  28 + .baseUrl(XingHuoChatModel.BASE_URL)
  29 + .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey
  30 + .build())
  31 + .defaultOptions(OpenAiChatOptions.builder()
  32 + .model("generalv3.5") // 模型
  33 + .temperature(0.7)
  34 + .build())
  35 + .build();
  36 +
  37 + private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel);
26 38
27 @Test 39 @Test
28 @Disabled 40 @Disabled