From f6dcf8afa978de03152e2a1f74a4eeab08e2bc48 Mon Sep 17 00:00:00 2001 From: xs Date: Tue, 30 Sep 2025 17:25:18 +0800 Subject: [PATCH] =?UTF-8?q?1.5.8=E5=90=8E=E7=AB=AF=20AI=20Token=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E8=AE=B0=E5=BD=95=E4=BF=9D=E5=AD=98=E5=92=8C=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiChatMessageController.java | 17 ++ .../ai/controller/AiTokenUsageController.java | 117 +++++++++ .../ai/domain/AiChatMessageDetail.java | 12 +- .../org/dromara/ai/domain/AiTokenUsage.java | 61 +++++ .../ai/domain/bo/AiChatMessageDetailBo.java | 11 + .../dromara/ai/domain/bo/AiTokenUsageBo.java | 73 ++++++ .../ai/domain/vo/AiChatMessageDetailVo.java | 22 ++ .../dromara/ai/domain/vo/AiTokenUsageVo.java | 83 +++++++ .../ai/mapper/AiChatMessageDetailMapper.java | 13 + .../dromara/ai/mapper/AiTokenUsageMapper.java | 37 +++ .../dromara/ai/process/dto/AIResponse.java | 34 +-- .../dromara/ai/process/dto/TokenUsage.java | 46 ++++ .../IUnifiedAIProviderProcessor.java | 51 +++- .../impl/BaseAIProviderProcessor.java | 234 +++++++++++++++++- .../processor/impl/DeepSeekProcessor.java | 185 +++++++++----- .../processor/impl/TencentLkeProcessor.java | 87 +++++-- .../impl/TongYiQianWenProcessor.java | 36 ++- .../processor/utils/ProcessorUtils.java | 25 ++ .../service/IAiChatMessageDetailService.java | 8 + .../ai/service/IAiTokenUsageService.java | 78 ++++++ .../service/impl/AIAssistantServiceImpl.java | 39 ++- .../impl/AiChatMessageDetailServiceImpl.java | 23 ++ .../impl/AiKnowledgeBaseServiceImpl.java | 26 +- .../service/impl/AiTokenUsageServiceImpl.java | 162 ++++++++++++ .../mapper/ai/AiChatMessageDetailMapper.xml | 23 ++ .../mapper/ai/AiTokenUsageMapper.xml | 24 ++ 26 files changed, 1412 insertions(+), 115 deletions(-) create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java index 756253b4..786eb501 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java @@ -6,6 +6,9 @@ import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import org.dromara.ai.domain.bo.AiChatMessageDetailBo; +import org.dromara.ai.domain.vo.AiChatMessageDetailVo; +import org.dromara.ai.service.IAiChatMessageDetailService; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -37,6 +40,8 @@ public class AiChatMessageController extends BaseController { private final IAiChatMessageService aiChatMessageService; + private final IAiChatMessageDetailService aiChatMessageDetailService; + /** * 查询聊天消息列表 */ @@ -114,4 +119,16 @@ public class AiChatMessageController extends BaseController { List list = aiChatMessageService.queryList(bo); return R.ok(list); } + + + + /** + * 查询使用详情列表 + */ +// @SaCheckPermission("ai:aiChatMessage:list") + @GetMapping("/listDetail") + public TableDataInfo listDetail(AiChatMessageDetailBo bo, PageQuery pageQuery) { + return aiChatMessageDetailService.queryPageJoinList(bo, pageQuery); + } + } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java new file mode 100644 index 00000000..c7046216 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java @@ -0,0 +1,117 @@ +package org.dromara.ai.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.ai.domain.vo.AiTokenUsageVo; +import org.dromara.ai.domain.bo.AiTokenUsageBo; +import org.dromara.ai.service.IAiTokenUsageService; +import org.dromara.common.mybatis.core.page.TableDataInfo; + +/** + * 用户token使用详情 + * 前端访问路由地址为:/ai/tokenUsage + * + * @author xins + * @date 2025-09-30 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/tokenUsage") +public class AiTokenUsageController extends BaseController { + + private final IAiTokenUsageService aiTokenUsageService; + + /** + * 查询用户token使用详情列表 + */ + @SaCheckPermission("ai:tokenUsage:list") + @GetMapping("/list") + public TableDataInfo list(AiTokenUsageBo bo, PageQuery pageQuery) { + return aiTokenUsageService.queryPageJoinList(bo, pageQuery); + } + + /** + * 导出用户token使用详情列表 + */ + @SaCheckPermission("ai:tokenUsage:export") + @Log(title = "用户token使用详情", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(AiTokenUsageBo bo, HttpServletResponse response) { + List list = aiTokenUsageService.queryList(bo); + ExcelUtil.exportExcel(list, "用户token使用详情", AiTokenUsageVo.class, response); + } + + /** + * 获取用户token使用详情详细信息 + * + * @param tokenUsageId 主键 + */ + @SaCheckPermission("ai:tokenUsage:query") + @GetMapping("/{tokenUsageId}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long tokenUsageId) { + return R.ok(aiTokenUsageService.queryById(tokenUsageId)); + } + + /** + * 新增用户token使用详情 + */ + @SaCheckPermission("ai:tokenUsage:add") + @Log(title = "用户token使用详情", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody AiTokenUsageBo bo) { + return toAjax(aiTokenUsageService.insertByBo(bo)); + } + + /** + * 修改用户token使用详情 + */ + @SaCheckPermission("ai:tokenUsage:edit") + @Log(title = "用户token使用详情", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody AiTokenUsageBo bo) { + return toAjax(aiTokenUsageService.updateByBo(bo)); + } + + /** + * 删除用户token使用详情 + * + * @param tokenUsageIds 主键串 + */ + @SaCheckPermission("ai:tokenUsage:remove") + @Log(title = "用户token使用详情", businessType = BusinessType.DELETE) + @DeleteMapping("/{tokenUsageIds}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] tokenUsageIds) { + return toAjax(aiTokenUsageService.deleteWithValidByIds(List.of(tokenUsageIds), true)); + } + + + /** + * 下拉框查询用户token使用详情列表 + */ + + @GetMapping("/getAiTokenUsageList") + public R> getAiTokenUsageList(AiTokenUsageBo bo) { + List list = aiTokenUsageService.queryList(bo); + return R.ok(list); + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java index d50c9562..a1ac11e7 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java @@ -37,6 +37,12 @@ public class AiChatMessageDetail extends TenantEntity { */ private String sessionId; + /** + * 聊天类型(1AI提问,2知识库提问,3生成SQL,4AI智能填报,5获取向量) + */ + private String messageDetailType; + + /** * 提问内容 */ @@ -72,6 +78,11 @@ public class AiChatMessageDetail extends TenantEntity { */ private Long knowledgeBaseId; + /** + * 知识库内容ID,获取向量时使用,关联ai_knowledge_content + */ + private Long knowledgeContentId; + /** * 是否携带历史内容(1是,0否) */ @@ -82,5 +93,4 @@ public class AiChatMessageDetail extends TenantEntity { */ private String completeFlag; - } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java new file mode 100644 index 00000000..911ea876 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java @@ -0,0 +1,61 @@ +package org.dromara.ai.domain; + +import org.dromara.common.tenant.core.TenantEntity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 用户token使用详情对象 ai_token_usage + * + * @author xins + * @date 2025-09-30 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ai_token_usage") +public class AiTokenUsage extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @TableId(value = "token_usage_id", type = IdType.AUTO) + private Long tokenUsageId; + + /** + * 用户 + */ + private Long userId; + + /** + * 待结算token + */ + private Long token; + + /** + * 模型ID,关联ai_model + */ + private Long modelId; + + /** + * 提问token数量 + */ + private Long promptToken; + + /** + * 回复token数量 + */ + private Long completionToken; + + /** + * 累计使用token + */ + private Long totalToken; + + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java index 13cb0677..6377094e 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java @@ -38,6 +38,12 @@ public class AiChatMessageDetailBo extends BaseEntity { @NotNull(message = "会话ID不能为空", groups = { AddGroup.class, EditGroup.class }) private String sessionId; + /** + * 聊天类型(1AI提问,2知识库提问,3生成SQL,4AI智能填报,5获取向量) + */ + @NotNull(message = "会话类型不能为空", groups = { AddGroup.class, EditGroup.class }) + private String messageDetailType; + /** * 提问内容 */ @@ -80,6 +86,11 @@ public class AiChatMessageDetailBo extends BaseEntity { @NotNull(message = "知识库ID,关联ai_knowledge_base不能为空", groups = { AddGroup.class, EditGroup.class }) private Long knowledgeBaseId; + /** + * 知识库内容ID,获取向量时使用,关联ai_knowledge_content + */ + private Long knowledgeContentId; + /** * 是否携带历史内容(1是,0否) */ diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java new file mode 100644 index 00000000..f39c6af8 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java @@ -0,0 +1,73 @@ +package org.dromara.ai.domain.bo; + +import org.dromara.ai.domain.AiTokenUsage; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; + +/** + * 用户token使用详情业务对象 ai_token_usage + * + * @author xins + * @date 2025-09-30 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = AiTokenUsage.class, reverseConvertGenerate = false) +public class AiTokenUsageBo extends BaseEntity { + + /** + * 主键 + */ + @NotNull(message = "主键不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long tokenUsageId; + + /** + * 用户 + */ + @NotNull(message = "用户不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long userId; + + /** + * 待结算token + */ + @NotNull(message = "待结算token不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long token; + + /** + * 模型ID,关联ai_model + */ + @NotNull(message = "模型ID,关联ai_model不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long modelId; + + /** + * 提问token数量 + */ + @NotNull(message = "提问token数量不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long promptToken; + + /** + * 回复token数量 + */ + @NotNull(message = "回复token数量不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long completionToken; + + /** + * 累计使用token + */ + @NotNull(message = "累计使用token不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long totalToken; + + + /** + * 用户昵称 + */ + private String nickName; + + + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java index 19d28e43..679b4f3f 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java @@ -1,8 +1,11 @@ package org.dromara.ai.domain.vo; +import jakarta.validation.constraints.NotNull; import org.dromara.ai.domain.AiChatMessageDetail; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; import org.dromara.common.excel.annotation.ExcelDictFormat; import org.dromara.common.excel.convert.ExcelDictConvert; import io.github.linpeilie.annotations.AutoMapper; @@ -40,6 +43,11 @@ public class AiChatMessageDetailVo implements Serializable { @ExcelProperty(value = "聊天信息ID,关联ai_chat_message") private Long chatMessageId; + /** + * 聊天类型(1AI提问,2知识库提问,3生成SQL,4AI智能填报,5获取向量) + */ + private String messageDetailType; + /** * 会话ID */ @@ -88,6 +96,11 @@ public class AiChatMessageDetailVo implements Serializable { @ExcelProperty(value = "知识库ID,关联ai_knowledge_base") private Long knowledgeBaseId; + /** + * 知识库内容ID,获取向量时使用,关联ai_knowledge_content + */ + private Long knowledgeContentId; + /** * 是否携带历史内容(1是,0否) */ @@ -101,5 +114,14 @@ public class AiChatMessageDetailVo implements Serializable { @ExcelProperty(value = "完整标识(1是,0否),代表回复信息是否完整回复,中间可以暂停继续。") private String completeFlag; + /** + * 用户昵称 + */ + private String nickName; + + /** + * AI模型名称 + */ + private String modelName; } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java new file mode 100644 index 00000000..592f75e9 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java @@ -0,0 +1,83 @@ +package org.dromara.ai.domain.vo; + +import org.dromara.ai.domain.AiTokenUsage; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + + + +/** + * 用户token使用详情视图对象 ai_token_usage + * + * @author xins + * @date 2025-09-30 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = AiTokenUsage.class) +public class AiTokenUsageVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @ExcelProperty(value = "主键") + private Long tokenUsageId; + + /** + * 用户 + */ + @ExcelProperty(value = "用户") + private Long userId; + + /** + * 待结算token + */ + @ExcelProperty(value = "待结算token") + private Long token; + + /** + * 模型ID,关联ai_model + */ + @ExcelProperty(value = "模型ID,关联ai_model") + private Long modelId; + + /** + * 提问token数量 + */ + @ExcelProperty(value = "提问token数量") + private Long promptToken; + + /** + * 回复token数量 + */ + @ExcelProperty(value = "回复token数量") + private Long completionToken; + + /** + * 累计使用token + */ + @ExcelProperty(value = "累计使用token") + private Long totalToken; + + + /** + * 用户昵称 + */ + private String nickName; + + /** + * AI模型名称 + */ + private String modelName; +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java index 9e4118bd..469553ea 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java @@ -1,7 +1,13 @@ package org.dromara.ai.mapper; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.vo.AiChatMessageDetailVo; +import org.dromara.common.mybatis.annotation.DataColumn; +import org.dromara.common.mybatis.annotation.DataPermission; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; /** @@ -12,4 +18,11 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; */ public interface AiChatMessageDetailMapper extends BaseMapperPlus { + @DataPermission({ + @DataColumn(key = "deptName", value = "acmd.create_dept"), + @DataColumn(key = "userName", value = "acmd.user_id") + }) + Page selectAiChatMessageDetailJoinList(@Param("page") Page page, + @Param(Constants.WRAPPER) Wrapper queryWrapper); + } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java new file mode 100644 index 00000000..c4c4ff6d --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java @@ -0,0 +1,37 @@ +package org.dromara.ai.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.dromara.ai.domain.AiTokenUsage; +import org.dromara.ai.domain.bo.AiTokenUsageBo; +import org.dromara.ai.domain.vo.AiTokenUsageVo; +import org.dromara.common.mybatis.annotation.DataColumn; +import org.dromara.common.mybatis.annotation.DataPermission; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +import java.util.List; + +/** + * 用户token使用详情Mapper接口 + * + * @author xins + * @date 2025-09-30 + */ +public interface AiTokenUsageMapper extends BaseMapperPlus { + + + /** + * 获取用户token使用记录,join sys_user and ai_model + * @param page + * @param queryWrapper + * @return List + */ + @DataPermission({ + @DataColumn(key = "deptName", value = "atu.create_dept"), + @DataColumn(key = "userName", value = "atu.user_id") + }) + Page selectAiTokenUsageJoinList(@Param("page") Page page, + @Param(Constants.WRAPPER) Wrapper queryWrapper); +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java index a2c01649..4f514859 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java @@ -37,18 +37,18 @@ public class AIResponse { /** * 使用token情况 */ - private Usage usage; + private TokenUsage tokenUsage; - public AIResponse(boolean success, String errorMessage, String content, Usage usage) { + public AIResponse(boolean success, String errorMessage, String content, TokenUsage tokenUsage) { this.success = success; this.errorMessage = errorMessage; this.content = content; - this.usage = usage; + this.tokenUsage = tokenUsage; } // 成功响应的便捷构造函数 - public AIResponse(String content, Usage usage) { - this(true, null, content, usage); + public AIResponse(String content, TokenUsage tokenUsage) { + this(true, null, content, tokenUsage); } // 错误响应的便捷构造函数 @@ -59,16 +59,16 @@ public class AIResponse { /** * Token使用情况类 */ - @Data - public static class Usage { - private int promptTokens; - private int completionTokens; - private int totalTokens; - - public Usage(int promptTokens, int completionTokens, int totalTokens) { - this.promptTokens = promptTokens; - this.completionTokens = completionTokens; - this.totalTokens = totalTokens; - } - } +// @Data +// public static class Usage { +// private int promptTokens; +// private int completionTokens; +// private int totalTokens; +// +// public Usage(int promptTokens, int completionTokens, int totalTokens) { +// this.promptTokens = promptTokens; +// this.completionTokens = completionTokens; +// this.totalTokens = totalTokens; +// } +// } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java new file mode 100644 index 00000000..3d7df560 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java @@ -0,0 +1,46 @@ +package org.dromara.ai.process.dto; + +/** + * @Author xins + * @Date 2025/9/25 16:28 + * @Description: 调用AI模型使用的Token情况dto + */ +// 如果还没有这个类,需要创建 +public class TokenUsage { + private Long promptToken; + private Long completionToken; + private Long totalToken; + + // 构造方法、getter、setter + public TokenUsage() {} + + public TokenUsage(Long promptToken, Long completionToken, Long totalToken) { + this.promptToken = promptToken; + this.completionToken = completionToken; + this.totalToken = totalToken; + } + + public Long getPromptToken() { + return promptToken; + } + + public void setPromptToken(Long promptToken) { + this.promptToken = promptToken; + } + + public Long getCompletionToken() { + return completionToken; + } + + public void setCompletionToken(Long completionTokens) { + this.completionToken = completionToken; + } + + public Long getTotalToken() { + return totalToken; + } + + public void setTotalToken(Long totalToken) { + this.totalToken = totalToken; + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java index ecefa8da..c771c781 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java @@ -1,8 +1,10 @@ package org.dromara.ai.process.provider.processor; +import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; +import org.dromara.ai.process.dto.TokenUsage; import org.dromara.ai.process.enums.AIProviderEnum; import org.dromara.system.api.model.LoginUser; import reactor.core.publisher.Flux; @@ -24,6 +26,13 @@ public interface IUnifiedAIProviderProcessor { */ Mono chat(AIRequest request); + /** + * 发送聊天请求(流式) + * @param request 聊天请求参数 + * @return 流式内容Flux + */ + Flux chatStreamContent(AIRequest request, LoginUser loginUser); + /** * 发送聊天请求(流式) * @param request 聊天请求参数 @@ -42,19 +51,53 @@ public interface IUnifiedAIProviderProcessor { /** - * 获取单个文本的向量表示 - * @param text 输入文本 + * 获取单个文本的向量数据 + * @param aiRequest * @return 向量表示列表(Double类型) * @throws RuntimeException 如果向量化过程中发生错误 */ public List getEmbedding(AIRequest aiRequest); /** - * 获取多个文本的向量表示 - * @param texts 输入文本数组 + * 获取单个文本的向量数据 + * @param embeddingResponse + * @return + */ + public List getEmbedding(GetEmbeddingResponse embeddingResponse); + + + /** + * 获取多个文本的向量数据和Usage + * @param aiRequest + * @return 向量表示列表(Double类型) + * @throws RuntimeException 如果向量化过程中发生错误 + */ + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest); + + /** + * 获取多个文本的向量数据 + * @param aiRequest * @return 向量表示列表的列表(每个文本对应一个Double列表) * @throws RuntimeException 如果向量化过程中发生错误 */ public List> getEmbeddings(AIRequest aiRequest); + /** + * 保存token使用情况 + * @param messageDetailType + * @param questionContent + * @param answerContent + * @param tokenUsage + * @param modelId + * @param knowledgeBaseId + * @param knowledgeContentId + * @param chatMessageId + * @param sessionId + * @param takeFlag + * @param completeFlag + * @param userId + */ + public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage, + Long modelId, Long knowledgeBaseId, Long knowledgeContentId, + Long chatMessageId,String sessionId,String takeFlag,String completeFlag,Long userId); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java index f1c538e4..9fb73f4f 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java @@ -1,11 +1,28 @@ package org.dromara.ai.process.provider.processor.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.yulichang.toolkit.JoinWrappers; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import org.dromara.ai.domain.AiChatMessage; +import org.dromara.ai.domain.AiChatMessageDetail; +import org.dromara.ai.domain.AiTokenUsage; +import org.dromara.ai.mapper.AiChatMessageDetailMapper; +import org.dromara.ai.mapper.AiChatMessageMapper; +import org.dromara.ai.mapper.AiTokenUsageMapper; +import org.dromara.ai.process.dto.AIMessage; +import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; +import org.dromara.ai.process.dto.TokenUsage; +import org.dromara.ai.process.enums.AIChatMessageTypeEnum; import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor; import org.dromara.ai.test.ChatRequest; +import org.dromara.common.constant.HwMomAiConstants; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.system.api.model.LoginUser; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.WebClient; @@ -14,6 +31,7 @@ import reactor.core.publisher.Mono; import reactor.util.retry.Retry; import java.time.Duration; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +44,12 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce protected final ObjectMapper objectMapper; protected final WebClient webClient; + @Autowired + private AiChatMessageMapper aiChatMessageMapper; + @Autowired + private AiChatMessageDetailMapper aiChatMessageDetailMapper; + @Autowired + private AiTokenUsageMapper aiTokenUsageMapper; // 用于解析流式JSON块的模式 private static final Pattern JSON_PATTERN = Pattern.compile("\\{(?:[^{}]|\\{(?:[^{}]|\\{[^{}]*\\})*\\})*\\}"); @@ -50,7 +74,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce /** * 处理流式响应的JSON块 */ - protected String parseStreamChunk(String jsonChunk) { + protected String parseStreamChunkContent(String jsonChunk) { try { return extractContentFromStreamJson(jsonChunk); } catch (Exception e) { @@ -116,7 +140,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce /** * 执行流式HTTP请求 */ - protected Flux executeStreamRequest(String url, String requestBody, String apiKey) { + protected Flux executeStreamRequestContent(String url, String requestBody, String apiKey) { // String prompt = "你好"; // requestBody = String.format( // "{\"model\":\"deepseek-chat\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}", @@ -135,12 +159,216 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce .filter(chunk -> chunk != null && !chunk.isBlank()) .filter(chunk -> !chunk.equals("[DONE]")) .filter(chunk -> chunk.startsWith("{") && chunk.endsWith("}")) - .map(this::parseStreamChunk) + .map(this::parseStreamChunkContent) .filter(content -> content != null && !content.isEmpty()) .onErrorResume(e -> Flux.error(new RuntimeException("流式请求失败: " + e.getMessage()))); } + + + + + + /** + * 处理流式响应的JSON块,返回包含内容和token信息的对象 + */ + protected StreamChunkResult parseStreamChunk(String jsonChunk) { + try { + return extractContentAndTokensFromStreamJson(jsonChunk); + } catch (Exception e) { + return new StreamChunkResult(null, null); + } + } + + /** + * 从流式JSON中提取内容和token信息(由子类实现) + */ + protected abstract StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception; + + + /** + * 执行流式HTTP请求,返回包含内容和token信息的Flux + */ + protected Flux executeStreamRequest(String url, String requestBody, String apiKey) { + return webClient.post() + .uri(url) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE) + .bodyValue(requestBody) + .retrieve() + .bodyToFlux(String.class) + .timeout(Duration.ofSeconds(60)) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) + .flatMap(this::extractJsonChunks) + .filter(chunk -> chunk != null && !chunk.isBlank()) + .filter(chunk -> !chunk.equals("[DONE]")) + .filter(chunk -> chunk.startsWith("{") && chunk.endsWith("}")) + .map(this::parseStreamChunk) + .filter(result -> result.hasContent() || result.hasTokenUsage()) + .onErrorResume(e -> Flux.error(new RuntimeException("流式请求失败: " + e.getMessage()))); + } + + + protected void saveChatMessage(AIRequest request, String fullResponse, TokenUsage tokenUsage, LoginUser loginUser) { + try { + String sessionId = request.getSessionId(); + AiChatMessage aiChatMessage = aiChatMessageMapper + .selectOne(new LambdaQueryWrapper() + .eq(AiChatMessage::getSessionId, sessionId)); + List messages = request.getMessages(); + if (aiChatMessage == null) { + aiChatMessage = new AiChatMessage(); + aiChatMessage.setSessionId(request.getSessionId()); + aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic())); + aiChatMessage.setMessageType(AIChatMessageTypeEnum.AI_CHAT.getCode()); + aiChatMessage.setModelId(request.getModelId()); + aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId()); +// aiChatMessage.setTotalToken(); + aiChatMessage.setTenantId(loginUser.getTenantId()); + aiChatMessage.setCreateBy(loginUser.getUserId()); + aiChatMessage.setCreateDept(loginUser.getDeptId()); + + aiChatMessageMapper.insert(aiChatMessage); + } else { + + } + + + saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION, objectMapper.writeValueAsString(request.getQuestionContent()), + objectMapper.writeValueAsString(fullResponse), tokenUsage, request.getModelId(), request.getKnowledgeBaseId(), null, + aiChatMessage.getChatMessageId(),request.getSessionId(),request.getCarryHistoryFlag(),"1", loginUser.getUserId()); + + +// AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); +// aiChatMessageDetail.setChatMessageId(aiChatMessage.getChatMessageId()); +// aiChatMessageDetail.setSessionId(request.getSessionId()); +// aiChatMessageDetail.setQuestionContent(objectMapper.writeValueAsString(request.getQuestionContent())); +// aiChatMessageDetail.setAnswerContent(objectMapper.writeValueAsString(fullResponse)); +// // 设置token使用信息 +// if (tokenUsage != null) { +// aiChatMessageDetail.setPromptToken((long) tokenUsage.getPromptTokens()); +// aiChatMessageDetail.setCompletionToken((long) tokenUsage.getCompletionTokens()); +// aiChatMessageDetail.setTotalToken((long) tokenUsage.getTotalTokens()); +// } +// aiChatMessageDetail.setModelId(request.getModelId()); +// aiChatMessageDetail.setKnowledgeBaseId(request.getKnowledgeBaseId()); +// aiChatMessageDetail.setTakeFlag(request.getCarryHistoryFlag()); +// aiChatMessageDetail.setCompleteFlag("1"); +// aiChatMessageDetail.setTenantId(loginUser.getTenantId()); +// aiChatMessageDetail.setCreateBy(loginUser.getUserId()); +// aiChatMessageDetail.setCreateDept(loginUser.getDeptId()); +// +// aiChatMessageDetailMapper.insert(aiChatMessageDetail); + } catch (Exception e) { + throw new RuntimeException("保存聊天记录失败", e); + } +// log.info("聊天记录已保存,ID: {}", record.getId()); + + } + + + /** + * 保存token使用情况 + * @param messageDetailType + * @param questionContent + * @param answerContent + * @param tokenUsage + * @param modelId + * @param knowledgeBaseId + * @param knowledgeContentId + * @param chatMessageId + * @param sessionId + * @param takeFlag + * @param completeFlag + * @param userId + */ + @Override + public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage, + Long modelId, Long knowledgeBaseId, Long knowledgeContentId, + Long chatMessageId,String sessionId,String takeFlag,String completeFlag,Long userId) { + Long promptToken = tokenUsage!=null ? tokenUsage.getPromptToken():null; + Long completionToken = tokenUsage!=null ? tokenUsage.getCompletionToken():null; + Long totalToken = tokenUsage!=null ? tokenUsage.getTotalToken():null; + + AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); + aiChatMessageDetail.setMessageDetailType(messageDetailType); + aiChatMessageDetail.setQuestionContent(questionContent); + aiChatMessageDetail.setAnswerContent(answerContent); + aiChatMessageDetail.setPromptToken(promptToken); + aiChatMessageDetail.setCompletionToken(completionToken); + aiChatMessageDetail.setTotalToken(totalToken); + aiChatMessageDetail.setModelId(modelId); + aiChatMessageDetail.setKnowledgeBaseId(knowledgeBaseId); + aiChatMessageDetail.setKnowledgeContentId(knowledgeContentId); + aiChatMessageDetail.setTakeFlag(takeFlag); + aiChatMessageDetail.setCompleteFlag(completeFlag); + aiChatMessageDetail.setChatMessageId(chatMessageId); + aiChatMessageDetail.setSessionId(sessionId); + aiChatMessageDetailMapper.insert(aiChatMessageDetail); + + MPJLambdaWrapper lqw = JoinWrappers.lambda(AiTokenUsage.class) + .selectAll(AiTokenUsage.class) + .eq(userId != null, AiTokenUsage::getUserId, userId) + .eq(modelId != null, AiTokenUsage::getModelId, modelId); + + AiTokenUsage aiTokenUsage = aiTokenUsageMapper.selectOne(lqw); + if (aiTokenUsage == null) { + aiTokenUsage = new AiTokenUsage(); + aiTokenUsage.setPromptToken(promptToken); + aiTokenUsage.setCompletionToken(completionToken); + aiTokenUsage.setTotalToken(totalToken); + aiTokenUsage.setModelId(modelId); + aiTokenUsage.setUserId(userId); + aiTokenUsageMapper.insert(aiTokenUsage); + } else { + if (promptToken != null) { + Long currentPromptToken = aiTokenUsage.getPromptToken() == null ? 0L : aiTokenUsage.getPromptToken(); + aiTokenUsage.setPromptToken(currentPromptToken + promptToken); + } + + if (completionToken != null) { + Long currentCompletionToken = aiTokenUsage.getCompletionToken() == null ? 0L : aiTokenUsage.getCompletionToken(); + aiTokenUsage.setCompletionToken(currentCompletionToken + completionToken); + } + aiTokenUsage.setTotalToken(aiTokenUsage.getTotalToken() + totalToken); + + aiTokenUsageMapper.updateById(aiTokenUsage); + } + + } + + + + /** + * 流式chunk结果类 + */ + protected static class StreamChunkResult { + private final String content; + private final TokenUsage tokenUsage; + + public StreamChunkResult(String content, TokenUsage tokenUsage) { + this.content = content; + this.tokenUsage = tokenUsage; + } + + // getters + public String getContent() { return content; } + public TokenUsage getTokenUsage() { return tokenUsage; } + + public boolean hasContent() { + return content != null && !content.isEmpty(); + } + + public boolean hasTokenUsage() { + return tokenUsage != null; + } + } + + + + // /** // * 流式回复 // * @param request diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java index d6a78a20..4d1616f6 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.vo.AiModelVo; @@ -17,6 +18,7 @@ import org.dromara.ai.mapper.AiModelMapper; import org.dromara.ai.process.dto.AIMessage; import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; +import org.dromara.ai.process.dto.TokenUsage; import org.dromara.ai.process.enums.AIChatMessageTypeEnum; import org.dromara.ai.process.enums.AIProviderEnum; import org.dromara.ai.service.IAiChatMessageService; @@ -54,11 +56,6 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { @Autowired private AiModelMapper aiModelMapper; - @Autowired - private AiChatMessageMapper aiChatMessageMapper; - @Autowired - private AiChatMessageDetailMapper aiChatMessageDetailMapper; - public Mono chatTest(AIRequest request) { AIMessage aiMessage = new AIMessage(); @@ -101,7 +98,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } @Override - public Flux chatStream(AIRequest request, LoginUser loginUser) { + public Flux chatStreamContent(AIRequest request, LoginUser loginUser) { try { ObjectNode rootNode = objectMapper.createObjectNode(); rootNode.put("model", deepSeekChatModel); @@ -119,13 +116,13 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { configureApiKey(request); - return executeStreamRequest(API_URL, requestBody, request.getApiKey()).doOnNext(chunk -> { + return executeStreamRequestContent(API_URL, requestBody, request.getApiKey()).doOnNext(chunk -> { // 收集每个chunk fullResponseBuilder.append(chunk); }) .doOnComplete(() -> { // 流完成后保存到数据库 - saveChatMessage(request, fullResponseBuilder.toString(), loginUser); + saveChatMessage(request, fullResponseBuilder.toString(), null,loginUser); }) .doOnError(error -> { // 错误处理 @@ -171,6 +168,106 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } } + + @Override + public Flux chatStream(AIRequest request, LoginUser loginUser) { + try { + ObjectNode rootNode = objectMapper.createObjectNode(); + rootNode.put("model", deepSeekChatModel); + rootNode.set("messages", objectMapper.valueToTree(request.getMessages())); + rootNode.put("stream", true); + + if (request.getTemperature() != null) { + rootNode.put("temperature", request.getTemperature()); + } + + String requestBody = objectMapper.writeValueAsString(rootNode); + configureApiKey(request); + + // 用于收集完整响应和token信息 + StringBuilder fullResponseBuilder = new StringBuilder(); + TokenUsage finalTokenUsage = new TokenUsage(0L, 0L, 0L); + + return executeStreamRequest(API_URL, requestBody, request.getApiKey()) + .doOnNext(chunkResult -> { + // 收集内容 + if (chunkResult.hasContent()) { + fullResponseBuilder.append(chunkResult.getContent()); + } + + // 更新token使用信息(最后一个包含usage的chunk会覆盖之前的) + if (chunkResult.hasTokenUsage()) { + TokenUsage usage = chunkResult.getTokenUsage(); + finalTokenUsage.setPromptToken(usage.getPromptToken()); + finalTokenUsage.setCompletionToken(usage.getCompletionToken()); + finalTokenUsage.setTotalToken(usage.getTotalToken()); + } + }) + .map(chunkResult -> chunkResult.hasContent() ? chunkResult.getContent() : "") + .filter(content -> !content.isEmpty()) + .doOnComplete(() -> { + // 流完成后保存到数据库,包含token信息 + saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser); + }) + .doOnError(error -> { + // 即使出错也尝试保存已收集的内容 + saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser); + }); + } catch (IOException e) { + return Flux.error(new RuntimeException("构建请求失败: " + e.getMessage())); + } + } + + + @Override + protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { + try { + JsonNode node = new ObjectMapper() + .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) + .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) + .readTree(jsonChunk); + + // 提取内容 + String content = extractContent(node); + + // 提取token使用信息 + TokenUsage tokenUsage = extractTokenUsage(node); + + return new StreamChunkResult(content, tokenUsage); + } catch (Exception e) { + // 尝试提取content的原始文本(最后手段) + String content = extractContentRaw(jsonChunk); + return new StreamChunkResult(content, null); + } + } + + private String extractContent(JsonNode node) { + String content = node.path("delta").path("content").asText(); + if (content.isEmpty()) { + JsonNode choices = node.path("choices"); + if (choices.isArray() && choices.size() > 0) { + content = choices.get(0).path("delta").path("content").asText(); + } + } + return content; + } + + private TokenUsage extractTokenUsage(JsonNode node) { + JsonNode usage = node.path("usage"); + if (!usage.isMissingNode() && !usage.isEmpty()) { + long promptTokens = usage.path("prompt_tokens").asLong(); + long completionTokens = usage.path("completion_tokens").asLong(); + long totalTokens = usage.path("total_tokens").asLong(); + + // 只有在有实际值时才返回TokenUsage + if (promptTokens > 0 || completionTokens > 0 || totalTokens > 0) { + return new TokenUsage(promptTokens, completionTokens, totalTokens); + } + } + return null; + } + + @Override protected AIResponse extractAIResponse(String json) throws Exception { JsonNode node = objectMapper.readTree(json); @@ -181,13 +278,17 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } JsonNode usageNode = node.path("usage"); - AIResponse.Usage usage = new AIResponse.Usage( - usageNode.path("prompt_tokens").asInt(), - usageNode.path("completion_tokens").asInt(), - usageNode.path("total_tokens").asInt() - ); +// AIResponse.Usage usage = new AIResponse.Usage( +// usageNode.path("prompt_tokens").asInt(), +// usageNode.path("completion_tokens").asInt(), +// usageNode.path("total_tokens").asInt() +// ); - return new AIResponse(content, usage); + TokenUsage tokenUsage = new TokenUsage( usageNode.path("prompt_tokens").asLong(), + usageNode.path("completion_tokens").asLong(), + usageNode.path("total_tokens").asLong()); + + return new AIResponse(content, tokenUsage); } @Override @@ -205,6 +306,16 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { return List.of(); } + @Override + public List getEmbedding(GetEmbeddingResponse embeddingResponse){ + return List.of(); + } + + @Override + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) { + return null; + } + @Override public List> getEmbeddings(AIRequest aiRequest) { return List.of(); @@ -235,53 +346,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } - private void saveChatMessage(AIRequest request, String fullResponse, LoginUser loginUser) { - try { - String sessionId = request.getSessionId(); - AiChatMessage aiChatMessage = aiChatMessageMapper - .selectOne(new LambdaQueryWrapper() - .eq(AiChatMessage::getSessionId, sessionId)); - List messages = request.getMessages(); - if (aiChatMessage == null) { - aiChatMessage = new AiChatMessage(); - aiChatMessage.setSessionId(request.getSessionId()); - aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic())); - aiChatMessage.setMessageType(AIChatMessageTypeEnum.AI_CHAT.getCode()); - aiChatMessage.setModelId(request.getModelId()); - aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId()); -// aiChatMessage.setTotalToken(); - aiChatMessage.setTenantId(loginUser.getTenantId()); - aiChatMessage.setCreateBy(loginUser.getUserId()); - aiChatMessage.setCreateDept(loginUser.getDeptId()); - aiChatMessageMapper.insert(aiChatMessage); - } else { - - } - - AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); - aiChatMessageDetail.setChatMessageId(aiChatMessage.getChatMessageId()); - aiChatMessageDetail.setSessionId(request.getSessionId()); - aiChatMessageDetail.setQuestionContent(objectMapper.writeValueAsString(request.getQuestionContent())); - aiChatMessageDetail.setAnswerContent(objectMapper.writeValueAsString(fullResponse)); -// aiChatMessageDetail.setPromptToken(1L); -// aiChatMessageDetail.setCompletionToken(1L); -// aiChatMessageDetail.setTotalToken(1L); - aiChatMessageDetail.setModelId(request.getModelId()); - aiChatMessageDetail.setKnowledgeBaseId(request.getKnowledgeBaseId()); - aiChatMessageDetail.setTakeFlag(request.getCarryHistoryFlag()); - aiChatMessageDetail.setCompleteFlag("1"); - aiChatMessageDetail.setTenantId(loginUser.getTenantId()); - aiChatMessageDetail.setCreateBy(loginUser.getUserId()); - aiChatMessageDetail.setCreateDept(loginUser.getDeptId()); - - aiChatMessageDetailMapper.insert(aiChatMessageDetail); - } catch (Exception e) { - throw new RuntimeException("保存聊天记录失败", e); - } -// log.info("聊天记录已保存,ID: {}", record.getId()); - - } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java index 8b9de4a8..b51ca894 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java @@ -8,7 +8,9 @@ import com.tencentcloudapi.lkeap.v20240522.LkeapClient; import com.tencentcloudapi.lkeap.v20240522.models.EmbeddingObject; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingRequest; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; +import com.tencentcloudapi.lkeap.v20240522.models.Usage; import lombok.extern.slf4j.Slf4j; +import org.dromara.ai.process.provider.processor.utils.ProcessorUtils; import org.dromara.ai.test.vectorization.config.EmbeddingConfig; import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; @@ -31,7 +33,7 @@ import java.util.List; */ @Slf4j @Component -public class TencentLkeProcessor implements IUnifiedAIProviderProcessor { +public class TencentLkeProcessor extends BaseAIProviderProcessor { // 客户端配置 private ClientProfile clientProfile; // 配置属性 @@ -86,6 +88,53 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor { return embeddings.isEmpty() ? new ArrayList<>() : embeddings.get(0); } + @Override + public List getEmbedding(GetEmbeddingResponse embeddingResponse) { + EmbeddingObject[] embeddingObjects = embeddingResponse.getData(); + // 转换结果格式 + List> result = new ArrayList<>(); + for (EmbeddingObject obj : embeddingObjects) { + result.add(ProcessorUtils.convertFloatArrayToDoubleList(obj.getEmbedding())); + } + return result.isEmpty() ? new ArrayList<>() : result.get(0); + } + + /** + * 获取多个文本的向量数据和Usage + * @param aiRequest + * @return 向量表示列表(Double类型) + * @throws RuntimeException 如果向量化过程中发生错误 + */ + @Override + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) { + try { + String apiKey = aiRequest.getApiKey(); + String apiSecret = aiRequest.getApiSecret(); + // 创建凭证对象 + Credential cred = new Credential(apiKey, apiSecret); + + // 创建客户端实例 + LkeapClient client = new LkeapClient( + cred, + properties.getTencentLke().getRegion(), + clientProfile + ); + + // 创建请求对象 + GetEmbeddingRequest req = new GetEmbeddingRequest(); + req.setModel(properties.getTencentLke().getModel()); + req.setInputs(aiRequest.getTexts()); + + // 发送请求并获取响应 + GetEmbeddingResponse resp = client.GetEmbedding(req); + return resp; + } catch (TencentCloudSDKException e) { + log.error("Failed to get embeddings from Tencent Cloud", e); + throw new RuntimeException("Failed to get embeddings", e); + } + } + + @Override public List> getEmbeddings(AIRequest aiRequest) { try { @@ -109,11 +158,13 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor { // 发送请求并获取响应 GetEmbeddingResponse resp = client.GetEmbedding(req); EmbeddingObject[] embeddingObjects = resp.getData(); + //获取token使用数量,然后保存 + Usage usage = resp.getUsage(); // 转换结果格式 List> result = new ArrayList<>(); for (EmbeddingObject obj : embeddingObjects) { - result.add(convertFloatArrayToDoubleList(obj.getEmbedding())); + result.add(ProcessorUtils.convertFloatArrayToDoubleList(obj.getEmbedding())); } return result; @@ -129,19 +180,6 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor { return AIProviderEnum.TENCENT_LKE; } - /** - * 将Float数组转换为Double列表 - * @param floatArray Float数组 - * @return Double列表 - */ - private List convertFloatArrayToDoubleList(Float[] floatArray) { - List doubleList = new ArrayList<>(floatArray.length); - for (Float f : floatArray) { - doubleList.add(f.doubleValue()); - } - return doubleList; - } - @Override public Mono chatTest(AIRequest request) { @@ -153,9 +191,28 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor { return null; } + @Override + public Flux chatStreamContent(AIRequest request, LoginUser loginUser) { + return null; + } + @Override public Flux chatStream(AIRequest request, LoginUser loginUser) { return null; } + @Override + protected String extractContentFromStreamJson(String jsonChunk) throws Exception { + return ""; + } + + @Override + protected AIResponse extractAIResponse(String json) throws Exception { + return null; + } + + @Override + protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { + return null; + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java index d0363a3c..bbb28706 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.mapper.AiChatMessageDetailMapper; @@ -14,6 +15,7 @@ import org.dromara.ai.mapper.AiChatMessageMapper; import org.dromara.ai.process.dto.AIMessage; import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; +import org.dromara.ai.process.dto.TokenUsage; import org.dromara.ai.process.enums.AIProviderEnum; import org.dromara.common.encrypt.utils.EncryptUtils; import org.dromara.system.api.model.LoginUser; @@ -90,8 +92,9 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor { } } + @Override - public Flux chatStream(AIRequest request, LoginUser loginUser) { + public Flux chatStreamContent(AIRequest request, LoginUser loginUser) { try { ObjectNode rootNode = objectMapper.createObjectNode(); rootNode.put("model", deepSeekChatModel); @@ -169,15 +172,17 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor { } JsonNode usageNode = node.path("usage"); - AIResponse.Usage usage = new AIResponse.Usage( - usageNode.path("prompt_tokens").asInt(), - usageNode.path("completion_tokens").asInt(), - usageNode.path("total_tokens").asInt() + TokenUsage tokenUsage = new TokenUsage( + usageNode.path("prompt_tokens").asLong(), + usageNode.path("completion_tokens").asLong(), + usageNode.path("total_tokens").asLong() ); - return new AIResponse(content, usage); + return new AIResponse(content, tokenUsage); } + + @Override public AIProviderEnum supportedProvider() { return AIProviderEnum.TONGYI_QIANWEN; @@ -193,6 +198,16 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor { return List.of(); } + @Override + public List getEmbedding(GetEmbeddingResponse embeddingResponse){ + return List.of(); + } + + @Override + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) { + return null; + } + @Override public List> getEmbeddings(AIRequest aiRequest) { return List.of(); @@ -247,4 +262,13 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor { } + @Override + public Flux chatStream(AIRequest request, LoginUser loginUser) { + return null; + } + + @Override + protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { + return null; + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java new file mode 100644 index 00000000..1e170565 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java @@ -0,0 +1,25 @@ +package org.dromara.ai.process.provider.processor.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author xins + * @Date 2025/9/26 17:12 + * @Description: + */ +public class ProcessorUtils { + + /** + * 将Float数组转换为Double列表 + * @param floatArray Float数组 + * @return Double列表 + */ + public static List convertFloatArrayToDoubleList(Float[] floatArray) { + List doubleList = new ArrayList<>(floatArray.length); + for (Float f : floatArray) { + doubleList.add(f.doubleValue()); + } + return doubleList; + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java index ca0bc660..460951c3 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java @@ -74,4 +74,12 @@ public interface IAiChatMessageDetailService { * @return LIST */ public List getAIChatMessages(String sessionId); + + /** + * 分页获取会话详情信息,join user and model + * @param bo + * @param pageQuery + * @return + */ + public TableDataInfo queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java new file mode 100644 index 00000000..57942f75 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java @@ -0,0 +1,78 @@ +package org.dromara.ai.service; + +import org.dromara.ai.domain.AiTokenUsage; +import org.dromara.ai.domain.vo.AiTokenUsageVo; +import org.dromara.ai.domain.bo.AiTokenUsageBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 用户token使用详情Service接口 + * + * @author xins + * @date 2025-09-30 + */ +public interface IAiTokenUsageService { + + /** + * 查询用户token使用详情 + * + * @param tokenUsageId 主键 + * @return 用户token使用详情 + */ + AiTokenUsageVo queryById(Long tokenUsageId); + + /** + * 分页查询用户token使用详情列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 用户token使用详情分页列表 + */ + TableDataInfo queryPageList(AiTokenUsageBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的用户token使用详情列表 + * + * @param bo 查询条件 + * @return 用户token使用详情列表 + */ + List queryList(AiTokenUsageBo bo); + + /** + * 新增用户token使用详情 + * + * @param bo 用户token使用详情 + * @return 是否新增成功 + */ + Boolean insertByBo(AiTokenUsageBo bo); + + /** + * 修改用户token使用详情 + * + * @param bo 用户token使用详情 + * @return 是否修改成功 + */ + Boolean updateByBo(AiTokenUsageBo bo); + + /** + * 校验并批量删除用户token使用详情信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 分页查询用户token使用详情列表,Join sys_user and ai_model + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 用户token使用详情分页列表 + */ + public TableDataInfo queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery); +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java index 29270da2..4e852ad3 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java @@ -5,17 +5,17 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.core.JsonProcessingException; +import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; +import com.tencentcloudapi.lkeap.v20240522.models.Usage; import org.dromara.ai.domain.AiFormSettingDetail; import org.dromara.ai.domain.AiModel; import org.dromara.ai.domain.dto.AiTableConditionWrapper; import org.dromara.ai.domain.dto.AiTableData; import org.dromara.ai.domain.dto.AiTableQueryCondition; import org.dromara.ai.mapper.AiModelMapper; -import org.dromara.ai.process.dto.AIFillFormRequest; -import org.dromara.ai.process.dto.AIResponse; +import org.dromara.ai.mapper.AiTokenUsageMapper; +import org.dromara.ai.process.dto.*; import org.dromara.ai.mapper.SQLServerDatabaseMetaMapper; -import org.dromara.ai.process.dto.AIMessage; -import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.provider.processor.AIProviderProcessorFactory; import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor; import org.dromara.ai.service.IAIAssistantService; @@ -37,7 +37,7 @@ import java.util.stream.Collectors; /** * @Author xins * @Date 2025/7/17 14:31 - * @Description: + * @Description:AI助手 */ @Service public class AIAssistantServiceImpl implements IAIAssistantService { @@ -128,6 +128,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { AIMessage message = messages.get(messages.size() - 1); String messageContent = message.getContent().toString(); aiRequest.setText(messageContent); + aiRequest.setTexts(new String[]{messageContent}); StringBuilder sb = new StringBuilder(messageContent); Long embeddingModelId = aiRequest.getEmbeddingModelId(); @@ -141,7 +142,18 @@ public class AIAssistantServiceImpl implements IAIAssistantService { aiRequest.setApiKey(EncryptUtils.decryptByBase64(aiModel.getApiKey())); aiRequest.setApiSecret(EncryptUtils.decryptByBase64(aiModel.getApiSecret())); IUnifiedAIProviderProcessor tencentLkeProcessor = aiProviderProcessorFactory.getProcessorByPlatformId(aiModel.getPlatformId()); - List queryEmbedding = tencentLkeProcessor.getEmbedding(aiRequest); + + GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest); + //获取token使用数量,然后保存 + Usage usage = embeddingResponses.getUsage(); + TokenUsage tokenUsage = new TokenUsage(null,null,usage.getTotalTokens()); + + tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR,"AI问答获取向量", null, tokenUsage, + embeddingModelId, aiRequest.getKnowledgeBaseId(), null, + null, null, "0", "1",LoginHelper.getUserId()); + + + List queryEmbedding = tencentLkeProcessor.getEmbedding(embeddingResponses); int topK = aiRequest.getRetrieveLimit() == null || aiRequest.getRetrieveLimit() <= 0 ? 5 : aiRequest.getRetrieveLimit();//retrieveLimit,检索限制 @@ -270,6 +282,9 @@ public class AIAssistantServiceImpl implements IAIAssistantService { // WHERE del_flag = '0' // ORDER BY order_num, dept_id; //``` + processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL,prompt, content,response.block().getTokenUsage(), + aiRequest.getModelId(), null,null, + null,null,"0","1",LoginHelper.getUserId()); return extractSqlFromContent(content); } else { @@ -278,6 +293,9 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } + + + /** * 获取格式化的数据库结构描述 */ @@ -413,6 +431,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { AIRequest aiRequest = new AIRequest(); aiRequest.setMessages(Collections.singletonList(aiMessage)); + //todo modelId aiRequest.setModelId(1L); Mono response = processor.chat(aiRequest); @@ -420,6 +439,10 @@ public class AIAssistantServiceImpl implements IAIAssistantService { String content = response.block().getContent().toString(); JSONObject contentJson = JSONObject.parseObject(content); parseRelateTable(aiFormSettingDetailList, contentJson); + processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM,sb.toString(), content, response.block().getTokenUsage(), + 1L, null, null, + null, null, "0", "1",LoginHelper.getUserId()); + System.out.println(contentJson.toJSONString()); return contentJson; @@ -462,7 +485,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { //[{fieldKey:'del_flag',fieldValue:'0',conditionSymbol:'='}] // 创建条件包装类 AiTableConditionWrapper wrapper = new AiTableConditionWrapper(); - if(StringUtils.isNotBlank(relateFilterCondition)){ + if (StringUtils.isNotBlank(relateFilterCondition)) { // 解析JSON字符串为条件列表 List conditions = JSON.parseArray(relateFilterCondition, AiTableQueryCondition.class); wrapper.setAiTableQueryConditions(conditions); @@ -470,7 +493,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } - List> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper,"", ""); + List> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper, "", ""); JSONArray relateTableDataArr = new JSONArray(); for (Map tableD : tableData) { // JSONObject tableDataJson = new JSONObject(); diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java index 6ee47888..f6cca9e9 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java @@ -1,5 +1,10 @@ package org.dromara.ai.service.impl; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.dromara.ai.domain.AiTokenUsage; +import org.dromara.ai.domain.bo.AiTokenUsageBo; +import org.dromara.ai.domain.vo.AiTokenUsageVo; import org.dromara.ai.process.dto.AIMessage; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; @@ -175,4 +180,22 @@ public class AiChatMessageDetailServiceImpl implements IAiChatMessageDetailServi return aiMessages; } + + /** + * 分页获取会话详情信息,join user and model + * @param bo + * @param pageQuery + * @return + */ + @Override + public TableDataInfo queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery) { + QueryWrapper wrapper = Wrappers.query(); + wrapper + .eq(ObjectUtil.isNotNull(bo.getMessageDetailType()), "acmd.message_detail_type", bo.getMessageDetailType()) + .eq(ObjectUtil.isNotNull(bo.getCreateBy()), "acmd.create_by", bo.getCreateBy()) + .eq(ObjectUtil.isNotNull(bo.getModelId()), "acmd.model_id", bo.getModelId()) + .orderByAsc("acmd.message_detail_id"); + Page page = baseMapper.selectAiChatMessageDetailJoinList(pageQuery.build(), wrapper); + return TableDataInfo.build(page); + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java index 286fb3d9..7ce190e8 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java @@ -6,6 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.github.yulichang.interfaces.MPJBaseJoin; import com.google.protobuf.ServiceException; +import com.tencentcloudapi.lkeap.v20240522.models.EmbeddingObject; +import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; +import com.tencentcloudapi.lkeap.v20240522.models.Usage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.ListUtils; import org.dromara.ai.domain.*; @@ -20,8 +23,11 @@ import org.dromara.ai.mapper.AiKnowledgeContentMapper; import org.dromara.ai.domain.dto.AIKnowledgeEmbedding; import org.dromara.ai.mapper.AiModelMapper; import org.dromara.ai.process.dto.AIRequest; +import org.dromara.ai.process.dto.TokenUsage; import org.dromara.ai.process.provider.processor.impl.TencentLkeProcessor; +import org.dromara.ai.process.provider.processor.utils.ProcessorUtils; import org.dromara.ai.vectordb.service.IVectorDBService; +import org.dromara.common.constant.HwMomAiConstants; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.encrypt.utils.EncryptUtils; @@ -31,6 +37,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import lombok.RequiredArgsConstructor; +import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.stereotype.Service; import org.dromara.ai.domain.bo.AiKnowledgeBaseBo; import org.dromara.ai.domain.vo.AiKnowledgeBaseVo; @@ -344,7 +351,24 @@ public class AiKnowledgeBaseServiceImpl implements IAiKnowledgeBaseService { try { for (int i = 0; i < partitionChunkList.size(); i++) { aiRequest.setTexts(partitionChunkList.get(i).toArray(new String[0]));//超过5个后调用腾讯云API返回错误,too manyembeddings - List> vectorList = tencentLkeProcessor.getEmbeddings(aiRequest); + + GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest); + EmbeddingObject[] embeddingObjects = embeddingResponses.getData(); + //获取token使用数量,然后保存 + Usage usage = embeddingResponses.getUsage(); + TokenUsage tokenUsage = new TokenUsage(null,null,usage.getTotalTokens()); + tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_CONTENT_VECTOR,"AI上传知识库内容获取向量", null, tokenUsage, + modelId, aiRequest.getKnowledgeBaseId(), aiKnowledgeEmbedding.getKnowledgeContentId(), + null, null, "0", "1", LoginHelper.getUserId()); + + + // 转换结果格式 + List> vectorList = new ArrayList<>(); + for (EmbeddingObject obj : embeddingObjects) { + vectorList.add(ProcessorUtils.convertFloatArrayToDoubleList(obj.getEmbedding())); + } + + vectorDBService.insertKnowledgeEmbedding(aiKnowledgeEmbedding.getKnowledgeBaseId(), aiKnowledgeEmbedding.getKnowledgeContentId(), partitionChunkList.get(i), vectorList, partitionFidList.get(i)); diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java new file mode 100644 index 00000000..237843df --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java @@ -0,0 +1,162 @@ +package org.dromara.ai.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.dromara.common.core.constant.SystemConstants; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StreamUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.toolkit.JoinWrappers; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.dromara.ai.domain.bo.AiTokenUsageBo; +import org.dromara.ai.domain.vo.AiTokenUsageVo; +import org.dromara.ai.domain.AiTokenUsage; +import org.dromara.ai.mapper.AiTokenUsageMapper; +import org.dromara.ai.service.IAiTokenUsageService; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * 用户token使用详情Service业务层处理 + * + * @author xins + * @date 2025-09-30 + */ +@RequiredArgsConstructor +@Service +public class AiTokenUsageServiceImpl implements IAiTokenUsageService { + + private final AiTokenUsageMapper baseMapper; + + /** + * 查询用户token使用详情 + * + * @param tokenUsageId 主键 + * @return 用户token使用详情 + */ + @Override + public AiTokenUsageVo queryById(Long tokenUsageId){ + return baseMapper.selectVoById(tokenUsageId); + } + + /** + * 分页查询用户token使用详情列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 用户token使用详情分页列表 + */ + @Override + public TableDataInfo queryPageList(AiTokenUsageBo bo, PageQuery pageQuery) { + MPJLambdaWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的用户token使用详情列表 + * + * @param bo 查询条件 + * @return 用户token使用详情列表 + */ + @Override + public List queryList(AiTokenUsageBo bo) { + MPJLambdaWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private MPJLambdaWrapper buildQueryWrapper(AiTokenUsageBo bo) { + Map params = bo.getParams(); + MPJLambdaWrapper lqw = JoinWrappers.lambda(AiTokenUsage.class) + .selectAll(AiTokenUsage.class) + .eq(bo.getTokenUsageId() != null, AiTokenUsage::getTokenUsageId, bo.getTokenUsageId()) + .eq(bo.getUserId() != null, AiTokenUsage::getUserId, bo.getUserId()) + .eq(bo.getToken() != null, AiTokenUsage::getToken, bo.getToken()) + .eq(bo.getModelId() != null, AiTokenUsage::getModelId, bo.getModelId()) + .eq(bo.getPromptToken() != null, AiTokenUsage::getPromptToken, bo.getPromptToken()) + .eq(bo.getCompletionToken() != null, AiTokenUsage::getCompletionToken, bo.getCompletionToken()) + .eq(bo.getTotalToken() != null, AiTokenUsage::getTotalToken, bo.getTotalToken()) + .orderByDesc(AiTokenUsage::getCreateTime); + return lqw; + } + + /** + * 新增用户token使用详情 + * + * @param bo 用户token使用详情 + * @return 是否新增成功 + */ + @Override + public Boolean insertByBo(AiTokenUsageBo bo) { + AiTokenUsage add = MapstructUtils.convert(bo, AiTokenUsage.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setTokenUsageId(add.getTokenUsageId()); + } + return flag; + } + + /** + * 修改用户token使用详情 + * + * @param bo 用户token使用详情 + * @return 是否修改成功 + */ + @Override + public Boolean updateByBo(AiTokenUsageBo bo) { + AiTokenUsage update = MapstructUtils.convert(bo, AiTokenUsage.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(AiTokenUsage entity){ + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 校验并批量删除用户token使用详情信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteByIds(ids) > 0; + } + + + /** + * 分页查询用户token使用详情列表,Join sys_user and ai_model + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 用户token使用详情分页列表 + */ + @Override + public TableDataInfo queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery) { + QueryWrapper wrapper = Wrappers.query(); + wrapper + .eq(ObjectUtil.isNotNull(bo.getModelId()), "atu.model_id", bo.getModelId()) + .like(StringUtils.isNotBlank(bo.getNickName()), "su.nick_name", bo.getNickName()) + .orderByAsc("atu.token_usage_id"); + Page page = baseMapper.selectAiTokenUsageJoinList(pageQuery.build(), wrapper); + return TableDataInfo.build(page); + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml index 9b97b2a1..b1ebea3f 100644 --- a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml +++ b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml @@ -4,4 +4,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + + diff --git a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml new file mode 100644 index 00000000..90d8c808 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml @@ -0,0 +1,24 @@ + + + + + + + +