mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-07-24 16:05:08 +08:00
Merge remote-tracking branch 'origin/master-jdk21-ai' into master-jdk21-ai
This commit is contained in:
@ -23,7 +23,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
@ -79,9 +78,8 @@ public class AiChatConversationController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// TODO 芋艿:这个 url 可以改下
|
||||
@DeleteMapping("/delete-my-all-except-pinned")
|
||||
@Operation(summary = "删除所有对话(置顶除外)")
|
||||
@DeleteMapping("/delete-by-unpinned")
|
||||
@Operation(summary = "删除未置顶的聊天对话")
|
||||
public CommonResult<Boolean> deleteChatConversationMyByUnpinned() {
|
||||
chatConversationService.deleteChatConversationMyByUnpinned(getLoginUserId());
|
||||
return success(true);
|
||||
|
@ -1,15 +1,13 @@
|
||||
|
||||
### chat call
|
||||
POST {{baseUrl}}/admin-api/ai/chat/message/send
|
||||
### 发送消息(段式)
|
||||
POST {{baseUrl}}/ai/chat/message/send
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
{
|
||||
"conversationId": "1781604279872581649",
|
||||
"conversationId": "1781604279872581724",
|
||||
"content": "你是 OpenAI 么?"
|
||||
}
|
||||
|
||||
|
||||
### 发送消息(流式)
|
||||
POST {{baseUrl}}/ai/chat/message/send-stream
|
||||
Content-Type: application/json
|
||||
@ -20,11 +18,10 @@ Authorization: {{token}}
|
||||
"content": "1+1=?"
|
||||
}
|
||||
|
||||
### message list
|
||||
GET {{baseUrl}}/admin-api/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
|
||||
### 获得指定对话的消息列表
|
||||
GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
|
||||
Authorization: {{token}}
|
||||
|
||||
|
||||
### message delete
|
||||
DELETE {{baseUrl}}/admin-api/ai/chat/message/delete?id=50
|
||||
Authorization: {{token}}
|
||||
### 删除消息
|
||||
DELETE {{baseUrl}}/ai/chat/message/delete?id=50
|
||||
Authorization: {{token}}
|
@ -21,10 +21,10 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@ -51,14 +51,14 @@ public class AiChatMessageController {
|
||||
|
||||
@Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢")
|
||||
@PostMapping("/send")
|
||||
public CommonResult<AiChatMessageRespVO> sendMessage(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
|
||||
return success(chatMessageService.sendMessage(sendReqVO));
|
||||
public CommonResult<AiChatMessageSendRespVO> sendMessage(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
|
||||
return success(chatMessageService.sendMessage(sendReqVO, getLoginUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
|
||||
@PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
|
||||
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
|
||||
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
|
||||
return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ public class AiChatMessageRespVO {
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo")
|
||||
private String model; // 参见 AiOpenAiModelEnum 枚举类
|
||||
private String model;
|
||||
|
||||
@Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
|
||||
private Long modelId;
|
||||
|
@ -31,14 +31,6 @@ public class AiChatMessageSendRespVO {
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
// ========== 扩展字段 ==========
|
||||
|
||||
@Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://iocoder.cn/1.png")
|
||||
private String userAvatar;
|
||||
|
||||
@Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://iocoder.cn/2.png")
|
||||
private String roleAvatar;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
### 生成图片:OpenAI(DALL)
|
||||
POST {{baseUrl}}/ai/image/draw
|
||||
Content-Type: application/json
|
||||
@ -29,8 +28,7 @@ Authorization: {{token}}
|
||||
"style": "vivid"
|
||||
}
|
||||
|
||||
### 生成图片:生成图片
|
||||
|
||||
### 生成图片:生成图片(Midjourney)
|
||||
POST {{baseUrl}}/ai/image/midjourney/imagine
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
@ -40,6 +38,5 @@ Authorization: {{token}}
|
||||
"model": "midjourney",
|
||||
"width": "1",
|
||||
"height": "1",
|
||||
"version": "6.0",
|
||||
"base64Array": []
|
||||
"version": "6.0"
|
||||
}
|
@ -68,7 +68,7 @@ public class AiImageController {
|
||||
|
||||
@Operation(summary = "生成图片")
|
||||
@PostMapping("/draw")
|
||||
public CommonResult<Long> drawImage(@Validated @RequestBody AiImageDrawReqVO drawReqVO) {
|
||||
public CommonResult<Long> drawImage(@Valid @RequestBody AiImageDrawReqVO drawReqVO) {
|
||||
return success(imageService.drawImage(getLoginUserId(), drawReqVO));
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ public class AiImageController {
|
||||
|
||||
@Operation(summary = "【Midjourney】生成图片")
|
||||
@PostMapping("/midjourney/imagine")
|
||||
public CommonResult<Long> midjourneyImagine(@Validated @RequestBody AiMidjourneyImagineReqVO reqVO) {
|
||||
public CommonResult<Long> midjourneyImagine(@Valid @RequestBody AiMidjourneyImagineReqVO reqVO) {
|
||||
Long imageId = imageService.midjourneyImagine(getLoginUserId(), reqVO);
|
||||
return success(imageId);
|
||||
}
|
||||
@ -92,14 +92,14 @@ public class AiImageController {
|
||||
@Operation(summary = "【Midjourney】通知图片进展", description = "由 Midjourney Proxy 回调")
|
||||
@PostMapping("/midjourney/notify") // 必须是 POST 方法,否则会报错
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> midjourneyNotify(@Validated @RequestBody MidjourneyApi.Notify notify) {
|
||||
public CommonResult<Boolean> midjourneyNotify(@Valid @RequestBody MidjourneyApi.Notify notify) {
|
||||
imageService.midjourneyNotify(notify);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "【Midjourney】Action 操作(二次生成图片)", description = "例如说:放大、缩小、U1、U2 等")
|
||||
@PostMapping("/midjourney/action")
|
||||
public CommonResult<Long> midjourneyAction(@Validated @RequestBody AiMidjourneyActionReqVO reqVO) {
|
||||
public CommonResult<Long> midjourneyAction(@Valid @RequestBody AiMidjourneyActionReqVO reqVO) {
|
||||
Long imageId = imageService.midjourneyAction(getLoginUserId(), reqVO);
|
||||
return success(imageId);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ Authorization: {{token}}
|
||||
{
|
||||
"platform": "Suno",
|
||||
"generateMode": 2,
|
||||
"prompt": "周末啦!",
|
||||
"prompt": "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。",
|
||||
"model": "chirp-v3.5",
|
||||
"tags": ["Happy"],
|
||||
"title": "Happy Song"
|
||||
@ -21,6 +21,6 @@ Authorization: {{token}}
|
||||
"platform": "Suno",
|
||||
"generateMode": 1,
|
||||
"model": "chirp-v3.5",
|
||||
"gptDescriptionPrompt": "今天是星球六,结果是个下雨天,希望心情很美丽",
|
||||
"prompt": "happy music",
|
||||
"makeInstrumental": false
|
||||
}
|
@ -46,7 +46,7 @@ public class AiSunoGenerateReqVO {
|
||||
|
||||
@Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
|
||||
@NotEmpty(message = "模型不能为空")
|
||||
private String model; // 参见 AiModelEnum 枚举
|
||||
private String model;
|
||||
|
||||
@Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
|
||||
private List<String> tags;
|
||||
|
@ -26,9 +26,10 @@ public class AiWriteController {
|
||||
private AiWriteService writeService;
|
||||
|
||||
@PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@PermitAll
|
||||
@Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
|
||||
@PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
|
||||
public Flux<CommonResult<String>> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) {
|
||||
return writeService.generateWriteContent(generateReqVO, getLoginUserId());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,11 +14,10 @@ public class AiWriteGenerateReqVO {
|
||||
@InEnum(AiWriteTypeEnum.class)
|
||||
private Integer type;
|
||||
|
||||
// TODO @xin:如果非必填,可以不用写 requiredMode
|
||||
@Schema(description = "写作内容提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1.撰写:田忌赛马;2.回复:不批")
|
||||
@Schema(description = "写作内容提示", example = "1.撰写:田忌赛马;2.回复:不批")
|
||||
private String prompt;
|
||||
|
||||
@Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "领导我要辞职")
|
||||
@Schema(description = "原文", example = "领导我要辞职")
|
||||
private String originalContent;
|
||||
|
||||
@Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiModelEnum;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@ -73,8 +72,6 @@ public class AiChatConversationDO extends BaseDO {
|
||||
private Long modelId;
|
||||
/**
|
||||
* 模型标志
|
||||
*
|
||||
* 枚举 {@link AiModelEnum}
|
||||
*/
|
||||
private String model;
|
||||
|
||||
|
@ -5,7 +5,6 @@ import org.springframework.ai.chat.messages.MessageType;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiModelEnum;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
@ -69,8 +68,6 @@ public class AiChatMessageDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 模型标志
|
||||
*
|
||||
* 枚举 {@link AiModelEnum}
|
||||
*/
|
||||
private String model;
|
||||
/**
|
||||
|
@ -2,8 +2,9 @@ package cn.iocoder.yudao.module.ai.service.chat;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.*;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@ -22,9 +23,10 @@ public interface AiChatMessageService {
|
||||
* 发送消息
|
||||
*
|
||||
* @param sendReqVO 发送信息
|
||||
* @param userId 用户编号
|
||||
* @return 发送结果
|
||||
*/
|
||||
AiChatMessageRespVO sendMessage(AiChatMessageSendReqVO sendReqVO);
|
||||
AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
|
@ -4,35 +4,28 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
|
||||
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
|
||||
import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.qianfan.QianFanChatOptions;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
@ -64,47 +57,37 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
@Resource
|
||||
private AiChatModelService chatModalService;
|
||||
@Resource
|
||||
private AiChatRoleService chatRoleService;
|
||||
@Resource
|
||||
private AiApiKeyService apiKeyService;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AiChatMessageRespVO sendMessage(AiChatMessageSendReqVO req) {
|
||||
return null; // TODO 芋艿:一起改
|
||||
// Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// // 查询对话
|
||||
// AiChatConversationDO conversation = chatConversationService.validateExists(req.getConversationId());
|
||||
// // 获取对话模型
|
||||
// AiChatModelDO chatModel = chatModalService.validateChatModel(conversation.getModelId());
|
||||
// // 获取角色信息
|
||||
// AiChatRoleDO chatRoleDO = conversation.getRoleId() != null ? chatRoleService.validateChatRole(conversation.getRoleId()) : null;
|
||||
// // 获取 client 类型
|
||||
// AiPlatformEnum platformEnum = AiPlatformEnum.validatePlatform(chatModel.getPlatform());
|
||||
// // 保存 chat message
|
||||
// createChatMessage(conversation.getId(), MessageType.USER, loginUserId, conversation.getRoleId(),
|
||||
// chatModel.getModel(), chatModel.getId(), req.getContent());
|
||||
// String content = null;
|
||||
// int tokens = 0;
|
||||
// try {
|
||||
// // 创建 chat 需要的 Prompt
|
||||
// Prompt prompt = new Prompt(req.getContent());
|
||||
// // TODO @芋艿 @范 看要不要支持这些
|
||||
//// req.setTopK(req.getTopK());
|
||||
//// req.setTopP(req.getTopP());
|
||||
//// req.setTemperature(req.getTemperature());
|
||||
// // 发送 call 调用
|
||||
// ChatClient chatClient = chatClientFactory.getChatClient(platformEnum);
|
||||
// ChatResponse call = chatClient.call(prompt);
|
||||
// content = call.getResult().getOutput().getContent();
|
||||
// // 更新 conversation
|
||||
// } catch (Exception e) {
|
||||
// content = ExceptionUtil.getMessage(e);
|
||||
// } finally {
|
||||
// // 保存 chat message
|
||||
// createChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
|
||||
// chatModel.getModel(), chatModel.getId(), content);
|
||||
// }
|
||||
// return new AiChatMessageRespVO().setContent(content);
|
||||
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
|
||||
// 1.1 校验对话存在
|
||||
AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId());
|
||||
if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
|
||||
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
|
||||
}
|
||||
List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
|
||||
// 1.2 校验模型
|
||||
AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
|
||||
ChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
|
||||
|
||||
// 2. 插入 user 发送消息
|
||||
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
|
||||
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext());
|
||||
|
||||
// 3.1 插入 assistant 接收消息
|
||||
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
|
||||
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
|
||||
|
||||
// 3.2 创建 chat 需要的 Prompt
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
|
||||
ChatResponse chatResponse = chatClient.call(prompt);
|
||||
|
||||
// 3.3 段式返回
|
||||
String newContent = chatResponse.getResult().getOutput().getContent();
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
|
||||
return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
|
||||
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -112,14 +95,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
// 1.1 校验对话存在
|
||||
AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId());
|
||||
if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
|
||||
throw exception(CHAT_CONVERSATION_NOT_EXISTS); // TODO 芋艿:异常情况的对接;
|
||||
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
|
||||
}
|
||||
List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
|
||||
// 1.2 校验模型
|
||||
AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
|
||||
StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
|
||||
// 1.3 获取用户头像、角色头像
|
||||
AiChatRoleDO role = conversation.getRoleId() != null ? chatRoleService.getChatRole(conversation.getRoleId()) : null;
|
||||
StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
|
||||
|
||||
// 2. 插入 user 发送消息
|
||||
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
|
||||
@ -149,9 +130,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
// TODO @芋艿:失败的情况下,要不要删除消息
|
||||
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()));
|
||||
}).onErrorResume(error -> {
|
||||
return Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR));
|
||||
});
|
||||
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
|
||||
}
|
||||
|
||||
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
|
||||
@ -164,46 +143,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
}
|
||||
// 1.2 history message 历史消息
|
||||
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
|
||||
contextMessages.forEach(message -> {
|
||||
// TODO @芋艿:看看有没优化空间
|
||||
if (MessageType.USER.getValue().equals(message.getType())) {
|
||||
chatMessages.add(new UserMessage(message.getContent()));
|
||||
} else {
|
||||
chatMessages.add(new AssistantMessage(message.getContent()));
|
||||
}
|
||||
});
|
||||
contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
|
||||
// 1.3 user message 新发送消息
|
||||
chatMessages.add(new UserMessage(sendReqVO.getContent()));
|
||||
|
||||
// 2. 构建 ChatOptions 对象
|
||||
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
|
||||
ChatOptions chatOptions = buildChatOptions(platform, model.getModel(),
|
||||
ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
|
||||
conversation.getTemperature(), conversation.getMaxTokens());
|
||||
return new Prompt(chatMessages, chatOptions);
|
||||
}
|
||||
|
||||
private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
|
||||
Float temperatureF = temperature != null ? temperature.floatValue() : null;
|
||||
//noinspection EnhancedSwitchMigration
|
||||
switch (platform) {
|
||||
case OPENAI:
|
||||
return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
case OLLAMA:
|
||||
return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
|
||||
case YI_YAN:
|
||||
// TODO 芋艿:貌似 model 只要一设置,就报错
|
||||
// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
case XING_HUO:
|
||||
return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
|
||||
.setMaxTokens(maxTokens);
|
||||
case QIAN_WEN:
|
||||
return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
|
||||
default:
|
||||
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从历史消息中,获得倒序的 n 组消息作为消息上下文
|
||||
*
|
||||
|
@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageR
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.image.ImageModel;
|
||||
|
||||
import java.util.List;
|
||||
@ -76,12 +76,12 @@ public interface AiApiKeyService {
|
||||
// ========== 与 spring-ai 集成 ==========
|
||||
|
||||
/**
|
||||
* 获得 StreamingChatClient 对象
|
||||
* 获得 ChatModel 对象
|
||||
*
|
||||
* @param id 编号
|
||||
* @return StreamingChatClient 对象
|
||||
* @return ChatModel 对象
|
||||
*/
|
||||
StreamingChatModel getStreamingChatClient(Long id);
|
||||
ChatModel getChatClient(Long id);
|
||||
|
||||
/**
|
||||
* 获得 ImageClient 对象
|
||||
|
@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.image.ImageModel;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@ -98,10 +98,10 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
|
||||
// ========== 与 spring-ai 集成 ==========
|
||||
|
||||
@Override
|
||||
public StreamingChatModel getStreamingChatClient(Long id) {
|
||||
public ChatModel getChatClient(Long id) {
|
||||
AiApiKeyDO apiKey = validateApiKey(id);
|
||||
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
|
||||
return clientFactory.getOrCreateStreamingChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
|
||||
return clientFactory.getOrCreateChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@ -49,10 +50,10 @@ public class AiMusicServiceImpl implements AiMusicService {
|
||||
private FileApi fileApi;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
|
||||
// 1. 调用 Suno 生成音乐
|
||||
SunoApi sunoApi = apiKeyService.getSunoApi();
|
||||
// TODO 芋艿:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的 --xin:大部分ok的,补充了error_message
|
||||
List<SunoApi.MusicData> musicDataList;
|
||||
if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
|
||||
// 1.1 描述模式
|
||||
@ -164,14 +165,9 @@ public class AiMusicServiceImpl implements AiMusicService {
|
||||
*/
|
||||
private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
|
||||
return convertList(musicList, musicData -> {
|
||||
Integer status;
|
||||
if (Objects.equals("complete", musicData.status())) {
|
||||
status = AiMusicStatusEnum.SUCCESS.getStatus();
|
||||
} else if (Objects.equals("error", musicData.status())) {
|
||||
status = AiMusicStatusEnum.FAIL.getStatus();
|
||||
} else {
|
||||
status = AiMusicStatusEnum.IN_PROGRESS.getStatus();
|
||||
}
|
||||
Integer status = Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus()
|
||||
: Objects.equals("error", musicData.status()) ? AiMusicStatusEnum.FAIL.getStatus()
|
||||
: AiMusicStatusEnum.IN_PROGRESS.getStatus();
|
||||
return new AiMusicDO()
|
||||
.setTaskId(musicData.id()).setModel(musicData.modelName())
|
||||
.setDescription(musicData.gptDescriptionPrompt())
|
||||
|
@ -2,28 +2,25 @@ package cn.iocoder.yudao.module.ai.service.write;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
|
||||
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.DictTypeConstants;
|
||||
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ai.enums.write.*;
|
||||
import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
|
||||
import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
|
||||
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.qianfan.QianFanChatOptions;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@ -45,25 +42,30 @@ public class AiWriteServiceImpl implements AiWriteService {
|
||||
private AiApiKeyService apiKeyService;
|
||||
@Resource
|
||||
private AiChatModelService chatModalService;
|
||||
|
||||
@Resource
|
||||
private AiWriteMapper writeMapper; // TODO @xin:上面空一行;因为同类之间不要空行,非同类空行;
|
||||
private DictDataApi dictDataApi;
|
||||
|
||||
@Resource
|
||||
private AiWriteMapper writeMapper;
|
||||
|
||||
@Override
|
||||
public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
|
||||
// 1.1 校验模型
|
||||
// TODO @xin:可以约定大于配置先,查询某个名字。例如说,写作助手!然后写作助手,上面是有个 model 的,可以使用它。
|
||||
AiChatModelDO model = chatModalService.validateChatModel(14L);
|
||||
StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
|
||||
// 1.1 校验模型 TODO 芋艿 是不是取默认的模型也ok?;那可以,有限拿 chatRole 的角色;如果没有,则获取默认的;
|
||||
AiChatModelDO model = chatModalService.getRequiredDefaultChatModel();
|
||||
StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
|
||||
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
|
||||
ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
|
||||
|
||||
// 1.2 插入写作信息
|
||||
// TODO @xin:建议把 writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()),写在 toBean 的 consumer 里;原因是,让这个 set 保持完整性
|
||||
AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class);
|
||||
writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()));
|
||||
|
||||
// 2.1 构建提示词
|
||||
ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
|
||||
Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
|
||||
Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
|
||||
|
||||
// 2.2 流式返回
|
||||
StringBuffer contentBuffer = new StringBuffer();
|
||||
return streamResponse.map(chunk -> {
|
||||
@ -83,11 +85,13 @@ public class AiWriteServiceImpl implements AiWriteService {
|
||||
private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) {
|
||||
String template;
|
||||
Integer writeType = generateReqVO.getType();
|
||||
String format = AiWriteFormatEnum.valueOfFormat(generateReqVO.getFormat()).getName();
|
||||
String tone = AiWriteToneEnum.valueOfTone(generateReqVO.getTone()).getName();
|
||||
String language = AiLanguageEnum.valueOfLanguage(generateReqVO.getLanguage()).getName();
|
||||
String length = AiWriteLengthEnum.valueOfLength(generateReqVO.getLength()).getName();
|
||||
String format = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_FORMAT, generateReqVO.getFormat());
|
||||
String tone = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_TONE, generateReqVO.getFormat());
|
||||
String language = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LANGUAGE, generateReqVO.getFormat());
|
||||
String length = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LENGTH, generateReqVO.getFormat());
|
||||
// TODO @xin:建议改成 if return 哈;更简洁;
|
||||
if (Objects.equals(writeType, AiWriteTypeEnum.WRITING.getType())) {
|
||||
// TODO @xin:写成静态枚举哈
|
||||
template = "请撰写一篇关于 [{}] 的文章。文章的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
|
||||
return StrUtil.format(template, generateReqVO.getPrompt(), format, tone, language, length);
|
||||
} else if (Objects.equals(writeType, AiWriteTypeEnum.REPLY.getType())) {
|
||||
@ -98,27 +102,4 @@ public class AiWriteServiceImpl implements AiWriteService {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 芋艿:复用
|
||||
private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
|
||||
Float temperatureF = temperature != null ? temperature.floatValue() : null;
|
||||
//noinspection EnhancedSwitchMigration
|
||||
switch (platform) {
|
||||
case OPENAI:
|
||||
return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
case OLLAMA:
|
||||
return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
|
||||
case YI_YAN:
|
||||
// TODO 芋艿:貌似 model 只要一设置,就报错
|
||||
// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
case XING_HUO:
|
||||
return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
|
||||
.setMaxTokens(maxTokens);
|
||||
case QIAN_WEN:
|
||||
return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
|
||||
default:
|
||||
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user