1.5.8后端

AI Token使用记录保存和查询功能
hwmom-htk
xs 6 months ago
parent f7d3253c9b
commit f6dcf8afa9

@ -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<AiChatMessageVo> list = aiChatMessageService.queryList(bo);
return R.ok(list);
}
/**
* 使
*/
// @SaCheckPermission("ai:aiChatMessage:list")
@GetMapping("/listDetail")
public TableDataInfo<AiChatMessageDetailVo> listDetail(AiChatMessageDetailBo bo, PageQuery pageQuery) {
return aiChatMessageDetailService.queryPageJoinList(bo, pageQuery);
}
}

@ -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<AiTokenUsageVo> 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<AiTokenUsageVo> list = aiTokenUsageService.queryList(bo);
ExcelUtil.exportExcel(list, "用户token使用详情", AiTokenUsageVo.class, response);
}
/**
* token使
*
* @param tokenUsageId
*/
@SaCheckPermission("ai:tokenUsage:query")
@GetMapping("/{tokenUsageId}")
public R<AiTokenUsageVo> 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<Void> 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<Void> 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<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] tokenUsageIds) {
return toAjax(aiTokenUsageService.deleteWithValidByIds(List.of(tokenUsageIds), true));
}
/**
* token使
*/
@GetMapping("/getAiTokenUsageList")
public R<List<AiTokenUsageVo>> getAiTokenUsageList(AiTokenUsageBo bo) {
List<AiTokenUsageVo> list = aiTokenUsageService.queryList(bo);
return R.ok(list);
}
}

@ -37,6 +37,12 @@ public class AiChatMessageDetail extends TenantEntity {
*/
private String sessionId;
/**
* 1AI23SQL4AI5
*/
private String messageDetailType;
/**
*
*/
@ -72,6 +78,11 @@ public class AiChatMessageDetail extends TenantEntity {
*/
private Long knowledgeBaseId;
/**
* ID,使ai_knowledge_content
*/
private Long knowledgeContentId;
/**
* 10
*/
@ -82,5 +93,4 @@ public class AiChatMessageDetail extends TenantEntity {
*/
private String completeFlag;
}

@ -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;
/**
* IDai_model
*/
private Long modelId;
/**
* token
*/
private Long promptToken;
/**
* token
*/
private Long completionToken;
/**
* 使token
*/
private Long totalToken;
}

@ -38,6 +38,12 @@ public class AiChatMessageDetailBo extends BaseEntity {
@NotNull(message = "会话ID不能为空", groups = { AddGroup.class, EditGroup.class })
private String sessionId;
/**
* 1AI23SQL4AI5
*/
@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;
/**
* 10
*/

@ -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;
/**
* IDai_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;
}

@ -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;
/**
* 1AI23SQL4AI5
*/
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;
/**
* 10
*/
@ -101,5 +114,14 @@ public class AiChatMessageDetailVo implements Serializable {
@ExcelProperty(value = "完整标识(1是0否),代表回复信息是否完整回复,中间可以暂停继续。")
private String completeFlag;
/**
*
*/
private String nickName;
/**
* AI
*/
private String modelName;
}

@ -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;
/**
* IDai_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;
}

@ -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<AiChatMessageDetail, AiChatMessageDetailVo> {
@DataPermission({
@DataColumn(key = "deptName", value = "acmd.create_dept"),
@DataColumn(key = "userName", value = "acmd.user_id")
})
Page<AiChatMessageDetailVo> selectAiChatMessageDetailJoinList(@Param("page") Page<AiChatMessageDetailVo> page,
@Param(Constants.WRAPPER) Wrapper<AiChatMessageDetail> queryWrapper);
}

@ -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<AiTokenUsage, AiTokenUsageVo> {
/**
* token使join sys_user and ai_model
* @param page
* @param queryWrapper
* @return List<AiTokenUsageVo>
*/
@DataPermission({
@DataColumn(key = "deptName", value = "atu.create_dept"),
@DataColumn(key = "userName", value = "atu.user_id")
})
Page<AiTokenUsageVo> selectAiTokenUsageJoinList(@Param("page") Page<AiTokenUsageVo> page,
@Param(Constants.WRAPPER) Wrapper<AiTokenUsage> queryWrapper);
}

@ -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;
// }
// }
}

