diff --git a/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts b/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts index 8867614b1..0876a7388 100644 --- a/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts +++ b/chat2db-client/src/blocks/Setting/AiSetting/aiTypeConfig.ts @@ -14,6 +14,7 @@ const AITypeName = { [AIType.TONGYIQIANWENAI]: i18n('setting.tab.aiType.tongyiqianwen'), [AIType.OPENAI]: 'Open AI', [AIType.AZUREAI]: 'Azure AI', + [AIType.MINIMAXAI]: 'MiniMax AI', [AIType.RESTAI]: i18n('setting.tab.custom'), }; @@ -53,6 +54,11 @@ const AIFormConfig: Record = { apiHost: true, model: true, }, + [AIType.MINIMAXAI]: { + apiKey: true, + apiHost: 'https://api.minimax.io/v1/chat/completions', + model: 'MiniMax-M2.7', + }, [AIType.RESTAI]: { apiKey: true, apiHost: true, diff --git a/chat2db-client/src/typings/ai.ts b/chat2db-client/src/typings/ai.ts index da79e4b7f..0ee56c4de 100644 --- a/chat2db-client/src/typings/ai.ts +++ b/chat2db-client/src/typings/ai.ts @@ -6,6 +6,7 @@ export enum AIType { TONGYIQIANWENAI='TONGYIQIANWENAI', OPENAI = 'OPENAI', AZUREAI = 'AZUREAI', + MINIMAXAI = 'MINIMAXAI', RESTAI = 'RESTAI', } diff --git a/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java b/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java index 6e8fd3ef7..b19656abb 100644 --- a/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java +++ b/chat2db-server/chat2db-server-domain/chat2db-server-domain-api/src/main/java/ai/chat2db/server/domain/api/enums/AiSqlSourceEnum.java @@ -62,6 +62,11 @@ public enum AiSqlSourceEnum implements BaseEnum { */ FASTCHATAI("FAST CHAT AI"), + /** + * MINIMAX AI + */ + MINIMAXAI("MINIMAX AI"), + ; final String description; diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java index 8010a1258..dffc15b67 100644 --- a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/ChatController.java @@ -33,6 +33,8 @@ import ai.chat2db.server.web.api.controller.ai.fastchat.listener.FastChatAIEventSourceListener; import ai.chat2db.server.web.api.controller.ai.fastchat.model.FastChatMessage; import ai.chat2db.server.web.api.controller.ai.fastchat.model.FastChatRole; +import ai.chat2db.server.web.api.controller.ai.minimax.client.MiniMaxAIClient; +import ai.chat2db.server.web.api.controller.ai.minimax.listener.MiniMaxAIEventSourceListener; import ai.chat2db.server.web.api.controller.ai.openai.client.OpenAIClient; import ai.chat2db.server.web.api.controller.ai.openai.listener.OpenAIEventSourceListener; import ai.chat2db.server.web.api.controller.ai.request.ChatQueryRequest; @@ -250,6 +252,8 @@ public SseEmitter distributeAISql(ChatQueryRequest queryRequest, SseEmitter sseE return chatWithTongyiChatAi(queryRequest, sseEmitter, uid); case ZHIPUAI: return chatWithZhipuChatAi(queryRequest, sseEmitter, uid); + case MINIMAXAI: + return chatWithMiniMaxAi(queryRequest, sseEmitter, uid); } return chatWithOpenAi(queryRequest, sseEmitter, uid); } @@ -454,6 +458,27 @@ private SseEmitter chatWithBaichuanAi(ChatQueryRequest queryRequest, SseEmitter return sseEmitter; } + /** + * chat with MiniMax AI + * + * @param queryRequest + * @param sseEmitter + * @param uid + * @return + * @throws IOException + */ + private SseEmitter chatWithMiniMaxAi(ChatQueryRequest queryRequest, SseEmitter sseEmitter, String uid) throws IOException { + String prompt = buildPrompt(queryRequest); + List messages = getFastChatMessage(uid, prompt); + + buildSseEmitter(sseEmitter, uid); + + MiniMaxAIEventSourceListener sourceListener = new MiniMaxAIEventSourceListener(sseEmitter); + MiniMaxAIClient.getInstance().streamCompletions(messages, sourceListener); + LocalCache.CACHE.put(uid, JSONUtil.toJsonStr(messages), LocalCache.TIMEOUT); + return sseEmitter; + } + /** * get fast chat message * diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/client/MiniMaxAIClient.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/client/MiniMaxAIClient.java new file mode 100644 index 000000000..f518d3fc6 --- /dev/null +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/client/MiniMaxAIClient.java @@ -0,0 +1,78 @@ +package ai.chat2db.server.web.api.controller.ai.minimax.client; + +import ai.chat2db.server.domain.api.model.Config; +import ai.chat2db.server.domain.api.service.ConfigService; +import ai.chat2db.server.web.api.util.ApplicationContextUtil; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * MiniMax AI client + * + * @author octo-patch + */ +@Slf4j +public class MiniMaxAIClient { + + /** + * MiniMax AI API Key + */ + public static final String MINIMAX_API_KEY = "minimax.ai.apiKey"; + + /** + * MiniMax AI API Host + */ + public static final String MINIMAX_HOST = "minimax.ai.host"; + + /** + * MiniMax AI Model + */ + public static final String MINIMAX_MODEL = "minimax.ai.model"; + + private static MiniMaxAIStreamClient MINIMAX_AI_STREAM_CLIENT; + + public static MiniMaxAIStreamClient getInstance() { + if (MINIMAX_AI_STREAM_CLIENT != null) { + return MINIMAX_AI_STREAM_CLIENT; + } else { + return singleton(); + } + } + + private static MiniMaxAIStreamClient singleton() { + if (MINIMAX_AI_STREAM_CLIENT == null) { + synchronized (MiniMaxAIClient.class) { + if (MINIMAX_AI_STREAM_CLIENT == null) { + refresh(); + } + } + } + return MINIMAX_AI_STREAM_CLIENT; + } + + /** + * Refresh client + */ + public static void refresh() { + String apiUrl = ""; + String apiKey = ""; + String model = ""; + ConfigService configService = ApplicationContextUtil.getBean(ConfigService.class); + Config apiHostConfig = configService.find(MINIMAX_HOST).getData(); + if (apiHostConfig != null && StringUtils.isNotBlank(apiHostConfig.getContent())) { + apiUrl = apiHostConfig.getContent(); + } + Config config = configService.find(MINIMAX_API_KEY).getData(); + if (config != null) { + apiKey = config.getContent(); + } + Config modelConfig = configService.find(MINIMAX_MODEL).getData(); + if (modelConfig != null && StringUtils.isNotBlank(modelConfig.getContent())) { + model = modelConfig.getContent(); + } + MINIMAX_AI_STREAM_CLIENT = MiniMaxAIStreamClient.builder().apiKey(apiKey).apiHost(apiUrl).model(model) + .build(); + } + +} diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/client/MiniMaxAIStreamClient.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/client/MiniMaxAIStreamClient.java new file mode 100644 index 000000000..9b099b85e --- /dev/null +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/client/MiniMaxAIStreamClient.java @@ -0,0 +1,169 @@ +package ai.chat2db.server.web.api.controller.ai.minimax.client; + +import ai.chat2db.server.tools.common.exception.ParamBusinessException; +import ai.chat2db.server.web.api.controller.ai.fastchat.interceptor.FastChatHeaderAuthorizationInterceptor; +import ai.chat2db.server.web.api.controller.ai.fastchat.model.FastChatCompletionsOptions; +import ai.chat2db.server.web.api.controller.ai.fastchat.model.FastChatMessage; +import cn.hutool.http.ContentType; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import okhttp3.sse.EventSources; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * MiniMax AI stream client + * + * @author octo-patch + */ +@Slf4j +public class MiniMaxAIStreamClient { + + private static final String DEFAULT_HOST = "https://api.minimax.io/v1/chat/completions"; + + private static final String DEFAULT_MODEL = "MiniMax-M2.7"; + + /** + * apikey + */ + @Getter + @NotNull + private String apiKey; + + /** + * apiHost + */ + @Getter + @NotNull + private String apiHost; + + /** + * model + */ + @Getter + private String model; + + /** + * okHttpClient + */ + @Getter + private OkHttpClient okHttpClient; + + /** + * Construct instance object + * + * @param builder + */ + public MiniMaxAIStreamClient(Builder builder) { + this.apiKey = builder.apiKey; + this.apiHost = StringUtils.isNotBlank(builder.apiHost) ? builder.apiHost : DEFAULT_HOST; + this.model = StringUtils.isNotBlank(builder.model) ? builder.model : DEFAULT_MODEL; + this.okHttpClient = new OkHttpClient + .Builder() + .addInterceptor(new FastChatHeaderAuthorizationInterceptor(this.apiKey)) + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(50, TimeUnit.SECONDS) + .readTimeout(50, TimeUnit.SECONDS) + .build(); + } + + /** + * builder + * + * @return + */ + public static MiniMaxAIStreamClient.Builder builder() { + return new MiniMaxAIStreamClient.Builder(); + } + + /** + * builder + */ + public static final class Builder { + private String apiKey; + + private String apiHost; + + private String model; + + private OkHttpClient okHttpClient; + + public Builder() { + } + + public MiniMaxAIStreamClient.Builder apiKey(String apiKeyValue) { + this.apiKey = apiKeyValue; + return this; + } + + public MiniMaxAIStreamClient.Builder apiHost(String apiHostValue) { + this.apiHost = apiHostValue; + return this; + } + + public MiniMaxAIStreamClient.Builder model(String modelValue) { + this.model = modelValue; + return this; + } + + public MiniMaxAIStreamClient.Builder okHttpClient(OkHttpClient val) { + this.okHttpClient = val; + return this; + } + + public MiniMaxAIStreamClient build() { + return new MiniMaxAIStreamClient(this); + } + } + + /** + * Stream completions + * + * @param chatMessages + * @param eventSourceListener + */ + public void streamCompletions(List chatMessages, EventSourceListener eventSourceListener) { + if (CollectionUtils.isEmpty(chatMessages)) { + log.error("param error: MiniMax AI Prompt cannot be empty"); + throw new ParamBusinessException("prompt"); + } + if (Objects.isNull(eventSourceListener)) { + log.error("param error: MiniMaxAIEventSourceListener cannot be empty"); + throw new ParamBusinessException(); + } + log.info("MiniMax AI, prompt:{}", chatMessages.get(chatMessages.size() - 1).getContent()); + try { + FastChatCompletionsOptions chatCompletionsOptions = new FastChatCompletionsOptions(chatMessages); + chatCompletionsOptions.setStream(true); + chatCompletionsOptions.setModel(this.model); + + EventSource.Factory factory = EventSources.createFactory(this.okHttpClient); + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + String requestBody = mapper.writeValueAsString(chatCompletionsOptions); + Request request = new Request.Builder() + .url(apiHost) + .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody)) + .build(); + EventSource eventSource = factory.newEventSource(request, eventSourceListener); + log.info("finish invoking MiniMax AI"); + } catch (Exception e) { + log.error("MiniMax AI error", e); + eventSourceListener.onFailure(null, e, null); + throw new ParamBusinessException(); + } + } +} diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/listener/MiniMaxAIEventSourceListener.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/listener/MiniMaxAIEventSourceListener.java new file mode 100644 index 000000000..89f05f6d9 --- /dev/null +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/listener/MiniMaxAIEventSourceListener.java @@ -0,0 +1,129 @@ +package ai.chat2db.server.web.api.controller.ai.minimax.listener; + +import java.io.IOException; +import java.util.Objects; + +import ai.chat2db.server.web.api.controller.ai.minimax.model.MiniMaxChatCompletions; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unfbx.chatgpt.entity.chat.Message; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * MiniMax AI EventSourceListener + * + * @author octo-patch + */ +@Slf4j +public class MiniMaxAIEventSourceListener extends EventSourceListener { + + private SseEmitter sseEmitter; + + public MiniMaxAIEventSourceListener(SseEmitter sseEmitter) { + this.sseEmitter = sseEmitter; + } + + private ObjectMapper mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + /** + * {@inheritDoc} + */ + @Override + public void onOpen(EventSource eventSource, Response response) { + log.info("MiniMax AI establishing sse connection..."); + } + + /** + * {@inheritDoc} + */ + @SneakyThrows + @Override + public void onEvent(EventSource eventSource, String id, String type, String data) { + log.info("MiniMax AI return data:{}", data); + String end = "[DONE]"; + if (data.equals(end)) { + log.info("MiniMax AI returns data finished"); + sseEmitter.send(SseEmitter.event() + .id(end) + .data(end) + .reconnectTime(3000)); + sseEmitter.complete(); + return; + } + Message message = new Message(); + if (StringUtils.isNotBlank(data)) { + MiniMaxChatCompletions chatCompletions = mapper.readValue(data, MiniMaxChatCompletions.class); + String text = chatCompletions.getChoices().get(0).getDelta() == null ? + chatCompletions.getChoices().get(0).getText() + : chatCompletions.getChoices().get(0).getDelta().getContent(); + message.setContent(text); + sseEmitter.send(SseEmitter.event() + .id(id) + .data(message) + .reconnectTime(3000)); + } + } + + @SneakyThrows + @Override + public void onClosed(EventSource eventSource) { + log.info("MiniMax AI close sse connection..."); + try { + sseEmitter.send(SseEmitter.event() + .id("[DONE]") + .data("[DONE]") + .reconnectTime(3000)); + } catch (IOException e) { + throw new RuntimeException(e); + } + sseEmitter.complete(); + } + + @Override + public void onFailure(EventSource eventSource, Throwable t, Response response) { + try { + if (Objects.isNull(response)) { + String message = t.getMessage(); + Message sseMessage = new Message(); + sseMessage.setContent(message); + sseEmitter.send(SseEmitter.event() + .id("[ERROR]") + .data(sseMessage)); + sseEmitter.send(SseEmitter.event() + .id("[DONE]") + .data("[DONE]")); + sseEmitter.complete(); + return; + } + ResponseBody body = response.body(); + String bodyString = null; + if (Objects.nonNull(body)) { + bodyString = body.string(); + log.error("MiniMax AI sse body error: {}, exception: {}", bodyString, t); + } else { + log.error("MiniMax AI sse response error: {}, exception: {}", response, t); + } + if (Objects.nonNull(eventSource)) { + eventSource.cancel(); + } + Message message = new Message(); + message.setContent("MiniMax AI Error:" + bodyString); + sseEmitter.send(SseEmitter.event() + .id("[ERROR]") + .data(message)); + sseEmitter.send(SseEmitter.event() + .id("[DONE]") + .data("[DONE]")); + sseEmitter.complete(); + } catch (Exception exception) { + log.error("Exception in sending data:", exception); + } + } +} diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/model/MiniMaxChatCompletions.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/model/MiniMaxChatCompletions.java new file mode 100644 index 000000000..516bcddd2 --- /dev/null +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/ai/minimax/model/MiniMaxChatCompletions.java @@ -0,0 +1,70 @@ +package ai.chat2db.server.web.api.controller.ai.minimax.model; + +import ai.chat2db.server.web.api.controller.ai.fastchat.model.FastChatChoice; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * MiniMax AI chat completions response + * + * @author octo-patch + */ +@Data +public class MiniMaxChatCompletions { + + /* + * A unique identifier associated with this chat completions response. + */ + private String id; + + /* + * The first timestamp associated with generation activity for this completions response, + * represented as seconds since the beginning of the Unix epoch of 00:00 on 1 Jan 1970. + */ + private int created; + + /** + * model + */ + private String model; + + /** + * object + */ + private String object; + + /* + * The collection of completions choices associated with this completions response. + */ + @JsonProperty(value = "choices") + private List choices; + + @JsonCreator + private MiniMaxChatCompletions( + @JsonProperty(value = "id") String id, + @JsonProperty(value = "created") int created, + @JsonProperty(value = "model") String model, + @JsonProperty(value = "object") String object, + @JsonProperty(value = "choices") List choices) { + this.id = id; + this.created = created; + this.model = model; + this.object = object; + this.choices = choices; + } + + public String getId() { + return this.id; + } + + public int getCreated() { + return this.created; + } + + public List getChoices() { + return this.choices; + } +} diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java index 0ab1daad6..914eaa7fd 100644 --- a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/config/ConfigController.java @@ -16,6 +16,7 @@ import ai.chat2db.server.web.api.controller.ai.baichuan.client.BaichuanAIClient; import ai.chat2db.server.web.api.controller.ai.chat2db.client.Chat2dbAIClient; import ai.chat2db.server.web.api.controller.ai.fastchat.client.FastChatAIClient; +import ai.chat2db.server.web.api.controller.ai.minimax.client.MiniMaxAIClient; import ai.chat2db.server.web.api.controller.ai.rest.client.RestAIClient; import ai.chat2db.server.web.api.controller.ai.tongyi.client.TongyiChatAIClient; import ai.chat2db.server.web.api.controller.ai.wenxin.client.WenxinAIClient; @@ -103,6 +104,9 @@ public ActionResult addChatGptSystemConfig(@RequestBody AIConfigCreateRequest re case ZHIPUAI: saveZhipuChatAIConfig(request); break; + case MINIMAXAI: + saveMiniMaxAIConfig(request); + break; } return ActionResult.isSuccess(); } @@ -275,6 +279,24 @@ private void saveBaichuanAIConfig(AIConfigCreateRequest request) { BaichuanAIClient.refresh(); } + /** + * save MiniMax AI config + * + * @param request + */ + private void saveMiniMaxAIConfig(AIConfigCreateRequest request) { + SystemConfigParam apikeyParam = SystemConfigParam.builder().code(MiniMaxAIClient.MINIMAX_API_KEY) + .content(request.getApiKey()).build(); + configService.createOrUpdate(apikeyParam); + SystemConfigParam apiHostParam = SystemConfigParam.builder().code(MiniMaxAIClient.MINIMAX_HOST) + .content(request.getApiHost()).build(); + configService.createOrUpdate(apiHostParam); + SystemConfigParam modelParam = SystemConfigParam.builder().code(MiniMaxAIClient.MINIMAX_MODEL) + .content(request.getModel()).build(); + configService.createOrUpdate(modelParam); + MiniMaxAIClient.refresh(); + } + @GetMapping("/system_config/{code}") public DataResult getSystemConfig(@PathVariable("code") String code) { DataResult result = configService.find(code); @@ -380,6 +402,14 @@ public DataResult getChatAiSystemConfig(String aiSqlSource) { config.setApiHost(Objects.nonNull(zhipuApiHost.getData()) ? zhipuApiHost.getData().getContent() : ""); config.setModel(Objects.nonNull(zhipuModel.getData()) ? zhipuModel.getData().getContent() : ""); break; + case MINIMAXAI: + DataResult minimaxApiKey = configService.find(MiniMaxAIClient.MINIMAX_API_KEY); + DataResult minimaxApiHost = configService.find(MiniMaxAIClient.MINIMAX_HOST); + DataResult minimaxModel = configService.find(MiniMaxAIClient.MINIMAX_MODEL); + config.setApiKey(Objects.nonNull(minimaxApiKey.getData()) ? minimaxApiKey.getData().getContent() : ""); + config.setApiHost(Objects.nonNull(minimaxApiHost.getData()) ? minimaxApiHost.getData().getContent() : ""); + config.setModel(Objects.nonNull(minimaxModel.getData()) ? minimaxModel.getData().getContent() : ""); + break; default: break; }