@ -0,0 +1,46 @@
package org.dromara.ai.process.dto;
/**
* @Author xins
* @Date 2025/9/25 16:28
* @Description: AI使Tokendto
*/
// 如果还没有这个类,需要创建
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;
}
}

@ -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<AIResponse> chat(AIRequest request);
/**
*
* @param request
* @return Flux
*/
Flux<String> chatStreamContent(AIRequest request, LoginUser loginUser);
/**
*
* @param request
@ -42,19 +51,53 @@ public interface IUnifiedAIProviderProcessor {
/**
*
* @param text
*
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
public List<Double> getEmbedding(AIRequest aiRequest);
/**
*
* @param texts
*
* @param embeddingResponse
* @return
*/
public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse);
/**
* Usage
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest);
/**
*
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
public List<List<Double>> 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);
}

@ -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<String> executeStreamRequest(String url, String requestBody, String apiKey) {
protected Flux<String> 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())));
}
/**
* JSONtoken
*/
protected StreamChunkResult parseStreamChunk(String jsonChunk) {
try {
return extractContentAndTokensFromStreamJson(jsonChunk);
} catch (Exception e) {
return new StreamChunkResult(null, null);
}
}
/**
* JSONtoken
*/
protected abstract StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception;
/**
* HTTPtokenFlux
*/
protected Flux<StreamChunkResult> 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<AiChatMessage>()
.eq(AiChatMessage::getSessionId, sessionId));
List<AIMessage> 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<AiTokenUsage> 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

@ -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<AIResponse> chatTest(AIRequest request) {
AIMessage aiMessage = new AIMessage();
@ -101,7 +98,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
public Flux<String> 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<String> 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<Double> getEmbedding(GetEmbeddingResponse embeddingResponse){
return List.of();
}
@Override
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) {
return null;
}
@Override
public List<List<Double>> 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<AiChatMessage>()
.eq(AiChatMessage::getSessionId, sessionId));
List<AIMessage> 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());
}
}

@ -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<Double> getEmbedding(GetEmbeddingResponse embeddingResponse) {
EmbeddingObject[] embeddingObjects = embeddingResponse.getData();
// 转换结果格式
List<List<Double>> 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<List<Double>> 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<List<Double>> 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;
}
/**
* FloatDouble
* @param floatArray Float
* @return Double
*/
private List<Double> convertFloatArrayToDoubleList(Float[] floatArray) {
List<Double> doubleList = new ArrayList<>(floatArray.length);
for (Float f : floatArray) {
doubleList.add(f.doubleValue());
}
return doubleList;
}
@Override
public Mono<AIResponse> chatTest(AIRequest request) {
@ -153,9 +191,28 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor {
return null;
}
@Override
public Flux<String> chatStreamContent(AIRequest request, LoginUser loginUser) {
return null;
}
@Override
public Flux<String> 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;
}
}

@ -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<String> chatStream(AIRequest request, LoginUser loginUser) {
public Flux<String> 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<Double> getEmbedding(GetEmbeddingResponse embeddingResponse){
return List.of();
}
@Override
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) {
return null;
}
@Override
public List<List<Double>> getEmbeddings(AIRequest aiRequest) {
return List.of();
@ -247,4 +262,13 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor {
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
return null;
}
@Override
protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception {
return null;
}
}

@ -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 {
/**
* FloatDouble
* @param floatArray Float
* @return Double
*/
public static List<Double> convertFloatArrayToDoubleList(Float[] floatArray) {
List<Double> doubleList = new ArrayList<>(floatArray.length);
for (Float f : floatArray) {
doubleList.add(f.doubleValue());
}
return doubleList;
}
}

@ -74,4 +74,12 @@ public interface IAiChatMessageDetailService {
* @return LIST<AIMessage></AIMessage>
*/
public List<AIMessage> getAIChatMessages(String sessionId);
/**
* join user and model
* @param bo
* @param pageQuery
* @return
*/
public TableDataInfo<AiChatMessageDetailVo> queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery);
}

@ -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<AiTokenUsageVo> queryPageList(AiTokenUsageBo bo, PageQuery pageQuery);
/**
* token使
*
* @param bo
* @return token使
*/
List<AiTokenUsageVo> 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<Long> ids, Boolean isValid);
/**
* token使,Join sys_user and ai_model
*
* @param bo
* @param pageQuery
* @return token使
*/
public TableDataInfo<AiTokenUsageVo> queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery);
}

@ -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<Double> 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<Double> 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<AIResponse> 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<AiTableQueryCondition> conditions = JSON.parseArray(relateFilterCondition, AiTableQueryCondition.class);
wrapper.setAiTableQueryConditions(conditions);
@ -470,7 +493,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
}
List<Map<String, Object>> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper,"", "");
List<Map<String, Object>> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper, "", "");
JSONArray relateTableDataArr = new JSONArray();
for (Map<String, Object> tableD : tableData) {
// JSONObject tableDataJson = new JSONObject();

@ -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<AiChatMessageDetailVo> queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery) {
QueryWrapper<AiChatMessageDetail> 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<AiChatMessageDetailVo> page = baseMapper.selectAiChatMessageDetailJoinList(pageQuery.build(), wrapper);
return TableDataInfo.build(page);
}
}

@ -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<List<Double>> 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<List<Double>> 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));

@ -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<AiTokenUsageVo> queryPageList(AiTokenUsageBo bo, PageQuery pageQuery) {
MPJLambdaWrapper<AiTokenUsage> lqw = buildQueryWrapper(bo);
Page<AiTokenUsageVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* token使
*
* @param bo
* @return token使
*/
@Override
public List<AiTokenUsageVo> queryList(AiTokenUsageBo bo) {
MPJLambdaWrapper<AiTokenUsage> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private MPJLambdaWrapper<AiTokenUsage> buildQueryWrapper(AiTokenUsageBo bo) {
Map<String, Object> params = bo.getParams();
MPJLambdaWrapper<AiTokenUsage> 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<Long> 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<AiTokenUsageVo> queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery) {
QueryWrapper<AiTokenUsage> 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<AiTokenUsageVo> page = baseMapper.selectAiTokenUsageJoinList(pageQuery.build(), wrapper);
return TableDataInfo.build(page);
}
}

@ -4,4 +4,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.ai.mapper.AiChatMessageDetailMapper">
<select id="selectAiChatMessageDetailJoinList" resultType="AiChatMessageDetailVo">
select
acmd.message_detail_id,
acmd.create_by,
acmd.question_content,
acmd.answer_content,
acmd.prompt_token,
acmd.completion_token,
acmd.total_token,
acmd.take_flag,
acmd.message_detail_type,
acmd.knowledge_base_id,
acmd.knowledge_content_id,
su.nick_name,
am.model_name
from ai_chat_message_detail acmd
left join sys_user su on acmd.create_by=su.user_id
left join ai_model am on acmd.model_id = am.model_id
${ew.getCustomSqlSegment}
</select>
</mapper>

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.ai.mapper.AiTokenUsageMapper">
<select id="selectAiTokenUsageJoinList" resultType="AiTokenUsageVo">
select
atu.token_usage_id,
atu.user_id,
atu.model_id,
atu.prompt_token,
atu.completion_token,
atu.total_token,
su.nick_name,
am.model_name
from ai_token_usage atu
left join sys_user su on atu.user_id=su.user_id
left join ai_model am on atu.model_id = am.model_id
${ew.getCustomSqlSegment}
</select>
</mapper>
Loading…
Cancel
Save